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            // Colour only when stdout is a terminal, and never under
334            // `NO_COLOR` — `Color::Auto`'s rule, called rather than
335            // copied.
336            //
337            // This was inlined at first, on a comment claiming
338            // `rustango::logging` is gated on `admin` + `tenancy`. It is
339            // not: `pub mod logging` is ungated and its contents are
340            // `runtime`-gated — the same feature that gates this macro
341            // (`pub use rustango_macros::main` is `#[cfg(feature =
342            // "runtime")]`). So wherever this expands, `Color` is
343            // reachable, and the copy was justified by a gate that does
344            // not exist. `should_colour` carries a deliberate
345            // `NO_COLOR`-set-but-empty subtlety that the inline happened
346            // to match today and nothing kept matching tomorrow.
347            //
348            // Without any of it the subscriber inherits
349            // tracing-subscriber's default, `cfg!(feature = "ansi")` —
350            // true since the framework turned that feature on — so
351            // `./app > app.log` and every container redirecting stdout
352            // collected escape codes. `Setup::install` has its own
353            // check; this is the default entrypoint and had none.
354            let __ansi = #root::logging::Color::Auto.should_colour();
355            // `try_init` so duplicate installers (e.g. tests already
356            // holding a subscriber) don't panic.
357            let _ = tracing_subscriber::fmt()
358                .with_env_filter(
359                    EnvFilter::try_from_default_env()
360                        .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
361                )
362                .with_ansi(__ansi)
363                .try_init();
364        }
365        let __rt = #builder_call
366            .enable_all()
367            .build()
368            .expect("failed to build tokio runtime");
369        __rt.block_on(async move #user_body)
370    }})?;
371
372    Ok(quote! {
373        #input
374    })
375}
376
377enum Flavor {
378    MultiThread,
379    CurrentThread,
380}
381
382fn parse_flavor(args: &TokenStream2) -> Flavor {
383    // Cheap parser: look for the literal token sequence
384    // `flavor = "current_thread"`. Everything else (including
385    // bare `multi_thread` or no args) defaults to multi-thread.
386    let s = args.to_string();
387    if s.contains("current_thread") {
388        Flavor::CurrentThread
389    } else {
390        Flavor::MultiThread
391    }
392}
393
394/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
395struct QInput {
396    base_path: syn::Path,
397    field: syn::Ident,
398    value: syn::Expr,
399}
400
401impl syn::parse::Parse for QInput {
402    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
403        let base_path: syn::Path = input.parse()?;
404        input.parse::<syn::Token![.]>()?;
405        let field: syn::Ident = input.parse()?;
406        input.parse::<syn::Token![=]>()?;
407        let value: syn::Expr = input.parse()?;
408        Ok(QInput {
409            base_path,
410            field,
411            value,
412        })
413    }
414}
415
416fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
417    let q: QInput = syn::parse2(input)?;
418    let root = rustango_root();
419    let field_str = q.field.to_string();
420    let field_span = q.field.span();
421    let (base, suffix) = match field_str.find("__") {
422        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
423        None => (field_str.as_str(), ""),
424    };
425    if base.is_empty() {
426        return Err(syn::Error::new(
427            field_span,
428            "Q!(): field name is empty before `__` suffix",
429        ));
430    }
431    let base_ident = syn::Ident::new(base, field_span);
432    let value = &q.value;
433    let path = &q.base_path;
434
435    // Most suffixes map directly to a Column method with the value
436    // forwarded unchanged. Some need value-shape massaging (wildcards
437    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
438    // ISNULL). Unknown suffixes fail the build.
439    let expanded = match suffix {
440        "" | "exact" => quote! {
441            #root::core::Column::eq(#path::#base_ident, #value)
442        },
443        "ne" => quote! {
444            #root::core::Column::ne(#path::#base_ident, #value)
445        },
446        "gt" => quote! {
447            #root::core::Column::gt(#path::#base_ident, #value)
448        },
449        "gte" => quote! {
450            #root::core::Column::gte(#path::#base_ident, #value)
451        },
452        "lt" => quote! {
453            #root::core::Column::lt(#path::#base_ident, #value)
454        },
455        "lte" => quote! {
456            #root::core::Column::lte(#path::#base_ident, #value)
457        },
458        // The escaped-LIKE lookups delegate to the typed Column
459        // methods, which escape the value and pair it with the
460        // *Escaped ops so `%` / `_` match literally on every dialect
461        // (#1257) — the macro no longer owns any pattern wrapping.
462        "iexact" => quote! {
463            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
464        },
465        "contains" => quote! {
466            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
467        },
468        "icontains" => quote! {
469            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
470        },
471        "startswith" => quote! {
472            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
473        },
474        "istartswith" => quote! {
475            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
476        },
477        "endswith" => quote! {
478            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
479        },
480        "iendswith" => quote! {
481            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
482        },
483        "in" => quote! {
484            #root::core::Column::is_in(#path::#base_ident, #value)
485        },
486        "not_in" => quote! {
487            #root::core::Column::not_in(#path::#base_ident, #value)
488        },
489        "isnull" => {
490            // Must be a bool literal at macro time so we can route to
491            // is_null vs is_not_null without a runtime branch.
492            let b = match value {
493                syn::Expr::Lit(syn::ExprLit {
494                    lit: syn::Lit::Bool(b),
495                    ..
496                }) => b.value(),
497                _ => {
498                    return Err(syn::Error::new_spanned(
499                        value,
500                        "Q!(): `__isnull` requires a `true` or `false` literal",
501                    ));
502                }
503            };
504            if b {
505                quote! { #root::core::Column::is_null(#path::#base_ident) }
506            } else {
507                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
508            }
509        }
510        "between" => {
511            // Accept a tuple literal `(lo, hi)`.
512            let tuple = match value {
513                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
514                _ => {
515                    return Err(syn::Error::new_spanned(
516                        value,
517                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
518                    ));
519                }
520            };
521            let lo = &tuple.elems[0];
522            let hi = &tuple.elems[1];
523            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
524        }
525        "regex" => quote! {
526            #root::core::Column::regex(#path::#base_ident, #value)
527        },
528        "iregex" => quote! {
529            #root::core::Column::iregex(#path::#base_ident, #value)
530        },
531        _ => {
532            return Err(syn::Error::new(
533                field_span,
534                format!(
535                    "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",
536                    suffix
537                ),
538            ));
539        }
540    };
541    Ok(expanded)
542}
543
544fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
545    // Default to "./migrations" if invoked without args.
546    let path_str = if input.is_empty() {
547        "./migrations".to_string()
548    } else {
549        let lit: LitStr = syn::parse2(input)?;
550        lit.value()
551    };
552
553    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
554        syn::Error::new(
555            proc_macro2::Span::call_site(),
556            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
557        )
558    })?;
559    let abs = std::path::Path::new(&manifest).join(&path_str);
560
561    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
562    if abs.is_dir() {
563        let read = std::fs::read_dir(&abs).map_err(|e| {
564            syn::Error::new(
565                proc_macro2::Span::call_site(),
566                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
567            )
568        })?;
569        for entry in read.flatten() {
570            let path = entry.path();
571            if !path.is_file() {
572                continue;
573            }
574            if path.extension().and_then(|s| s.to_str()) != Some("json") {
575                continue;
576            }
577            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
578                continue;
579            };
580            entries.push((stem.to_owned(), path));
581        }
582    }
583    entries.sort_by(|a, b| a.0.cmp(&b.0));
584
585    // Compile-time chain validation: read each migration's JSON,
586    // pull `name` and `prev` (file-stem-keyed for the chain check),
587    // and verify every `prev` points to another migration in the
588    // slice. Mismatches between the file stem and the embedded
589    // `name` field — and broken `prev` chains — fail at MACRO
590    // EXPANSION time so a misshapen migration set never compiles.
591    //
592    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
593    // migrations + a Rust proc-macro that reads them is the unique
594    // combo nothing else in the Django-shape Rust camp can match
595    // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
596    // Rwf's are raw SQL — none have a static chain to validate).
597    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
598    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
599    for (stem, path) in &entries {
600        let raw = std::fs::read_to_string(path).map_err(|e| {
601            syn::Error::new(
602                proc_macro2::Span::call_site(),
603                format!(
604                    "embed_migrations!: cannot read {} for chain validation: {e}",
605                    path.display()
606                ),
607            )
608        })?;
609        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
610            syn::Error::new(
611                proc_macro2::Span::call_site(),
612                format!(
613                    "embed_migrations!: {} is not valid JSON: {e}",
614                    path.display()
615                ),
616            )
617        })?;
618        let name = json
619            .get("name")
620            .and_then(|v| v.as_str())
621            .ok_or_else(|| {
622                syn::Error::new(
623                    proc_macro2::Span::call_site(),
624                    format!(
625                        "embed_migrations!: {} is missing the `name` field",
626                        path.display()
627                    ),
628                )
629            })?
630            .to_owned();
631        if name != *stem {
632            return Err(syn::Error::new(
633                proc_macro2::Span::call_site(),
634                format!(
635                    "embed_migrations!: file stem `{stem}` does not match the migration's \
636                     `name` field `{name}` — rename the file or fix the JSON",
637                ),
638            ));
639        }
640        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
641        chain_names.push(name.clone());
642        prev_refs.push((name, prev));
643    }
644
645    let name_set: std::collections::HashSet<&str> =
646        chain_names.iter().map(String::as_str).collect();
647    for (name, prev) in &prev_refs {
648        if let Some(p) = prev {
649            if !name_set.contains(p.as_str()) {
650                return Err(syn::Error::new(
651                    proc_macro2::Span::call_site(),
652                    format!(
653                        "embed_migrations!: broken migration chain — `{name}` declares \
654                         prev=`{p}` but no migration with that name exists in {}",
655                        abs.display()
656                    ),
657                ));
658            }
659        }
660    }
661
662    let pairs: Vec<TokenStream2> = entries
663        .iter()
664        .map(|(name, path)| {
665            let path_lit = path.display().to_string();
666            quote! { (#name, ::core::include_str!(#path_lit)) }
667        })
668        .collect();
669
670    Ok(quote! {
671        {
672            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
673            __RUSTANGO_EMBEDDED
674        }
675    })
676}
677
678fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
679    let root = rustango_root();
680    let struct_name = &input.ident;
681
682    let Data::Struct(data) = &input.data else {
683        return Err(syn::Error::new_spanned(
684            struct_name,
685            "Model can only be derived on structs",
686        ));
687    };
688    let Fields::Named(named) = &data.fields else {
689        return Err(syn::Error::new_spanned(
690            struct_name,
691            "Model requires a struct with named fields",
692        ));
693    };
694
695    let container = parse_container_attrs(input)?;
696    let table = container
697        .table
698        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
699    let model_name = struct_name.to_string();
700
701    let collected = collect_fields(named, &table)?;
702
703    // Validate that #[rustango(display = "…")] names a real field.
704    if let Some((ref display, span)) = container.display {
705        if !collected.field_names.iter().any(|n| n == display) {
706            return Err(syn::Error::new(
707                span,
708                format!("`display = \"{display}\"` does not match any field on this struct"),
709            ));
710        }
711    }
712    let display = container.display.map(|(name, _)| name);
713    let app_label = container.app.clone();
714
715    // Validate admin field-name lists against declared field names.
716    // Note: `list_display` is intentionally NOT validated here. As of
717    // v0.32 it may also reference inventory-registered computed
718    // fields (via `register_admin_computed!`) whose existence the
719    // macro can't see at compile time — they're submitted from any
720    // crate that depends on rustango. The runtime list-view resolves
721    // unknown names against the inventory + silently drops the
722    // truly-bogus ones, which is the cheaper trade-off versus
723    // forcing a per-Model attr to opt out.
724    if let Some(admin) = &container.admin {
725        for (label, list) in [
726            ("search_fields", &admin.search_fields),
727            ("readonly_fields", &admin.readonly_fields),
728            ("list_filter", &admin.list_filter),
729        ] {
730            if let Some((names, span)) = list {
731                for name in names {
732                    if !collected.field_names.iter().any(|n| n == name) {
733                        return Err(syn::Error::new(
734                            *span,
735                            format!(
736                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
737                            ),
738                        ));
739                    }
740                }
741            }
742        }
743        if let Some((pairs, span)) = &admin.ordering {
744            for (name, _) in pairs {
745                if !collected.field_names.iter().any(|n| n == name) {
746                    return Err(syn::Error::new(
747                        *span,
748                        format!(
749                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
750                        ),
751                    ));
752                }
753            }
754        }
755        if let Some((groups, span)) = &admin.fieldsets {
756            for (_, fields) in groups {
757                for name in fields {
758                    if !collected.field_names.iter().any(|n| n == name) {
759                        return Err(syn::Error::new(
760                            *span,
761                            format!(
762                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
763                            ),
764                        ));
765                    }
766                }
767            }
768        }
769    }
770    if let Some(audit) = &container.audit {
771        if let Some((names, span)) = &audit.track {
772            for name in names {
773                if !collected.field_names.iter().any(|n| n == name) {
774                    return Err(syn::Error::new(
775                        *span,
776                        format!(
777                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
778                        ),
779                    ));
780                }
781            }
782        }
783    }
784
785    // Issue #291 / T2.5 — validate each `default_order` column name
786    // against the model's collected fields. Typos fail at macro-expand
787    // time, not at the database.
788    for (col, _desc, span) in &container.default_order {
789        if !collected.field_names.iter().any(|n| n == col) {
790            return Err(syn::Error::new(
791                *span,
792                format!(
793                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
794                ),
795            ));
796        }
797    }
798
799    // Build the audit_track list for ModelSchema: None when no audit attr,
800    // Some(empty) when audit present without track, Some(names) when explicit.
801    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
802        audit
803            .track
804            .as_ref()
805            .map(|(names, _)| names.clone())
806            .unwrap_or_default()
807    });
808
809    // Merge field-level indexes into the container's index list.
810    let mut all_indexes: Vec<IndexAttr> = container.indexes;
811    for field in &named.named {
812        let ident = field.ident.as_ref().expect("named");
813        let col = to_snake_case(&ident.to_string()); // column name fallback
814                                                     // Re-parse field attrs to check for index flag
815        if let Ok(fa) = parse_field_attrs(field) {
816            if fa.index {
817                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
818                let auto_name = if fa.index_unique {
819                    format!("{table}_{col_name}_uq_idx")
820                } else {
821                    format!("{table}_{col_name}_idx")
822                };
823                all_indexes.push(IndexAttr {
824                    name: fa.index_name.or(Some(auto_name)),
825                    columns: vec![col_name],
826                    unique: fa.index_unique,
827                    method: fa.index_method,
828                    where_clause: None,
829                    include: Vec::new(),
830                });
831            }
832        }
833    }
834
835    let model_impl = model_impl_tokens(
836        struct_name,
837        &model_name,
838        &table,
839        display.as_deref(),
840        app_label.as_deref(),
841        container.admin.as_ref(),
842        &container.default_order,
843        &collected.field_schemas,
844        collected.soft_delete_column.as_deref(),
845        container.permissions,
846        audit_track_names.as_deref(),
847        &container.m2m,
848        &all_indexes,
849        &container.checks,
850        &container.excludes,
851        &container.composite_fks,
852        &container.generic_fks,
853        container.scope.as_deref(),
854        container.is_view,
855        container.verbose_name.as_deref(),
856        container.verbose_name_plural.as_deref(),
857        container.managed,
858        container.base_manager_name.as_deref(),
859        container.order_with_respect_to.as_deref(),
860        container.proxy,
861        &container.required_db_features,
862        container.required_db_vendor.as_deref(),
863        container.default_related_name.as_deref(),
864        container.db_table_comment.as_deref(),
865        container
866            .get_latest_by
867            .as_ref()
868            .map(|(c, d)| (c.as_str(), *d)),
869        &container.extra_permissions,
870        &container.default_permissions,
871        &container.global_scopes,
872        &container.reverse_has_relations,
873        &container.generic_has_relations,
874    );
875    let module_ident = column_module_ident(struct_name);
876    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
877    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
878        let track_set: Option<std::collections::HashSet<&str>> = audit
879            .track
880            .as_ref()
881            .map(|(names, _)| names.iter().map(String::as_str).collect());
882        collected
883            .column_entries
884            .iter()
885            .filter(|c| {
886                track_set
887                    .as_ref()
888                    .map_or(true, |s| s.contains(c.name.as_str()))
889            })
890            .collect()
891    });
892    let inherent_impl = inherent_impl_tokens(
893        struct_name,
894        &collected,
895        collected.primary_key.as_ref(),
896        &column_consts,
897        audited_fields.as_deref(),
898        &all_indexes,
899        &container.manager_fns,
900    );
901    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
902    let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
903    let reverse_helpers = reverse_helper_tokens(
904        struct_name,
905        &collected.fk_relations,
906        container.default_related_name.as_deref(),
907    );
908    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
909    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
910    // Issue #817 — `#[rustango(through(...))]` accessors.
911    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
912    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
913    let reverse_has_accessors =
914        reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
915    let generic_fk_accessors = generic_fk_accessor_tokens(
916        struct_name,
917        &container.generic_fks,
918        &collected.column_entries,
919    );
920
921    // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
922    // emits an empty extension trait so users can add methods via
923    // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
924    // trait declaration. See `crates/rustango/src/manager.rs` for the
925    // pattern this replaces.
926    let manager_trait = container.manager_ext.as_ref().map(|name| {
927        let model_name_str = struct_name.to_string();
928        let doc = format!(
929            "Custom-Manager extension trait for [`{model_name_str}`]. \
930             Generated by `#[rustango(manager(ext = ...))]`. Add methods \
931             via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
932        );
933        quote! {
934            #[doc = #doc]
935            pub trait #name: ::core::marker::Sized {}
936        }
937    });
938
939    Ok(quote! {
940        #model_impl
941        #inherent_impl
942        #from_row_impl
943        #column_module
944        #reverse_helpers
945        #m2m_accessors
946        #generic_m2m_accessors
947        #through_accessors
948        #reverse_has_accessors
949        #generic_fk_accessors
950        #manager_trait
951
952        #root::core::inventory::submit! {
953            #root::core::ModelEntry {
954                schema: <#struct_name as #root::core::Model>::SCHEMA,
955                // `module_path!()` evaluates at the registration site,
956                // so a Model declared in `crate::blog::models` records
957                // `"<crate>::blog::models"` and `resolved_app_label()`
958                // can infer "blog" without an explicit attribute.
959                module_path: ::core::module_path!(),
960            }
961        }
962    })
963}
964
965/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
966/// matches `field_name` against the model's FK fields and, for a
967/// match, decodes the FK target via the parent's macro-generated
968/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
969/// `ForeignKey::Loaded` on `self`.
970///
971/// Always emitted (with empty arms for FK-less models, which
972/// return `Ok(false)` for any field name) so the `T: LoadRelated`
973/// trait bound on `fetch_on` is universally satisfied — users
974/// never have to think about implementing it.
975fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
976    let root = rustango_root();
977    let arms = fk_relations.iter().map(|rel| {
978        let parent_ty = &rel.parent_type;
979        let fk_col = rel.fk_column.as_str();
980        // FK field's Rust ident matches its SQL column name in v0.8
981        // (no `column = "..."` rename ships on FK fields).
982        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
983        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
984        let assign = if rel.nullable {
985            quote! {
986                self.#field_ident = ::core::option::Option::Some(
987                    #root::sql::ForeignKey::loaded(_pk, _parent),
988                );
989            }
990        } else {
991            quote! {
992                self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
993            }
994        };
995        quote! {
996            #fk_col => {
997                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
998                // Audit #451 — multi-hop `select_related("a__b__c")`:
999                // stitch the deeper relation onto this parent first,
1000                // decoding it at the accumulated `__next_alias`. The
1001                // parent type also impls `LoadRelated`, so this recurses
1002                // the FK chain to arbitrary depth.
1003                if let ::core::option::Option::Some(__r) = __rest {
1004                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1005                        &mut _parent, row, __r, &__next_alias,
1006                    )?;
1007                }
1008                // Loud-in-debug, default-in-release: a divergence
1009                // between the FK field's declared `K` (drives the
1010                // expected `SqlValue::<Variant>`) and the parent's
1011                // `__rustango_pk_value` output is a macro-internal
1012                // invariant break — surfacing the panic in dev
1013                // catches it before users hit silent PK=0 corruption.
1014                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1015                    #root::core::SqlValue::#variant_ident(v) => v,
1016                    _other => {
1017                        ::core::debug_assert!(
1018                            false,
1019                            "rustango macro bug: load_related on FK `{}` expected \
1020                             SqlValue::{} from parent's __rustango_pk_value but got \
1021                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1022                            #fk_col,
1023                            ::core::stringify!(#variant_ident),
1024                            _other,
1025                        );
1026                        #default_expr
1027                    }
1028                };
1029                #assign
1030                ::core::result::Result::Ok(true)
1031            }
1032        }
1033    });
1034    quote! {
1035        #[cfg(feature = "postgres")]
1036        impl #root::sql::LoadRelated for #struct_name {
1037            #[allow(unused_variables)]
1038            fn __rustango_load_related(
1039                &mut self,
1040                row: &#root::sql::sqlx::postgres::PgRow,
1041                field_name: &str,
1042                alias: &str,
1043            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1044                // Audit #451 — split the multi-hop path: `base` is the FK
1045                // on THIS model, `__rest` (if any) is the remaining chain
1046                // to stitch onto the loaded parent. `__next_alias` is the
1047                // accumulated join alias the parent's columns live under
1048                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1049                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1050                    match field_name.split_once("__") {
1051                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1052                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1053                    };
1054                let __next_alias: ::std::string::String = match __rest {
1055                    ::core::option::Option::Some(__r) => {
1056                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1057                        ::std::format!("{}__{}", alias, __rb)
1058                    }
1059                    ::core::option::Option::None => ::std::string::String::new(),
1060                };
1061                match __base {
1062                    #( #arms )*
1063                    _ => ::core::result::Result::Ok(false),
1064                }
1065            }
1066        }
1067    }
1068}
1069
1070/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1071/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1072/// which expands to a `LoadRelatedMy` impl when rustango is built with
1073/// the `mysql` feature, and to nothing otherwise. The decoded parent
1074/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1075/// decoder, also batch8) so the dual emission is symmetric across
1076/// backends.
1077fn load_related_impl_my_tokens(
1078    struct_name: &syn::Ident,
1079    fk_relations: &[FkRelation],
1080) -> TokenStream2 {
1081    let root = rustango_root();
1082    let arms = fk_relations.iter().map(|rel| {
1083        let parent_ty = &rel.parent_type;
1084        let fk_col = rel.fk_column.as_str();
1085        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1086        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1087        let assign = if rel.nullable {
1088            quote! {
1089                __self.#field_ident = ::core::option::Option::Some(
1090                    #root::sql::ForeignKey::loaded(_pk, _parent),
1091                );
1092            }
1093        } else {
1094            quote! {
1095                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1096            }
1097        };
1098        // `self` IS hygiene-tracked through macro_rules — emitted from
1099        // a different context than the `&mut self` parameter inside
1100        // the macro_rules-expanded fn. Pass it through as `__self`
1101        // and let the macro_rules rebind it to the receiver.
1102        quote! {
1103            #fk_col => {
1104                let mut _parent: #parent_ty =
1105                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1106                // Audit #451 — multi-hop: stitch the deeper relation onto
1107                // the parent at the accumulated alias (see PG twin).
1108                if let ::core::option::Option::Some(__r) = __rest {
1109                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1110                        &mut _parent, row, __r, &__next_alias,
1111                    )?;
1112                }
1113                // See note in `load_related_impl_tokens` (PG twin) —
1114                // the same loud-in-debug invariant guard.
1115                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1116                    #root::core::SqlValue::#variant_ident(v) => v,
1117                    _other => {
1118                        ::core::debug_assert!(
1119                            false,
1120                            "rustango macro bug: load_related on FK `{}` expected \
1121                             SqlValue::{} from parent's __rustango_pk_value but got \
1122                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1123                            #fk_col,
1124                            ::core::stringify!(#variant_ident),
1125                            _other,
1126                        );
1127                        #default_expr
1128                    }
1129                };
1130                #assign
1131                ::core::result::Result::Ok(true)
1132            }
1133        }
1134    });
1135    quote! {
1136        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1137            #( #arms )*
1138        });
1139    }
1140}
1141
1142/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1143/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1144/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1145fn load_related_impl_sqlite_tokens(
1146    struct_name: &syn::Ident,
1147    fk_relations: &[FkRelation],
1148) -> TokenStream2 {
1149    let root = rustango_root();
1150    let arms = fk_relations.iter().map(|rel| {
1151        let parent_ty = &rel.parent_type;
1152        let fk_col = rel.fk_column.as_str();
1153        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1154        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1155        let assign = if rel.nullable {
1156            quote! {
1157                __self.#field_ident = ::core::option::Option::Some(
1158                    #root::sql::ForeignKey::loaded(_pk, _parent),
1159                );
1160            }
1161        } else {
1162            quote! {
1163                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1164            }
1165        };
1166        quote! {
1167            #fk_col => {
1168                let mut _parent: #parent_ty =
1169                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1170                // Audit #451 — multi-hop: stitch the deeper relation onto
1171                // the parent at the accumulated alias (see PG twin).
1172                if let ::core::option::Option::Some(__r) = __rest {
1173                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1174                        &mut _parent, row, __r, &__next_alias,
1175                    )?;
1176                }
1177                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1178                    #root::core::SqlValue::#variant_ident(v) => v,
1179                    _other => {
1180                        ::core::debug_assert!(
1181                            false,
1182                            "rustango macro bug: load_related on FK `{}` expected \
1183                             SqlValue::{} from parent's __rustango_pk_value but got \
1184                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1185                            #fk_col,
1186                            ::core::stringify!(#variant_ident),
1187                            _other,
1188                        );
1189                        #default_expr
1190                    }
1191                };
1192                #assign
1193                ::core::result::Result::Ok(true)
1194            }
1195        }
1196    });
1197    quote! {
1198        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1199            #( #arms )*
1200        });
1201    }
1202}
1203
1204/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1205/// matches `field_name` against the model's FK fields and returns
1206/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1207/// group children by parent PK.
1208///
1209/// Always emitted (with `_ => None` for FK-less models) so the
1210/// trait bound on `fetch_with_prefetch` is universally satisfied.
1211fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1212    let root = rustango_root();
1213    let arms = fk_relations.iter().map(|rel| {
1214        let fk_col = rel.fk_column.as_str();
1215        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1216        if rel.pk_kind == DetectedKind::I64 {
1217            // i64 FK — return the stored PK so prefetch_related can
1218            // group children by it. Nullable variant unwraps via
1219            // `as_ref().map(...)`: an unset (NULL) FK column yields
1220            // `None` and that child sits out of the grouping (correct
1221            // semantics — it has no parent to attach to).
1222            if rel.nullable {
1223                quote! {
1224                    #fk_col => self.#field_ident
1225                        .as_ref()
1226                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1227                }
1228            } else {
1229                quote! {
1230                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1231                }
1232            }
1233        } else {
1234            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1235            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1236            // i64-keyed grouping path — the trait signature is
1237            // `Option<i64>` and a non-i64 PK can't lower into it.
1238            // The FK still works for everything else (CRUD, lazy
1239            // load via `.get()`, select_related JOINs); only the
1240            // bulk prefetch grouper needs the integer key.
1241            quote! {
1242                #fk_col => ::core::option::Option::None,
1243            }
1244        }
1245    });
1246    // PK-type-agnostic version: every FK arm emits an
1247    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1248    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1249    // opt OUT of the legacy i64 method (it returns None) but opt IN
1250    // here.
1251    let value_arms = fk_relations.iter().map(|rel| {
1252        let fk_col = rel.fk_column.as_str();
1253        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1254        if rel.nullable {
1255            quote! {
1256                #fk_col => self.#field_ident
1257                    .as_ref()
1258                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1259                        #root::sql::ForeignKey::pk(fk)
1260                    )),
1261            }
1262        } else {
1263            quote! {
1264                #fk_col => ::core::option::Option::Some(
1265                    ::core::convert::Into::<#root::core::SqlValue>::into(
1266                        self.#field_ident.pk()
1267                    )
1268                ),
1269            }
1270        }
1271    });
1272    quote! {
1273        impl #root::sql::FkPkAccess for #struct_name {
1274            #[allow(unused_variables)]
1275            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1276                match field_name {
1277                    #( #arms )*
1278                    _ => ::core::option::Option::None,
1279                }
1280            }
1281            #[allow(unused_variables)]
1282            fn __rustango_fk_pk_value(
1283                &self,
1284                field_name: &str,
1285            ) -> ::core::option::Option<#root::core::SqlValue> {
1286                match field_name {
1287                    #( #value_arms )*
1288                    _ => ::core::option::Option::None,
1289                }
1290            }
1291        }
1292    }
1293}
1294
1295/// For every `ForeignKey<Parent>` field on `Child`, emit
1296/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1297/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1298/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1299/// — the canonical reverse-FK fetch. One round trip, no N+1.
1300///
1301/// **PG-only emission**: the accessor is bounded on
1302/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1303/// both of which are gated behind the `postgres` cargo feature. The
1304/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1305/// model derive itself compiles on tri-dialect / sqlite-only
1306/// downstream builds — the accessor just isn't materialised. A tri-
1307/// dialect `_set_pool` variant is a separate follow-up.
1308fn reverse_helper_tokens(
1309    child_ident: &syn::Ident,
1310    fk_relations: &[FkRelation],
1311    default_related_name: Option<&str>,
1312) -> TokenStream2 {
1313    let root = rustango_root();
1314    if fk_relations.is_empty() {
1315        return TokenStream2::new();
1316    }
1317    // Method-name resolution per FK (issue #816 + follow-up):
1318    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1319    //      itself — wins over everything else. Django's
1320    //      `ForeignKey(related_name="...")`.
1321    //   2. Container-level `default_related_name = "..."` on the
1322    //      child — Django's `class Meta: default_related_name`.
1323    //      Applies to every FK on this model that didn't override.
1324    //   3. Fallback: `<child_snake>_set` — Django's `<child>_set`
1325    //      convention. `Post` → `post_set`, `BlogComment` →
1326    //      `blog_comment_set`. Avoids English-plural edge cases.
1327    //
1328    // The PG-on-executor variant keeps the resolved name; the
1329    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1330    // framework's convention for the `&Pool` flavor of every helper).
1331    let default_pg_suffix = default_related_name
1332        .map(str::to_owned)
1333        .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1334    let impls = fk_relations.iter().map(|rel| {
1335        let pg_suffix = rel
1336            .related_name
1337            .clone()
1338            .unwrap_or_else(|| default_pg_suffix.clone());
1339        let pool_suffix = format!("{}_pool", pg_suffix);
1340        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1341        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1342        let parent_ty = &rel.parent_type;
1343        let fk_col = rel.fk_column.as_str();
1344        let doc = format!(
1345            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1346             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1347             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1348             further `{child_ident}::objects()` filters via direct queryset use."
1349        );
1350        let pool_doc = format!(
1351            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1352             [`#root::sql::Pool`] and dispatches per backend so the \
1353             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1354             Use this from framework code that holds a `&Pool` (admin, \
1355             tenancy resolver, viewset handlers); reach for the executor- \
1356             bound variant when you already have a typed `sqlx::Executor`."
1357        );
1358        quote! {
1359            #[cfg(feature = "postgres")]
1360            impl #parent_ty {
1361                #[doc = #doc]
1362                ///
1363                /// # Errors
1364                /// Returns [`#root::sql::ExecError`] for SQL-writing
1365                /// or driver failures.
1366                pub async fn #pg_method_ident<'_c, _E>(
1367                    &self,
1368                    _executor: _E,
1369                ) -> ::core::result::Result<
1370                    ::std::vec::Vec<#child_ident>,
1371                    #root::sql::ExecError,
1372                >
1373                where
1374                    _E: #root::sql::sqlx::Executor<
1375                        '_c,
1376                        Database = #root::sql::sqlx::Postgres,
1377                    >,
1378                {
1379                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1380                    #root::query::QuerySet::<#child_ident>::new()
1381                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1382                        .fetch_on(_executor)
1383                        .await
1384                }
1385            }
1386
1387            impl #parent_ty {
1388                #[doc = #pool_doc]
1389                ///
1390                /// # Errors
1391                /// Returns [`#root::sql::ExecError`] for SQL-writing
1392                /// or driver failures.
1393                pub async fn #pool_method_ident(
1394                    &self,
1395                    pool: &#root::sql::Pool,
1396                ) -> ::core::result::Result<
1397                    ::std::vec::Vec<#child_ident>,
1398                    #root::sql::ExecError,
1399                > {
1400                    use #root::sql::FetcherPool as _;
1401                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1402                    #root::query::QuerySet::<#child_ident>::new()
1403                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1404                        .fetch(pool)
1405                        .await
1406                }
1407            }
1408        }
1409    });
1410    quote! { #( #impls )* }
1411}
1412
1413/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1414/// relation declared on the model.
1415/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1416/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1417///
1418/// Closes #239 + #240 — the Django-shape `comment.content_object` /
1419/// `comment.content_object = post` ergonomics on top of the existing
1420/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1421///
1422/// `column_entries` is passed so we can resolve each `ct_column` /
1423/// `pk_column` SQL name back to its Rust field ident — the macro
1424/// only sees the column-side strings in the attribute, but the
1425/// emitted accessor needs to read the actual struct field.
1426fn generic_fk_accessor_tokens(
1427    struct_name: &syn::Ident,
1428    generic_fks: &[GenericFkAttr],
1429    column_entries: &[ColumnEntry],
1430) -> TokenStream2 {
1431    let root = rustango_root();
1432    if generic_fks.is_empty() {
1433        return TokenStream2::new();
1434    }
1435    let methods = generic_fks.iter().filter_map(|gfk| {
1436        // Resolve `ct_column` + `pk_column` to the struct's Rust
1437        // field idents. A typo (column name doesn't match any field)
1438        // emits no method for that registration — the user will see
1439        // the compiler reject the SCHEMA literal anyway, so there's
1440        // a clear error path without us double-reporting.
1441        let ct_ident = column_entries
1442            .iter()
1443            .find(|c| c.column == gfk.ct_column)
1444            .map(|c| c.ident.clone())?;
1445        let pk_ident = column_entries
1446            .iter()
1447            .find(|c| c.column == gfk.pk_column)
1448            .map(|c| c.ident.clone())?;
1449
1450        let accessor_ident =
1451            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1452        let setter_ident =
1453            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1454        let name_literal = gfk.name.as_str();
1455
1456        Some(quote! {
1457            #[doc = concat!(
1458                "Resolve the polymorphic `",
1459                #name_literal,
1460                "` relation. Reads `self.",
1461                stringify!(#ct_ident),
1462                "` + `self.",
1463                stringify!(#pk_ident),
1464                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1465                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1466                #name_literal,
1467                "\", ...))]`."
1468            )]
1469            pub async fn #accessor_ident(
1470                &self,
1471                pool: &#root::sql::Pool,
1472            ) -> ::core::result::Result<
1473                ::core::option::Option<#root::__serde_json::Value>,
1474                #root::sql::ExecError,
1475            > {
1476                let gfk = #root::contenttypes::GenericForeignKey::new(
1477                    self.#ct_ident as i64,
1478                    self.#pk_ident as i64,
1479                );
1480                gfk.get_object(pool).await
1481            }
1482
1483            #[doc = concat!(
1484                "Set the polymorphic `",
1485                #name_literal,
1486                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1487                stringify!(#ct_ident),
1488                "` and `self.",
1489                stringify!(#pk_ident),
1490                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1491                #name_literal,
1492                "\", ...))]`."
1493            )]
1494            pub async fn #setter_ident<T: #root::core::Model>(
1495                &mut self,
1496                pool: &#root::sql::Pool,
1497                target_pk: i64,
1498            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1499                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1500                    pool,
1501                    target_pk,
1502                ).await?;
1503                self.#ct_ident = gfk.content_type_id as _;
1504                self.#pk_ident = gfk.object_pk as _;
1505                ::core::result::Result::Ok(())
1506            }
1507        })
1508    });
1509    quote! {
1510        impl #struct_name {
1511            #( #methods )*
1512        }
1513    }
1514}
1515
1516fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1517    let root = rustango_root();
1518    if m2m_relations.is_empty() {
1519        return TokenStream2::new();
1520    }
1521    let methods = m2m_relations.iter().map(|rel| {
1522        let method_name = format!("{}_m2m", rel.name);
1523        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1524        let through = rel.through.as_str();
1525        let src_col = rel.src.as_str();
1526        let dst_col = rel.dst.as_str();
1527        quote! {
1528            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1529                #root::sql::M2MManager {
1530                    src_pk: self.__rustango_pk_value(),
1531                    through: #through,
1532                    src_col: #src_col,
1533                    dst_col: #dst_col,
1534                }
1535            }
1536        }
1537    });
1538    quote! {
1539        impl #struct_name {
1540            #( #methods )*
1541        }
1542    }
1543}
1544
1545/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1546/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1547fn generic_m2m_accessor_tokens(
1548    struct_name: &syn::Ident,
1549    relations: &[GenericM2MAttr],
1550) -> TokenStream2 {
1551    let root = rustango_root();
1552    if relations.is_empty() {
1553        return TokenStream2::new();
1554    }
1555    let methods = relations.iter().map(|rel| {
1556        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1557        let through = rel.through.as_str();
1558        let pk_col = rel.pk_column.as_str();
1559        let ct_col = rel.ct_column.as_str();
1560        let related_col = rel.related_column.as_str();
1561        quote! {
1562            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1563                #root::sql::GenericM2MManager {
1564                    src_pk: self.__rustango_pk_value(),
1565                    src_schema: <Self as #root::core::Model>::SCHEMA,
1566                    through: #through,
1567                    pk_col: #pk_col,
1568                    ct_col: #ct_col,
1569                    dst_col: #related_col,
1570                }
1571            }
1572        }
1573    });
1574    quote! {
1575        impl #struct_name {
1576            #( #methods )*
1577        }
1578    }
1579}
1580
1581/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1582/// associated functions for each `#[rustango(reverse_has(...))]`
1583/// attribute. Issue #830.
1584///
1585/// The two emitted functions return ready-to-use `WhereExpr` nodes
1586/// that downstream callers drop into
1587/// `QuerySet::<Self>::where_raw(...)`:
1588///
1589/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1590///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1591///   `whereHas` parity (without the closure-style sub-predicate
1592///   refinement — that's a follow-up; users can layer additional
1593///   predicates by constructing the SelectQuery themselves and
1594///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1595/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1596///   `whereDoesntHave` parity.
1597///
1598/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1599/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1600/// machinery threads the outer-table reference through automatically
1601/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1602fn reverse_has_accessor_tokens(
1603    struct_name: &syn::Ident,
1604    reverse_has_relations: &[ReverseHasAttr],
1605) -> TokenStream2 {
1606    let root = rustango_root();
1607    if reverse_has_relations.is_empty() {
1608        return TokenStream2::new();
1609    }
1610    let methods = reverse_has_relations.iter().map(|rel| {
1611        let exists_name = format!("{}_exists_expr", rel.name);
1612        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1613        let count_name = format!("{}_count", rel.name);
1614        let fetch_name = format!("{}_fetch", rel.name);
1615        let first_name = format!("{}_first", rel.name);
1616        let pluck_name = format!("{}_pluck", rel.name);
1617        let accessor_name = rel.name.as_str();
1618        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1619        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1620        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1621        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1622        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1623        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1624        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1625        let child = &rel.child;
1626        let child_fk_column = rel.child_fk_column.as_str();
1627        let self_pk_column = rel.self_pk_column.as_str();
1628        let exists_doc = format!(
1629            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1630             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1631             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1632             filter to {struct_name}s with at least one matching child.",
1633        );
1634        let not_exists_doc = format!(
1635            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1636             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1637             <outer>.{self_pk_column})`. Drop into \
1638             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1639             {struct_name}s with **no** matching child.",
1640        );
1641        let count_doc = format!(
1642            "Eloquent `$model->{name}->count()` analog — returns \
1643             the number of `{child}` rows whose `{child_fk_column}` \
1644             matches this `{struct_name}` instance's primary key. \
1645             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1646             {child_fk_column} = <self.pk>`.",
1647            name = rel.name,
1648        );
1649        let accessor_doc = format!(
1650            "Eloquent `$model->{name}` accessor — returns a \
1651             `QuerySet<{child}>` filtered to rows whose \
1652             `{child_fk_column}` matches this `{struct_name}` \
1653             instance's primary key. **Chainable**: compose `.filter()` \
1654             / `.order_by()` / `.limit()` etc. on top, then call \
1655             `.fetch(&pool)` (the QuerySet trait method) when \
1656             done. For the simple \"fetch all\" hot path with no \
1657             further composition, prefer the bare-name \
1658             `{name}_fetch(&pool)` companion.",
1659            name = rel.name,
1660        );
1661        let fetch_doc = format!(
1662            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1663             over `{name}(&self).fetch(&pool)`. Use this when \
1664             you don't need further `.filter()` / `.order_by()` \
1665             composition; falls back to the chainable accessor when \
1666             you do. Avoids the `_pool` suffix on the most common \
1667             call-site shape.",
1668            name = rel.name,
1669        );
1670        quote! {
1671            #[doc = #accessor_doc]
1672            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1673                #root::query::QuerySet::<#child>::new()
1674                    .filter(#child_fk_column, self.__rustango_pk_value())
1675            }
1676
1677            #[doc = #fetch_doc]
1678            pub async fn #fetch_ident(
1679                &self,
1680                pool: &#root::sql::Pool,
1681            ) -> ::core::result::Result<
1682                ::std::vec::Vec<#child>,
1683                #root::sql::ExecError,
1684            > {
1685                use #root::sql::FetcherPool as _;
1686                self.#accessor_ident().fetch(pool).await
1687            }
1688
1689            /// Eloquent `$model->relation->first()` / `hasOne`
1690            /// semantics — bare-name shortcut over
1691            /// `self.<name>().first(&pool)`. Returns `None` when no
1692            /// child rows match. Useful when the relation is
1693            /// nominally many-to-one in shape but at most one row is
1694            /// expected (latest comment, primary tag, etc.).
1695            pub async fn #first_ident(
1696                &self,
1697                pool: &#root::sql::Pool,
1698            ) -> ::core::result::Result<
1699                ::core::option::Option<#child>,
1700                #root::sql::ExecError,
1701            > {
1702                self.#accessor_ident().first(pool).await
1703            }
1704
1705            /// Eloquent `$model->relation->pluck($col)` — project a
1706            /// single column from the child rows into a `Vec<U>`.
1707            /// Skips the typed `Child` decode, which is cheaper when
1708            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1709            pub async fn #pluck_ident<U>(
1710                &self,
1711                col: &'static str,
1712                pool: &#root::sql::Pool,
1713            ) -> ::core::result::Result<
1714                ::std::vec::Vec<U>,
1715                #root::sql::ExecError,
1716            >
1717            where
1718                U: #root::sql::MaybePgScalar
1719                    + #root::sql::MaybeMyScalar
1720                    + #root::sql::MaybeSqliteScalar
1721                    + ::core::marker::Send
1722                    + ::core::marker::Unpin,
1723            {
1724                self.#accessor_ident()
1725                    .values_list_flat(col)
1726                    .fetch::<U>(pool)
1727                    .await
1728            }
1729
1730            #[doc = #exists_doc]
1731            pub fn #exists_ident() -> #root::core::WhereExpr {
1732                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1733                let child_schema =
1734                    <#child as #root::core::Model>::SCHEMA;
1735                let inner = SelectQuery {
1736                    where_clause: WhereExpr::ExprCompare {
1737                        lhs: Expr::Column(#child_fk_column),
1738                        op: Op::Eq,
1739                        rhs: Expr::OuterRef(#self_pk_column),
1740                    },
1741                    ..SelectQuery::new(child_schema)
1742                };
1743                WhereExpr::Exists(::std::boxed::Box::new(inner))
1744            }
1745
1746            #[doc = #not_exists_doc]
1747            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1748                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1749                let child_schema =
1750                    <#child as #root::core::Model>::SCHEMA;
1751                let inner = SelectQuery {
1752                    where_clause: WhereExpr::ExprCompare {
1753                        lhs: Expr::Column(#child_fk_column),
1754                        op: Op::Eq,
1755                        rhs: Expr::OuterRef(#self_pk_column),
1756                    },
1757                    ..SelectQuery::new(child_schema)
1758                };
1759                WhereExpr::NotExists(::std::boxed::Box::new(inner))
1760            }
1761
1762            #[doc = #count_doc]
1763            pub async fn #count_ident(
1764                &self,
1765                pool: &#root::sql::Pool,
1766            ) -> ::core::result::Result<
1767                i64,
1768                #root::sql::ExecError,
1769            > {
1770                use #root::sql::CounterPool as _;
1771                #root::query::QuerySet::<#child>::new()
1772                    .filter(#child_fk_column, self.__rustango_pk_value())
1773                    .count(pool)
1774                    .await
1775            }
1776        }
1777    });
1778    quote! {
1779        impl #struct_name {
1780            #( #methods )*
1781        }
1782    }
1783}
1784
1785/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1786/// `#[rustango(through(...))]` attribute. Issue #817.
1787///
1788/// Each accessor builds a correlated subquery via
1789/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1790/// intermediate table, filters on the FK column pointing at this
1791/// model, and projects the intermediate PK. The outer queryset
1792/// filters the far model by its FK-to-intermediate column being in
1793/// that set.
1794///
1795/// The returned `QuerySet<Far>` is **chainable** — the subquery
1796/// lives inside a `where_raw` clause so the user's later
1797/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1798/// disturb it.
1799///
1800/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1801/// SQLite — no LATERAL or backend-specific syntax involved.
1802fn through_accessor_tokens(
1803    struct_name: &syn::Ident,
1804    through_relations: &[ThroughAttr],
1805) -> TokenStream2 {
1806    let root = rustango_root();
1807    if through_relations.is_empty() {
1808        return TokenStream2::new();
1809    }
1810    let methods = through_relations.iter().map(|rel| {
1811        let method_name = format!("{}_through", rel.name);
1812        let count_name = format!("{}_through_count", rel.name);
1813        let fetch_name = format!("{}_through_fetch", rel.name);
1814        let first_name = format!("{}_through_first", rel.name);
1815        let pluck_name = format!("{}_through_pluck", rel.name);
1816        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1817        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1818        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1819        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1820        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1821        let far = &rel.far;
1822        let intermediate = &rel.intermediate;
1823        let far_fk_column = rel.far_fk_column.as_str();
1824        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1825        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1826        let doc = format!(
1827            "Eloquent `hasManyThrough` accessor — returns a \
1828             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1829             instance through the intermediate `{intermediate}` table. \
1830             Generated SQL shape: \
1831             `… WHERE {far_fk_column} IN (SELECT \
1832             {intermediate_pk_column} FROM <{intermediate}> WHERE \
1833             {intermediate_fk_column} = self.pk)`. Chainable like any \
1834             other QuerySet — compose `.filter()` / `.order_by()` / \
1835             `.limit()` etc. on top.",
1836        );
1837        let count_doc = format!(
1838            "Eloquent `$model->{name}->count()` analog for the \
1839             through-relation — returns the number of `{far}` rows \
1840             reachable through `{intermediate}`. Equivalent to \
1841             `self.{name}_through().count(pool)` but spelled \
1842             as a bare instance method for parity with the \
1843             `reverse_has` `<name>_count` shape.",
1844            name = rel.name,
1845        );
1846        quote! {
1847            #[doc = #doc]
1848            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1849                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1850                let intermediate_schema =
1851                    <#intermediate as #root::core::Model>::SCHEMA;
1852                let sub = SelectQuery {
1853                    where_clause: WhereExpr::Predicate(Filter {
1854                        column: #intermediate_fk_column,
1855                        op: Op::Eq,
1856                        value: self.__rustango_pk_value(),
1857                    }),
1858                    projection: ::core::option::Option::Some(
1859                        ::std::vec![#intermediate_pk_column],
1860                    ),
1861                    ..SelectQuery::new(intermediate_schema)
1862                };
1863                #root::query::QuerySet::<#far>::new().where_raw(
1864                    WhereExpr::InSubquery {
1865                        column: #far_fk_column,
1866                        negated: false,
1867                        subquery: ::std::boxed::Box::new(sub),
1868                    },
1869                )
1870            }
1871
1872            #[doc = #count_doc]
1873            pub async fn #count_ident(
1874                &self,
1875                pool: &#root::sql::Pool,
1876            ) -> ::core::result::Result<
1877                i64,
1878                #root::sql::ExecError,
1879            > {
1880                use #root::sql::CounterPool as _;
1881                self.#method_ident().count(pool).await
1882            }
1883
1884            /// Eloquent `$model->relation->get()` for the
1885            /// through-relation — bare-name hot-path over
1886            /// `self.<name>_through().fetch(&pool)`. Use this
1887            /// when you don't need further `.filter()` /
1888            /// `.order_by()` composition; falls back to the
1889            /// chainable accessor when you do. Avoids the `_pool`
1890            /// suffix on the most common call-site shape.
1891            pub async fn #fetch_ident(
1892                &self,
1893                pool: &#root::sql::Pool,
1894            ) -> ::core::result::Result<
1895                ::std::vec::Vec<#far>,
1896                #root::sql::ExecError,
1897            > {
1898                use #root::sql::FetcherPool as _;
1899                self.#method_ident().fetch(pool).await
1900            }
1901
1902            /// Eloquent `hasOneThrough` analog — bare-name shortcut
1903            /// over `self.<name>_through().first(&pool)`. Returns
1904            /// `None` when no far rows are reachable through the
1905            /// intermediate. Useful when at most one row is
1906            /// expected (latest comment by country, primary tag,
1907            /// etc.).
1908            pub async fn #first_ident(
1909                &self,
1910                pool: &#root::sql::Pool,
1911            ) -> ::core::result::Result<
1912                ::core::option::Option<#far>,
1913                #root::sql::ExecError,
1914            > {
1915                self.#method_ident().first(pool).await
1916            }
1917
1918            /// Pluck a single column from the far rows into `Vec<U>`
1919            /// — cheaper than the typed `<Far>` decode when only one
1920            /// scalar column is needed.
1921            pub async fn #pluck_ident<U>(
1922                &self,
1923                col: &'static str,
1924                pool: &#root::sql::Pool,
1925            ) -> ::core::result::Result<
1926                ::std::vec::Vec<U>,
1927                #root::sql::ExecError,
1928            >
1929            where
1930                U: #root::sql::MaybePgScalar
1931                    + #root::sql::MaybeMyScalar
1932                    + #root::sql::MaybeSqliteScalar
1933                    + ::core::marker::Send
1934                    + ::core::marker::Unpin,
1935            {
1936                self.#method_ident()
1937                    .values_list_flat(col)
1938                    .fetch::<U>(pool)
1939                    .await
1940            }
1941        }
1942    });
1943    quote! {
1944        impl #struct_name {
1945            #( #methods )*
1946        }
1947    }
1948}
1949
1950struct ColumnEntry {
1951    /// The struct field ident, used both for the inherent const name on
1952    /// the model and for the inner column type's name.
1953    ident: syn::Ident,
1954    /// The struct's field type, used as `Column::Value`.
1955    value_ty: Type,
1956    /// Rust-side field name (e.g. `"id"`).
1957    name: String,
1958    /// SQL-side column name (e.g. `"user_id"`).
1959    column: String,
1960    /// `#root::core::FieldType::I64` etc.
1961    field_type_tokens: TokenStream2,
1962}
1963
1964struct CollectedFields {
1965    field_schemas: Vec<TokenStream2>,
1966    from_row_inits: Vec<TokenStream2>,
1967    /// Aliased counterparts of `from_row_inits` — read columns via
1968    /// `format!("{prefix}__{col}")` aliases so a Model can be
1969    /// decoded from a JOINed row's projected target columns.
1970    from_aliased_row_inits: Vec<TokenStream2>,
1971    /// Static column-name list — used by the simple insert path
1972    /// (no `Auto<T>` fields). Aligned with `insert_values`.
1973    insert_columns: Vec<TokenStream2>,
1974    /// Static `Into<SqlValue>` expressions, one per field. Aligned
1975    /// with `insert_columns`. Used by the simple insert path only.
1976    insert_values: Vec<TokenStream2>,
1977    /// Per-field push expressions for the dynamic (Auto-aware)
1978    /// insert path. Each statement either unconditionally pushes
1979    /// `(column, value)` or, for an `Auto<T>` field, conditionally
1980    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1981    insert_pushes: Vec<TokenStream2>,
1982    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1983    /// when `has_auto == false`.
1984    returning_cols: Vec<TokenStream2>,
1985    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1986    /// field. Run after `insert_returning` to populate the model.
1987    auto_assigns: Vec<TokenStream2>,
1988    /// `(ident, column_literal)` pairs for every Auto field. Used by
1989    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1990    /// instead of `self`.
1991    auto_field_idents: Vec<(syn::Ident, String)>,
1992    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
1993    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
1994    /// back into the struct after insert (MySQL has no RETURNING → the
1995    /// field stays at its placeholder, deferred, matching Django 6.0).
1996    generated_field_idents: Vec<(syn::Ident, String)>,
1997    /// Inner `T` of the first `Auto<T>` field, for the MySQL
1998    /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1999    first_auto_value_ty: Option<Type>,
2000    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2001    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2002    /// `columns`).
2003    bulk_pushes_no_auto: Vec<TokenStream2>,
2004    /// Bulk-insert per-row pushes for **all fields including Auto**.
2005    /// Used by the all-Auto-Set bulk path (Auto col included with the
2006    /// caller-supplied value).
2007    bulk_pushes_all: Vec<TokenStream2>,
2008    /// Column-name literals for non-Auto fields only (paired with
2009    /// `bulk_pushes_no_auto`).
2010    bulk_columns_no_auto: Vec<TokenStream2>,
2011    /// Column-name literals for every field including Auto (paired
2012    /// with `bulk_pushes_all`).
2013    bulk_columns_all: Vec<TokenStream2>,
2014    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2015    /// + the loop that asserts every row matches. One pair per Auto
2016    /// field. Empty when `has_auto == false`.
2017    bulk_auto_uniformity: Vec<TokenStream2>,
2018    /// Identifier of the first Auto field, used as the witness for
2019    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2020    first_auto_ident: Option<syn::Ident>,
2021    /// `true` if any field on the struct is `Auto<T>`.
2022    has_auto: bool,
2023    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2024    /// Gates `save()` codegen — only Auto PKs let us infer
2025    /// insert-vs-update from the in-memory value.
2026    pk_is_auto: bool,
2027    /// `Assignment` constructors for every non-PK column. Drives the
2028    /// UPDATE branch of `save()`.
2029    update_assignments: Vec<TokenStream2>,
2030    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2031    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2032    upsert_update_columns: Vec<TokenStream2>,
2033    primary_key: Option<(syn::Ident, String)>,
2034    column_entries: Vec<ColumnEntry>,
2035    /// Rust-side field names, in declaration order. Used to validate
2036    /// container attributes like `display = "…"`.
2037    field_names: Vec<String>,
2038    /// FK fields on this child model. Drives the reverse-relation
2039    /// helper emit — for each FK, the macro adds an inherent
2040    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2041    /// method on the parent type.
2042    fk_relations: Vec<FkRelation>,
2043    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2044    /// the model has one. Drives emission of the `soft_delete_on` /
2045    /// `restore_on` inherent methods. At most one such column per
2046    /// model is allowed; collect_fields rejects duplicates.
2047    soft_delete_column: Option<String>,
2048    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2049    /// companion to `soft_delete_column` for emitting predicates
2050    /// that need to read the field off `&self` (e.g. `trashed()`).
2051    soft_delete_field_ident: Option<syn::Ident>,
2052}
2053
2054#[derive(Clone)]
2055struct FkRelation {
2056    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2057    /// helper is emitted as `impl <ParentType> { … }`.
2058    parent_type: Type,
2059    /// SQL column name on the child table for this FK (e.g. `"author"`).
2060    /// Used in the generated `WHERE <fk_column> = $1` clause.
2061    fk_column: String,
2062    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2063    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2064    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2065    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2066    pk_kind: DetectedKind,
2067    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2068    /// FK column). Drives the `Some(...)` wrapping in load_related
2069    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2070    /// the codegen matches the field's declared shape.
2071    nullable: bool,
2072    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2073    /// override. When set, the reverse helper picks this name instead
2074    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2075    /// #816 (issue's "Related" note re: per-FK override).
2076    related_name: Option<String>,
2077}
2078
2079fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2080    let root = rustango_root();
2081    let cap = named.named.len();
2082    let mut out = CollectedFields {
2083        field_schemas: Vec::with_capacity(cap),
2084        from_row_inits: Vec::with_capacity(cap),
2085        from_aliased_row_inits: Vec::with_capacity(cap),
2086        insert_columns: Vec::with_capacity(cap),
2087        insert_values: Vec::with_capacity(cap),
2088        insert_pushes: Vec::with_capacity(cap),
2089        returning_cols: Vec::new(),
2090        auto_assigns: Vec::new(),
2091        auto_field_idents: Vec::new(),
2092        generated_field_idents: Vec::new(),
2093        first_auto_value_ty: None,
2094        bulk_pushes_no_auto: Vec::with_capacity(cap),
2095        bulk_pushes_all: Vec::with_capacity(cap),
2096        bulk_columns_no_auto: Vec::with_capacity(cap),
2097        bulk_columns_all: Vec::with_capacity(cap),
2098        bulk_auto_uniformity: Vec::new(),
2099        first_auto_ident: None,
2100        has_auto: false,
2101        pk_is_auto: false,
2102        update_assignments: Vec::with_capacity(cap),
2103        upsert_update_columns: Vec::with_capacity(cap),
2104        primary_key: None,
2105        column_entries: Vec::with_capacity(cap),
2106        field_names: Vec::with_capacity(cap),
2107        fk_relations: Vec::new(),
2108        soft_delete_column: None,
2109        soft_delete_field_ident: None,
2110    };
2111
2112    for field in &named.named {
2113        let info = process_field(field, table)?;
2114        out.field_names.push(info.ident.to_string());
2115        out.field_schemas.push(info.schema);
2116        out.from_row_inits.push(info.from_row_init);
2117        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2118        if let Some(parent_ty) = info.fk_inner.clone() {
2119            out.fk_relations.push(FkRelation {
2120                parent_type: parent_ty,
2121                fk_column: info.column.clone(),
2122                pk_kind: info.fk_pk_kind,
2123                nullable: info.nullable,
2124                related_name: info.related_name.clone(),
2125            });
2126        }
2127        if info.soft_delete {
2128            if out.soft_delete_column.is_some() {
2129                return Err(syn::Error::new_spanned(
2130                    field,
2131                    "only one field may be marked `#[rustango(soft_delete)]`",
2132                ));
2133            }
2134            out.soft_delete_column = Some(info.column.clone());
2135            out.soft_delete_field_ident = Some(info.ident.clone());
2136        }
2137        let column = info.column.as_str();
2138        let ident = info.ident;
2139        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2140        // skip every write path — Postgres recomputes the value
2141        // from EXPR. Push only the column-entry record (so typed
2142        // column constants still exist for filtering / projection)
2143        // and the schema literal (already pushed above) and move
2144        // on. No insert_columns/values, no insert_pushes, no
2145        // bulk_*, no update_assignments, no upsert_update_columns,
2146        // no returning_cols.
2147        if info.generated_as.is_some() {
2148            out.column_entries.push(ColumnEntry {
2149                ident: ident.clone(),
2150                value_ty: info.value_ty.clone(),
2151                name: ident.to_string(),
2152                column: info.column.clone(),
2153                field_type_tokens: info.field_type_tokens,
2154            });
2155            // #1028 — refresh the DB-computed value after insert on
2156            // RETURNING-capable backends. The column joins `returning_cols`
2157            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2158            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2159            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2160            // it keeps the placeholder (deferred refresh, matching Django).
2161            out.returning_cols.push(quote!(#column));
2162            out.generated_field_idents
2163                .push((ident.clone(), info.column.clone()));
2164            continue;
2165        }
2166        out.insert_columns.push(quote!(#column));
2167        out.insert_values.push(quote! {
2168            ::core::convert::Into::<#root::core::SqlValue>::into(
2169                ::core::clone::Clone::clone(&self.#ident)
2170            )
2171        });
2172        if info.auto {
2173            out.has_auto = true;
2174            if out.first_auto_ident.is_none() {
2175                out.first_auto_ident = Some(ident.clone());
2176                out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2177            }
2178            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2179            // before binding, so the value is already in
2180            // `self.#ident` after the insert_push — RETURNING is
2181            // redundant. Skip adding this column to returning_cols /
2182            // auto_assigns / auto_field_idents to avoid (a) an
2183            // unnecessary RETURNING column on every dialect, and (b)
2184            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2185            // integer PK.
2186            if !info.default_uuid_v7 {
2187                out.returning_cols.push(quote!(#column));
2188                out.auto_field_idents
2189                    .push((ident.clone(), info.column.clone()));
2190                out.auto_assigns.push(quote! {
2191                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2192                });
2193            }
2194            if info.default_uuid_v7 {
2195                // Rust-side UUIDv7 generation (issue #823, Eloquent
2196                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2197                // and bind; Auto::Set → bind the user's value. The
2198                // column is ALWAYS present in the INSERT statement —
2199                // no RETURNING / no DB DEFAULT needed.
2200                out.insert_pushes.push(quote! {
2201                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2202                        self.#ident = #root::sql::Auto::Set(
2203                            #root::__uuid::Uuid::now_v7(),
2204                        );
2205                    }
2206                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2207                        _columns.push(#column);
2208                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2209                            ::core::clone::Clone::clone(_v)
2210                        ));
2211                    }
2212                });
2213            } else {
2214                out.insert_pushes.push(quote! {
2215                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2216                        _columns.push(#column);
2217                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2218                            ::core::clone::Clone::clone(_v)
2219                        ));
2220                    }
2221                });
2222            }
2223            // Bulk: Auto fields appear only in the all-Set path,
2224            // never in the Unset path (we drop them from `columns`).
2225            out.bulk_columns_all.push(quote!(#column));
2226            out.bulk_pushes_all.push(quote! {
2227                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2228                    ::core::clone::Clone::clone(&_row.#ident)
2229                ));
2230            });
2231            // Uniformity check: every row's Auto state must match the
2232            // first row's. Mixed Set/Unset within one bulk_insert is
2233            // rejected here so the column list stays consistent.
2234            let ident_clone = ident.clone();
2235            out.bulk_auto_uniformity.push(quote! {
2236                for _r in rows.iter().skip(1) {
2237                    if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2238                        return ::core::result::Result::Err(
2239                            #root::sql::ExecError::Sql(
2240                                #root::sql::SqlError::BulkAutoMixed
2241                            )
2242                        );
2243                    }
2244                }
2245            });
2246        } else {
2247            out.insert_pushes.push(quote! {
2248                _columns.push(#column);
2249                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2250                    ::core::clone::Clone::clone(&self.#ident)
2251                ));
2252            });
2253            // Bulk: non-Auto fields appear in BOTH paths.
2254            out.bulk_columns_no_auto.push(quote!(#column));
2255            out.bulk_columns_all.push(quote!(#column));
2256            let push_expr = quote! {
2257                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2258                    ::core::clone::Clone::clone(&_row.#ident)
2259                ));
2260            };
2261            out.bulk_pushes_no_auto.push(push_expr.clone());
2262            out.bulk_pushes_all.push(push_expr);
2263        }
2264        if info.primary_key {
2265            if out.primary_key.is_some() {
2266                return Err(syn::Error::new_spanned(
2267                    field,
2268                    "only one field may be marked `#[rustango(primary_key)]`",
2269                ));
2270            }
2271            out.primary_key = Some((ident.clone(), info.column.clone()));
2272            if info.auto {
2273                out.pk_is_auto = true;
2274            }
2275        } else if info.auto_now_add {
2276            // Immutable post-insert: skip from UPDATE entirely.
2277        } else if info.auto_now {
2278            // `auto_now` columns: bind `chrono::Utc::now()` on every
2279            // UPDATE so the column is always overridden with the
2280            // wall-clock at write time, regardless of what value the
2281            // user left in the struct field.
2282            out.update_assignments.push(quote! {
2283                #root::core::Assignment {
2284                    column: #column,
2285                    value: ::core::convert::Into::<#root::core::Expr>::into(
2286                        ::core::convert::Into::<#root::core::SqlValue>::into(
2287                            #root::__chrono::Utc::now()
2288                        )
2289                    ),
2290                }
2291            });
2292            out.upsert_update_columns.push(quote!(#column));
2293        } else {
2294            out.update_assignments.push(quote! {
2295                #root::core::Assignment {
2296                    column: #column,
2297                    value: ::core::convert::Into::<#root::core::Expr>::into(
2298                        ::core::convert::Into::<#root::core::SqlValue>::into(
2299                            ::core::clone::Clone::clone(&self.#ident)
2300                        )
2301                    ),
2302                }
2303            });
2304            out.upsert_update_columns.push(quote!(#column));
2305        }
2306        out.column_entries.push(ColumnEntry {
2307            ident: ident.clone(),
2308            value_ty: info.value_ty.clone(),
2309            name: ident.to_string(),
2310            column: info.column.clone(),
2311            field_type_tokens: info.field_type_tokens,
2312        });
2313    }
2314    Ok(out)
2315}
2316
2317fn model_impl_tokens(
2318    struct_name: &syn::Ident,
2319    model_name: &str,
2320    table: &str,
2321    display: Option<&str>,
2322    app_label: Option<&str>,
2323    admin: Option<&AdminAttrs>,
2324    default_order: &[(String, bool, proc_macro2::Span)],
2325    field_schemas: &[TokenStream2],
2326    soft_delete_column: Option<&str>,
2327    permissions: bool,
2328    audit_track: Option<&[String]>,
2329    m2m_relations: &[M2MAttr],
2330    indexes: &[IndexAttr],
2331    checks: &[CheckAttr],
2332    excludes: &[ExcludeAttr],
2333    composite_fks: &[CompositeFkAttr],
2334    generic_fks: &[GenericFkAttr],
2335    scope: Option<&str>,
2336    is_view: bool,
2337    verbose_name: Option<&str>,
2338    verbose_name_plural: Option<&str>,
2339    managed: bool,
2340    base_manager_name: Option<&str>,
2341    order_with_respect_to: Option<&str>,
2342    proxy: bool,
2343    required_db_features: &[String],
2344    required_db_vendor: Option<&str>,
2345    default_related_name: Option<&str>,
2346    db_table_comment: Option<&str>,
2347    get_latest_by: Option<(&str, bool)>,
2348    extra_permissions: &[(String, String)],
2349    default_permissions: &[String],
2350    global_scopes: &[GlobalScopeAttr],
2351    reverse_has_relations: &[ReverseHasAttr],
2352    generic_has_relations: &[GenericHasAttr],
2353) -> TokenStream2 {
2354    let root = rustango_root();
2355    let display_tokens = if let Some(name) = display {
2356        quote!(::core::option::Option::Some(#name))
2357    } else {
2358        quote!(::core::option::Option::None)
2359    };
2360    let app_label_tokens = if let Some(name) = app_label {
2361        quote!(::core::option::Option::Some(#name))
2362    } else {
2363        quote!(::core::option::Option::None)
2364    };
2365    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2366        quote!(::core::option::Option::Some(#col))
2367    } else {
2368        quote!(::core::option::Option::None)
2369    };
2370    let audit_track_tokens = match audit_track {
2371        None => quote!(::core::option::Option::None),
2372        Some(names) => {
2373            let lits = names.iter().map(|n| n.as_str());
2374            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2375        }
2376    };
2377    let admin_tokens = admin_config_tokens(admin);
2378    // Default `tenant` so single-tenant projects (no `scope` attr
2379    // anywhere) keep the v0.24.x behavior. Container-attr parser
2380    // already validated the value is "registry" or "tenant".
2381    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2382        Some("registry") => quote!(#root::core::ModelScope::Registry),
2383        _ => quote!(#root::core::ModelScope::Tenant),
2384    };
2385    let verbose_name_tokens = optional_str(verbose_name);
2386    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2387    let base_manager_name_tokens = optional_str(base_manager_name);
2388    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2389    let required_db_features_lits: Vec<&str> =
2390        required_db_features.iter().map(String::as_str).collect();
2391    let required_db_vendor_tokens = optional_str(required_db_vendor);
2392    let default_related_name_tokens = optional_str(default_related_name);
2393    let db_table_comment_tokens = optional_str(db_table_comment);
2394    let get_latest_by_tokens = match get_latest_by {
2395        Some((col, desc)) => {
2396            quote!(::core::option::Option::Some((#col, #desc)))
2397        }
2398        None => quote!(::core::option::Option::None),
2399    };
2400    let extra_permission_tokens: Vec<_> = extra_permissions
2401        .iter()
2402        .map(|(c, l)| quote!((#c, #l)))
2403        .collect();
2404    let default_permission_tokens: Vec<_> = default_permissions
2405        .iter()
2406        .map(|action| quote!(#action))
2407        .collect();
2408    let indexes_tokens = indexes.iter().map(|idx| {
2409        // When no explicit `name = "..."` was given, derive a stable,
2410        // collision-free name from the table + columns (Django-shape
2411        // `<table>_<col>_<col>_idx`) instead of a shared literal that
2412        // would clash the moment a model declares two unnamed indexes.
2413        // Capped at Postgres's 63-char identifier limit.
2414        let derived_name = idx.name.clone().unwrap_or_else(|| {
2415            let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2416            if n.len() > 63 {
2417                n.truncate(63);
2418            }
2419            n
2420        });
2421        let name = derived_name.as_str();
2422        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2423        let unique = idx.unique;
2424        // Map the parsed method string onto the IndexMethod enum
2425        // variant — kept at the codegen layer so the IR doesn't
2426        // carry the string form.
2427        let method_variant = match idx.method.as_str() {
2428            "gin" => quote!(#root::core::IndexMethod::Gin),
2429            "gist" => quote!(#root::core::IndexMethod::Gist),
2430            "brin" => quote!(#root::core::IndexMethod::Brin),
2431            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2432            "hash" => quote!(#root::core::IndexMethod::Hash),
2433            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2434            _ => quote!(#root::core::IndexMethod::BTree),
2435        };
2436        let where_clause = match &idx.where_clause {
2437            Some(s) => quote!(::core::option::Option::Some(#s)),
2438            None => quote!(::core::option::Option::None),
2439        };
2440        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2441        quote! {
2442            #root::core::IndexSchema {
2443                name: #name,
2444                columns: &[ #(#cols),* ],
2445                unique: #unique,
2446                method: #method_variant,
2447                where_clause: #where_clause,
2448                include: &[ #(#include_lits),* ],
2449            }
2450        }
2451    });
2452    let checks_tokens = checks.iter().map(|c| {
2453        let name = c.name.as_str();
2454        let expr = c.expr.as_str();
2455        quote! {
2456            #root::core::CheckConstraint {
2457                name: #name,
2458                expr: #expr,
2459            }
2460        }
2461    });
2462    let excludes_tokens = excludes.iter().map(|e| {
2463        let name = e.name.as_str();
2464        let using = e.using.as_str();
2465        let element_tokens = e.elements.iter().map(|(col, op)| {
2466            let col_s = col.as_str();
2467            let op_s = op.as_str();
2468            quote!((#col_s, #op_s))
2469        });
2470        let where_tokens = match e.where_clause.as_deref() {
2471            Some(w) => quote!(::core::option::Option::Some(#w)),
2472            None => quote!(::core::option::Option::None),
2473        };
2474        quote! {
2475            #root::core::ExclusionConstraint {
2476                name: #name,
2477                using: #using,
2478                elements: &[ #(#element_tokens),* ],
2479                where_clause: #where_tokens,
2480            }
2481        }
2482    });
2483    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2484        let name = rel.name.as_str();
2485        let to = rel.to.as_str();
2486        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2487        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2488        quote! {
2489            #root::core::CompositeFkRelation {
2490                name: #name,
2491                to: #to,
2492                from: &[ #(#from_cols),* ],
2493                on: &[ #(#on_cols),* ],
2494            }
2495        }
2496    });
2497    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2498        let name = rel.name.as_str();
2499        let ct_col = rel.ct_column.as_str();
2500        let pk_col = rel.pk_column.as_str();
2501        quote! {
2502            #root::core::GenericRelation {
2503                name: #name,
2504                ct_column: #ct_col,
2505                pk_column: #pk_col,
2506            }
2507        }
2508    });
2509    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2510    // no `#[rustango(default_order = "...")]` attribute was supplied.
2511    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2512        let col_lit = col.as_str();
2513        quote! { (#col_lit, #desc) }
2514    });
2515
2516    // Issue #820 — `global_scopes` slice literal. Empty when no
2517    // `#[rustango(global_scope(...))]` attribute was supplied. The
2518    // `apply` path is re-emitted verbatim so it resolves in the
2519    // consumer's scope; the name is stored as a string literal.
2520    let global_scope_tokens = global_scopes.iter().map(|s| {
2521        let name = s.name.as_str();
2522        let apply = &s.apply;
2523        quote! {
2524            #root::core::GlobalScope {
2525                name: #name,
2526                apply: #apply,
2527            }
2528        }
2529    });
2530
2531    let m2m_tokens = m2m_relations.iter().map(|rel| {
2532        let name = rel.name.as_str();
2533        let to = rel.to.as_str();
2534        let through = rel.through.as_str();
2535        let src = rel.src.as_str();
2536        let dst = rel.dst.as_str();
2537        let auto_create = rel.auto_create;
2538        quote! {
2539            #root::core::M2MRelation {
2540                name: #name,
2541                to: #to,
2542                through: #through,
2543                src_col: #src,
2544                dst_col: #dst,
2545                auto_create: #auto_create,
2546            }
2547        }
2548    });
2549    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2550    // when the model declares `#[rustango(reverse_has(...))]`. Each
2551    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2552    // a const expression. Models without reverse_has fall through to
2553    // the trait's empty default — no override emitted.
2554    let reverse_relations_override = if reverse_has_relations.is_empty() {
2555        quote!()
2556    } else {
2557        let entries = reverse_has_relations.iter().map(|rel| {
2558            let name = rel.name.as_str();
2559            let child = &rel.child;
2560            let child_fk_column = rel.child_fk_column.as_str();
2561            let self_pk_column = rel.self_pk_column.as_str();
2562            quote! {
2563                #root::core::ReverseRelation {
2564                    name: #name,
2565                    child_schema: <#child as #root::core::Model>::SCHEMA,
2566                    child_fk_column: #child_fk_column,
2567                    self_pk_column: #self_pk_column,
2568                }
2569            }
2570        });
2571        quote! {
2572            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2573                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2574                RELS
2575            }
2576        }
2577    };
2578    // Issue #830 — `Model::generic_reverse_relations()` override for
2579    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2580    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2581        quote!()
2582    } else {
2583        let entries = generic_has_relations.iter().map(|rel| {
2584            let name = rel.name.as_str();
2585            let child = &rel.child;
2586            let ct_column = rel.ct_column.as_str();
2587            let pk_column = rel.pk_column.as_str();
2588            let self_pk_column = rel.self_pk_column.as_str();
2589            quote! {
2590                #root::core::GenericReverseRelation {
2591                    name: #name,
2592                    child_schema: <#child as #root::core::Model>::SCHEMA,
2593                    ct_column: #ct_column,
2594                    pk_column: #pk_column,
2595                    self_pk_column: #self_pk_column,
2596                }
2597            }
2598        });
2599        quote! {
2600            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2601                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2602                RELS
2603            }
2604        }
2605    };
2606    quote! {
2607        impl #root::core::Model for #struct_name {
2608            const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2609                name: #model_name,
2610                table: #table,
2611                fields: &[ #(#field_schemas),* ],
2612                display: #display_tokens,
2613                app_label: #app_label_tokens,
2614                admin: #admin_tokens,
2615                soft_delete_column: #soft_delete_tokens,
2616                permissions: #permissions,
2617                audit_track: #audit_track_tokens,
2618                m2m: &[ #(#m2m_tokens),* ],
2619                indexes: &[ #(#indexes_tokens),* ],
2620                check_constraints: &[ #(#checks_tokens),* ],
2621                exclusion_constraints: &[ #(#excludes_tokens),* ],
2622                composite_relations: &[ #(#composite_fk_tokens),* ],
2623                generic_relations: &[ #(#generic_fk_tokens),* ],
2624                scope: #scope_tokens,
2625                default_order: &[ #(#default_order_tokens),* ],
2626                is_view: #is_view,
2627                verbose_name: #verbose_name_tokens,
2628                verbose_name_plural: #verbose_name_plural_tokens,
2629                managed: #managed,
2630                base_manager_name: #base_manager_name_tokens,
2631                order_with_respect_to: #order_with_respect_to_tokens,
2632                proxy: #proxy,
2633                required_db_features: &[ #(#required_db_features_lits),* ],
2634                required_db_vendor: #required_db_vendor_tokens,
2635                default_related_name: #default_related_name_tokens,
2636                db_table_comment: #db_table_comment_tokens,
2637                get_latest_by: #get_latest_by_tokens,
2638                extra_permissions: &[ #(#extra_permission_tokens),* ],
2639                default_permissions: &[ #(#default_permission_tokens),* ],
2640                global_scopes: &[ #(#global_scope_tokens),* ],
2641            };
2642
2643            #reverse_relations_override
2644            #generic_reverse_relations_override
2645        }
2646    }
2647}
2648
2649/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2650/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2651/// otherwise a static reference to a populated `AdminConfig`.
2652fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2653    let root = rustango_root();
2654    let Some(admin) = admin else {
2655        return quote!(::core::option::Option::None);
2656    };
2657
2658    let list_display = admin
2659        .list_display
2660        .as_ref()
2661        .map(|(v, _)| v.as_slice())
2662        .unwrap_or(&[]);
2663    let list_display_lits = list_display.iter().map(|s| s.as_str());
2664
2665    let search_fields = admin
2666        .search_fields
2667        .as_ref()
2668        .map(|(v, _)| v.as_slice())
2669        .unwrap_or(&[]);
2670    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2671
2672    let readonly_fields = admin
2673        .readonly_fields
2674        .as_ref()
2675        .map(|(v, _)| v.as_slice())
2676        .unwrap_or(&[]);
2677    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2678
2679    let list_filter = admin
2680        .list_filter
2681        .as_ref()
2682        .map(|(v, _)| v.as_slice())
2683        .unwrap_or(&[]);
2684    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2685
2686    let actions = admin
2687        .actions
2688        .as_ref()
2689        .map(|(v, _)| v.as_slice())
2690        .unwrap_or(&[]);
2691    let actions_lits = actions.iter().map(|s| s.as_str());
2692
2693    let fieldsets = admin
2694        .fieldsets
2695        .as_ref()
2696        .map(|(v, _)| v.as_slice())
2697        .unwrap_or(&[]);
2698    let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2699        let title = title.as_str();
2700        let field_lits = fields.iter().map(|s| s.as_str());
2701        quote!(#root::core::Fieldset {
2702            title: #title,
2703            fields: &[ #( #field_lits ),* ],
2704        })
2705    });
2706
2707    let list_display_links = admin
2708        .list_display_links
2709        .as_ref()
2710        .map(|(v, _)| v.as_slice())
2711        .unwrap_or(&[]);
2712    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2713
2714    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2715    let actions_on_top = admin.actions_on_top.unwrap_or(true);
2716    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2717    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2718
2719    let prepopulated = admin
2720        .prepopulated_fields
2721        .as_ref()
2722        .map(|(v, _)| v.as_slice())
2723        .unwrap_or(&[]);
2724    let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2725        let target = target.as_str();
2726        let source_lits = sources.iter().map(|s| s.as_str());
2727        quote!(#root::core::PrepopulatedField {
2728            target: #target,
2729            sources: &[ #( #source_lits ),* ],
2730        })
2731    });
2732
2733    let raw_id_fields = admin
2734        .raw_id_fields
2735        .as_ref()
2736        .map(|(v, _)| v.as_slice())
2737        .unwrap_or(&[]);
2738    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2739
2740    let autocomplete_fields = admin
2741        .autocomplete_fields
2742        .as_ref()
2743        .map(|(v, _)| v.as_slice())
2744        .unwrap_or(&[]);
2745    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2746
2747    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2748    let list_select_related_tokens = match admin.list_select_related.as_deref() {
2749        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2750        Some("none") => quote!(#root::core::ListSelectRelated::None),
2751        Some(raw) => {
2752            let names: Vec<&str> = raw
2753                .split(',')
2754                .map(str::trim)
2755                .filter(|s| !s.is_empty())
2756                .collect();
2757            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2758        }
2759    };
2760
2761    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2762    // a Vec<(String, String)>. Empty / unset → no overrides.
2763    let formfield_pairs: Vec<(&str, &str)> = admin
2764        .formfield_overrides
2765        .as_ref()
2766        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2767        .unwrap_or_default();
2768    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2769        let field = *field;
2770        let widget = *widget;
2771        quote!((#field, #widget))
2772    });
2773
2774    let list_per_page = admin.list_per_page.unwrap_or(0);
2775
2776    let ordering_pairs = admin
2777        .ordering
2778        .as_ref()
2779        .map(|(v, _)| v.as_slice())
2780        .unwrap_or(&[]);
2781    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2782        let name = name.as_str();
2783        let desc = *desc;
2784        quote!((#name, #desc))
2785    });
2786
2787    quote! {
2788        ::core::option::Option::Some(&#root::core::AdminConfig {
2789            list_display: &[ #( #list_display_lits ),* ],
2790            search_fields: &[ #( #search_fields_lits ),* ],
2791            list_per_page: #list_per_page,
2792            ordering: &[ #( #ordering_tokens ),* ],
2793            readonly_fields: &[ #( #readonly_fields_lits ),* ],
2794            list_filter: &[ #( #list_filter_lits ),* ],
2795            actions: &[ #( #actions_lits ),* ],
2796            fieldsets: &[ #( #fieldset_tokens ),* ],
2797            list_display_links: &[ #( #list_display_links_lits ),* ],
2798            search_help_text: #search_help_text,
2799            actions_on_top: #actions_on_top,
2800            actions_on_bottom: #actions_on_bottom,
2801            date_hierarchy: #date_hierarchy,
2802            prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2803            raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2804            autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2805            list_select_related: #list_select_related_tokens,
2806            formfield_overrides: &[ #( #formfield_tokens ),* ],
2807        })
2808    }
2809}
2810
2811fn inherent_impl_tokens(
2812    struct_name: &syn::Ident,
2813    fields: &CollectedFields,
2814    primary_key: Option<&(syn::Ident, String)>,
2815    column_consts: &TokenStream2,
2816    audited_fields: Option<&[&ColumnEntry]>,
2817    indexes: &[IndexAttr],
2818    manager_fns: &[syn::Ident],
2819) -> TokenStream2 {
2820    let root = rustango_root();
2821    // Audit-emit fragments threaded into write paths. Non-empty only
2822    // when the model carries `#[rustango(audit(...))]`. They reborrow
2823    // `_executor` (a `&mut PgConnection` for audited models — the
2824    // macro switches the signature below) so the data write and the
2825    // audit INSERT both run on the same caller-supplied connection.
2826    let executor_passes_to_data_write = if audited_fields.is_some() {
2827        quote!(&mut *_executor)
2828    } else {
2829        quote!(_executor)
2830    };
2831    let executor_param = if audited_fields.is_some() {
2832        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2833    } else {
2834        quote!(_executor: _E)
2835    };
2836    let executor_generics = if audited_fields.is_some() {
2837        quote!()
2838    } else {
2839        quote!(<'_c, _E>)
2840    };
2841    let executor_where = if audited_fields.is_some() {
2842        quote!()
2843    } else {
2844        quote! {
2845            where
2846                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2847        }
2848    };
2849    // For audited models the `_on` methods take `&mut PgConnection`, so
2850    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2851    // must acquire a connection first. Non-audited models keep the
2852    // direct delegation since `&PgPool` IS an Executor.
2853    let pool_to_save_on = if audited_fields.is_some() {
2854        quote! {
2855            let mut _conn = pool.acquire().await?;
2856            self.save_on(&mut *_conn).await
2857        }
2858    } else {
2859        quote!(self.save_on(pool).await)
2860    };
2861    let pool_to_insert_on = if audited_fields.is_some() {
2862        quote! {
2863            let mut _conn = pool.acquire().await?;
2864            self.insert_on(&mut *_conn).await
2865        }
2866    } else {
2867        quote!(self.insert_on(pool).await)
2868    };
2869    let pool_to_delete_on = if audited_fields.is_some() {
2870        quote! {
2871            let mut _conn = pool.acquire().await?;
2872            self.delete_on(&mut *_conn).await
2873        }
2874    } else {
2875        quote!(self.delete_on(pool).await)
2876    };
2877    let pool_to_bulk_insert_on = if audited_fields.is_some() {
2878        quote! {
2879            let mut _conn = pool.acquire().await?;
2880            Self::bulk_insert_on(rows, &mut *_conn).await
2881        }
2882    } else {
2883        quote!(Self::bulk_insert_on(rows, pool).await)
2884    };
2885    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2886    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2887    // directly, but `upsert_on` for audited models takes
2888    // `&mut PgConnection` (the audit emit needs a real connection).
2889    // Add the missing acquire shim to keep audited Auto-PK upsert
2890    // compiling.
2891    let pool_to_upsert_on = if audited_fields.is_some() {
2892        quote! {
2893            let mut _conn = pool.acquire().await?;
2894            self.upsert_on(&mut *_conn).await
2895        }
2896    } else {
2897        quote!(self.upsert_on(pool).await)
2898    };
2899
2900    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2901    // (audit-on-connection over &Pool needs a bi-dialect transaction
2902    // helper, deferred). Two body shapes:
2903    // - has_auto: build InsertQuery skipping Auto::Unset columns,
2904    //   request Auto cols in `returning`, dispatch via
2905    //   `insert_returning_pool`, then on the returned `PgRow` /
2906    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
2907    //   row OR drop the single i64 into the first Auto field on MySQL
2908    //   (multi-Auto models on MySQL error at runtime since
2909    //   `LAST_INSERT_ID()` only reports one)
2910    // - non-Auto: build InsertQuery with explicit columns/values and
2911    //   call `insert_pool` (no returning needed)
2912    // pool_insert_method body for the audited Auto-PK case is moved
2913    // to after audit_pair_tokens / audit_pk_to_string (they live
2914    // ~150 lines below). This block keeps the non-audited and
2915    // non-Auto branches in place — the audited Auto-PK arm is
2916    // computed below and merged via the dispatch helper variable.
2917    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2918        // Audited models with explicit (non-Auto) PKs go through
2919        // the non-Auto insert path below — the audit emit is one
2920        // round-trip after the INSERT inside the same tx via
2921        // audit::save_one_with_audit? No, INSERT semantics
2922        // differ. For non-Auto PK + audited, route through a
2923        // dedicated insert + audit emit on the same tx, but defer
2924        // the macro emission to the audit-bundle-aware block below
2925        // — this `quote!()` placeholder gets overwritten there.
2926        quote!()
2927    } else if audited_fields.is_some() && fields.has_auto {
2928        // Audited Auto-PK insert_pool — assembled after the audit
2929        // bundles. Placeholder; real emission below.
2930        quote!()
2931    } else if fields.has_auto {
2932        let pushes = &fields.insert_pushes;
2933        let returning_cols = &fields.returning_cols;
2934        // When every `Auto<T>` field is filled Rust-side
2935        // (`default_uuid_v7`, issue #823), there is no column to read
2936        // back from the database — `returning_cols` is empty. Route
2937        // through plain `insert_pool` instead of
2938        // `insert_returning_pool` to skip the redundant RETURNING /
2939        // LAST_INSERT_ID round-trip.
2940        if fields.returning_cols.is_empty() {
2941            quote! {
2942                /// Insert this row against either backend. Every
2943                /// `Auto<T>` PK on this model is filled Rust-side
2944                /// (e.g. `default_uuid_v7`) before binding, so no
2945                /// RETURNING round-trip is needed.
2946                ///
2947                /// # Errors
2948                /// As [`Self::insert`].
2949                pub async fn insert_pool(
2950                    &mut self,
2951                    pool: &#root::sql::Pool,
2952                ) -> ::core::result::Result<(), #root::sql::ExecError> {
2953                    let mut _columns: ::std::vec::Vec<&'static str> =
2954                        ::std::vec::Vec::new();
2955                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2956                        ::std::vec::Vec::new();
2957                    #( #pushes )*
2958                    let _query = #root::core::InsertQuery {
2959                        model: <Self as #root::core::Model>::SCHEMA,
2960                        columns: _columns,
2961                        values: _values,
2962                        returning: ::std::vec::Vec::new(),
2963                        on_conflict: ::core::option::Option::None,
2964                    };
2965                    #root::sql::insert_pool(pool, &_query).await
2966                }
2967
2968                /// Eloquent `Model::insertOrIgnore()` — same shape
2969                /// as the auto-PK branch above. Returns `Ok(true)`
2970                /// when inserted, `Ok(false)` when a conflict caused
2971                /// the INSERT to silently skip.
2972                ///
2973                /// # Errors
2974                /// As [`Self::insert`].
2975                pub async fn insert_or_ignore(
2976                    &mut self,
2977                    pool: &#root::sql::Pool,
2978                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
2979                    let mut _columns: ::std::vec::Vec<&'static str> =
2980                        ::std::vec::Vec::new();
2981                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2982                        ::std::vec::Vec::new();
2983                    #( #pushes )*
2984                    let _query = #root::core::InsertQuery {
2985                        model: <Self as #root::core::Model>::SCHEMA,
2986                        columns: _columns,
2987                        values: _values,
2988                        returning: ::std::vec::Vec::new(),
2989                        on_conflict: ::core::option::Option::Some(
2990                            #root::core::ConflictClause::DoNothing,
2991                        ),
2992                    };
2993                    let dialect = pool.dialect();
2994                    let stmt = dialect.compile_insert(&_query)?;
2995                    let rows = #root::sql::raw_execute_pool(
2996                        pool, &stmt.sql, stmt.params,
2997                    ).await?;
2998                    ::core::result::Result::Ok(rows > 0)
2999                }
3000            }
3001        } else {
3002            quote! {
3003                /// Insert this row against either backend, populating any
3004                /// `Auto<T>` PK from the auto-assigned value.
3005                ///
3006                /// # Errors
3007                /// As [`Self::insert`].
3008                pub async fn insert_pool(
3009                    &mut self,
3010                    pool: &#root::sql::Pool,
3011                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3012                    let mut _columns: ::std::vec::Vec<&'static str> =
3013                        ::std::vec::Vec::new();
3014                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3015                        ::std::vec::Vec::new();
3016                    #( #pushes )*
3017                    let _query = #root::core::InsertQuery {
3018                        model: <Self as #root::core::Model>::SCHEMA,
3019                        columns: _columns,
3020                        values: _values,
3021                        returning: ::std::vec![ #( #returning_cols ),* ],
3022                        on_conflict: ::core::option::Option::None,
3023                    };
3024                    let _result = #root::sql::insert_returning_pool(
3025                        pool, &_query,
3026                    ).await?;
3027                    #root::sql::apply_auto_pk(_result, self)
3028                }
3029
3030                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3031                /// row or silently skip on unique-constraint
3032                /// violation. Returns `Ok(true)` when a row was
3033                /// inserted, `Ok(false)` when a conflict caused the
3034                /// INSERT to silently skip.
3035                ///
3036                /// **Caveat on auto-PK models**: when the row is
3037                /// skipped (conflict), this instance's `Auto<T>`
3038                /// fields stay `Unset` — no PK is back-populated
3039                /// because the server didn't auto-assign one. For
3040                /// "insert then read back the PK or the existing
3041                /// row's PK", use the `upsert` family or
3042                /// `get_or_create`.
3043                ///
3044                /// # Errors
3045                /// As [`Self::insert`].
3046                pub async fn insert_or_ignore(
3047                    &mut self,
3048                    pool: &#root::sql::Pool,
3049                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3050                    let mut _columns: ::std::vec::Vec<&'static str> =
3051                        ::std::vec::Vec::new();
3052                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3053                        ::std::vec::Vec::new();
3054                    #( #pushes )*
3055                    let _query = #root::core::InsertQuery {
3056                        model: <Self as #root::core::Model>::SCHEMA,
3057                        columns: _columns,
3058                        values: _values,
3059                        returning: ::std::vec::Vec::new(),
3060                        on_conflict: ::core::option::Option::Some(
3061                            #root::core::ConflictClause::DoNothing,
3062                        ),
3063                    };
3064                    let dialect = pool.dialect();
3065                    let stmt = dialect.compile_insert(&_query)?;
3066                    let rows = #root::sql::raw_execute_pool(
3067                        pool, &stmt.sql, stmt.params,
3068                    ).await?;
3069                    ::core::result::Result::Ok(rows > 0)
3070                }
3071            }
3072        }
3073    } else {
3074        let insert_columns = &fields.insert_columns;
3075        let insert_values = &fields.insert_values;
3076        quote! {
3077            /// Insert this row into its table against either backend.
3078            /// Equivalent to [`Self::insert`] but takes
3079            /// [`#root::sql::Pool`].
3080            ///
3081            /// # Errors
3082            /// As [`Self::insert`].
3083            pub async fn insert_pool(
3084                &self,
3085                pool: &#root::sql::Pool,
3086            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3087                let _query = #root::core::InsertQuery {
3088                    model: <Self as #root::core::Model>::SCHEMA,
3089                    columns: ::std::vec![ #( #insert_columns ),* ],
3090                    values: ::std::vec![ #( #insert_values ),* ],
3091                    returning: ::std::vec::Vec::new(),
3092                    on_conflict: ::core::option::Option::None,
3093                };
3094                #root::sql::insert_pool(pool, &_query).await
3095            }
3096
3097            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3098            /// or silently skip on unique-constraint violation. Maps
3099            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3100            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3101            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3102            /// `INSERT IGNORE INTO …`.
3103            ///
3104            /// Returns `Ok(true)` when a row was inserted,
3105            /// `Ok(false)` when a conflict caused the INSERT to
3106            /// silently skip.
3107            ///
3108            /// Use for "create-if-absent" patterns where you don't
3109            /// need the row back. For "find-or-create with the row
3110            /// returned", use the queryset-level
3111            /// `crate::sql::get_or_create` free function.
3112            ///
3113            /// # Errors
3114            /// As [`Self::insert`], plus any dialect-specific
3115            /// translation error from the ConflictClause writer.
3116            pub async fn insert_or_ignore(
3117                &self,
3118                pool: &#root::sql::Pool,
3119            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3120                let _query = #root::core::InsertQuery {
3121                    model: <Self as #root::core::Model>::SCHEMA,
3122                    columns: ::std::vec![ #( #insert_columns ),* ],
3123                    values: ::std::vec![ #( #insert_values ),* ],
3124                    returning: ::std::vec::Vec::new(),
3125                    on_conflict: ::core::option::Option::Some(
3126                        #root::core::ConflictClause::DoNothing,
3127                    ),
3128                };
3129                let dialect = pool.dialect();
3130                let stmt = dialect.compile_insert(&_query)?;
3131                let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3132                ::core::result::Result::Ok(rows > 0)
3133            }
3134        }
3135    };
3136
3137    // pool_save_method moved to after audit_pair_tokens /
3138    // audit_pk_to_string (they live ~70 lines below) — needed for
3139    // the audited branch which builds an UpdateQuery + PendingEntry
3140    // and dispatches via audit::save_one_with_audit.
3141
3142    // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3143    // are computed (they live ~80 lines below).
3144
3145    // Build the (column, JSON value) pair list used by every
3146    // snapshot-style audit emission. Reused across delete_on,
3147    // soft_delete_on, restore_on, and (later) bulk paths. Empty
3148    // when the model isn't audited.
3149    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3150        .map(|tracked| {
3151            tracked
3152                .iter()
3153                .map(|c| {
3154                    let column_lit = c.column.as_str();
3155                    let ident = &c.ident;
3156                    quote! {
3157                        (
3158                            #column_lit,
3159                            #root::__serde_json::to_value(&self.#ident)
3160                                .unwrap_or(#root::__serde_json::Value::Null),
3161                        )
3162                    }
3163                })
3164                .collect()
3165        })
3166        .unwrap_or_default();
3167    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3168        if fields.pk_is_auto {
3169            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3170        } else {
3171            quote!(::std::format!("{}", &self.#pk_ident))
3172        }
3173    } else {
3174        quote!(::std::string::String::new())
3175    };
3176    let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3177        if audited_fields.is_some() {
3178            let pairs = audit_pair_tokens.iter();
3179            let pk_str = audit_pk_to_string.clone();
3180            quote! {
3181                let _audit_entry = #root::audit::PendingEntry {
3182                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3183                    entity_pk: #pk_str,
3184                    operation: #op_path,
3185                    source: #root::audit::current_source(),
3186                    changes: #root::audit::snapshot_changes(&[
3187                        #( #pairs ),*
3188                    ]),
3189                };
3190                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3191            }
3192        } else {
3193            quote!()
3194        }
3195    };
3196    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3197    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3198    let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3199    let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3200
3201    // `save_pool(&Pool)` — emitted for every model with a PK.
3202    // Audited Auto-PK models are deferred (the Auto::Unset →
3203    // insert_pool path needs the audited-insert flow from a future
3204    // batch). Three body shapes:
3205    // - non-audited, plain PK: build UpdateQuery + dispatch through
3206    //   sql::update_pool
3207    // - non-audited, Auto-PK: same, but Auto::Unset routes to
3208    //   self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3209    // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3210    //   through audit::save_one_with_audit (per-backend tx wraps
3211    //   UPDATE + audit emit atomically). Snapshot-style audit (post-
3212    //   write field values) — diff-style audit (with pre-UPDATE
3213    //   SELECT for `before` values) needs per-tracked-column codegen
3214    //   that doesn't fit the runtime-helper pattern; legacy &PgPool
3215    //   `save` keeps the diff for now.
3216    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3217        let pk_column_lit = pk_col.as_str();
3218        let assignments = &fields.update_assignments;
3219        if audited_fields.is_some() {
3220            if fields.pk_is_auto {
3221                // Auto-PK + audited: defer. The Auto::Unset insert
3222                // path needs a transactional INSERT + LAST_INSERT_ID
3223                // + audit emit flow — that's a follow-up batch.
3224                quote!()
3225            } else {
3226                let pairs = audit_pair_tokens.iter();
3227                let pairs2 = audit_pair_tokens.iter();
3228                let pk_str = audit_pk_to_string.clone();
3229                let pk_str2 = audit_pk_to_string.clone();
3230                quote! {
3231                    /// Save (UPDATE) this row against either backend
3232                    /// with audit emission inside the same transaction.
3233                    /// Bi-dialect counterpart of [`Self::save`] for
3234                    /// audited models with non-`Auto<T>` PKs.
3235                    ///
3236                    /// Captures **post-write** field state (snapshot
3237                    /// audit). The legacy &PgPool [`Self::save`]
3238                    /// captures BEFORE+AFTER for true diff audit;
3239                    /// porting that to the &Pool path needs runtime
3240                    /// per-tracked-column decoding and is deferred.
3241                    ///
3242                    /// # Errors
3243                    /// As [`Self::save`].
3244                    pub async fn save_pool(
3245                        &mut self,
3246                        pool: &#root::sql::Pool,
3247                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3248                        let _query = #root::core::UpdateQuery {
3249                            model: <Self as #root::core::Model>::SCHEMA,
3250                            set: ::std::vec![ #( #assignments ),* ],
3251                            where_clause: #root::core::WhereExpr::Predicate(
3252                                #root::core::Filter {
3253                                    column: #pk_column_lit,
3254                                    op: #root::core::Op::Eq,
3255                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3256                                        ::core::clone::Clone::clone(&self.#pk_ident)
3257                                    ),
3258                                }
3259                            ),
3260                        };
3261                        let _audit_entry = #root::audit::PendingEntry {
3262                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3263                            entity_pk: #pk_str,
3264                            operation: #root::audit::AuditOp::Update,
3265                            source: #root::audit::current_source(),
3266                            changes: #root::audit::snapshot_changes(&[
3267                                #( #pairs ),*
3268                            ]),
3269                        };
3270                        let _affected = #root::audit::save_one_with_audit(
3271                            pool, &_query, &_audit_entry,
3272                        ).await?;
3273                        ::core::result::Result::Ok(_affected)
3274                    }
3275
3276                    /// `save_pool` narrowed to a Rust-field allowlist — issue #66
3277                    /// (Django `Model.save(update_fields=[...])`).
3278                    /// Audit emission shrinks to the same column set so
3279                    /// the audit log reflects exactly what was written.
3280                    ///
3281                    /// # Errors
3282                    /// As [`Self::save_pool`], plus
3283                    /// [`#root::core::QueryError::UnknownField`] wrapped
3284                    /// in `ExecError::Query` for unknown field names.
3285                    pub async fn save_partial(
3286                        &mut self,
3287                        fields: &[&str],
3288                        pool: &#root::sql::Pool,
3289                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3290                        if fields.is_empty() {
3291                            #root::__tracing::warn!(
3292                                target: "rustango::save_partial",
3293                                model = <Self as #root::core::Model>::SCHEMA.name,
3294                                "save_partial called with empty field list — no-op"
3295                            );
3296                            return ::core::result::Result::Ok(0);
3297                        }
3298                        let _schema = <Self as #root::core::Model>::SCHEMA;
3299                        let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3300                            ::std::collections::HashSet::with_capacity(fields.len());
3301                        for f in fields {
3302                            match _schema.field(f) {
3303                                ::core::option::Option::Some(fs) => {
3304                                    _wanted_cols.insert(fs.column);
3305                                }
3306                                ::core::option::Option::None => {
3307                                    return ::core::result::Result::Err(
3308                                        #root::sql::ExecError::Query(
3309                                            #root::core::QueryError::UnknownField {
3310                                                model: _schema.name,
3311                                                field: (*f).to_owned(),
3312                                            }
3313                                        )
3314                                    );
3315                                }
3316                            }
3317                        }
3318                        let _full: ::std::vec::Vec<#root::core::Assignment> =
3319                            ::std::vec![ #( #assignments ),* ];
3320                        let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3321                            .into_iter()
3322                            .filter(|a| _wanted_cols.contains(a.column))
3323                            .collect();
3324                        if _filtered.is_empty() {
3325                            #root::__tracing::warn!(
3326                                target: "rustango::save_partial",
3327                                model = _schema.name,
3328                                "save_partial: every named field maps to a non-assignable column — no-op"
3329                            );
3330                            return ::core::result::Result::Ok(0);
3331                        }
3332                        let _query = #root::core::UpdateQuery {
3333                            model: _schema,
3334                            set: _filtered,
3335                            where_clause: #root::core::WhereExpr::Predicate(
3336                                #root::core::Filter {
3337                                    column: #pk_column_lit,
3338                                    op: #root::core::Op::Eq,
3339                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3340                                        ::core::clone::Clone::clone(&self.#pk_ident)
3341                                    ),
3342                                }
3343                            ),
3344                        };
3345                        // Narrow the audit snapshot to the same column set.
3346                        let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3347                            ::std::vec![ #( #pairs2 ),* ];
3348                        let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3349                            _all_pairs
3350                                .into_iter()
3351                                .filter(|(col, _)| _wanted_cols.contains(col))
3352                                .collect();
3353                        let _audit_entry = #root::audit::PendingEntry {
3354                            entity_table: _schema.table,
3355                            entity_pk: #pk_str2,
3356                            operation: #root::audit::AuditOp::Update,
3357                            source: #root::audit::current_source(),
3358                            changes: #root::audit::snapshot_changes(&_narrowed),
3359                        };
3360                        let _affected = #root::audit::save_one_with_audit(
3361                            pool, &_query, &_audit_entry,
3362                        ).await?;
3363                        ::core::result::Result::Ok(_affected)
3364                    }
3365
3366                    /// Typed-column counterpart of [`Self::save_partial`] —
3367                    /// issue #67. `fields` is a tuple of [`Column`]
3368                    /// constants whose `Model` matches `Self`; typos and
3369                    /// model mismatches surface at *compile time*
3370                    /// (`Author::name` inside a `Post::save_partial_typed`
3371                    /// call is a type error, no runtime check).
3372                    ///
3373                    /// ```ignore
3374                    /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3375                    /// ```
3376                    ///
3377                    /// Lowers to [`Self::save_partial`] under the hood;
3378                    /// audit narrowing + every other semantic is identical.
3379                    ///
3380                    /// [`Column`]: #root::core::Column
3381                    ///
3382                    /// # Errors
3383                    /// As [`Self::save_partial`].
3384                    pub async fn save_partial_typed<
3385                        L: #root::core::TypedFieldList<Self>,
3386                    >(
3387                        &mut self,
3388                        fields: L,
3389                        pool: &#root::sql::Pool,
3390                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3391                        let _names = fields.rust_field_names();
3392                        let _refs: ::std::vec::Vec<&str> =
3393                            _names.iter().copied().collect();
3394                        self.save_partial(&_refs, pool).await
3395                    }
3396                }
3397            }
3398        } else {
3399            let dispatch_unset = if fields.pk_is_auto {
3400                quote! {
3401                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3402                        return self.insert_pool(pool).await.map(|()| 1u64);
3403                    }
3404                }
3405            } else {
3406                quote!()
3407            };
3408            quote! {
3409                /// Save this row to its table against either backend.
3410                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3411                /// `UPDATE` keyed on the PK.
3412                ///
3413                /// # Errors
3414                /// As [`Self::save`].
3415                pub async fn save_pool(
3416                    &mut self,
3417                    pool: &#root::sql::Pool,
3418                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3419                    #dispatch_unset
3420                    let _query = #root::core::UpdateQuery {
3421                        model: <Self as #root::core::Model>::SCHEMA,
3422                        set: ::std::vec![ #( #assignments ),* ],
3423                        where_clause: #root::core::WhereExpr::Predicate(
3424                            #root::core::Filter {
3425                                column: #pk_column_lit,
3426                                op: #root::core::Op::Eq,
3427                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3428                                    ::core::clone::Clone::clone(&self.#pk_ident)
3429                                ),
3430                            }
3431                        ),
3432                    };
3433                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3434                    ::core::result::Result::Ok(_affected)
3435                }
3436
3437                /// Save (UPDATE) only the listed Rust-side fields,
3438                /// leaving every other column untouched. Issue #66 —
3439                /// Django's `Model.save(update_fields=[...])` shape.
3440                ///
3441                /// `fields` are Rust-side struct field names; the macro
3442                /// resolves each to its SQL column. Unknown field
3443                /// names return [`#root::core::QueryError::UnknownField`]
3444                /// wrapped in `ExecError::Query`. An empty list is a
3445                /// no-op (returns `Ok(())` and logs a `tracing::warn!`),
3446                /// matching Django's "nothing to do" semantic.
3447                ///
3448                /// Use this when:
3449                /// * you only mutated a couple of fields on a wide row
3450                ///   (avoid re-writing every column on every save), or
3451                /// * two writers diverged after their initial read and
3452                ///   you want to preserve the other writer's changes to
3453                ///   columns you didn't touch.
3454                ///
3455                /// Auto-PK models with an unset PK return
3456                /// [`#root::core::QueryError::UnknownField`] with
3457                /// field name `<pk>` — `save_partial` is an
3458                /// UPDATE-only path. Call [`Self::insert_pool`]
3459                /// (or [`Self::save_pool`] which dispatches based on
3460                /// PK state) for the INSERT case.
3461                ///
3462                /// # Errors
3463                /// As [`Self::save_pool`], plus `UnknownField` for
3464                /// unknown / empty / Auto-Unset cases.
3465                pub async fn save_partial(
3466                    &mut self,
3467                    fields: &[&str],
3468                    pool: &#root::sql::Pool,
3469                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3470                    if fields.is_empty() {
3471                        #root::__tracing::warn!(
3472                            target: "rustango::save_partial",
3473                            model = <Self as #root::core::Model>::SCHEMA.name,
3474                            "save_partial called with empty field list — no-op"
3475                        );
3476                        return ::core::result::Result::Ok(0);
3477                    }
3478                    let _schema = <Self as #root::core::Model>::SCHEMA;
3479                    // Validate field names against the schema.
3480                    let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3481                        ::std::collections::HashSet::with_capacity(fields.len());
3482                    for f in fields {
3483                        match _schema.field(f) {
3484                            ::core::option::Option::Some(fs) => {
3485                                _wanted_cols.insert(fs.column);
3486                            }
3487                            ::core::option::Option::None => {
3488                                return ::core::result::Result::Err(
3489                                    #root::sql::ExecError::Query(
3490                                        #root::core::QueryError::UnknownField {
3491                                            model: _schema.name,
3492                                            field: (*f).to_owned(),
3493                                        }
3494                                    )
3495                                );
3496                            }
3497                        }
3498                    }
3499                    // Build the full assignment vec, then keep only the
3500                    // assignments whose column is in `_wanted_cols`.
3501                    let _full: ::std::vec::Vec<#root::core::Assignment> =
3502                        ::std::vec![ #( #assignments ),* ];
3503                    let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3504                        .into_iter()
3505                        .filter(|a| _wanted_cols.contains(a.column))
3506                        .collect();
3507                    if _filtered.is_empty() {
3508                        // All field names valid, but they all map to
3509                        // non-assignable slots (PK column, computed/
3510                        // virtual fields, relations without an
3511                        // assignment). Same no-op semantic as Django.
3512                        #root::__tracing::warn!(
3513                            target: "rustango::save_partial",
3514                            model = _schema.name,
3515                            "save_partial: every named field maps to a non-assignable column — no-op"
3516                        );
3517                        return ::core::result::Result::Ok(0);
3518                    }
3519                    let _query = #root::core::UpdateQuery {
3520                        model: _schema,
3521                        set: _filtered,
3522                        where_clause: #root::core::WhereExpr::Predicate(
3523                            #root::core::Filter {
3524                                column: #pk_column_lit,
3525                                op: #root::core::Op::Eq,
3526                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3527                                    ::core::clone::Clone::clone(&self.#pk_ident)
3528                                ),
3529                            }
3530                        ),
3531                    };
3532                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3533                    ::core::result::Result::Ok(_affected)
3534                }
3535
3536                /// Typed-column counterpart of [`Self::save_partial`] —
3537                /// issue #67. `fields` is a tuple of [`Column`]
3538                /// constants whose `Model` matches `Self`; typos and
3539                /// model mismatches surface at *compile time*
3540                /// (`Author::name` inside a `Post::save_partial_typed`
3541                /// call is a type error, no runtime check).
3542                ///
3543                /// ```ignore
3544                /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3545                /// ```
3546                ///
3547                /// Lowers to [`Self::save_partial`] under the hood — the
3548                /// tuple is reduced to a `&[&str]` slice of Rust-side
3549                /// field names and forwarded.
3550                ///
3551                /// [`Column`]: #root::core::Column
3552                ///
3553                /// # Errors
3554                /// As [`Self::save_partial`].
3555                pub async fn save_partial_typed<
3556                    L: #root::core::TypedFieldList<Self>,
3557                >(
3558                    &mut self,
3559                    fields: L,
3560                    pool: &#root::sql::Pool,
3561                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3562                    let _names = fields.rust_field_names();
3563                    let _refs: ::std::vec::Vec<&str> =
3564                        _names.iter().copied().collect();
3565                    self.save_partial(&_refs, pool).await
3566                }
3567            }
3568        }
3569    } else {
3570        quote!()
3571    };
3572
3573    // Audited `insert_pool` (overrides the placeholder set higher up
3574    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3575    // audited models get insert_pool routing through
3576    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3577    // + auto-PK readback + audit emit). Snapshot-style audit (the
3578    // PendingEntry's `changes` carries post-write field values).
3579    let pool_insert_method = if audited_fields.is_some() {
3580        if let Some(_) = primary_key {
3581            let pushes = if fields.has_auto {
3582                fields.insert_pushes.clone()
3583            } else {
3584                // For non-Auto-PK models, the macro normally builds
3585                // {columns, values} from fields.insert_columns +
3586                // fields.insert_values rather than insert_pushes.
3587                // Map those into the pushes shape.
3588                fields
3589                    .insert_columns
3590                    .iter()
3591                    .zip(&fields.insert_values)
3592                    .map(|(col, val)| {
3593                        quote! {
3594                            _columns.push(#col);
3595                            _values.push(#val);
3596                        }
3597                    })
3598                    .collect()
3599            };
3600            let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3601                fields.returning_cols.clone()
3602            } else {
3603                // Non-Auto-PK: still need RETURNING something for the
3604                // audit helper's contract (it errors on empty
3605                // returning). Return the PK column so the audit row
3606                // can carry the assigned PK back. Some non-Auto PKs
3607                // are server-side-default (e.g. UUIDv4 default), so
3608                // RETURNING is genuinely useful.
3609                primary_key
3610                    .map(|(_, col)| {
3611                        let lit = col.as_str();
3612                        vec![quote!(#lit)]
3613                    })
3614                    .unwrap_or_default()
3615            };
3616            let pairs = audit_pair_tokens.iter();
3617            let pk_str = audit_pk_to_string.clone();
3618            quote! {
3619                /// Insert this row against either backend with audit
3620                /// emission inside the same transaction. Bi-dialect
3621                /// counterpart of [`Self::insert`] for audited models.
3622                ///
3623                /// Snapshot-style audit (post-write field values).
3624                ///
3625                /// # Errors
3626                /// As [`Self::insert`].
3627                pub async fn insert_pool(
3628                    &mut self,
3629                    pool: &#root::sql::Pool,
3630                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3631                    let mut _columns: ::std::vec::Vec<&'static str> =
3632                        ::std::vec::Vec::new();
3633                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3634                        ::std::vec::Vec::new();
3635                    #( #pushes )*
3636                    let _query = #root::core::InsertQuery {
3637                        model: <Self as #root::core::Model>::SCHEMA,
3638                        columns: _columns,
3639                        values: _values,
3640                        returning: ::std::vec![ #( #returning_cols ),* ],
3641                        on_conflict: ::core::option::Option::None,
3642                    };
3643                    let _audit_entry = #root::audit::PendingEntry {
3644                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
3645                        entity_pk: #pk_str,
3646                        operation: #root::audit::AuditOp::Create,
3647                        source: #root::audit::current_source(),
3648                        changes: #root::audit::snapshot_changes(&[
3649                            #( #pairs ),*
3650                        ]),
3651                    };
3652                    let _result = #root::audit::insert_one_with_audit(
3653                        pool, &_query, &_audit_entry,
3654                    ).await?;
3655                    #root::sql::apply_auto_pk(_result, self)
3656                }
3657            }
3658        } else {
3659            quote!()
3660        }
3661    } else {
3662        // Keep the non-audited pool_insert_method we built earlier.
3663        pool_insert_method
3664    };
3665
3666    // Update audited save_pool: now that insert_pool is wired for
3667    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3668    // insert_pool. Non-audited save_pool already does this.
3669    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3670    // Replaces the snapshot-only emission with a per-backend transaction
3671    // body that:
3672    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3673    //     column), capturing BEFORE values
3674    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3675    //  3. builds AFTER pairs from &self
3676    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3677    //     AuditOp::Update + diff_changes(...) on the same tx connection
3678    //  5. commits
3679    //
3680    // Per-backend arms inline the SQL string + placeholder shape, then
3681    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3682    // is polymorphic over Row type — the same tokens work against
3683    // PgRow and MySqlRow as long as the field's Rust type implements
3684    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3685    // primitives + chrono/uuid/serde_json::Value all do).
3686    let pool_save_method = if let Some(tracked) = audited_fields {
3687        if let Some((pk_ident, pk_col)) = primary_key {
3688            let pk_column_lit = pk_col.as_str();
3689            // Two iterators — quote!'s `#(#var)*` consumes the
3690            // iterator, and we need to splice the same after-pairs
3691            // sequence into both per-backend arms.
3692            let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3693            let pk_str = audit_pk_to_string.clone();
3694            // Per-tracked-column BEFORE-pair token list. Each entry
3695            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3696            // The Row alias resolves to PgRow / MySqlRow per call site,
3697            // so the same template generates both the PG and MySQL bodies.
3698            let mk_before_pairs =
3699                |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3700                    tracked
3701                        .iter()
3702                        .map(|c| {
3703                            let column_lit = c.column.as_str();
3704                            let value_ty = &c.value_ty;
3705                            quote! {
3706                                (
3707                                    #column_lit,
3708                                    match #getter::<#value_ty>(
3709                                        _audit_before_row, #column_lit,
3710                                    ) {
3711                                        ::core::result::Result::Ok(v) => {
3712                                            #root::__serde_json::to_value(&v)
3713                                                .unwrap_or(#root::__serde_json::Value::Null)
3714                                        }
3715                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3716                                    },
3717                                )
3718                            }
3719                        })
3720                        .collect()
3721                };
3722            let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3723                mk_before_pairs(quote!(#root::sql::try_get_returning));
3724            let before_pairs_my: Vec<proc_macro2::TokenStream> =
3725                mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3726            let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3727                mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3728            let pg_select_cols: String = tracked
3729                .iter()
3730                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3731                .collect::<Vec<_>>()
3732                .join(", ");
3733            let my_select_cols: String = tracked
3734                .iter()
3735                .map(|c| format!("`{}`", c.column.replace('`', "``")))
3736                .collect::<Vec<_>>()
3737                .join(", ");
3738            // SQLite uses double-quote identifier quoting (same as
3739            // Postgres in default config), so the column-list shape
3740            // matches PG.
3741            let sqlite_select_cols: String = pg_select_cols.clone();
3742            let pk_value_for_bind = if fields.pk_is_auto {
3743                quote!(self.#pk_ident.get().copied().unwrap_or_default())
3744            } else {
3745                quote!(::core::clone::Clone::clone(&self.#pk_ident))
3746            };
3747            let assignments = &fields.update_assignments;
3748            let unset_dispatch = if fields.has_auto {
3749                quote! {
3750                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3751                        return self.insert_pool(pool).await.map(|()| 1u64);
3752                    }
3753                }
3754            } else {
3755                quote!()
3756            };
3757            quote! {
3758                /// Save this row against either backend with audit
3759                /// emission (diff-style: BEFORE+AFTER) inside the
3760                /// same transaction. Auto::Unset PK routes to
3761                /// insert_pool. Bi-dialect counterpart of
3762                /// [`Self::save`] for audited models.
3763                ///
3764                /// The audit row's `changes` JSON contains one
3765                /// `{ "field": { "before": …, "after": … } }` entry
3766                /// per tracked column whose value actually changed
3767                /// — same shape as the existing &PgPool save() emits.
3768                ///
3769                /// # Errors
3770                /// As [`Self::save`].
3771                pub async fn save_pool(
3772                    &mut self,
3773                    pool: &#root::sql::Pool,
3774                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3775                    #unset_dispatch
3776                    let _query = #root::core::UpdateQuery {
3777                        model: <Self as #root::core::Model>::SCHEMA,
3778                        set: ::std::vec![ #( #assignments ),* ],
3779                        where_clause: #root::core::WhereExpr::Predicate(
3780                            #root::core::Filter {
3781                                column: #pk_column_lit,
3782                                op: #root::core::Op::Eq,
3783                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3784                                    ::core::clone::Clone::clone(&self.#pk_ident)
3785                                ),
3786                            }
3787                        ),
3788                    };
3789                    let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3790                        ::std::vec![ #( #after_pairs_pg ),* ];
3791                    #root::audit::save_one_with_diff(
3792                        pool,
3793                        &_query,
3794                        #pk_column_lit,
3795                        ::core::convert::Into::<#root::core::SqlValue>::into(
3796                            #pk_value_for_bind,
3797                        ),
3798                        <Self as #root::core::Model>::SCHEMA.table,
3799                        #pk_str,
3800                        _after_pairs,
3801                        #pg_select_cols,
3802                        #my_select_cols,
3803                        #sqlite_select_cols,
3804                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3805                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3806                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3807                    ).await
3808                }
3809            }
3810        } else {
3811            quote!()
3812        }
3813    } else {
3814        pool_save_method
3815    };
3816
3817    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3818    // body shapes:
3819    // - non-audited: simple dispatch through `sql::delete_pool`
3820    // - audited: routes through `audit::delete_one_with_audit`,
3821    //   which opens a per-backend transaction wrapping DELETE +
3822    //   audit emit so the data write and audit row commit atomically.
3823    let pool_delete_method = {
3824        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3825        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3826        if let Some(pk_ident) = pk_ident_for_pool {
3827            if audited_fields.is_some() {
3828                let pairs = audit_pair_tokens.iter();
3829                let pk_str = audit_pk_to_string.clone();
3830                quote! {
3831                    /// Delete this row against either backend with audit
3832                    /// emission inside the same transaction. Bi-dialect
3833                    /// counterpart of [`Self::delete`] for audited models.
3834                    ///
3835                    /// # Errors
3836                    /// As [`Self::delete`].
3837                    pub async fn delete_pool(
3838                        &self,
3839                        pool: &#root::sql::Pool,
3840                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3841                        let _query = #root::core::DeleteQuery {
3842                            model: <Self as #root::core::Model>::SCHEMA,
3843                            where_clause: #root::core::WhereExpr::Predicate(
3844                                #root::core::Filter {
3845                                    column: #pk_column_lit,
3846                                    op: #root::core::Op::Eq,
3847                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3848                                        ::core::clone::Clone::clone(&self.#pk_ident)
3849                                    ),
3850                                }
3851                            ),
3852                        };
3853                        let _audit_entry = #root::audit::PendingEntry {
3854                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3855                            entity_pk: #pk_str,
3856                            operation: #root::audit::AuditOp::Delete,
3857                            source: #root::audit::current_source(),
3858                            changes: #root::audit::snapshot_changes(&[
3859                                #( #pairs ),*
3860                            ]),
3861                        };
3862                        #root::audit::delete_one_with_audit(
3863                            pool, &_query, &_audit_entry,
3864                        ).await
3865                    }
3866                }
3867            } else {
3868                quote! {
3869                    /// Delete the row identified by this instance's primary key
3870                    /// against either backend. Equivalent to [`Self::delete`] but
3871                    /// takes [`#root::sql::Pool`] and dispatches per backend.
3872                    ///
3873                    /// # Errors
3874                    /// As [`Self::delete`].
3875                    pub async fn delete_pool(
3876                        &self,
3877                        pool: &#root::sql::Pool,
3878                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3879                        let _query = #root::core::DeleteQuery {
3880                            model: <Self as #root::core::Model>::SCHEMA,
3881                            where_clause: #root::core::WhereExpr::Predicate(
3882                                #root::core::Filter {
3883                                    column: #pk_column_lit,
3884                                    op: #root::core::Op::Eq,
3885                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3886                                        ::core::clone::Clone::clone(&self.#pk_ident)
3887                                    ),
3888                                }
3889                            ),
3890                        };
3891                        #root::sql::delete_pool(pool, &_query).await
3892                    }
3893                }
3894            }
3895        } else {
3896            quote!()
3897        }
3898    };
3899
3900    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3901    // matching this instance's PK and overwrite the in-memory state
3902    // with the freshly-fetched columns. Django's `refresh_from_db`.
3903    // Issue #825. Only emitted when the model declares a PK; non-PK
3904    // models can't address a specific row.
3905    //
3906    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3907    // every field from `self`; resets the PK to `Auto::Unset` when
3908    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3909    // a fresh autoincrement value. Non-Auto PKs preserve the source
3910    // PK — the caller must overwrite before insert. Pure-Rust, no
3911    // I/O, no dialect surface.
3912    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
3913        let other_field_clones: Vec<TokenStream2> = fields
3914            .column_entries
3915            .iter()
3916            .filter(|c| &c.ident != pk_ident)
3917            .map(|c| {
3918                let ident = &c.ident;
3919                quote! {
3920                    #ident: ::core::clone::Clone::clone(&self.#ident)
3921                }
3922            })
3923            .collect();
3924        let pk_clone_token = if fields.pk_is_auto {
3925            quote! { #pk_ident: #root::sql::Auto::Unset }
3926        } else {
3927            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
3928        };
3929        let replicate_doc = if fields.pk_is_auto {
3930            quote! {
3931                /// Eloquent-style `replicate()` — return a clone of this
3932                /// row with the primary key reset to [`Auto::Unset`] so
3933                /// the copy is ready for `insert_pool` / `save_pool` to
3934                /// allocate a fresh autoincrement value. Every other
3935                /// field is `Clone`d verbatim. Issue #825.
3936                ///
3937                /// `auto_now_add` / `auto_now` timestamp fields are
3938                /// **not** reset (Eloquent's `replicate` doesn't reset
3939                /// them either) — pass them through the normal insert
3940                /// path if you want fresh values, or assign them
3941                /// explicitly after the call.
3942            }
3943        } else {
3944            quote! {
3945                /// Eloquent-style `replicate()` — clone this row
3946                /// verbatim. Because the primary key is **not** an
3947                /// `Auto<T>`, the clone keeps the source PK; the
3948                /// caller must overwrite `copy.<pk>` before inserting
3949                /// to avoid a unique-key violation. Issue #825.
3950            }
3951        };
3952        // 2026-06-07 — field-name / shortcut collision guard.
3953        //
3954        // The macro emits both `pub const <field>: <field>_col = ...`
3955        // (per-field typed-column const, used by the typed-builder
3956        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
3957        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
3958        // model has a field named e.g. `count`, both items would land
3959        // in the same inherent impl with the same name, and the
3960        // compiler rejects the derive with "duplicate definitions".
3961        //
3962        // Drop the conflicting shortcut for that model. Callers can
3963        // still reach the same behavior via
3964        // `QuerySet::<Model>::default().count(&pool)`.
3965        let column_names: ::std::collections::HashSet<String> = fields
3966            .column_entries
3967            .iter()
3968            .map(|c| c.ident.to_string())
3969            .collect();
3970        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
3971            if column_names.contains(name) {
3972                quote! {}
3973            } else {
3974                tokens
3975            }
3976        };
3977        let count_method = emit_if_no_field_collision(
3978            "count",
3979            quote! {
3980                /// Count rows of this model — `SELECT COUNT(*) FROM
3981                /// <table>`. Eloquent `Model::count()` parity.
3982                ///
3983                /// Skipped on models that already declare a field named
3984                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
3985                /// in that case.
3986                ///
3987                /// # Errors
3988                /// As [`CounterPool::count`].
3989                ///
3990                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
3991                pub async fn count(
3992                    pool: &#root::sql::Pool,
3993                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
3994                    use #root::sql::CounterPool as _;
3995                    #root::query::QuerySet::<Self>::default()
3996                        .count(pool)
3997                        .await
3998                }
3999            },
4000        );
4001        let value_method = emit_if_no_field_collision(
4002            "value",
4003            quote! {
4004                /// Pluck a single scalar from the first row.
4005                /// Eloquent `Model::query()->value($col)` parity.
4006                ///
4007                /// Skipped on models that already declare a field named
4008                /// `value`. Drop into
4009                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4010                ///
4011                /// # Errors
4012                /// As `ValuesFlatQuerySet::first`.
4013                pub async fn value<U>(
4014                    col: &str,
4015                    pool: &#root::sql::Pool,
4016                ) -> ::core::result::Result<
4017                    ::core::option::Option<U>,
4018                    #root::sql::ExecError,
4019                >
4020                where
4021                    U: #root::sql::MaybePgScalar
4022                        + #root::sql::MaybeMyScalar
4023                        + #root::sql::MaybeSqliteScalar
4024                        + ::core::marker::Send
4025                        + ::core::marker::Unpin,
4026                {
4027                    let _col_static: &'static str = Self::__resolve_col(col)?;
4028                    #root::query::QuerySet::<Self>::default()
4029                        .values_list_flat(_col_static)
4030                        .first::<U>(pool)
4031                        .await
4032                }
4033            },
4034        );
4035        let sum_method = emit_if_no_field_collision(
4036            "sum",
4037            quote! {
4038                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4039                /// Skipped on models that already declare a field named
4040                /// `sum`.
4041                ///
4042                /// # Errors
4043                /// As [`#root::sql::fetch_aggregate_pool`].
4044                pub async fn sum<U>(
4045                    col: &str,
4046                    pool: &#root::sql::Pool,
4047                ) -> ::core::result::Result<
4048                    ::core::option::Option<U>,
4049                    #root::sql::ExecError,
4050                >
4051                where
4052                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4053                        + #root::sql::MaybeMyFromRow
4054                        + #root::sql::MaybeSqliteFromRow
4055                        + ::core::marker::Send
4056                        + ::core::marker::Unpin,
4057                {
4058                    Self::__aggregate_one_pool::<U>(
4059                        col,
4060                        |c| #root::core::AggregateExpr::Sum(c),
4061                        pool,
4062                    )
4063                    .await
4064                }
4065            },
4066        );
4067        let avg_method = emit_if_no_field_collision(
4068            "avg",
4069            quote! {
4070                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4071                /// models that already declare a field named `avg`.
4072                ///
4073                /// # Errors
4074                /// As [`#root::sql::fetch_aggregate_pool`].
4075                pub async fn avg<U>(
4076                    col: &str,
4077                    pool: &#root::sql::Pool,
4078                ) -> ::core::result::Result<
4079                    ::core::option::Option<U>,
4080                    #root::sql::ExecError,
4081                >
4082                where
4083                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4084                        + #root::sql::MaybeMyFromRow
4085                        + #root::sql::MaybeSqliteFromRow
4086                        + ::core::marker::Send
4087                        + ::core::marker::Unpin,
4088                {
4089                    Self::__aggregate_one_pool::<U>(
4090                        col,
4091                        |c| #root::core::AggregateExpr::Avg(c),
4092                        pool,
4093                    )
4094                    .await
4095                }
4096            },
4097        );
4098        let min_method = emit_if_no_field_collision(
4099            "min",
4100            quote! {
4101                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4102                /// models that already declare a field named `min`.
4103                ///
4104                /// # Errors
4105                /// As [`#root::sql::fetch_aggregate_pool`].
4106                pub async fn min<U>(
4107                    col: &str,
4108                    pool: &#root::sql::Pool,
4109                ) -> ::core::result::Result<
4110                    ::core::option::Option<U>,
4111                    #root::sql::ExecError,
4112                >
4113                where
4114                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4115                        + #root::sql::MaybeMyFromRow
4116                        + #root::sql::MaybeSqliteFromRow
4117                        + ::core::marker::Send
4118                        + ::core::marker::Unpin,
4119                {
4120                    Self::__aggregate_one_pool::<U>(
4121                        col,
4122                        |c| #root::core::AggregateExpr::Min(c),
4123                        pool,
4124                    )
4125                    .await
4126                }
4127            },
4128        );
4129        let max_method = emit_if_no_field_collision(
4130            "max",
4131            quote! {
4132                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4133                /// models that already declare a field named `max`.
4134                ///
4135                /// # Errors
4136                /// As [`#root::sql::fetch_aggregate_pool`].
4137                pub async fn max<U>(
4138                    col: &str,
4139                    pool: &#root::sql::Pool,
4140                ) -> ::core::result::Result<
4141                    ::core::option::Option<U>,
4142                    #root::sql::ExecError,
4143                >
4144                where
4145                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4146                        + #root::sql::MaybeMyFromRow
4147                        + #root::sql::MaybeSqliteFromRow
4148                        + ::core::marker::Send
4149                        + ::core::marker::Unpin,
4150                {
4151                    Self::__aggregate_one_pool::<U>(
4152                        col,
4153                        |c| #root::core::AggregateExpr::Max(c),
4154                        pool,
4155                    )
4156                    .await
4157                }
4158            },
4159        );
4160        let first_method = emit_if_no_field_collision(
4161            "first",
4162            quote! {
4163                /// First row of this model. Eloquent `Model::first()`.
4164                /// Skipped on models that already declare a field named
4165                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4166                ///
4167                /// # Errors
4168                /// As `QuerySet::first`.
4169                pub async fn first(
4170                    pool: &#root::sql::Pool,
4171                ) -> ::core::result::Result<
4172                    ::core::option::Option<Self>,
4173                    #root::sql::ExecError,
4174                > {
4175                    #root::query::QuerySet::<Self>::default()
4176                        .first(pool)
4177                        .await
4178                }
4179            },
4180        );
4181        let last_method = emit_if_no_field_collision(
4182            "last",
4183            quote! {
4184                /// Last row of this model by primary-key DESC.
4185                /// Eloquent `Model::query()->latest('id')->first()`
4186                /// parity — fetches the highest-PK row without
4187                /// requiring the caller to spell the PK column.
4188                /// Returns `None` on an empty table.
4189                ///
4190                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4191                /// Skipped on models that already declare a field
4192                /// named `last`.
4193                ///
4194                /// # Errors
4195                /// As `QuerySet::last`.
4196                pub async fn last(
4197                    pool: &#root::sql::Pool,
4198                ) -> ::core::result::Result<
4199                    ::core::option::Option<Self>,
4200                    #root::sql::ExecError,
4201                > {
4202                    #root::query::QuerySet::<Self>::default()
4203                        .last(pool)
4204                        .await
4205                }
4206            },
4207        );
4208        quote! {
4209            /// Re-SELECT this row by its primary key and overwrite
4210            /// every in-memory field with the freshly-fetched value.
4211            /// Django's [`Model.refresh_from_db`]. Issue #825.
4212            ///
4213            /// Use this when the row may have been modified by another
4214            /// process / connection / job since you read it — e.g. after
4215            /// a queued task callback, or to re-sync stale UI state
4216            /// before re-saving.
4217            ///
4218            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4219            /// when the primary key no longer matches any row (e.g.
4220            /// the row was deleted concurrently).
4221            ///
4222            /// # Errors
4223            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4224            /// the PK no longer exists.
4225            ///
4226            /// [`Model.refresh_from_db`]: https://docs.djangoproject.com/en/5.1/ref/models/instances/#django.db.models.Model.refresh_from_db
4227            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4228            pub async fn refresh_from_db(
4229                &mut self,
4230                pool: &#root::sql::Pool,
4231            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4232                use #root::sql::FetcherPool as _;
4233                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4234                    ::core::clone::Clone::clone(&self.#pk_ident),
4235                );
4236                let mut _rows: ::std::vec::Vec<Self> =
4237                    #root::query::QuerySet::<Self>::default()
4238                        .filter(::core::stringify!(#pk_ident), _pk_val)
4239                        .limit(1)
4240                        .fetch(pool)
4241                        .await?;
4242                match _rows.into_iter().next() {
4243                    ::core::option::Option::Some(_fresh) => {
4244                        *self = _fresh;
4245                        ::core::result::Result::Ok(())
4246                    }
4247                    ::core::option::Option::None => ::core::result::Result::Err(
4248                        #root::sql::ExecError::Driver(
4249                            #root::sql::sqlx::Error::RowNotFound,
4250                        ),
4251                    ),
4252                }
4253            }
4254
4255            /// Atomically increment the integer column `col` by
4256            /// `by` for this row. Equivalent to
4257            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4258            /// Eloquent `Model::increment($col, $by)` / Django
4259            /// `Model.objects.filter(pk=…).update(col=F('col')+$by)`
4260            /// parity.
4261            ///
4262            /// **Doesn't mutate `self`** — the in-memory copy is now
4263            /// stale; call [`Self::refresh_from_db_pool`] /
4264            /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4265            /// affected count (0 when the PK doesn't match any row,
4266            /// 1 on success).
4267            ///
4268            /// `col` is the Rust field name as a string; unknown
4269            /// fields surface as `UnknownField` at runtime. Negative
4270            /// `by` values atomically decrement (see also
4271            /// [`Self::decrement_pool`]).
4272            ///
4273            /// # Errors
4274            /// As [`UpdaterPool::execute_pool`].
4275            ///
4276            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4277            pub async fn increment(
4278                &self,
4279                col: &str,
4280                by: i64,
4281                pool: &#root::sql::Pool,
4282            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4283                Self::__increment_one(self, col, by, pool).await
4284            }
4285
4286            /// Sibling of [`Self::increment`] — atomically
4287            /// decrement this row's `col` by `by`. Eloquent
4288            /// `$model->decrement($col, $by)` parity. Equivalent to
4289            /// `self.increment(col, -by, &pool)`; the separate name
4290            /// keeps call sites readable.
4291            ///
4292            /// # Errors
4293            /// As [`Self::increment`].
4294            pub async fn decrement(
4295                &self,
4296                col: &str,
4297                by: i64,
4298                pool: &#root::sql::Pool,
4299            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4300                Self::__increment_one(self, col, -by, pool).await
4301            }
4302
4303            /// Bulk-increment: add `by` to `col` on every row of the
4304            /// table. Eloquent `Model::query()->increment($col, $by)`
4305            /// parity. Use for counters, score adjustments, view
4306            /// rollups.
4307            ///
4308            /// # Errors
4309            /// As [`UpdaterPool::execute_pool`].
4310            ///
4311            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4312            pub async fn increment_each(
4313                col: &str,
4314                by: i64,
4315                pool: &#root::sql::Pool,
4316            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4317                Self::__increment_all(col, by, pool).await
4318            }
4319
4320            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4321            ///
4322            /// # Errors
4323            /// As [`Self::increment_each`].
4324            pub async fn decrement_each(
4325                col: &str,
4326                by: i64,
4327                pool: &#root::sql::Pool,
4328            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4329                Self::__increment_all(col, -by, pool).await
4330            }
4331
4332            /// Internal: forward to
4333            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4334            /// One-line wrapper kept as a per-Model method so the
4335            /// macro's emitted `increment` / `decrement` instance
4336            /// calls don't have to thread `Self` through manually.
4337            #[doc(hidden)]
4338            pub async fn __increment_one(
4339                this: &Self,
4340                col: &str,
4341                by: i64,
4342                pool: &#root::sql::Pool,
4343            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4344                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4345                    ::core::clone::Clone::clone(&this.#pk_ident),
4346                );
4347                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4348                    ::core::stringify!(#pk_ident),
4349                    _pk_val,
4350                    col,
4351                    by,
4352                    pool,
4353                )
4354                .await
4355            }
4356
4357            /// Internal: forward to
4358            /// [`#root::sql::model_shortcuts::increment_all_pool`].
4359            #[doc(hidden)]
4360            pub async fn __increment_all(
4361                col: &str,
4362                by: i64,
4363                pool: &#root::sql::Pool,
4364            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4365                #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4366            }
4367
4368            /// Internal: forward to
4369            /// [`#root::sql::model_shortcuts::resolve_col`].
4370            #[doc(hidden)]
4371            pub fn __resolve_col(
4372                col: &str,
4373            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4374                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4375            }
4376
4377            /// Internal: forward to
4378            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4379            #[doc(hidden)]
4380            #[must_use]
4381            pub fn __add_signed_expr(
4382                col_static: &'static str,
4383                signed_by: i64,
4384            ) -> #root::core::Expr {
4385                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4386            }
4387
4388            /// Re-SELECT this row by its primary key and return a
4389            /// **new** instance with the freshly-fetched fields.
4390            /// Eloquent `Model::fresh()` parity — non-mutating
4391            /// counterpart of [`Self::refresh_from_db_pool`].
4392            ///
4393            /// Returns `Ok(None)` when the row was deleted
4394            /// concurrently — vs [`Self::refresh_from_db_pool`]
4395            /// which surfaces that as `RowNotFound` because
4396            /// in-place mutation has nothing to write to.
4397            ///
4398            /// Useful when you want to compare the in-memory
4399            /// instance against the persisted state (audit-style
4400            /// diffs, conflict detection) without mutating the
4401            /// reference you already hold.
4402            ///
4403            /// # Errors
4404            /// As [`FetcherPool::fetch`].
4405            ///
4406            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4407            pub async fn fresh(
4408                &self,
4409                pool: &#root::sql::Pool,
4410            ) -> ::core::result::Result<
4411                ::core::option::Option<Self>,
4412                #root::sql::ExecError,
4413            > {
4414                use #root::sql::FetcherPool as _;
4415                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4416                    ::core::clone::Clone::clone(&self.#pk_ident),
4417                );
4418                let _rows: ::std::vec::Vec<Self> =
4419                    #root::query::QuerySet::<Self>::default()
4420                        .filter(::core::stringify!(#pk_ident), _pk_val)
4421                        .limit(1)
4422                        .fetch(pool)
4423                        .await?;
4424                ::core::result::Result::Ok(_rows.into_iter().next())
4425            }
4426
4427            #replicate_doc
4428            #[must_use]
4429            pub fn replicate(&self) -> Self {
4430                Self {
4431                    #pk_clone_token,
4432                    #( #other_field_clones, )*
4433                }
4434            }
4435
4436            #first_method
4437
4438            #last_method
4439
4440            /// Throwing counterpart of [`Self::first_pool`] —
4441            /// errors with `RowNotFound` when the table is empty.
4442            /// Eloquent `Model::firstOrFail()` parity.
4443            ///
4444            /// # Errors
4445            /// As [`Self::first_pool`]; additionally
4446            /// [`sqlx::Error::RowNotFound`] on empty tables.
4447            ///
4448            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4449            pub async fn first_or_fail(
4450                pool: &#root::sql::Pool,
4451            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4452                // Route through the queryset rather than `Self::first`,
4453                // which is suppressed on models with a field named
4454                // `first` (field/shortcut collision guard).
4455                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4456                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4457                    ::core::option::Option::None => ::core::result::Result::Err(
4458                        #root::sql::ExecError::Driver(
4459                            #root::sql::sqlx::Error::RowNotFound,
4460                        ),
4461                    ),
4462                }
4463            }
4464
4465            /// Single-column projection — `SELECT <col> FROM
4466            /// <table>`. Returns `Vec<U>` where each element is the
4467            /// decoded value of the column. Eloquent
4468            /// `Model::pluck($column)` / Django
4469            /// `Model.objects.values_list('col', flat=True)` parity.
4470            ///
4471            /// Thin wrapper over `QuerySet::<Self>::default()
4472            /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4473            /// be decodable from the column's SQL type on every
4474            /// dialect the binary targets (common picks: `i64` /
4475            /// `i32` / `String` / `bool` / `f64`).
4476            ///
4477            /// # Errors
4478            /// As `ValuesFlatQuerySet::fetch`.
4479            pub async fn pluck<U>(
4480                col: &'static str,
4481                pool: &#root::sql::Pool,
4482            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4483            where
4484                U: #root::sql::MaybePgScalar
4485                    + #root::sql::MaybeMyScalar
4486                    + #root::sql::MaybeSqliteScalar
4487                    + ::core::marker::Send
4488                    + ::core::marker::Unpin,
4489            {
4490                #root::query::QuerySet::<Self>::default()
4491                    .values_list_flat(col)
4492                    .fetch::<U>(pool)
4493                    .await
4494            }
4495
4496            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4497            /// stream every row of this model in batches of `n`,
4498            /// invoking the callback once per batch. Stable PK-ASC
4499            /// ordering so the LIMIT/OFFSET pagination is
4500            /// deterministic across drivers.
4501            ///
4502            /// The callback is async — it can do further DB work
4503            /// (writes, related-row lookups, queue dispatch) per
4504            /// batch. Return `Err(...)` from the callback to abort
4505            /// the iteration early; the error bubbles up.
4506            ///
4507            /// **When to use**: bulk processing flows that can't
4508            /// fit the whole table in memory — sending newsletters,
4509            /// running data migrations, computing summary
4510            /// statistics. For small / known-bounded tables, plain
4511            /// `Self::all(&pool)` is simpler.
4512            ///
4513            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4514            /// large tables. For multi-million-row scans prefer
4515            /// keyset-by-PK (the standard "WHERE id > last_seen"
4516            /// shape) over `chunk(...)`.
4517            ///
4518            /// Skipped on models without a primary key — chunking
4519            /// needs a stable order to avoid skipping / repeating
4520            /// rows across batches.
4521            pub async fn chunk<F, Fut>(
4522                n: i64,
4523                pool: &#root::sql::Pool,
4524                mut cb: F,
4525            ) -> ::core::result::Result<(), #root::sql::ExecError>
4526            where
4527                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4528                Fut: ::core::future::Future<
4529                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4530                >,
4531            {
4532                use #root::sql::FetcherPool as _;
4533                let pk_col = match Self::primary_key_column() {
4534                    ::core::option::Option::Some(c) => c,
4535                    ::core::option::Option::None => {
4536                        return ::core::result::Result::Ok(());
4537                    }
4538                };
4539                let mut offset: i64 = 0;
4540                loop {
4541                    let rows: ::std::vec::Vec<Self> =
4542                        #root::query::QuerySet::<Self>::default()
4543                            .order_by(&[(pk_col, false)])
4544                            .limit(n)
4545                            .offset(offset)
4546                            .fetch(pool)
4547                            .await?;
4548                    if rows.is_empty() {
4549                        return ::core::result::Result::Ok(());
4550                    }
4551                    let len = rows.len() as i64;
4552                    cb(rows).await?;
4553                    if len < n {
4554                        return ::core::result::Result::Ok(());
4555                    }
4556                    offset += n;
4557                }
4558            }
4559
4560            /// Eloquent `Model::chunkById($n, fn (...))` — same
4561            /// per-batch callback shape as [`Self::chunk`], but uses
4562            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4563            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4564            /// — the right choice for multi-million-row sweeps.
4565            ///
4566            /// Requires the primary key to be a signed integer type
4567            /// (`i64` / `i32`); the keyset comparison rides on
4568            /// `__rustango_pk_value()` lowering through
4569            /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4570            /// non-integer PKs (UUID, String) — those should use
4571            /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4572            /// keyset loop.
4573            ///
4574            /// Callback errors abort iteration; the error bubbles up
4575            /// unchanged. Empty table → callback invoked zero times.
4576            pub async fn chunk_by_id<F, Fut>(
4577                n: i64,
4578                pool: &#root::sql::Pool,
4579                mut cb: F,
4580            ) -> ::core::result::Result<(), #root::sql::ExecError>
4581            where
4582                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4583                Fut: ::core::future::Future<
4584                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4585                >,
4586            {
4587                use #root::sql::FetcherPool as _;
4588                let pk_col = match Self::primary_key_column() {
4589                    ::core::option::Option::Some(c) => c,
4590                    ::core::option::Option::None => {
4591                        return ::core::result::Result::Ok(());
4592                    }
4593                };
4594                // Track the largest PK seen so the next batch picks
4595                // up from there. `i64::MIN` as the sentinel — the
4596                // very first iteration's `> MIN` matches every row,
4597                // so the loop entry is uniform with subsequent
4598                // iterations.
4599                let mut last_seen: i64 = i64::MIN;
4600                loop {
4601                    let key = ::std::format!("{}__gt", pk_col);
4602                    let rows: ::std::vec::Vec<Self> =
4603                        #root::query::QuerySet::<Self>::default()
4604                            .filter(key.as_str(), last_seen)
4605                            .order_by(&[(pk_col, false)])
4606                            .limit(n)
4607                            .fetch(pool)
4608                            .await?;
4609                    if rows.is_empty() {
4610                        return ::core::result::Result::Ok(());
4611                    }
4612                    let len = rows.len() as i64;
4613                    // Capture the last row's PK BEFORE moving rows
4614                    // into the callback.
4615                    let max_pk = match rows
4616                        .last()
4617                        .map(|r| r.__rustango_pk_value())
4618                    {
4619                        ::core::option::Option::Some(
4620                            #root::core::SqlValue::I64(v),
4621                        ) => v,
4622                        ::core::option::Option::Some(
4623                            #root::core::SqlValue::I32(v),
4624                        ) => i64::from(v),
4625                        _ => return ::core::result::Result::Ok(()),
4626                    };
4627                    cb(rows).await?;
4628                    if len < n {
4629                        return ::core::result::Result::Ok(());
4630                    }
4631                    last_seen = max_pk;
4632                }
4633            }
4634
4635            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4636            /// per-row callback companion to [`Self::chunk`].
4637            /// Streams every row in keyset-paginated batches of
4638            /// `batch` size, calling `cb` once per row.
4639            ///
4640            /// Inherits the keyset-paginated scan of
4641            /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4642            /// non-integer PKs are silently a no-op).
4643            ///
4644            /// ```ignore
4645            /// Post::each(500, &pool, |p| async move {
4646            ///     reindex(p).await?;
4647            ///     Ok(())
4648            /// }).await?;
4649            /// ```
4650            ///
4651            /// Return `Err(...)` from the callback to abort
4652            /// iteration; the error bubbles up unchanged.
4653            pub async fn each<F, Fut>(
4654                batch: i64,
4655                pool: &#root::sql::Pool,
4656                mut cb: F,
4657            ) -> ::core::result::Result<(), #root::sql::ExecError>
4658            where
4659                F: ::core::ops::FnMut(Self) -> Fut,
4660                Fut: ::core::future::Future<
4661                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4662                >,
4663            {
4664                use #root::sql::FetcherPool as _;
4665                let pk_col = match Self::primary_key_column() {
4666                    ::core::option::Option::Some(c) => c,
4667                    ::core::option::Option::None => {
4668                        return ::core::result::Result::Ok(());
4669                    }
4670                };
4671                let mut last_seen: i64 = i64::MIN;
4672                loop {
4673                    let key = ::std::format!("{}__gt", pk_col);
4674                    let rows: ::std::vec::Vec<Self> =
4675                        #root::query::QuerySet::<Self>::default()
4676                            .filter(key.as_str(), last_seen)
4677                            .order_by(&[(pk_col, false)])
4678                            .limit(batch)
4679                            .fetch(pool)
4680                            .await?;
4681                    if rows.is_empty() {
4682                        return ::core::result::Result::Ok(());
4683                    }
4684                    let len = rows.len() as i64;
4685                    let max_pk = match rows
4686                        .last()
4687                        .map(|r| r.__rustango_pk_value())
4688                    {
4689                        ::core::option::Option::Some(
4690                            #root::core::SqlValue::I64(v),
4691                        ) => v,
4692                        ::core::option::Option::Some(
4693                            #root::core::SqlValue::I32(v),
4694                        ) => i64::from(v),
4695                        _ => return ::core::result::Result::Ok(()),
4696                    };
4697                    for row in rows {
4698                        cb(row).await?;
4699                    }
4700                    if len < batch {
4701                        return ::core::result::Result::Ok(());
4702                    }
4703                    last_seen = max_pk;
4704                }
4705            }
4706
4707            /// Delete every row of this model — `TRUNCATE TABLE
4708            /// <table> RESTART IDENTITY CASCADE` on Postgres,
4709            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4710            /// support `TRUNCATE` inside foreign-key constraints
4711            /// or — for SQLite — at all). Eloquent `Model::truncate()`
4712            /// / Django `Model.objects.all().delete()` parity.
4713            ///
4714            /// **Use only in tests / fixture-reset flows.** Production
4715            /// writes through this would silently bypass the
4716            /// `pre_delete` / `post_delete` signals (no per-row hooks
4717            /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4718            /// row's audit-log entry.
4719            ///
4720            /// # Errors
4721            /// As [`raw_execute_pool`].
4722            ///
4723            /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4724            pub async fn truncate(
4725                pool: &#root::sql::Pool,
4726            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4727                let _table = <Self as #root::core::Model>::SCHEMA.table;
4728                let _dialect = pool.dialect();
4729                let _quoted = _dialect.quote_ident(_table);
4730                let _sql = if _dialect.name() == "postgres" {
4731                    ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4732                } else {
4733                    ::std::format!("DELETE FROM {}", _quoted)
4734                };
4735                #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4736            }
4737
4738            /// Bulk-delete every row whose primary key is in
4739            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4740            /// Returns the affected row count.
4741            ///
4742            /// Eloquent `Model::destroy([1, 2, 3])` / Django
4743            /// `Model.objects.filter(pk__in=[...]).delete()` parity.
4744            /// Empty `pks` is a no-op (returns 0).
4745            ///
4746            /// Accepts any iterable whose elements are
4747            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4748            /// `[i64; N]`, etc.
4749            ///
4750            /// # Errors
4751            /// As `delete_pool`.
4752            pub async fn destroy<V>(
4753                pks: impl ::core::iter::IntoIterator<Item = V>,
4754                pool: &#root::sql::Pool,
4755            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4756            where
4757                V: ::core::convert::Into<#root::core::SqlValue>,
4758            {
4759                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4760                    pks.into_iter().map(::core::convert::Into::into).collect();
4761                if _values.is_empty() {
4762                    return ::core::result::Result::Ok(0);
4763                }
4764                let _query = #root::core::DeleteQuery {
4765                    model: <Self as #root::core::Model>::SCHEMA,
4766                    where_clause: #root::core::WhereExpr::Predicate(
4767                        #root::core::Filter {
4768                            column: <Self as #root::core::Model>::SCHEMA
4769                                .primary_key()
4770                                .ok_or_else(|| {
4771                                    #root::sql::ExecError::Sql(
4772                                        #root::sql::SqlError::MissingPrimaryKey,
4773                                    )
4774                                })?
4775                                .column,
4776                            op: #root::core::Op::In,
4777                            value: #root::core::SqlValue::List(_values),
4778                        },
4779                    ),
4780                };
4781                #root::sql::delete_pool(pool, &_query).await
4782            }
4783
4784            /// Fetch every row where `<col> = <val>`. Eloquent
4785            /// `Model::where($col, $val)->get()` / Django
4786            /// `Model.objects.filter(col=val).all()` parity.
4787            ///
4788            /// Thin wrapper over `QuerySet::<Self>::default()
4789            /// .filter(col, val).fetch(pool)`. For one row,
4790            /// use [`Self::first_where_pool`]; for a chain that
4791            /// needs further `.filter()` / `.order_by()` /
4792            /// `.limit()`, drop down to `Self::query().filter(...)`
4793            /// directly.
4794            ///
4795            /// `val` accepts any value `Into<SqlValue>` so plain
4796            /// strings, ints, UUIDs, etc. all work.
4797            ///
4798            /// # Errors
4799            /// As [`FetcherPool::fetch`].
4800            ///
4801            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4802            pub async fn where_(
4803                col: &str,
4804                val: impl ::core::convert::Into<#root::core::SqlValue>,
4805                pool: &#root::sql::Pool,
4806            ) -> ::core::result::Result<
4807                ::std::vec::Vec<Self>,
4808                #root::sql::ExecError,
4809            > {
4810                use #root::sql::FetcherPool as _;
4811                #root::query::QuerySet::<Self>::default()
4812                    .filter(col, val)
4813                    .fetch(pool)
4814                    .await
4815            }
4816
4817            /// Fetch every row where `<col> IN (vals)`. Eloquent
4818            /// `Model::whereIn($col, $vals)->get()` parity. Empty
4819            /// `vals` returns no rows (matches SQL's empty-IN
4820            /// semantics).
4821            ///
4822            /// `vals` accepts any iterable whose items are
4823            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4824            /// etc.
4825            ///
4826            /// # Errors
4827            /// As [`FetcherPool::fetch`].
4828            ///
4829            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4830            pub async fn where_in<V>(
4831                col: &str,
4832                vals: impl ::core::iter::IntoIterator<Item = V>,
4833                pool: &#root::sql::Pool,
4834            ) -> ::core::result::Result<
4835                ::std::vec::Vec<Self>,
4836                #root::sql::ExecError,
4837            >
4838            where
4839                V: ::core::convert::Into<#root::core::SqlValue>,
4840            {
4841                use #root::sql::FetcherPool as _;
4842                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4843                    vals.into_iter().map(::core::convert::Into::into).collect();
4844                if _values.is_empty() {
4845                    return ::core::result::Result::Ok(::std::vec::Vec::new());
4846                }
4847                let _key = ::std::format!("{}__in", col);
4848                #root::query::QuerySet::<Self>::default()
4849                    .filter(&_key, #root::core::SqlValue::List(_values))
4850                    .fetch(pool)
4851                    .await
4852            }
4853
4854            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4855            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4856            /// `vals` returns every row (matches SQL's empty-NOT-IN
4857            /// semantics — vacuously true for every row).
4858            ///
4859            /// # Errors
4860            /// As [`FetcherPool::fetch`].
4861            ///
4862            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4863            pub async fn where_not_in<V>(
4864                col: &str,
4865                vals: impl ::core::iter::IntoIterator<Item = V>,
4866                pool: &#root::sql::Pool,
4867            ) -> ::core::result::Result<
4868                ::std::vec::Vec<Self>,
4869                #root::sql::ExecError,
4870            >
4871            where
4872                V: ::core::convert::Into<#root::core::SqlValue>,
4873            {
4874                use #root::sql::FetcherPool as _;
4875                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4876                    vals.into_iter().map(::core::convert::Into::into).collect();
4877                if _values.is_empty() {
4878                    return #root::query::QuerySet::<Self>::default()
4879                        .fetch(pool)
4880                        .await;
4881                }
4882                let _key = ::std::format!("{}__not_in", col);
4883                #root::query::QuerySet::<Self>::default()
4884                    .filter(&_key, #root::core::SqlValue::List(_values))
4885                    .fetch(pool)
4886                    .await
4887            }
4888
4889            /// Fetch every row where `<col> IS NULL`. Eloquent
4890            /// `Model::whereNull($col)->get()` parity.
4891            ///
4892            /// # Errors
4893            /// As [`FetcherPool::fetch`].
4894            ///
4895            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4896            pub async fn where_null(
4897                col: &str,
4898                pool: &#root::sql::Pool,
4899            ) -> ::core::result::Result<
4900                ::std::vec::Vec<Self>,
4901                #root::sql::ExecError,
4902            > {
4903                use #root::sql::FetcherPool as _;
4904                let _key = ::std::format!("{}__isnull", col);
4905                #root::query::QuerySet::<Self>::default()
4906                    .filter(&_key, #root::core::SqlValue::Bool(true))
4907                    .fetch(pool)
4908                    .await
4909            }
4910
4911            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4912            /// `Model::whereNotNull($col)->get()` parity.
4913            ///
4914            /// # Errors
4915            /// As [`FetcherPool::fetch`].
4916            ///
4917            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4918            pub async fn where_not_null(
4919                col: &str,
4920                pool: &#root::sql::Pool,
4921            ) -> ::core::result::Result<
4922                ::std::vec::Vec<Self>,
4923                #root::sql::ExecError,
4924            > {
4925                use #root::sql::FetcherPool as _;
4926                let _key = ::std::format!("{}__isnull", col);
4927                #root::query::QuerySet::<Self>::default()
4928                    .filter(&_key, #root::core::SqlValue::Bool(false))
4929                    .fetch(pool)
4930                    .await
4931            }
4932
4933            /// Fetch up to `n` rows in random order. Eloquent
4934            /// `Model::inRandomOrder()->limit($n)->get()` /
4935            /// `Model::query()->inRandomOrder()->get()->take($n)`
4936            /// parity. **Performance caveat**: random ordering
4937            /// forces a full table scan + per-row random key sort;
4938            /// the optimizer cannot use an index. Prefer a
4939            /// `pk >= rand_offset LIMIT N` walk for huge tables.
4940            ///
4941            /// # Errors
4942            /// As [`FetcherPool::fetch`].
4943            ///
4944            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4945            pub async fn random_n(
4946                n: i64,
4947                pool: &#root::sql::Pool,
4948            ) -> ::core::result::Result<
4949                ::std::vec::Vec<Self>,
4950                #root::sql::ExecError,
4951            > {
4952                use #root::sql::FetcherPool as _;
4953                #root::query::QuerySet::<Self>::default()
4954                    .order_random()
4955                    .limit(n)
4956                    .fetch(pool)
4957                    .await
4958            }
4959
4960            /// Fetch one row in random order. Eloquent
4961            /// `Model::inRandomOrder()->first()` parity. Same
4962            /// performance caveat as [`Self::random_n_pool`].
4963            ///
4964            /// # Errors
4965            /// As [`FetcherPool::fetch`].
4966            ///
4967            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4968            pub async fn random(
4969                pool: &#root::sql::Pool,
4970            ) -> ::core::result::Result<
4971                ::core::option::Option<Self>,
4972                #root::sql::ExecError,
4973            > {
4974                ::core::result::Result::Ok(
4975                    Self::random_n(1, pool).await?.into_iter().next(),
4976                )
4977            }
4978
4979            /// Fetch every row ordered ASC by `field`. Eloquent
4980            /// `Model::oldest($field)->get()` parity — the multi-row
4981            /// counterpart of [`Self::earliest_pool`].
4982            ///
4983            /// # Errors
4984            /// As [`FetcherPool::fetch`].
4985            ///
4986            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4987            pub async fn oldest(
4988                field: &str,
4989                pool: &#root::sql::Pool,
4990            ) -> ::core::result::Result<
4991                ::std::vec::Vec<Self>,
4992                #root::sql::ExecError,
4993            > {
4994                use #root::sql::FetcherPool as _;
4995                #root::query::QuerySet::<Self>::default()
4996                    .order_by(&[(field, false)])
4997                    .fetch(pool)
4998                    .await
4999            }
5000
5001            /// Fetch every row ordered DESC by `field`. Eloquent
5002            /// `Model::latest($field)->get()` parity — the multi-row
5003            /// counterpart of [`Self::latest_pool`].
5004            ///
5005            /// # Errors
5006            /// As [`FetcherPool::fetch`].
5007            ///
5008            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5009            pub async fn newest(
5010                field: &str,
5011                pool: &#root::sql::Pool,
5012            ) -> ::core::result::Result<
5013                ::std::vec::Vec<Self>,
5014                #root::sql::ExecError,
5015            > {
5016                use #root::sql::FetcherPool as _;
5017                #root::query::QuerySet::<Self>::default()
5018                    .order_by(&[(field, true)])
5019                    .fetch(pool)
5020                    .await
5021            }
5022
5023            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5024            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5025            /// Routes through the existing `__year` lookup suffix
5026            /// (issue #829).
5027            ///
5028            /// # Errors
5029            /// As [`FetcherPool::fetch`].
5030            ///
5031            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5032            pub async fn where_year(
5033                col: &str,
5034                year: i64,
5035                pool: &#root::sql::Pool,
5036            ) -> ::core::result::Result<
5037                ::std::vec::Vec<Self>,
5038                #root::sql::ExecError,
5039            > {
5040                use #root::sql::FetcherPool as _;
5041                let _key = ::std::format!("{}__year", col);
5042                #root::query::QuerySet::<Self>::default()
5043                    .filter(&_key, #root::core::SqlValue::I64(year))
5044                    .fetch(pool)
5045                    .await
5046            }
5047
5048            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5049            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5050            /// `month` is 1–12.
5051            ///
5052            /// # Errors
5053            /// As [`FetcherPool::fetch`].
5054            ///
5055            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5056            pub async fn where_month(
5057                col: &str,
5058                month: i64,
5059                pool: &#root::sql::Pool,
5060            ) -> ::core::result::Result<
5061                ::std::vec::Vec<Self>,
5062                #root::sql::ExecError,
5063            > {
5064                use #root::sql::FetcherPool as _;
5065                let _key = ::std::format!("{}__month", col);
5066                #root::query::QuerySet::<Self>::default()
5067                    .filter(&_key, #root::core::SqlValue::I64(month))
5068                    .fetch(pool)
5069                    .await
5070            }
5071
5072            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5073            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5074            /// `day` is 1–31.
5075            ///
5076            /// # Errors
5077            /// As [`FetcherPool::fetch`].
5078            ///
5079            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5080            pub async fn where_day(
5081                col: &str,
5082                day: i64,
5083                pool: &#root::sql::Pool,
5084            ) -> ::core::result::Result<
5085                ::std::vec::Vec<Self>,
5086                #root::sql::ExecError,
5087            > {
5088                use #root::sql::FetcherPool as _;
5089                let _key = ::std::format!("{}__day", col);
5090                #root::query::QuerySet::<Self>::default()
5091                    .filter(&_key, #root::core::SqlValue::I64(day))
5092                    .fetch(pool)
5093                    .await
5094            }
5095
5096            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5097            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5098            /// `hour` is 0–23.
5099            ///
5100            /// # Errors
5101            /// As [`FetcherPool::fetch`].
5102            ///
5103            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5104            pub async fn where_hour(
5105                col: &str,
5106                hour: i64,
5107                pool: &#root::sql::Pool,
5108            ) -> ::core::result::Result<
5109                ::std::vec::Vec<Self>,
5110                #root::sql::ExecError,
5111            > {
5112                use #root::sql::FetcherPool as _;
5113                let _key = ::std::format!("{}__hour", col);
5114                #root::query::QuerySet::<Self>::default()
5115                    .filter(&_key, #root::core::SqlValue::I64(hour))
5116                    .fetch(pool)
5117                    .await
5118            }
5119
5120            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5121            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5122            /// `minute` is 0–59.
5123            ///
5124            /// # Errors
5125            /// As [`FetcherPool::fetch`].
5126            ///
5127            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5128            pub async fn where_minute(
5129                col: &str,
5130                minute: i64,
5131                pool: &#root::sql::Pool,
5132            ) -> ::core::result::Result<
5133                ::std::vec::Vec<Self>,
5134                #root::sql::ExecError,
5135            > {
5136                use #root::sql::FetcherPool as _;
5137                let _key = ::std::format!("{}__minute", col);
5138                #root::query::QuerySet::<Self>::default()
5139                    .filter(&_key, #root::core::SqlValue::I64(minute))
5140                    .fetch(pool)
5141                    .await
5142            }
5143
5144            /// Fetch every row where `<col> LIKE <pattern>` —
5145            /// caller-supplied pattern (must include `%` / `_`
5146            /// wildcards manually). Eloquent `Model::whereLike`
5147            /// parity.
5148            ///
5149            /// # Errors
5150            /// As [`FetcherPool::fetch`].
5151            ///
5152            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5153            pub async fn where_like(
5154                col: &str,
5155                pattern: impl ::core::convert::Into<::std::string::String>,
5156                pool: &#root::sql::Pool,
5157            ) -> ::core::result::Result<
5158                ::std::vec::Vec<Self>,
5159                #root::sql::ExecError,
5160            > {
5161                use #root::sql::FetcherPool as _;
5162                let _key = ::std::format!("{}__like", col);
5163                #root::query::QuerySet::<Self>::default()
5164                    .filter(
5165                        &_key,
5166                        #root::core::SqlValue::String(pattern.into()),
5167                    )
5168                    .fetch(pool)
5169                    .await
5170            }
5171
5172            /// Fetch every row where `<col> ILIKE <pattern>` —
5173            /// case-insensitive LIKE (PG native, MySQL/SQLite
5174            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5175            ///
5176            /// # Errors
5177            /// As [`FetcherPool::fetch`].
5178            ///
5179            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5180            pub async fn where_ilike(
5181                col: &str,
5182                pattern: impl ::core::convert::Into<::std::string::String>,
5183                pool: &#root::sql::Pool,
5184            ) -> ::core::result::Result<
5185                ::std::vec::Vec<Self>,
5186                #root::sql::ExecError,
5187            > {
5188                use #root::sql::FetcherPool as _;
5189                let _key = ::std::format!("{}__ilike", col);
5190                #root::query::QuerySet::<Self>::default()
5191                    .filter(
5192                        &_key,
5193                        #root::core::SqlValue::String(pattern.into()),
5194                    )
5195                    .fetch(pool)
5196                    .await
5197            }
5198
5199            /// Fetch every row where `<col>` starts with `prefix`
5200            /// (auto-appends `%`). Django `__startswith` / Eloquent
5201            /// `whereLike("col", "$prefix%")` parity.
5202            ///
5203            /// # Errors
5204            /// As [`FetcherPool::fetch`].
5205            ///
5206            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5207            pub async fn where_starts_with(
5208                col: &str,
5209                prefix: impl ::core::convert::Into<::std::string::String>,
5210                pool: &#root::sql::Pool,
5211            ) -> ::core::result::Result<
5212                ::std::vec::Vec<Self>,
5213                #root::sql::ExecError,
5214            > {
5215                use #root::sql::FetcherPool as _;
5216                let _key = ::std::format!("{}__startswith", col);
5217                #root::query::QuerySet::<Self>::default()
5218                    .filter(
5219                        &_key,
5220                        #root::core::SqlValue::String(prefix.into()),
5221                    )
5222                    .fetch(pool)
5223                    .await
5224            }
5225
5226            /// Fetch every row where `<col>` ends with `suffix`
5227            /// (auto-prepends `%`). Django `__endswith` / Eloquent
5228            /// `whereLike("col", "%$suffix")` parity.
5229            ///
5230            /// # Errors
5231            /// As [`FetcherPool::fetch`].
5232            ///
5233            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5234            pub async fn where_ends_with(
5235                col: &str,
5236                suffix: impl ::core::convert::Into<::std::string::String>,
5237                pool: &#root::sql::Pool,
5238            ) -> ::core::result::Result<
5239                ::std::vec::Vec<Self>,
5240                #root::sql::ExecError,
5241            > {
5242                use #root::sql::FetcherPool as _;
5243                let _key = ::std::format!("{}__endswith", col);
5244                #root::query::QuerySet::<Self>::default()
5245                    .filter(
5246                        &_key,
5247                        #root::core::SqlValue::String(suffix.into()),
5248                    )
5249                    .fetch(pool)
5250                    .await
5251            }
5252
5253            /// Fetch every row where `<col>` contains `substr`
5254            /// (auto-wraps with `%`). Django `__contains` /
5255            /// Eloquent `whereLike("col", "%$substr%")` parity.
5256            ///
5257            /// # Errors
5258            /// As [`FetcherPool::fetch`].
5259            ///
5260            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5261            pub async fn where_contains(
5262                col: &str,
5263                substr: impl ::core::convert::Into<::std::string::String>,
5264                pool: &#root::sql::Pool,
5265            ) -> ::core::result::Result<
5266                ::std::vec::Vec<Self>,
5267                #root::sql::ExecError,
5268            > {
5269                use #root::sql::FetcherPool as _;
5270                let _key = ::std::format!("{}__contains", col);
5271                #root::query::QuerySet::<Self>::default()
5272                    .filter(
5273                        &_key,
5274                        #root::core::SqlValue::String(substr.into()),
5275                    )
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_gt(
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!("{}__gt", 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_gte(
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!("{}__gte", 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_lt(
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!("{}__lt", 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_lte(
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!("{}__lte", col);
5366                #root::query::QuerySet::<Self>::default()
5367                    .filter(&_key, val)
5368                    .fetch(pool)
5369                    .await
5370            }
5371
5372            /// Fetch every row where `<col> <> val`. Eloquent
5373            /// `Model::where($col, "!=", $val)->get()` parity.
5374            ///
5375            /// # Errors
5376            /// As [`FetcherPool::fetch`].
5377            ///
5378            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5379            pub async fn where_ne(
5380                col: &str,
5381                val: impl ::core::convert::Into<#root::core::SqlValue>,
5382                pool: &#root::sql::Pool,
5383            ) -> ::core::result::Result<
5384                ::std::vec::Vec<Self>,
5385                #root::sql::ExecError,
5386            > {
5387                use #root::sql::FetcherPool as _;
5388                let _key = ::std::format!("{}__ne", col);
5389                #root::query::QuerySet::<Self>::default()
5390                    .filter(&_key, val)
5391                    .fetch(pool)
5392                    .await
5393            }
5394
5395            /// Fetch every row matching `<col> = val` for ANY of the
5396            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5397            /// parity. Empty `cols` returns no rows.
5398            ///
5399            /// Resolves each `&str` column to its SCHEMA-registered
5400            /// `&'static str` once and builds a single OR-composed
5401            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5402            ///
5403            /// # Errors
5404            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5405            /// when any column is not declared on the model.
5406            ///
5407            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5408            pub async fn where_any(
5409                cols: &[&str],
5410                val: impl ::core::convert::Into<#root::core::SqlValue>,
5411                pool: &#root::sql::Pool,
5412            ) -> ::core::result::Result<
5413                ::std::vec::Vec<Self>,
5414                #root::sql::ExecError,
5415            > {
5416                Self::__where_multi(cols, val, false, pool).await
5417            }
5418
5419            /// Fetch every row matching `<col> = val` for ALL listed
5420            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5421            /// Empty `cols` returns every row (vacuous AND).
5422            ///
5423            /// # Errors
5424            /// As [`Self::where_any`].
5425            pub async fn where_all(
5426                cols: &[&str],
5427                val: impl ::core::convert::Into<#root::core::SqlValue>,
5428                pool: &#root::sql::Pool,
5429            ) -> ::core::result::Result<
5430                ::std::vec::Vec<Self>,
5431                #root::sql::ExecError,
5432            > {
5433                Self::__where_multi(cols, val, true, pool).await
5434            }
5435
5436            /// Internal: build a Q expression composing `cols` via
5437            /// AND (`all`) or OR (`!all`), then fetch. Backs
5438            /// `where_any` / `where_all`.
5439            #[doc(hidden)]
5440            pub async fn __where_multi(
5441                cols: &[&str],
5442                val: impl ::core::convert::Into<#root::core::SqlValue>,
5443                all: bool,
5444                pool: &#root::sql::Pool,
5445            ) -> ::core::result::Result<
5446                ::std::vec::Vec<Self>,
5447                #root::sql::ExecError,
5448            > {
5449                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5450                    .await
5451            }
5452
5453            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5454            /// parity / Django `Model.objects.all()[:n]`. PK-ordered
5455            /// is NOT guaranteed without an explicit `order_by` —
5456            /// drop into `Self::query()` for that.
5457            ///
5458            /// # Errors
5459            /// As [`FetcherPool::fetch`].
5460            ///
5461            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5462            pub async fn take(
5463                n: i64,
5464                pool: &#root::sql::Pool,
5465            ) -> ::core::result::Result<
5466                ::std::vec::Vec<Self>,
5467                #root::sql::ExecError,
5468            > {
5469                use #root::sql::FetcherPool as _;
5470                #root::query::QuerySet::<Self>::default()
5471                    .limit(n)
5472                    .fetch(pool)
5473                    .await
5474            }
5475
5476            /// Fetch the page-th window of `per_page` rows
5477            /// (1-indexed). Eloquent
5478            /// `Model::query()->forPage($page, $perPage)->get()`
5479            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5480            /// LIMIT per_page`; for large offsets this is O(N) —
5481            /// prefer keyset pagination via PK on hot paths.
5482            ///
5483            /// # Errors
5484            /// As [`FetcherPool::fetch`].
5485            ///
5486            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5487            pub async fn for_page(
5488                page: i64,
5489                per_page: i64,
5490                pool: &#root::sql::Pool,
5491            ) -> ::core::result::Result<
5492                ::std::vec::Vec<Self>,
5493                #root::sql::ExecError,
5494            > {
5495                use #root::sql::FetcherPool as _;
5496                let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5497                #root::query::QuerySet::<Self>::default()
5498                    .limit(per_page)
5499                    .offset(_offset)
5500                    .fetch(pool)
5501                    .await
5502            }
5503
5504            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5505            /// one page of rows AND the total row count in one
5506            /// call. Returns `(rows, total)`. Two queries: a
5507            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5508            /// for the total.
5509            ///
5510            /// Useful for paginated UIs that need both the visible
5511            /// rows AND a "Page X of Y" / total-count widget. For
5512            /// large tables prefer keyset pagination + cached count;
5513            /// every call to `paginate` re-counts the full table.
5514            ///
5515            /// Same 1-indexed `page` convention as [`Self::for_page`].
5516            ///
5517            /// # Errors
5518            /// As [`Self::for_page`] and [`Self::count`].
5519            pub async fn paginate(
5520                page: i64,
5521                per_page: i64,
5522                pool: &#root::sql::Pool,
5523            ) -> ::core::result::Result<
5524                (::std::vec::Vec<Self>, i64),
5525                #root::sql::ExecError,
5526            > {
5527                // Route through the queryset rather than `Self::count`:
5528                // the `count` inherent method is suppressed on models
5529                // that declare a field named `count` (field/shortcut
5530                // collision guard), and `paginate` must still compile
5531                // for those models.
5532                let total = {
5533                    use #root::sql::CounterPool as _;
5534                    #root::query::QuerySet::<Self>::default()
5535                        .count(pool)
5536                        .await?
5537                };
5538                let rows = Self::for_page(page, per_page, pool).await?;
5539                ::core::result::Result::Ok((rows, total))
5540            }
5541
5542            /// Bulk-update — set `set_col = set_val` on every row
5543            /// matching `where_col = where_val`. Returns affected row
5544            /// count. Eloquent
5545            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5546            /// parity.
5547            ///
5548            /// For multi-column updates drop into the queryset
5549            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5550            ///
5551            /// # Errors
5552            /// As [`UpdaterPool::execute_pool`].
5553            ///
5554            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5555            pub async fn update_where(
5556                where_col: &str,
5557                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5558                set_col: &str,
5559                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5560                pool: &#root::sql::Pool,
5561            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5562                use #root::sql::UpdaterPool as _;
5563                #root::query::QuerySet::<Self>::default()
5564                    .filter(where_col, where_val)
5565                    .update()
5566                    .set(set_col, set_val)
5567                    .execute_pool(pool)
5568                    .await
5569            }
5570
5571            /// Bulk-delete — remove every row matching
5572            /// `where_col = where_val`. Returns affected row count.
5573            /// Eloquent
5574            /// `Model::where($where_col, $where_val)->delete()` parity.
5575            ///
5576            /// For more complex filters drop into the queryset
5577            /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5578            ///
5579            /// # Errors
5580            /// As [`#root::sql::delete_pool`].
5581            pub async fn delete_where(
5582                where_col: &str,
5583                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5584                pool: &#root::sql::Pool,
5585            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5586                let _query = #root::core::DeleteQuery {
5587                    model: <Self as #root::core::Model>::SCHEMA,
5588                    where_clause: #root::core::WhereExpr::Predicate(
5589                        #root::core::Filter {
5590                            column: <Self as #root::core::Model>::SCHEMA
5591                                .field(where_col)
5592                                .ok_or_else(|| {
5593                                    #root::sql::ExecError::Query(
5594                                        #root::core::QueryError::UnknownField {
5595                                            model: <Self as #root::core::Model>::SCHEMA.name,
5596                                            field: ::std::string::ToString::to_string(where_col),
5597                                        },
5598                                    )
5599                                })?
5600                                .column,
5601                            op: #root::core::Op::Eq,
5602                            value: ::core::convert::Into::into(where_val),
5603                        },
5604                    ),
5605                };
5606                #root::sql::delete_pool(pool, &_query).await
5607            }
5608
5609            /// Bulk-update — set `set_col = set_val` on EVERY row of
5610            /// this model's table. **No WHERE clause** — use with
5611            /// care. Eloquent `Model::query()->update([$col => $val])`
5612            /// parity.
5613            ///
5614            /// Use for backfills, one-shot reset flows, etc. The
5615            /// returned count is rows affected.
5616            ///
5617            /// # Errors
5618            /// As [`UpdaterPool::execute_pool`].
5619            ///
5620            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5621            pub async fn update_all(
5622                set_col: &str,
5623                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5624                pool: &#root::sql::Pool,
5625            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5626                use #root::sql::UpdaterPool as _;
5627                #root::query::QuerySet::<Self>::default()
5628                    .update()
5629                    .set(set_col, set_val)
5630                    .execute_pool(pool)
5631                    .await
5632            }
5633
5634            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5635            /// Eloquent `Model::whereNotLike` parity. Pattern is
5636            /// passed verbatim — caller controls `%` / `_`.
5637            ///
5638            /// # Errors
5639            /// As [`FetcherPool::fetch`].
5640            ///
5641            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5642            pub async fn where_not_like(
5643                col: &str,
5644                pattern: impl ::core::convert::Into<::std::string::String>,
5645                pool: &#root::sql::Pool,
5646            ) -> ::core::result::Result<
5647                ::std::vec::Vec<Self>,
5648                #root::sql::ExecError,
5649            > {
5650                use #root::sql::FetcherPool as _;
5651                let _key = ::std::format!("{}__not_like", col);
5652                #root::query::QuerySet::<Self>::default()
5653                    .filter(
5654                        &_key,
5655                        #root::core::SqlValue::String(pattern.into()),
5656                    )
5657                    .fetch(pool)
5658                    .await
5659            }
5660
5661            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5662            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5663            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5664            ///
5665            /// # Errors
5666            /// As [`FetcherPool::fetch`].
5667            ///
5668            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5669            pub async fn where_not_ilike(
5670                col: &str,
5671                pattern: impl ::core::convert::Into<::std::string::String>,
5672                pool: &#root::sql::Pool,
5673            ) -> ::core::result::Result<
5674                ::std::vec::Vec<Self>,
5675                #root::sql::ExecError,
5676            > {
5677                use #root::sql::FetcherPool as _;
5678                let _key = ::std::format!("{}__not_ilike", col);
5679                #root::query::QuerySet::<Self>::default()
5680                    .filter(
5681                        &_key,
5682                        #root::core::SqlValue::String(pattern.into()),
5683                    )
5684                    .fetch(pool)
5685                    .await
5686            }
5687
5688            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5689            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5690            /// parity.
5691            ///
5692            /// # Errors
5693            /// As [`FetcherPool::fetch`].
5694            ///
5695            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5696            pub async fn where_not_between(
5697                col: &str,
5698                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5699                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5700                pool: &#root::sql::Pool,
5701            ) -> ::core::result::Result<
5702                ::std::vec::Vec<Self>,
5703                #root::sql::ExecError,
5704            > {
5705                use #root::sql::FetcherPool as _;
5706                let _key = ::std::format!("{}__not_between", col);
5707                let _vals = #root::core::SqlValue::List(::std::vec![
5708                    ::core::convert::Into::into(lo),
5709                    ::core::convert::Into::into(hi),
5710                ]);
5711                #root::query::QuerySet::<Self>::default()
5712                    .filter(&_key, _vals)
5713                    .fetch(pool)
5714                    .await
5715            }
5716
5717            /// Returns the SQL table name for this model. Eloquent
5718            /// `$model->getTable()` parity.
5719            #[must_use]
5720            pub fn table_name() -> &'static str {
5721                <Self as #root::core::Model>::SCHEMA.table
5722            }
5723
5724            /// Returns the SQL column name of this model's primary
5725            /// key, or `None` when the model has no
5726            /// `#[rustango(primary_key)]`. Eloquent
5727            /// `$model->getKeyName()` parity.
5728            #[must_use]
5729            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5730                <Self as #root::core::Model>::SCHEMA
5731                    .primary_key()
5732                    .map(|f| f.column)
5733            }
5734
5735            /// Fetch every row where `<col> BETWEEN lo AND hi`
5736            /// (inclusive on both ends — same as SQL). Eloquent
5737            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5738            ///
5739            /// # Errors
5740            /// As [`FetcherPool::fetch`].
5741            ///
5742            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5743            pub async fn where_between(
5744                col: &str,
5745                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5746                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5747                pool: &#root::sql::Pool,
5748            ) -> ::core::result::Result<
5749                ::std::vec::Vec<Self>,
5750                #root::sql::ExecError,
5751            > {
5752                use #root::sql::FetcherPool as _;
5753                let _key = ::std::format!("{}__between", col);
5754                let _vals = #root::core::SqlValue::List(::std::vec![
5755                    ::core::convert::Into::into(lo),
5756                    ::core::convert::Into::into(hi),
5757                ]);
5758                #root::query::QuerySet::<Self>::default()
5759                    .filter(&_key, _vals)
5760                    .fetch(pool)
5761                    .await
5762            }
5763
5764            /// Fetch the first row where `<col> = <val>`. Returns
5765            /// `Ok(None)` when no row matches. Eloquent
5766            /// `Model::firstWhere($col, $val)` / Django
5767            /// `Model.objects.filter(col=val).first()` parity.
5768            ///
5769            /// Thin wrapper over `QuerySet::<Self>::default()
5770            /// .filter(col, val).first(pool)`. Use this when you
5771            /// want one row identified by a non-PK column (e.g.
5772            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5773            ///
5774            /// `val` accepts any value `Into<SqlValue>` so plain
5775            /// strings, ints, UUIDs, etc. all work.
5776            ///
5777            /// # Errors
5778            /// As `QuerySet::first`.
5779            pub async fn first_where(
5780                col: &str,
5781                val: impl ::core::convert::Into<#root::core::SqlValue>,
5782                pool: &#root::sql::Pool,
5783            ) -> ::core::result::Result<
5784                ::core::option::Option<Self>,
5785                #root::sql::ExecError,
5786            > {
5787                #root::query::QuerySet::<Self>::default()
5788                    .filter(col, val)
5789                    .first(pool)
5790                    .await
5791            }
5792
5793            #value_method
5794
5795            /// Fetch the row with the largest `field` value —
5796            /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5797            /// `Ok(None)` for an empty table. Eloquent
5798            /// `Model::latest($field)->first()` / Django
5799            /// `Model.objects.latest(field)` (non-throwing) parity.
5800            /// Thin wrapper over `QuerySet::<Self>::default()
5801            /// .latest(field, pool)`.
5802            ///
5803            /// **Field name** is the Rust field ident as a string
5804            /// (not the SQL column). Unknown fields surface as
5805            /// `ExecError::Query(QueryError::UnknownField)` at
5806            /// compile time.
5807            ///
5808            /// # Errors
5809            /// As `QuerySet::latest`.
5810            pub async fn latest(
5811                field: &str,
5812                pool: &#root::sql::Pool,
5813            ) -> ::core::result::Result<
5814                ::core::option::Option<Self>,
5815                #root::sql::ExecError,
5816            > {
5817                #root::query::QuerySet::<Self>::default()
5818                    .latest(field, pool)
5819                    .await
5820            }
5821
5822            /// Sibling of [`Self::latest_pool`] — fetches the row
5823            /// with the smallest `field` value (`ORDER BY <field>
5824            /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5825            /// ->first()` / Django `Model.objects.earliest(field)`
5826            /// parity.
5827            ///
5828            /// # Errors
5829            /// As [`Self::latest_pool`].
5830            pub async fn earliest(
5831                field: &str,
5832                pool: &#root::sql::Pool,
5833            ) -> ::core::result::Result<
5834                ::core::option::Option<Self>,
5835                #root::sql::ExecError,
5836            > {
5837                #root::query::QuerySet::<Self>::default()
5838                    .earliest(field, pool)
5839                    .await
5840            }
5841
5842            #count_method
5843
5844            /// `true` when the table contains at least one row.
5845            /// Eloquent `Model::query()->exists()` / Django
5846            /// `Model.objects.exists()` parity. Thin wrapper over
5847            /// `QuerySet::<Self>::default().exists(pool)`.
5848            ///
5849            /// # Errors
5850            /// As [`ExistsPool::exists`].
5851            ///
5852            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5853            pub async fn exists(
5854                pool: &#root::sql::Pool,
5855            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5856                use #root::sql::ExistsPool as _;
5857                #root::query::QuerySet::<Self>::default()
5858                    .exists(pool)
5859                    .await
5860            }
5861
5862            /// Inverse of [`Self::exists`] — returns `true` when
5863            /// the table has zero rows. Eloquent
5864            /// `Model::doesntExist()` parity.
5865            ///
5866            /// # Errors
5867            /// As [`Self::exists`].
5868            pub async fn doesnt_exist(
5869                pool: &#root::sql::Pool,
5870            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5871                Self::exists(pool).await.map(|e| !e)
5872            }
5873
5874            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5875            /// `true` when a row with primary key `pk` exists in
5876            /// the table. Sugar over
5877            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5878            ///
5879            /// Differs from [`Self::exists`] (which checks "any row
5880            /// in the table") by checking a specific PK existence.
5881            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5882            /// because the row is never materialized — the SQL is
5883            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5884            ///
5885            /// # Errors
5886            /// As [`#root::sql::ExistsPool::contains_pk`].
5887            pub async fn contains_pk(
5888                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5889                pool: &#root::sql::Pool,
5890            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5891                use #root::sql::ExistsPool as _;
5892                #root::query::QuerySet::<Self>::default()
5893                    .contains_pk(pool, pk)
5894                    .await
5895            }
5896
5897            #sum_method
5898            #avg_method
5899            #min_method
5900            #max_method
5901
5902            /// Internal: forward to
5903            /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5904            /// Backs `sum` / `avg` / `min` / `max`.
5905            #[doc(hidden)]
5906            pub async fn __aggregate_one_pool<U>(
5907                col: &str,
5908                build: fn(&'static str) -> #root::core::AggregateExpr,
5909                pool: &#root::sql::Pool,
5910            ) -> ::core::result::Result<
5911                ::core::option::Option<U>,
5912                #root::sql::ExecError,
5913            >
5914            where
5915                (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5916                    + #root::sql::MaybeMyFromRow
5917                    + #root::sql::MaybeSqliteFromRow
5918                    + ::core::marker::Send
5919                    + ::core::marker::Unpin,
5920            {
5921                #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
5922                    .await
5923            }
5924
5925            /// Fetch every row of this model from `pool`. Eloquent
5926            /// `Model::all()` parity — a thin wrapper over
5927            /// `QuerySet::<Self>::default().fetch(pool)`.
5928            ///
5929            /// **Use with care on large tables** — there's no
5930            /// pagination or limit; the entire table is materialized
5931            /// into memory. For anything beyond fixture / lookup
5932            /// tables, page through `QuerySet::<Self>::default()
5933            /// .order_by(...).limit(N).offset(M).fetch(pool)`
5934            /// or stream via `.iterator(chunk_size)`.
5935            ///
5936            /// # Errors
5937            /// As [`FetcherPool::fetch`].
5938            ///
5939            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5940            pub async fn all(
5941                pool: &#root::sql::Pool,
5942            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5943            {
5944                use #root::sql::FetcherPool as _;
5945                #root::query::QuerySet::<Self>::default()
5946                    .fetch(pool)
5947                    .await
5948            }
5949
5950            /// Look up every row whose primary key is in `pks`.
5951            /// Returns the matching rows in **inventory** order — NOT
5952            /// the order of `pks`. Empty `pks` returns an empty
5953            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5954            /// with a list) / Django `Model.objects.filter(pk__in=[...])`
5955            /// parity.
5956            ///
5957            /// Thin wrapper over `QuerySet::<Self>::default()
5958            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
5959            /// Caller-supplied PKs that don't match a row are
5960            /// silently skipped (the returned vec is shorter than
5961            /// the input list). For an order-preserving / "fail
5962            /// when any missing" variant, build the queryset
5963            /// explicitly with `in_bulk` instead.
5964            ///
5965            /// Accepts any iterable whose elements are
5966            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5967            /// `Vec<Uuid>`, etc.
5968            ///
5969            /// # Errors
5970            /// As [`FetcherPool::fetch`].
5971            ///
5972            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5973            pub async fn find_many<V>(
5974                pks: impl ::core::iter::IntoIterator<Item = V>,
5975                pool: &#root::sql::Pool,
5976            ) -> ::core::result::Result<
5977                ::std::vec::Vec<Self>,
5978                #root::sql::ExecError,
5979            >
5980            where
5981                V: ::core::convert::Into<#root::core::SqlValue>,
5982            {
5983                use #root::sql::FetcherPool as _;
5984                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5985                    pks.into_iter().map(::core::convert::Into::into).collect();
5986                if _values.is_empty() {
5987                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5988                }
5989                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
5990                #root::query::QuerySet::<Self>::default()
5991                    .filter(&_key, #root::core::SqlValue::List(_values))
5992                    .fetch(pool)
5993                    .await
5994            }
5995
5996            /// Look up the row whose primary key equals `pk`. Returns
5997            /// `Ok(None)` when no row matches; this is the
5998            /// non-throwing counterpart of Django's `.get(pk=…)`
5999            /// (which raises `DoesNotExist`). Eloquent `Model::find`
6000            /// shape — accepts any value `Into<SqlValue>`.
6001            ///
6002            /// One-liner shortcut for the common
6003            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6004            /// .limit(1).fetch(pool).await?.into_iter().next()`
6005            /// dance.
6006            ///
6007            /// # Errors
6008            /// As [`FetcherPool::fetch`].
6009            ///
6010            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6011            pub async fn find(
6012                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6013                pool: &#root::sql::Pool,
6014            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6015            {
6016                use #root::sql::FetcherPool as _;
6017                let _pk_val: #root::core::SqlValue = pk.into();
6018                let mut _rows: ::std::vec::Vec<Self> =
6019                    #root::query::QuerySet::<Self>::default()
6020                        .filter(::core::stringify!(#pk_ident), _pk_val)
6021                        .limit(1)
6022                        .fetch(pool)
6023                        .await?;
6024                ::core::result::Result::Ok(_rows.into_iter().next())
6025            }
6026
6027            /// Look up the row whose primary key equals `pk`. Errors
6028            /// when no row matches — the throwing counterpart of
6029            /// [`Self::find_pool`]. Eloquent `Model::findOrFail` /
6030            /// Django `Model.objects.get(pk=…)` (which raises
6031            /// `DoesNotExist`) parity.
6032            ///
6033            /// Translates the miss into
6034            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6035            /// so callers can `?`-bubble straight through the typical
6036            /// `ExecError` error chain.
6037            ///
6038            /// # Errors
6039            /// As [`Self::find_pool`]; additionally
6040            /// [`sqlx::Error::RowNotFound`] when no row matches.
6041            ///
6042            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6043            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6044            pub async fn find_or_fail(
6045                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6046                pool: &#root::sql::Pool,
6047            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6048                match Self::find(pk, pool).await? {
6049                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6050                    ::core::option::Option::None => ::core::result::Result::Err(
6051                        #root::sql::ExecError::Driver(
6052                            #root::sql::sqlx::Error::RowNotFound,
6053                        ),
6054                    ),
6055                }
6056            }
6057
6058            /// Look up multiple rows by primary key, **failing** if
6059            /// any requested PK is missing. Eloquent
6060            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6061            /// inventory order (NOT request order — same as
6062            /// [`Self::find_many`]).
6063            ///
6064            /// Empty `pks` returns an empty `Vec` (no rows requested
6065            /// → nothing to fail on).
6066            ///
6067            /// Differs from [`Self::find_many`] in that this method
6068            /// requires every PK to resolve. If even one is missing,
6069            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6070            /// in `ExecError::Driver`). Rows are deduped at the SQL
6071            /// layer, so passing the same PK twice in `pks` counts as
6072            /// one expected row.
6073            ///
6074            /// # Errors
6075            /// As [`Self::find_many`], plus `RowNotFound` when the
6076            /// returned row count is less than the count of distinct
6077            /// requested PKs.
6078            pub async fn find_many_or_fail<V>(
6079                pks: impl ::core::iter::IntoIterator<Item = V>,
6080                pool: &#root::sql::Pool,
6081            ) -> ::core::result::Result<
6082                ::std::vec::Vec<Self>,
6083                #root::sql::ExecError,
6084            >
6085            where
6086                V: ::core::convert::Into<#root::core::SqlValue>,
6087            {
6088                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6089                    pks.into_iter().map(::core::convert::Into::into).collect();
6090                if _values.is_empty() {
6091                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6092                }
6093                // Dedup the requested PK list before counting so
6094                // duplicate-PK requests don't false-fail (the SQL
6095                // `IN (...)` clause naturally dedups too).
6096                let mut _seen: ::std::collections::HashSet<
6097                    ::std::string::String,
6098                > = ::std::collections::HashSet::new();
6099                for v in &_values {
6100                    _seen.insert(v.to_display_string());
6101                }
6102                let _expected = _seen.len();
6103                let _rows = Self::find_many(_values, pool).await?;
6104                if _rows.len() < _expected {
6105                    return ::core::result::Result::Err(
6106                        #root::sql::ExecError::Driver(
6107                            #root::sql::sqlx::Error::RowNotFound,
6108                        ),
6109                    );
6110                }
6111                ::core::result::Result::Ok(_rows)
6112            }
6113
6114            /// Find by primary key or run `fallback` to produce a
6115            /// default row to return. Eloquent
6116            /// `Model::findOr($pk, fn() => …)` parity.
6117            ///
6118            /// Unlike [`Self::find_or_fail_pool`] (which raises on
6119            /// miss), this is the "give me something sensible"
6120            /// branch: typical use is "fetch the user's row, else
6121            /// fall back to an anonymous/guest stub".
6122            ///
6123            /// `fallback` runs only when no row matches — the DB
6124            /// round-trip happens unconditionally.
6125            ///
6126            /// # Errors
6127            /// As [`Self::find_pool`].
6128            pub async fn find_or<F>(
6129                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6130                pool: &#root::sql::Pool,
6131                fallback: F,
6132            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6133            where
6134                F: ::core::ops::FnOnce() -> Self,
6135            {
6136                ::core::result::Result::Ok(
6137                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6138                )
6139            }
6140
6141            /// Same as [`Self::find_or`] but also returns a `bool`
6142            /// indicating whether the row was found in the DB
6143            /// (`true`) or freshly built from `fallback` (`false`).
6144            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6145            /// in PHP the returned model also exposes `->exists`;
6146            /// here that's surfaced as the second tuple element.
6147            ///
6148            /// Useful for edit-or-create form handlers where the
6149            /// caller needs to know whether to PATCH or POST when
6150            /// the user submits the form.
6151            ///
6152            /// # Errors
6153            /// As [`Self::find`].
6154            pub async fn find_or_new<F>(
6155                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6156                pool: &#root::sql::Pool,
6157                fallback: F,
6158            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6159            where
6160                F: ::core::ops::FnOnce() -> Self,
6161            {
6162                match Self::find(pk, pool).await? {
6163                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6164                    ::core::option::Option::None => {
6165                        ::core::result::Result::Ok((fallback(), false))
6166                    }
6167                }
6168            }
6169
6170            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6171            /// [`Self::find_or_new`] but **persists** the new row
6172            /// when the PK isn't found. Returns `(row, exists: bool)`
6173            /// — `exists=true` when the PK was found,
6174            /// `false` when a fresh row was inserted.
6175            ///
6176            /// ```ignore
6177            /// let (post, found) = Post::find_or_insert(
6178            ///     pk,
6179            ///     &pool,
6180            ///     || Post { id: Auto::default(), title: "new".into() },
6181            /// ).await?;
6182            /// // `found` true → returned existing row;
6183            /// // `found` false → fallback was inserted, `post.id` populated.
6184            /// ```
6185            ///
6186            /// **Caveat**: two concurrent calls that both miss the
6187            /// find can both INSERT, violating uniqueness. Wrap in a
6188            /// transaction or rely on a UNIQUE constraint + handle
6189            /// the conflict error if you need race-free semantics.
6190            ///
6191            /// # Errors
6192            /// As [`Self::find`] and [`Self::save_pool`].
6193            pub async fn find_or_insert<F>(
6194                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6195                pool: &#root::sql::Pool,
6196                fallback: F,
6197            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6198            where
6199                F: ::core::ops::FnOnce() -> Self,
6200            {
6201                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6202                    return ::core::result::Result::Ok((_row, true));
6203                }
6204                let mut _new = fallback();
6205                _new.save_pool(pool).await?;
6206                ::core::result::Result::Ok((_new, false))
6207            }
6208
6209            /// Fetch the first row of the table, or run `fallback`
6210            /// when the table is empty. Eloquent
6211            /// `Model::firstOr(fn() => …)` parity.
6212            ///
6213            /// # Errors
6214            /// As [`Self::first_pool`].
6215            pub async fn first_or<F>(
6216                pool: &#root::sql::Pool,
6217                fallback: F,
6218            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6219            where
6220                F: ::core::ops::FnOnce() -> Self,
6221            {
6222                // Route through the queryset rather than `Self::first`,
6223                // which is suppressed on models with a field named
6224                // `first` (field/shortcut collision guard).
6225                ::core::result::Result::Ok(
6226                    #root::query::QuerySet::<Self>::default()
6227                        .first(pool)
6228                        .await?
6229                        .unwrap_or_else(fallback),
6230                )
6231            }
6232
6233            /// Fetch exactly one row matching `<col> = <val>`. Errors
6234            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6235            /// or more than one matches
6236            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6237            /// Eloquent `Model::sole($col, $val)` parity.
6238            ///
6239            /// # Errors
6240            /// As [`Self::where_pool`] plus the explicit
6241            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6242            pub async fn sole(
6243                col: &str,
6244                val: impl ::core::convert::Into<#root::core::SqlValue>,
6245                pool: &#root::sql::Pool,
6246            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6247                let mut _rows = Self::where_(col, val, pool).await?;
6248                match _rows.len() {
6249                    0 => ::core::result::Result::Err(
6250                        #root::sql::ExecError::Driver(
6251                            #root::sql::sqlx::Error::RowNotFound,
6252                        ),
6253                    ),
6254                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6255                    n => ::core::result::Result::Err(
6256                        #root::sql::ExecError::MultipleRowsReturned {
6257                            op: "sole",
6258                            table: <Self as #root::core::Model>::SCHEMA.name,
6259                            count: n,
6260                        },
6261                    ),
6262                }
6263            }
6264        }
6265    } else {
6266        quote!()
6267    };
6268
6269    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6270    // the non-audited `_pool` methods but execute against an open
6271    // `PoolTx` so the writes participate in the caller's transaction.
6272    // Auditing inside TX is deferred; these always use the plain
6273    // executor primitives regardless of whether the model is audited.
6274    let tx_insert_method = if fields.has_auto {
6275        let pushes = &fields.insert_pushes;
6276        let returning_cols = &fields.returning_cols;
6277        quote! {
6278            /// Insert this row inside an open transaction, populating
6279            /// any `Auto<T>` PK from the auto-assigned value. Works
6280            /// against any backend that `tx` wraps.
6281            ///
6282            /// # Errors
6283            /// As [`Self::insert_pool`].
6284            pub async fn insert_tx(
6285                &mut self,
6286                tx: &mut #root::sql::PoolTx<'_>,
6287            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6288                let mut _columns: ::std::vec::Vec<&'static str> =
6289                    ::std::vec::Vec::new();
6290                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6291                    ::std::vec::Vec::new();
6292                #( #pushes )*
6293                let _query = #root::core::InsertQuery {
6294                    model: <Self as #root::core::Model>::SCHEMA,
6295                    columns: _columns,
6296                    values: _values,
6297                    returning: ::std::vec![ #( #returning_cols ),* ],
6298                    on_conflict: ::core::option::Option::None,
6299                };
6300                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6301                #root::sql::apply_auto_pk(_result, self)
6302            }
6303        }
6304    } else {
6305        let insert_columns = &fields.insert_columns;
6306        let insert_values = &fields.insert_values;
6307        quote! {
6308            /// Insert this row inside an open transaction.
6309            ///
6310            /// # Errors
6311            /// As [`Self::insert_pool`].
6312            pub async fn insert_tx(
6313                &self,
6314                tx: &mut #root::sql::PoolTx<'_>,
6315            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6316                let _query = #root::core::InsertQuery {
6317                    model: <Self as #root::core::Model>::SCHEMA,
6318                    columns: ::std::vec![ #( #insert_columns ),* ],
6319                    values: ::std::vec![ #( #insert_values ),* ],
6320                    returning: ::std::vec::Vec::new(),
6321                    on_conflict: ::core::option::Option::None,
6322                };
6323                #root::sql::insert_tx(tx, &_query).await
6324            }
6325        }
6326    };
6327
6328    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6329        let pk_column_lit = pk_col.as_str();
6330        let assignments = &fields.update_assignments;
6331        let dispatch_unset = if fields.pk_is_auto {
6332            quote! {
6333                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6334                    return self.insert_tx(tx).await.map(|()| 1u64);
6335                }
6336            }
6337        } else {
6338            quote!()
6339        };
6340        quote! {
6341            /// Save this row inside an open transaction. `INSERT` when
6342            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6343            /// PK. Works against any backend that `tx` wraps.
6344            ///
6345            /// # Errors
6346            /// As [`Self::save_pool`].
6347            pub async fn save_tx(
6348                &mut self,
6349                tx: &mut #root::sql::PoolTx<'_>,
6350            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6351                #dispatch_unset
6352                let _query = #root::core::UpdateQuery {
6353                    model: <Self as #root::core::Model>::SCHEMA,
6354                    set: ::std::vec![ #( #assignments ),* ],
6355                    where_clause: #root::core::WhereExpr::Predicate(
6356                        #root::core::Filter {
6357                            column: #pk_column_lit,
6358                            op: #root::core::Op::Eq,
6359                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
6360                                ::core::clone::Clone::clone(&self.#pk_ident)
6361                            ),
6362                        }
6363                    ),
6364                };
6365                let _affected = #root::sql::update_tx(tx, &_query).await?;
6366                ::core::result::Result::Ok(_affected)
6367            }
6368        }
6369    } else {
6370        quote!()
6371    };
6372
6373    let tx_delete_method = {
6374        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6375        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6376        if let Some(pk_ident) = pk_ident_for_tx {
6377            quote! {
6378                /// Delete the row identified by this instance's PK
6379                /// inside an open transaction. Works against any backend
6380                /// that `tx` wraps.
6381                ///
6382                /// # Errors
6383                /// As [`Self::delete_pool`].
6384                pub async fn delete_tx(
6385                    &self,
6386                    tx: &mut #root::sql::PoolTx<'_>,
6387                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6388                    let _query = #root::core::DeleteQuery {
6389                        model: <Self as #root::core::Model>::SCHEMA,
6390                        where_clause: #root::core::WhereExpr::Predicate(
6391                            #root::core::Filter {
6392                                column: #pk_column_lit,
6393                                op: #root::core::Op::Eq,
6394                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6395                                    ::core::clone::Clone::clone(&self.#pk_ident)
6396                                ),
6397                            }
6398                        ),
6399                    };
6400                    #root::sql::delete_tx(tx, &_query).await
6401                }
6402            }
6403        } else {
6404            quote!()
6405        }
6406    };
6407
6408    // Update emission captures both BEFORE and AFTER state — runs an
6409    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6410    // each tracked field's prior value, then after the UPDATE diffs
6411    // against the in-memory `&self`. `diff_changes` drops unchanged
6412    // columns so the JSON only contains the actual delta.
6413    //
6414    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6415    // and binds `_audit_before_pairs`; `audit_update_post` runs
6416    // after the UPDATE and emits the PendingEntry.
6417    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6418        audited_fields
6419    {
6420        if tracked.is_empty() {
6421            (quote!(), quote!())
6422        } else {
6423            let select_cols: String = tracked
6424                .iter()
6425                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6426                .collect::<Vec<_>>()
6427                .join(", ");
6428            let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6429            let select_cols_lit = select_cols;
6430            let pk_column_lit_for_select = pk_column_for_select;
6431            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6432                if fields.pk_is_auto {
6433                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6434                } else {
6435                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6436                }
6437            } else {
6438                quote!(0_i64)
6439            };
6440            let before_pairs = tracked.iter().map(|c| {
6441                let column_lit = c.column.as_str();
6442                let value_ty = &c.value_ty;
6443                quote! {
6444                    (
6445                        #column_lit,
6446                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6447                            &_audit_before_row, #column_lit,
6448                        ) {
6449                            ::core::result::Result::Ok(v) => {
6450                                #root::__serde_json::to_value(&v)
6451                                    .unwrap_or(#root::__serde_json::Value::Null)
6452                            }
6453                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6454                        },
6455                    )
6456                }
6457            });
6458            let after_pairs = tracked.iter().map(|c| {
6459                let column_lit = c.column.as_str();
6460                let ident = &c.ident;
6461                quote! {
6462                    (
6463                        #column_lit,
6464                        #root::__serde_json::to_value(&self.#ident)
6465                            .unwrap_or(#root::__serde_json::Value::Null),
6466                    )
6467                }
6468            });
6469            let pk_str = audit_pk_to_string.clone();
6470            let pre = quote! {
6471                let _audit_select_sql = ::std::format!(
6472                    r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6473                    #select_cols_lit,
6474                    <Self as #root::core::Model>::SCHEMA.table,
6475                    #pk_column_lit_for_select,
6476                );
6477                let _audit_before_pairs:
6478                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6479                    match #root::sql::sqlx::query(&_audit_select_sql)
6480                        .bind(#pk_value_for_bind)
6481                        .fetch_optional(&mut *_executor)
6482                        .await
6483                    {
6484                        ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6485                            ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6486                        }
6487                        _ => ::core::option::Option::None,
6488                    };
6489            };
6490            let post = quote! {
6491                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6492                    let _audit_after:
6493                        ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6494                        ::std::vec![ #( #after_pairs ),* ];
6495                    let _audit_entry = #root::audit::PendingEntry {
6496                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
6497                        entity_pk: #pk_str,
6498                        operation: #root::audit::AuditOp::Update,
6499                        source: #root::audit::current_source(),
6500                        changes: #root::audit::diff_changes(
6501                            &_audit_before,
6502                            &_audit_after,
6503                        ),
6504                    };
6505                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6506                }
6507            };
6508            (pre, post)
6509        }
6510    } else {
6511        (quote!(), quote!())
6512    };
6513
6514    // Bulk-insert audit: capture every row's tracked fields after the
6515    // RETURNING populates each PK, then push one batched INSERT INTO
6516    // audit_log via `emit_many`. One round-trip regardless of N rows.
6517    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6518        let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6519            if fields.pk_is_auto {
6520                quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6521            } else {
6522                quote!(::std::format!("{}", &_row.#pk_ident))
6523            }
6524        } else {
6525            quote!(::std::string::String::new())
6526        };
6527        let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6528            let column_lit = c.column.as_str();
6529            let ident = &c.ident;
6530            quote! {
6531                (
6532                    #column_lit,
6533                    #root::__serde_json::to_value(&_row.#ident)
6534                        .unwrap_or(#root::__serde_json::Value::Null),
6535                )
6536            }
6537        });
6538        quote! {
6539            let _audit_source = #root::audit::current_source();
6540            let mut _audit_entries:
6541                ::std::vec::Vec<#root::audit::PendingEntry> =
6542                    ::std::vec::Vec::with_capacity(rows.len());
6543            for _row in rows.iter() {
6544                _audit_entries.push(#root::audit::PendingEntry {
6545                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
6546                    entity_pk: #row_pk_str,
6547                    operation: #root::audit::AuditOp::Create,
6548                    source: _audit_source.clone(),
6549                    changes: #root::audit::snapshot_changes(&[
6550                        #( #row_pairs ),*
6551                    ]),
6552                });
6553            }
6554            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6555        }
6556    } else {
6557        quote!()
6558    };
6559
6560    let save_method = if fields.pk_is_auto {
6561        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6562        let pk_column_lit = pk_column.as_str();
6563        let assignments = &fields.update_assignments;
6564        let upsert_cols = &fields.upsert_update_columns;
6565        let upsert_pushes = &fields.insert_pushes;
6566        let upsert_returning = &fields.returning_cols;
6567        let upsert_auto_assigns = &fields.auto_assigns;
6568        // Conflict target: prefer the first declared `unique_together`
6569        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6570        // `BIGSERIAL` and never collide on insert, so a PK-only target
6571        // would silently turn `upsert()` into "always-insert" for
6572        // surrogate-PK models with composite UNIQUE constraints — see
6573        // `RolePermission` / `UserRole` / `UserPermission` in the
6574        // tenancy permission engine. When no `unique_together` is
6575        // declared we keep the PK target (the original behaviour).
6576        let upsert_target_columns: Vec<String> = indexes
6577            .iter()
6578            .find(|i| i.unique && !i.columns.is_empty())
6579            .map(|i| i.columns.clone())
6580            .unwrap_or_else(|| vec![pk_column.clone()]);
6581        let upsert_target_lits = upsert_target_columns
6582            .iter()
6583            .map(String::as_str)
6584            .collect::<Vec<_>>();
6585        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6586            quote!(#root::core::ConflictClause::DoNothing)
6587        } else {
6588            quote!(#root::core::ConflictClause::DoUpdate {
6589                target: ::std::vec![ #( #upsert_target_lits ),* ],
6590                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6591            })
6592        };
6593        Some(quote! {
6594            /// Insert this row if its `Auto<T>` primary key is
6595            /// `Unset`, otherwise update the existing row matching the
6596            /// PK. Mirrors Django's `save()` — caller doesn't need to
6597            /// pick `insert` vs the bulk-update path manually.
6598            ///
6599            /// On the insert branch, populates the PK from `RETURNING`
6600            /// (same behavior as `insert`). On the update branch,
6601            /// writes every non-PK column back; if no row matches the
6602            /// PK, returns `Ok(())` silently.
6603            ///
6604            /// Only generated when the primary key is declared as
6605            /// `Auto<T>`. Models with a manually-managed PK must use
6606            /// `insert` or the QuerySet update builder.
6607            ///
6608            /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6609            /// driver-specific `PgPool`, so it does not exist at all on a
6610            /// build without the `postgres` feature: selecting `sqlite`
6611            /// makes the method vanish rather than fail with anything that
6612            /// names the cause.
6613            ///
6614            /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6615            /// all three backends. The naming is inverted — the short name
6616            /// is the narrow one — and that is tracked in #1293.
6617            ///
6618            /// # Errors
6619            /// Returns [`#root::sql::ExecError`] for SQL-writing
6620            /// or driver failures.
6621            #[cfg(feature = "postgres")]
6622            pub async fn save(
6623                &mut self,
6624                pool: &#root::sql::sqlx::PgPool,
6625            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6626                #pool_to_save_on
6627            }
6628
6629            /// Like [`Self::save`] but accepts any sqlx executor —
6630            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6631            /// escape hatch for tenant-scoped writes: schema-mode
6632            /// tenants share the registry pool but rely on a per-
6633            /// checkout `SET search_path`, so passing `&PgPool` would
6634            /// silently hit the wrong schema. Acquire a connection
6635            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6636            ///
6637            /// # Errors
6638            /// As [`Self::save`].
6639            #[cfg(feature = "postgres")]
6640            pub async fn save_on #executor_generics (
6641                &mut self,
6642                #executor_param,
6643            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6644            #executor_where
6645            {
6646                // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6647                // the rows-affected count (0 when the PK no longer exists,
6648                // the Django 6.0 `Model.NotUpdated` signal).
6649                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6650                    return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6651                }
6652                #audit_update_pre
6653                let _query = #root::core::UpdateQuery {
6654                    model: <Self as #root::core::Model>::SCHEMA,
6655                    set: ::std::vec![ #( #assignments ),* ],
6656                    where_clause: #root::core::WhereExpr::Predicate(
6657                        #root::core::Filter {
6658                            column: #pk_column_lit,
6659                            op: #root::core::Op::Eq,
6660                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
6661                                ::core::clone::Clone::clone(&self.#pk_ident)
6662                            ),
6663                        }
6664                    ),
6665                };
6666                let _affected = #root::sql::__macro_internals::update_on(
6667                    #executor_passes_to_data_write,
6668                    &_query,
6669                ).await?;
6670                #audit_update_post
6671                ::core::result::Result::Ok(_affected)
6672            }
6673
6674            /// Per-call override for the audit source. Runs
6675            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6676            /// scope so the resulting audit entry records `source`
6677            /// instead of the task-local default. Useful for seed
6678            /// scripts and one-off CLI tools that don't sit inside an
6679            /// admin handler. The override applies only to this call;
6680            /// no global state changes.
6681            ///
6682            /// # Errors
6683            /// As [`Self::save_on`].
6684            #[cfg(feature = "postgres")]
6685            pub async fn save_on_with #executor_generics (
6686                &mut self,
6687                #executor_param,
6688                source: #root::audit::AuditSource,
6689            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6690            #executor_where
6691            {
6692                #root::audit::with_source(source, self.save_on(_executor)).await
6693            }
6694
6695            /// Insert this row or update it in-place if the primary key already
6696            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6697            ///
6698            /// With `Auto::Unset` PK the server assigns a new key and no conflict
6699            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6700            /// inserted if absent or all non-PK columns are overwritten if present.
6701            ///
6702            /// # Errors
6703            /// As [`Self::insert_on`].
6704            #[cfg(feature = "postgres")]
6705            pub async fn upsert(
6706                &mut self,
6707                pool: &#root::sql::sqlx::PgPool,
6708            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6709                #pool_to_upsert_on
6710            }
6711
6712            /// Like [`Self::upsert`] but accepts any sqlx executor.
6713            /// See [`Self::save_on`] for tenancy-scoped rationale.
6714            ///
6715            /// # Errors
6716            /// As [`Self::upsert`].
6717            #[cfg(feature = "postgres")]
6718            pub async fn upsert_on #executor_generics (
6719                &mut self,
6720                #executor_param,
6721            ) -> ::core::result::Result<(), #root::sql::ExecError>
6722            #executor_where
6723            {
6724                let mut _columns: ::std::vec::Vec<&'static str> =
6725                    ::std::vec::Vec::new();
6726                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6727                    ::std::vec::Vec::new();
6728                #( #upsert_pushes )*
6729                let query = #root::core::InsertQuery {
6730                    model: <Self as #root::core::Model>::SCHEMA,
6731                    columns: _columns,
6732                    values: _values,
6733                    returning: ::std::vec![ #( #upsert_returning ),* ],
6734                    on_conflict: ::core::option::Option::Some(#conflict_clause),
6735                };
6736                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6737                    #executor_passes_to_data_write,
6738                    &query,
6739                ).await?;
6740                let _returning_row = &_returning_row_v;
6741                #( #upsert_auto_assigns )*
6742                ::core::result::Result::Ok(())
6743            }
6744        })
6745    } else {
6746        None
6747    };
6748
6749    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6750        let pk_column_lit = pk_column.as_str();
6751        // Optional `soft_delete_on` / `restore_on` companions when the
6752        // model has a `#[rustango(soft_delete)]` column. They land
6753        // alongside the regular `delete_on` so callers have both
6754        // options — a hard delete (audit-tracked as a real DELETE) and
6755        // a logical delete (audit-tracked as an UPDATE setting the
6756        // deleted_at column to NOW()).
6757        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6758            let col_lit = col;
6759            let sd_field_ident = fields
6760                .soft_delete_field_ident
6761                .clone()
6762                .expect("soft_delete_column without ident");
6763            quote! {
6764                /// Soft-delete this row by setting its
6765                /// `#[rustango(soft_delete)]` column to `NOW()`.
6766                /// Mirrors Django's `SoftDeleteModel.delete()` shape:
6767                /// the row stays in the table; query helpers can
6768                /// filter it out by checking the column for `IS NOT
6769                /// NULL`.
6770                ///
6771                /// # Errors
6772                /// As [`Self::delete`].
6773                #[cfg(feature = "postgres")]
6774                pub async fn soft_delete_on #executor_generics (
6775                    &self,
6776                    #executor_param,
6777                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6778                #executor_where
6779                {
6780                    let _query = #root::core::UpdateQuery {
6781                        model: <Self as #root::core::Model>::SCHEMA,
6782                        set: ::std::vec![
6783                            #root::core::Assignment {
6784                                column: #col_lit,
6785                                value: ::core::convert::Into::<#root::core::Expr>::into(
6786                                    ::core::convert::Into::<#root::core::SqlValue>::into(
6787                                        #root::__chrono::Utc::now()
6788                                    )
6789                                ),
6790                            },
6791                        ],
6792                        where_clause: #root::core::WhereExpr::Predicate(
6793                            #root::core::Filter {
6794                                column: #pk_column_lit,
6795                                op: #root::core::Op::Eq,
6796                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6797                                    ::core::clone::Clone::clone(&self.#pk_ident)
6798                                ),
6799                            }
6800                        ),
6801                    };
6802                    let _affected = #root::sql::__macro_internals::update_on(
6803                        #executor_passes_to_data_write,
6804                        &_query,
6805                    ).await?;
6806                    #audit_softdelete_emit
6807                    ::core::result::Result::Ok(_affected)
6808                }
6809
6810                /// Inverse of [`Self::soft_delete_on`] — clears the
6811                /// soft-delete column back to NULL so the row is
6812                /// considered live again.
6813                ///
6814                /// # Errors
6815                /// As [`Self::delete`].
6816                #[cfg(feature = "postgres")]
6817                pub async fn restore_on #executor_generics (
6818                    &self,
6819                    #executor_param,
6820                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6821                #executor_where
6822                {
6823                    let _query = #root::core::UpdateQuery {
6824                        model: <Self as #root::core::Model>::SCHEMA,
6825                        set: ::std::vec![
6826                            #root::core::Assignment {
6827                                column: #col_lit,
6828                                value: ::core::convert::Into::<#root::core::Expr>::into(
6829                                    #root::core::SqlValue::Null
6830                                ),
6831                            },
6832                        ],
6833                        where_clause: #root::core::WhereExpr::Predicate(
6834                            #root::core::Filter {
6835                                column: #pk_column_lit,
6836                                op: #root::core::Op::Eq,
6837                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6838                                    ::core::clone::Clone::clone(&self.#pk_ident)
6839                                ),
6840                            }
6841                        ),
6842                    };
6843                    let _affected = #root::sql::__macro_internals::update_on(
6844                        #executor_passes_to_data_write,
6845                        &_query,
6846                    ).await?;
6847                    #audit_restore_emit
6848                    ::core::result::Result::Ok(_affected)
6849                }
6850
6851                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6852                /// — takes [`#root::sql::Pool`] and dispatches per
6853                /// backend. Eloquent `Model::delete()` semantics on
6854                /// soft-delete-enabled models (closes #821 partial).
6855                ///
6856                /// Sets the `#[rustango(soft_delete)]` column to
6857                /// `NOW()` on every backend. Query helpers
6858                /// (`QuerySet::active()` / `only_trashed()`,
6859                /// `soft_delete::active_filter` /
6860                /// `compose_with_active`) filter trashed rows out by
6861                /// reading `IS NULL` on the same column.
6862                ///
6863                /// # Errors
6864                /// As [`#root::sql::update_pool`].
6865                pub async fn soft_delete(
6866                    &self,
6867                    pool: &#root::sql::Pool,
6868                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6869                    let _query = #root::core::UpdateQuery {
6870                        model: <Self as #root::core::Model>::SCHEMA,
6871                        set: ::std::vec![
6872                            #root::core::Assignment {
6873                                column: #col_lit,
6874                                value: ::core::convert::Into::<#root::core::Expr>::into(
6875                                    ::core::convert::Into::<#root::core::SqlValue>::into(
6876                                        #root::__chrono::Utc::now()
6877                                    )
6878                                ),
6879                            },
6880                        ],
6881                        where_clause: #root::core::WhereExpr::Predicate(
6882                            #root::core::Filter {
6883                                column: #pk_column_lit,
6884                                op: #root::core::Op::Eq,
6885                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6886                                    ::core::clone::Clone::clone(&self.#pk_ident)
6887                                ),
6888                            }
6889                        ),
6890                    };
6891                    #root::sql::update_pool(pool, &_query).await
6892                }
6893
6894                /// Tri-dialect counterpart of [`Self::restore_on`].
6895                /// Clears the `#[rustango(soft_delete)]` column back
6896                /// to `NULL`, marking the row live again. Eloquent
6897                /// `Model::restore()` parity.
6898                ///
6899                /// # Errors
6900                /// As [`#root::sql::update_pool`].
6901                pub async fn restore(
6902                    &self,
6903                    pool: &#root::sql::Pool,
6904                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6905                    let _query = #root::core::UpdateQuery {
6906                        model: <Self as #root::core::Model>::SCHEMA,
6907                        set: ::std::vec![
6908                            #root::core::Assignment {
6909                                column: #col_lit,
6910                                value: ::core::convert::Into::<#root::core::Expr>::into(
6911                                    #root::core::SqlValue::Null
6912                                ),
6913                            },
6914                        ],
6915                        where_clause: #root::core::WhereExpr::Predicate(
6916                            #root::core::Filter {
6917                                column: #pk_column_lit,
6918                                op: #root::core::Op::Eq,
6919                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6920                                    ::core::clone::Clone::clone(&self.#pk_ident)
6921                                ),
6922                            }
6923                        ),
6924                    };
6925                    #root::sql::update_pool(pool, &_query).await
6926                }
6927
6928                /// Hard-delete this row, ignoring the soft-delete
6929                /// column. Eloquent `Model::forceDelete()` parity —
6930                /// the escape hatch when you need to actually purge
6931                /// data (GDPR, fixture cleanup, etc.).
6932                ///
6933                /// Equivalent to [`Self::delete_pool`] (the framework's
6934                /// non-soft delete) — exposed under the Eloquent name
6935                /// for muscle-memory + so soft-delete-enabled models
6936                /// have all three operations (soft / restore / force)
6937                /// in one place.
6938                ///
6939                /// # Errors
6940                /// As [`Self::delete_pool`].
6941                pub async fn force_delete(
6942                    &self,
6943                    pool: &#root::sql::Pool,
6944                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6945                    Self::delete_pool(self, pool).await
6946                }
6947
6948                /// Returns `true` when this row is soft-deleted (its
6949                /// `#[rustango(soft_delete)]` column is currently
6950                /// set — Eloquent `$model->trashed()` parity).
6951                ///
6952                /// Pure in-memory predicate over `&self`; does not
6953                /// hit the database. Useful in admin/template code
6954                /// like `{% if post.trashed() %}…{% endif %}` and in
6955                /// guard clauses on restore/force-delete flows.
6956                pub fn trashed(&self) -> bool {
6957                    ::core::option::Option::is_some(&self.#sd_field_ident)
6958                }
6959
6960                /// Fetch every row whose `#[rustango(soft_delete)]`
6961                /// column is `NULL` (a.k.a. the live, non-trashed
6962                /// rows). Eloquent's default `Model::all()` behavior on
6963                /// a soft-delete model (Eloquent auto-scopes trashed
6964                /// rows out; rustango doesn't have auto-scoping yet —
6965                /// see issue #820 — so this is the explicit shortcut).
6966                ///
6967                /// One-liner over `QuerySet::<Self>::default()
6968                /// .active().fetch(pool)`. Closes #821 partial.
6969                ///
6970                /// # Errors
6971                /// As [`#root::sql::FetcherPool::fetch`].
6972                pub async fn active(
6973                    pool: &#root::sql::Pool,
6974                ) -> ::core::result::Result<
6975                    ::std::vec::Vec<Self>,
6976                    #root::sql::ExecError,
6977                > {
6978                    use #root::sql::FetcherPool as _;
6979                    #root::query::QuerySet::<Self>::default()
6980                        .active()
6981                        .fetch(pool)
6982                        .await
6983                }
6984
6985                /// Fetch ONLY soft-deleted rows. Eloquent
6986                /// `Model::onlyTrashed()->get()` parity — drives the
6987                /// admin "Trash" page, restore flows, GDPR purge
6988                /// scans, etc.
6989                ///
6990                /// One-liner over `QuerySet::<Self>::default()
6991                /// .only_trashed().fetch(pool)`. Closes #821
6992                /// partial.
6993                ///
6994                /// # Errors
6995                /// As [`#root::sql::FetcherPool::fetch`].
6996                pub async fn only_trashed(
6997                    pool: &#root::sql::Pool,
6998                ) -> ::core::result::Result<
6999                    ::std::vec::Vec<Self>,
7000                    #root::sql::ExecError,
7001                > {
7002                    use #root::sql::FetcherPool as _;
7003                    #root::query::QuerySet::<Self>::default()
7004                        .only_trashed()
7005                        .fetch(pool)
7006                        .await
7007                }
7008
7009                /// Fetch every row, both live and soft-deleted.
7010                /// Eloquent `Model::withTrashed()->get()` parity.
7011                ///
7012                /// Today every queryset already includes trashed rows
7013                /// (rustango has no global-scope tracking yet — issue
7014                /// #820), so this is functionally equivalent to
7015                /// [`Self::all_pool`]. Exposed as a named shortcut so
7016                /// soft-delete-aware code reads `Model::with_trashed_pool`
7017                /// rather than `Model::all_pool` — keeps intent visible
7018                /// in callers and stays correct when auto-scoping lands.
7019                ///
7020                /// Closes #821 partial.
7021                ///
7022                /// # Errors
7023                /// As [`#root::sql::FetcherPool::fetch`].
7024                pub async fn with_trashed(
7025                    pool: &#root::sql::Pool,
7026                ) -> ::core::result::Result<
7027                    ::std::vec::Vec<Self>,
7028                    #root::sql::ExecError,
7029                > {
7030                    use #root::sql::FetcherPool as _;
7031                    #root::query::QuerySet::<Self>::default()
7032                        .with_trashed()
7033                        .fetch(pool)
7034                        .await
7035                }
7036
7037            }
7038        } else {
7039            quote!()
7040        };
7041        quote! {
7042            /// Delete the row identified by this instance's primary key.
7043            ///
7044            /// Returns the number of rows affected (0 or 1).
7045            ///
7046            /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7047            /// a driver-specific `PgPool`, so it does not exist on a build
7048            /// without the `postgres` feature. [`Self::delete_pool`] takes
7049            /// `rustango::sql::Pool` and works on all three. The naming is
7050            /// inverted; tracked in #1293.
7051            ///
7052            /// # Errors
7053            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7054            /// driver failures.
7055            #[cfg(feature = "postgres")]
7056            pub async fn delete(
7057                &self,
7058                pool: &#root::sql::sqlx::PgPool,
7059            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7060                #pool_to_delete_on
7061            }
7062
7063            /// Like [`Self::delete`] but accepts any sqlx executor —
7064            /// for tenant-scoped deletes against an explicitly-acquired
7065            /// connection. See [`Self::save_on`] for the rationale.
7066            ///
7067            /// # Errors
7068            /// As [`Self::delete`].
7069            #[cfg(feature = "postgres")]
7070            pub async fn delete_on #executor_generics (
7071                &self,
7072                #executor_param,
7073            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7074            #executor_where
7075            {
7076                let query = #root::core::DeleteQuery {
7077                    model: <Self as #root::core::Model>::SCHEMA,
7078                    where_clause: #root::core::WhereExpr::Predicate(
7079                        #root::core::Filter {
7080                            column: #pk_column_lit,
7081                            op: #root::core::Op::Eq,
7082                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
7083                                ::core::clone::Clone::clone(&self.#pk_ident)
7084                            ),
7085                        }
7086                    ),
7087                };
7088                let _affected = #root::sql::__macro_internals::delete_on(
7089                    #executor_passes_to_data_write,
7090                    &query,
7091                ).await?;
7092                #audit_delete_emit
7093                ::core::result::Result::Ok(_affected)
7094            }
7095
7096            /// Per-call audit-source override for [`Self::delete_on`].
7097            /// See [`Self::save_on_with`] for shape rationale.
7098            ///
7099            /// # Errors
7100            /// As [`Self::delete_on`].
7101            #[cfg(feature = "postgres")]
7102            pub async fn delete_on_with #executor_generics (
7103                &self,
7104                #executor_param,
7105                source: #root::audit::AuditSource,
7106            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7107            #executor_where
7108            {
7109                #root::audit::with_source(source, self.delete_on(_executor)).await
7110            }
7111            #pool_delete_method
7112            #pool_insert_method
7113            #pool_save_method
7114            #refresh_replicate_methods
7115            #tx_delete_method
7116            #tx_insert_method
7117            #tx_save_method
7118            #soft_delete_methods
7119
7120            /// Returns `true` when `other` represents the same DB
7121            /// row as `self` — i.e. their primary keys compare
7122            /// equal. Eloquent `$model->is($other)` parity.
7123            ///
7124            /// Because both arguments are typed `&Self`, the
7125            /// model/table check is automatic — `Post::is` cannot
7126            /// be invoked against a `Comment` at compile time. Only
7127            /// the PK has to be compared at runtime.
7128            pub fn is(&self, other: &Self) -> bool {
7129                self.#pk_ident == other.#pk_ident
7130            }
7131
7132            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7133            /// parity.
7134            pub fn is_not(&self, other: &Self) -> bool {
7135                self.#pk_ident != other.#pk_ident
7136            }
7137
7138            /// Returns this row's primary-key value as an
7139            /// [`#root::core::SqlValue`]. Eloquent
7140            /// `$model->getKey()` parity.
7141            ///
7142            /// Useful when you need to thread the PK through a
7143            /// generic `Into<SqlValue>`-bound API without knowing the
7144            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7145            #[must_use]
7146            pub fn get_key(&self) -> #root::core::SqlValue {
7147                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7148            }
7149
7150        }
7151    });
7152
7153    let insert_method = if fields.has_auto {
7154        let pushes = &fields.insert_pushes;
7155        let returning_cols = &fields.returning_cols;
7156        let auto_assigns = &fields.auto_assigns;
7157        quote! {
7158            /// Insert this row into its table. Skips columns whose
7159            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7160            /// sequence fills them in, then reads each `Auto` column
7161            /// back via `RETURNING` and stores it on `self`.
7162            ///
7163            /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7164            /// a driver-specific `PgPool`, so it does not exist on a build
7165            /// without the `postgres` feature. [`Self::insert_pool`] takes
7166            /// `rustango::sql::Pool` and works on all three. The naming is
7167            /// inverted; tracked in #1293.
7168            ///
7169            /// # Errors
7170            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7171            /// driver failures.
7172            #[cfg(feature = "postgres")]
7173            pub async fn insert(
7174                &mut self,
7175                pool: &#root::sql::sqlx::PgPool,
7176            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7177                #pool_to_insert_on
7178            }
7179
7180            /// Like [`Self::insert`] but accepts any sqlx executor.
7181            /// See [`Self::save_on`] for tenancy-scoped rationale.
7182            ///
7183            /// # Errors
7184            /// As [`Self::insert`].
7185            #[cfg(feature = "postgres")]
7186            pub async fn insert_on #executor_generics (
7187                &mut self,
7188                #executor_param,
7189            ) -> ::core::result::Result<(), #root::sql::ExecError>
7190            #executor_where
7191            {
7192                let mut _columns: ::std::vec::Vec<&'static str> =
7193                    ::std::vec::Vec::new();
7194                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7195                    ::std::vec::Vec::new();
7196                #( #pushes )*
7197                let query = #root::core::InsertQuery {
7198                    model: <Self as #root::core::Model>::SCHEMA,
7199                    columns: _columns,
7200                    values: _values,
7201                    returning: ::std::vec![ #( #returning_cols ),* ],
7202                    on_conflict: ::core::option::Option::None,
7203                };
7204                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7205                    #executor_passes_to_data_write,
7206                    &query,
7207                ).await?;
7208                let _returning_row = &_returning_row_v;
7209                #( #auto_assigns )*
7210                #audit_insert_emit
7211                ::core::result::Result::Ok(())
7212            }
7213
7214            /// Per-call audit-source override for [`Self::insert_on`].
7215            /// See [`Self::save_on_with`] for shape rationale.
7216            ///
7217            /// # Errors
7218            /// As [`Self::insert_on`].
7219            #[cfg(feature = "postgres")]
7220            pub async fn insert_on_with #executor_generics (
7221                &mut self,
7222                #executor_param,
7223                source: #root::audit::AuditSource,
7224            ) -> ::core::result::Result<(), #root::sql::ExecError>
7225            #executor_where
7226            {
7227                #root::audit::with_source(source, self.insert_on(_executor)).await
7228            }
7229        }
7230    } else {
7231        let insert_columns = &fields.insert_columns;
7232        let insert_values = &fields.insert_values;
7233        quote! {
7234            /// Insert this row into its table.
7235            ///
7236            /// # Errors
7237            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7238            /// driver failures.
7239            #[cfg(feature = "postgres")]
7240            pub async fn insert(
7241                &self,
7242                pool: &#root::sql::sqlx::PgPool,
7243            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7244                self.insert_on(pool).await
7245            }
7246
7247            /// Like [`Self::insert`] but accepts any sqlx executor.
7248            /// See [`Self::save_on`] for tenancy-scoped rationale.
7249            ///
7250            /// # Errors
7251            /// As [`Self::insert`].
7252            #[cfg(feature = "postgres")]
7253            pub async fn insert_on<'_c, _E>(
7254                &self,
7255                _executor: _E,
7256            ) -> ::core::result::Result<(), #root::sql::ExecError>
7257            where
7258                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7259            {
7260                let query = #root::core::InsertQuery {
7261                    model: <Self as #root::core::Model>::SCHEMA,
7262                    columns: ::std::vec![ #( #insert_columns ),* ],
7263                    values: ::std::vec![ #( #insert_values ),* ],
7264                    returning: ::std::vec::Vec::new(),
7265                    on_conflict: ::core::option::Option::None,
7266                };
7267                #root::sql::__macro_internals::insert_on(_executor, &query).await
7268            }
7269        }
7270    };
7271
7272    let bulk_insert_method = if fields.has_auto {
7273        let cols_no_auto = &fields.bulk_columns_no_auto;
7274        let cols_all = &fields.bulk_columns_all;
7275        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7276        let pushes_all = &fields.bulk_pushes_all;
7277        let returning_cols = &fields.returning_cols;
7278        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7279        let uniformity = &fields.bulk_auto_uniformity;
7280        let first_auto_ident = fields
7281            .first_auto_ident
7282            .as_ref()
7283            .expect("has_auto implies first_auto_ident is Some");
7284        quote! {
7285            /// Bulk-insert `rows` in a single round-trip. Every row's
7286            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7287            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7288            /// (caller-supplied values). Mixed Set/Unset is rejected
7289            /// — call `insert` per row for that case.
7290            ///
7291            /// Empty slice is a no-op. Each row's `Auto` fields are
7292            /// populated from the `RETURNING` clause in input order
7293            /// before this returns.
7294            ///
7295            /// # Errors
7296            /// Returns [`#root::sql::ExecError`] for validation,
7297            /// SQL-writing, mixed-Auto rejection, or driver failures.
7298            #[cfg(feature = "postgres")]
7299            pub async fn bulk_insert(
7300                rows: &mut [Self],
7301                pool: &#root::sql::sqlx::PgPool,
7302            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7303                #pool_to_bulk_insert_on
7304            }
7305
7306            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7307            /// See [`Self::save_on`] for tenancy-scoped rationale.
7308            ///
7309            /// # Errors
7310            /// As [`Self::bulk_insert`].
7311            #[cfg(feature = "postgres")]
7312            pub async fn bulk_insert_on #executor_generics (
7313                rows: &mut [Self],
7314                #executor_param,
7315            ) -> ::core::result::Result<(), #root::sql::ExecError>
7316            #executor_where
7317            {
7318                if rows.is_empty() {
7319                    return ::core::result::Result::Ok(());
7320                }
7321                let _first_unset = matches!(
7322                    rows[0].#first_auto_ident,
7323                    #root::sql::Auto::Unset
7324                );
7325                #( #uniformity )*
7326
7327                let mut _all_rows: ::std::vec::Vec<
7328                    ::std::vec::Vec<#root::core::SqlValue>,
7329                > = ::std::vec::Vec::with_capacity(rows.len());
7330                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7331                    for _row in rows.iter() {
7332                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7333                            ::std::vec::Vec::new();
7334                        #( #pushes_no_auto )*
7335                        _all_rows.push(_row_vals);
7336                    }
7337                    ::std::vec![ #( #cols_no_auto ),* ]
7338                } else {
7339                    for _row in rows.iter() {
7340                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7341                            ::std::vec::Vec::new();
7342                        #( #pushes_all )*
7343                        _all_rows.push(_row_vals);
7344                    }
7345                    ::std::vec![ #( #cols_all ),* ]
7346                };
7347
7348                let _query = #root::core::BulkInsertQuery {
7349                    model: <Self as #root::core::Model>::SCHEMA,
7350                    columns: _columns,
7351                    rows: _all_rows,
7352                    returning: ::std::vec![ #( #returning_cols ),* ],
7353                    on_conflict: ::core::option::Option::None,
7354                };
7355                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7356                    #executor_passes_to_data_write,
7357                    &_query,
7358                ).await?;
7359                if _returned.len() != rows.len() {
7360                    return ::core::result::Result::Err(
7361                        #root::sql::ExecError::Sql(
7362                            #root::sql::SqlError::BulkInsertReturningMismatch {
7363                                expected: rows.len(),
7364                                actual: _returned.len(),
7365                            }
7366                        )
7367                    );
7368                }
7369                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7370                    #auto_assigns_for_row
7371                }
7372                #audit_bulk_insert_emit
7373                ::core::result::Result::Ok(())
7374            }
7375        }
7376    } else {
7377        let cols_all = &fields.bulk_columns_all;
7378        let pushes_all = &fields.bulk_pushes_all;
7379        quote! {
7380            /// Bulk-insert `rows` in a single round-trip. Every row's
7381            /// fields are written verbatim — there are no `Auto<T>`
7382            /// fields on this model.
7383            ///
7384            /// Empty slice is a no-op.
7385            ///
7386            /// # Errors
7387            /// Returns [`#root::sql::ExecError`] for validation,
7388            /// SQL-writing, or driver failures.
7389            #[cfg(feature = "postgres")]
7390            pub async fn bulk_insert(
7391                rows: &[Self],
7392                pool: &#root::sql::sqlx::PgPool,
7393            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7394                Self::bulk_insert_on(rows, pool).await
7395            }
7396
7397            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7398            /// See [`Self::save_on`] for tenancy-scoped rationale.
7399            ///
7400            /// # Errors
7401            /// As [`Self::bulk_insert`].
7402            #[cfg(feature = "postgres")]
7403            pub async fn bulk_insert_on<'_c, _E>(
7404                rows: &[Self],
7405                _executor: _E,
7406            ) -> ::core::result::Result<(), #root::sql::ExecError>
7407            where
7408                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7409            {
7410                if rows.is_empty() {
7411                    return ::core::result::Result::Ok(());
7412                }
7413                let mut _all_rows: ::std::vec::Vec<
7414                    ::std::vec::Vec<#root::core::SqlValue>,
7415                > = ::std::vec::Vec::with_capacity(rows.len());
7416                for _row in rows.iter() {
7417                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7418                        ::std::vec::Vec::new();
7419                    #( #pushes_all )*
7420                    _all_rows.push(_row_vals);
7421                }
7422                let _query = #root::core::BulkInsertQuery {
7423                    model: <Self as #root::core::Model>::SCHEMA,
7424                    columns: ::std::vec![ #( #cols_all ),* ],
7425                    rows: _all_rows,
7426                    returning: ::std::vec::Vec::new(),
7427                    on_conflict: ::core::option::Option::None,
7428                };
7429                let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7430                ::core::result::Result::Ok(())
7431            }
7432        }
7433    };
7434
7435    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7436    // (no postgres-feature gate); routes through the existing
7437    // `bulk_insert_pool` + per-dialect conflict writer.
7438    //
7439    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7440    // sequence picks the PK for fresh inserts; the UPDATE branch never
7441    // touches the Auto column.
7442    let bulk_upsert_pool_method = {
7443        // Pick the "no Auto" columns when the model has Auto fields,
7444        // else every column.
7445        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7446            (
7447                fields.bulk_columns_no_auto.clone(),
7448                fields.bulk_pushes_no_auto.clone(),
7449            )
7450        } else {
7451            (
7452                fields.bulk_columns_all.clone(),
7453                fields.bulk_pushes_all.clone(),
7454            )
7455        };
7456        quote! {
7457            /// Tri-dialect `bulk_create(update_conflicts=True)` — Django's
7458            /// canonical "import a batch idempotently" shape. Issue #267
7459            /// / T1.5.
7460            ///
7461            /// Per-row values are extracted and lowered into a
7462            /// [`#root::core::BulkInsertQuery`] with
7463            /// `on_conflict = DoUpdate { target, update_columns }`. The
7464            /// writer dispatches per-dialect:
7465            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7466            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7467            ///
7468            /// `target` names the column(s) whose unique constraint
7469            /// defines the conflict (typically a `unique` or
7470            /// `unique_together` natural-key column, NOT the `Auto<T>`
7471            /// PK). `update_cols` names the columns to overwrite on
7472            /// conflict — every other column is left untouched on the
7473            /// existing row.
7474            ///
7475            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7476            /// picks the PK on insert; the update path never touches
7477            /// the Auto column). Auto-set rows trigger a hard error.
7478            /// Empty slice is a no-op.
7479            ///
7480            /// # Errors
7481            /// Returns [`#root::sql::ExecError`] for validation,
7482            /// SQL-writing, or driver failures.
7483            pub async fn bulk_upsert_pool(
7484                rows: &[Self],
7485                target: &[&'static str],
7486                update_cols: &[&'static str],
7487                pool: &#root::sql::Pool,
7488            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7489                if rows.is_empty() {
7490                    return ::core::result::Result::Ok(());
7491                }
7492                let mut _all_rows: ::std::vec::Vec<
7493                    ::std::vec::Vec<#root::core::SqlValue>,
7494                > = ::std::vec::Vec::with_capacity(rows.len());
7495                for _row in rows.iter() {
7496                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7497                        ::std::vec::Vec::new();
7498                    #( #upsert_pushes )*
7499                    _all_rows.push(_row_vals);
7500                }
7501                let _query = #root::core::BulkInsertQuery {
7502                    model: <Self as #root::core::Model>::SCHEMA,
7503                    columns: ::std::vec![ #( #upsert_cols ),* ],
7504                    rows: _all_rows,
7505                    returning: ::std::vec::Vec::new(),
7506                    on_conflict: ::core::option::Option::Some(
7507                        #root::core::ConflictClause::DoUpdate {
7508                            target: target.to_vec(),
7509                            update_columns: update_cols.to_vec(),
7510                        }
7511                    ),
7512                };
7513                #root::sql::bulk_insert_pool(pool, &_query).await
7514            }
7515
7516            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7517            /// skip rows that would violate a unique constraint. Issue
7518            /// #267 / T1.5. Same per-dialect dispatch as
7519            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7520            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7521            /// <pivot> = <pivot>` (MySQL no-op write).
7522            ///
7523            /// # Errors
7524            /// As [`Self::bulk_upsert_pool`].
7525            pub async fn bulk_insert_or_ignore_pool(
7526                rows: &[Self],
7527                pool: &#root::sql::Pool,
7528            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7529                if rows.is_empty() {
7530                    return ::core::result::Result::Ok(());
7531                }
7532                let mut _all_rows: ::std::vec::Vec<
7533                    ::std::vec::Vec<#root::core::SqlValue>,
7534                > = ::std::vec::Vec::with_capacity(rows.len());
7535                for _row in rows.iter() {
7536                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7537                        ::std::vec::Vec::new();
7538                    #( #upsert_pushes )*
7539                    _all_rows.push(_row_vals);
7540                }
7541                let _query = #root::core::BulkInsertQuery {
7542                    model: <Self as #root::core::Model>::SCHEMA,
7543                    columns: ::std::vec![ #( #upsert_cols ),* ],
7544                    rows: _all_rows,
7545                    returning: ::std::vec::Vec::new(),
7546                    on_conflict: ::core::option::Option::Some(
7547                        #root::core::ConflictClause::DoNothing
7548                    ),
7549                };
7550                #root::sql::bulk_insert_pool(pool, &_query).await
7551            }
7552        }
7553    };
7554
7555    // Ergonomic `Model::bulk_update(objs, fields)` — Django's
7556    // `QuerySet.bulk_update`. The SQL/IR/executor stack
7557    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7558    // `write_bulk_update_*` writers) already existed; what was missing
7559    // was the per-model constructor that maps `&[Self]` + a runtime
7560    // column list into rows of `[pk, col_vals…]` so callers don't
7561    // hand-build the IR. Emitted only when the model has a primary key
7562    // (the PK is the join key and can't itself be updated).
7563    let bulk_update_method = match &fields.primary_key {
7564        None => quote! {},
7565        Some((pk_ident, pk_col)) => {
7566            // One pair of match arms per non-PK column: a validation arm
7567            // resolving the runtime name to its `&'static str` column,
7568            // and a value arm pushing that field off the row.
7569            let mut col_arms: Vec<TokenStream2> = Vec::new();
7570            let mut val_arms: Vec<TokenStream2> = Vec::new();
7571            for entry in &fields.column_entries {
7572                if &entry.column == pk_col {
7573                    continue;
7574                }
7575                let col = &entry.column;
7576                let ident = &entry.ident;
7577                col_arms.push(quote! { #col => #col, });
7578                val_arms.push(quote! {
7579                    #col => _row_vals.push(
7580                        ::core::convert::Into::<#root::core::SqlValue>::into(
7581                            ::core::clone::Clone::clone(&_o.#ident)
7582                        )
7583                    ),
7584                });
7585            }
7586            quote! {
7587                /// Django's `QuerySet.bulk_update(objs, fields)` — write
7588                /// per-row-different values for the named `fields` across
7589                /// every object in `objs` in a single statement, matched
7590                /// by primary key.
7591                ///
7592                /// `fields` names the **columns** to update. The primary
7593                /// key identifies each row and cannot itself be updated
7594                /// (pass it and you get
7595                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7596                /// Empty `objs` or `fields` is a no-op returning `0`.
7597                /// Objects whose PK matches no row are simply not updated.
7598                /// Returns the number of rows affected.
7599                ///
7600                /// Tri-dialect: lowers to one
7601                /// [`#root::core::BulkUpdateQuery`] and dispatches
7602                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7603                /// a CTE + correlated subquery on SQLite, an inner
7604                /// `JOIN (VALUES …)` on MySQL.
7605                ///
7606                /// # Errors
7607                /// [`#root::core::QueryError::UnknownField`] for a name
7608                /// that isn't a column on this model,
7609                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7610                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7611                /// SQL-writing / driver failures.
7612                pub async fn bulk_update(
7613                    objs: &[Self],
7614                    fields: &[&str],
7615                    pool: &#root::sql::Pool,
7616                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7617                    if objs.is_empty() || fields.is_empty() {
7618                        return ::core::result::Result::Ok(0);
7619                    }
7620                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7621                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7622                        ::std::vec::Vec::with_capacity(fields.len());
7623                    for &_f in fields {
7624                        let _col: &'static str = match _f {
7625                            #pk_col => {
7626                                return ::core::result::Result::Err(
7627                                    ::core::convert::Into::into(
7628                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7629                                            model: _model_name,
7630                                            field: ::std::string::ToString::to_string(_f),
7631                                        }
7632                                    )
7633                                );
7634                            }
7635                            #( #col_arms )*
7636                            _ => {
7637                                return ::core::result::Result::Err(
7638                                    ::core::convert::Into::into(
7639                                        #root::core::QueryError::UnknownField {
7640                                            model: _model_name,
7641                                            field: ::std::string::ToString::to_string(_f),
7642                                        }
7643                                    )
7644                                );
7645                            }
7646                        };
7647                        // A model whose only field is the primary key emits no
7648                        // non-PK arms, so every arm above diverges and this push
7649                        // is genuinely unreachable — correct, since such a model
7650                        // has nothing to bulk-update. Scoped to the statement
7651                        // rather than the fn so a real unreachability elsewhere
7652                        // in this body still warns. Without it every PK-only
7653                        // model warns in the *user's* crate, which they cannot
7654                        // act on (#1290).
7655                        #[allow(unreachable_code)]
7656                        _update_columns.push(_col);
7657                    }
7658                    let mut _rows: ::std::vec::Vec<
7659                        ::std::vec::Vec<#root::core::SqlValue>,
7660                    > = ::std::vec::Vec::with_capacity(objs.len());
7661                    for _o in objs.iter() {
7662                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7663                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7664                        // PK first — the writers expect `[pk, …update cols]`.
7665                        _row_vals.push(
7666                            ::core::convert::Into::<#root::core::SqlValue>::into(
7667                                ::core::clone::Clone::clone(&_o.#pk_ident)
7668                            )
7669                        );
7670                        for &_f in fields {
7671                            match _f {
7672                                #( #val_arms )*
7673                                // Unreachable: every name was validated
7674                                // against the same arm set above.
7675                                _ => {}
7676                            }
7677                        }
7678                        _rows.push(_row_vals);
7679                    }
7680                    let _query = #root::core::BulkUpdateQuery {
7681                        model: <Self as #root::core::Model>::SCHEMA,
7682                        update_columns: _update_columns,
7683                        rows: _rows,
7684                    };
7685                    #root::sql::bulk_update_pool(pool, &_query).await
7686                }
7687            }
7688        }
7689    };
7690
7691    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7692        quote! {
7693            /// Hidden runtime accessor for the primary-key value as a
7694            /// [`SqlValue`]. Used by reverse-relation helpers
7695            /// (`<parent>::<child>_set`) emitted from sibling models'
7696            /// FK fields. Not part of the public API.
7697            #[doc(hidden)]
7698            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7699                ::core::convert::Into::<#root::core::SqlValue>::into(
7700                    ::core::clone::Clone::clone(&self.#pk_ident)
7701                )
7702            }
7703        }
7704    });
7705
7706    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7707        quote! {
7708            impl #root::sql::HasPkValue for #struct_name {
7709                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7710                    ::core::convert::Into::<#root::core::SqlValue>::into(
7711                        ::core::clone::Clone::clone(&self.#pk_ident)
7712                    )
7713                }
7714            }
7715        }
7716    });
7717
7718    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7719
7720    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7721    // dispatch to the right per-backend body without the macro emitting
7722    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7723    // so audited models with non-Auto PKs (which still go through
7724    // `insert_one_with_audit` → `apply_auto_pk`) link.
7725    let assign_auto_pk_pool_impl = {
7726        let auto_assigns = &fields.auto_assigns;
7727        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7728        // so the body is structurally identical to `auto_assigns` —
7729        // only the helper name swaps from `try_get_returning` to
7730        // `try_get_returning_sqlite` so the closure typechecks against
7731        // a `SqliteRow` instead of a `PgRow`.
7732        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7733            .auto_field_idents
7734            .iter()
7735            .map(|(ident, column)| {
7736                quote! {
7737                    self.#ident = #root::sql::try_get_returning_sqlite(
7738                        _returning_row, #column
7739                    )?;
7740                }
7741            })
7742            .collect();
7743        // #1028 — decode each `generated_as` column from the same
7744        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7745        // own type drives the decode (no `Auto<T>` wrapper).
7746        let generated_assigns: Vec<TokenStream2> = fields
7747            .generated_field_idents
7748            .iter()
7749            .map(|(ident, column)| {
7750                quote! {
7751                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7752                }
7753            })
7754            .collect();
7755        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7756            .generated_field_idents
7757            .iter()
7758            .map(|(ident, column)| {
7759                quote! {
7760                    self.#ident = #root::sql::try_get_returning_sqlite(
7761                        _returning_row, #column
7762                    )?;
7763                }
7764            })
7765            .collect();
7766        let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7767            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7768            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7769            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7770            // those models can't use AUTO_INCREMENT). The trait is only
7771            // touched on the MySQL arm at runtime, so PG-only consumers
7772            // never see the bound failure.
7773            //
7774            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7775            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7776            // runtime with "multi-column RETURNING". MySQL has no
7777            // multi-column RETURNING semantic and a follow-up SELECT
7778            // would need cross-trait plumbing. Pragmatic shape: succeed
7779            // with the FIRST Auto field populated from LAST_INSERT_ID();
7780            // any other Auto fields stay `Auto::Unset`. Callers that
7781            // need the DB-defaulted timestamp / UUID can re-fetch the
7782            // row by PK after `save_pool`. Fixes the cookbook chapter
7783            // 12 dialect divergence.
7784            let value_ty = fields
7785                .first_auto_value_ty
7786                .as_ref()
7787                .expect("first_auto_value_ty set whenever first_auto_ident is");
7788            quote! {
7789                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7790                    ::rustango_from_mysql_auto_id(_id)?;
7791                self.#first = #root::sql::Auto::Set(_converted);
7792                ::core::result::Result::Ok(())
7793            }
7794        } else {
7795            quote! {
7796                let _ = _id;
7797                ::core::result::Result::Ok(())
7798            }
7799        };
7800        quote! {
7801            impl #root::sql::AssignAutoPkPool for #struct_name {
7802                fn __rustango_assign_from_pg_row(
7803                    &mut self,
7804                    _returning_row: &#root::sql::PgReturningRow,
7805                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7806                    #( #auto_assigns )*
7807                    #( #generated_assigns )*
7808                    ::core::result::Result::Ok(())
7809                }
7810                fn __rustango_assign_from_mysql_id(
7811                    &mut self,
7812                    _id: i64,
7813                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7814                    #mysql_body
7815                }
7816                fn __rustango_assign_from_sqlite_row(
7817                    &mut self,
7818                    _returning_row: &#root::sql::SqliteReturningRow,
7819                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7820                    #( #auto_assigns_sqlite )*
7821                    #( #generated_assigns_sqlite )*
7822                    ::core::result::Result::Ok(())
7823                }
7824            }
7825        }
7826    };
7827
7828    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7829    let aliased_row_helper = quote! {
7830        /// Decode a row's aliased target columns (produced by
7831        /// `select_related`'s LEFT JOIN) into a fresh instance of
7832        /// this model. Reads each column via
7833        /// `format!("{prefix}__{col}")`, matching the alias the
7834        /// SELECT writer emitted. Slice 9.0d.
7835        #[doc(hidden)]
7836        #[cfg(feature = "postgres")]
7837        pub fn __rustango_from_aliased_row(
7838            row: &#root::sql::sqlx::postgres::PgRow,
7839            prefix: &str,
7840        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7841            ::core::result::Result::Ok(Self {
7842                #( #from_aliased_row_inits ),*
7843            })
7844        }
7845    };
7846    // v0.23.0-batch8 — MySQL counterpart, gated through the
7847    // cfg-aware macro_rules so PG-only builds expand to nothing.
7848    let aliased_row_helper_my = quote! {
7849        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7850            #( #from_aliased_row_inits ),*
7851        });
7852    };
7853
7854    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7855    // pattern + cfg gate on the `sqlite` feature.
7856    let aliased_row_helper_sqlite = quote! {
7857        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7858            #( #from_aliased_row_inits ),*
7859        });
7860    };
7861
7862    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7863    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7864    let load_related_impl_sqlite =
7865        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7866
7867    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7868    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7869    // default `Self::objects()`. Each accessor returns a fresh
7870    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7871    // methods the user defined.
7872    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7873        .iter()
7874        .map(|fn_ident| {
7875            let model_name_str = struct_name.to_string();
7876            let fn_name_str = fn_ident.to_string();
7877            let doc = format!(
7878                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7879                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7880                 equivalent to `Self::objects()`. Chains with any \
7881                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7882                 extension methods."
7883            );
7884            quote! {
7885                #[doc = #doc]
7886                #[must_use]
7887                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7888                    #root::query::QuerySet::new()
7889                }
7890            }
7891        })
7892        .collect();
7893
7894    quote! {
7895        impl #struct_name {
7896            /// Start a new `QuerySet` over this model. Django shape.
7897            #[must_use]
7898            pub fn objects() -> #root::query::QuerySet<#struct_name> {
7899                #root::query::QuerySet::new()
7900            }
7901
7902            /// Eloquent-shape alias of [`Self::objects`]. Returns
7903            /// a fresh `QuerySet<Self>` ready for `.filter()` /
7904            /// `.where_()` / etc. Matches Laravel muscle-memory:
7905            ///
7906            /// ```ignore
7907            /// // Eloquent:    Post::query()->where('published', true)
7908            /// // Django:      Post.objects.filter(published=True)
7909            /// // rustango:    Post::query().filter("published", true)
7910            /// //         or:  Post::objects().filter("published", true)
7911            /// ```
7912            ///
7913            /// Both names point at the same underlying constructor;
7914            /// neither is preferred.
7915            #[must_use]
7916            pub fn query() -> #root::query::QuerySet<#struct_name> {
7917                #root::query::QuerySet::new()
7918            }
7919
7920            #( #extra_manager_fns )*
7921
7922            #insert_method
7923
7924            #bulk_insert_method
7925
7926            #bulk_upsert_pool_method
7927
7928            #bulk_update_method
7929
7930            #save_method
7931
7932            #pk_methods
7933
7934            #pk_value_helper
7935
7936            #aliased_row_helper
7937
7938            #column_consts
7939        }
7940
7941        #aliased_row_helper_my
7942
7943        #aliased_row_helper_sqlite
7944
7945        #load_related_impl
7946
7947        #load_related_impl_my
7948
7949        #load_related_impl_sqlite
7950
7951        #has_pk_value_impl
7952
7953        #fk_pk_access_impl
7954
7955        #assign_auto_pk_pool_impl
7956    }
7957}
7958
7959/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7960/// `auto_assigns` but reading from `_returning_row` and writing to
7961/// `_row_mut` instead of `self`.
7962fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7963    let root = rustango_root();
7964    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7965        let col_lit = column.as_str();
7966        quote! {
7967            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7968                _returning_row,
7969                #col_lit,
7970            )?;
7971        }
7972    });
7973    quote! { #( #lines )* }
7974}
7975
7976/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7977fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7978    let lines = entries.iter().map(|e| {
7979        let ident = &e.ident;
7980        let col_ty = column_type_ident(ident);
7981        quote! {
7982            #[allow(non_upper_case_globals)]
7983            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7984        }
7985    });
7986    quote! { #(#lines)* }
7987}
7988
7989/// Emit a hidden per-model module carrying one zero-sized type per field,
7990/// each with a `Column` impl pointing back at the model.
7991fn column_module_tokens(
7992    module_ident: &syn::Ident,
7993    struct_name: &syn::Ident,
7994    entries: &[ColumnEntry],
7995) -> TokenStream2 {
7996    let root = rustango_root();
7997    let items = entries.iter().map(|e| {
7998        let col_ty = column_type_ident(&e.ident);
7999        let value_ty = &e.value_ty;
8000        let name = &e.name;
8001        let column = &e.column;
8002        let field_type_tokens = &e.field_type_tokens;
8003        quote! {
8004            #[derive(::core::clone::Clone, ::core::marker::Copy)]
8005            pub struct #col_ty;
8006
8007            impl #root::core::Column for #col_ty {
8008                type Model = super::#struct_name;
8009                type Value = #value_ty;
8010                const NAME: &'static str = #name;
8011                const COLUMN: &'static str = #column;
8012                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8013            }
8014        }
8015    });
8016    quote! {
8017        #[doc(hidden)]
8018        #[allow(non_camel_case_types, non_snake_case)]
8019        pub mod #module_ident {
8020            // Re-import the parent scope so field types referencing
8021            // sibling models (e.g. `ForeignKey<Author>`) resolve
8022            // inside this submodule. Without this we'd hit
8023            // `proc_macro_derive_resolution_fallback` warnings.
8024            #[allow(unused_imports)]
8025            use super::*;
8026            #(#items)*
8027        }
8028    }
8029}
8030
8031fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8032    syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
8033}
8034
8035fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8036    syn::Ident::new(
8037        &format!("__rustango_cols_{struct_name}"),
8038        struct_name.span(),
8039    )
8040}
8041
8042fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
8043    let root = rustango_root();
8044    // The Postgres impl is always emitted — every rustango build pulls in
8045    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8046    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8047    // macro_rules whose body collapses to nothing when rustango is built
8048    // without the `mysql` feature. No user-facing feature shim required.
8049    //
8050    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8051    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8052    // back into a comma-separated list inside square brackets. Since each
8053    // entry is already in `field: expr` shape, the existing tokens slot in.
8054    quote! {
8055        #[cfg(feature = "postgres")]
8056        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8057            for #struct_name
8058        {
8059            fn from_row(
8060                row: &'r #root::sql::sqlx::postgres::PgRow,
8061            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8062                ::core::result::Result::Ok(Self {
8063                    #( #from_row_inits ),*
8064                })
8065            }
8066        }
8067
8068        #root::__impl_my_from_row!(#struct_name, |row| {
8069            #( #from_row_inits ),*
8070        });
8071
8072        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8073            #( #from_row_inits ),*
8074        });
8075    }
8076}
8077
8078struct ContainerAttrs {
8079    table: Option<String>,
8080    display: Option<(String, proc_macro2::Span)>,
8081    /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
8082    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8083    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8084    /// at runtime — this attribute is the override for cases where
8085    /// the inference is wrong (e.g. a model that conceptually belongs
8086    /// to one app but is physically in another module).
8087    app: Option<String>,
8088    /// Django ModelAdmin-shape per-model knobs from
8089    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8090    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8091    /// admin code falls back to `AdminConfig::DEFAULT`.
8092    admin: Option<AdminAttrs>,
8093    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8094    /// `None` when the model isn't audited — write paths emit no
8095    /// audit entries. When present, single-row writes capture
8096    /// before/after for the listed fields and bulk writes batch
8097    /// snapshots into one INSERT into `rustango_audit_log`.
8098    audit: Option<AuditAttrs>,
8099    /// `true` when `#[rustango(permissions)]` is present. Signals that
8100    /// `auto_create_permissions` should seed the four CRUD codenames for
8101    /// this model.
8102    permissions: bool,
8103    /// Many-to-many relations declared via
8104    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8105    ///                 src = "post_id", dst = "tag_id"))]`.
8106    m2m: Vec<M2MAttr>,
8107    /// Polymorphic M2M relations declared via
8108    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8109    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8110    ///   related_column = "tag_id"))]` (issue #818).
8111    generic_m2m: Vec<GenericM2MAttr>,
8112    /// Composite indexes declared via
8113    /// `#[rustango(index("col1, col2"))]` or
8114    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8115    /// Single-column indexes from `#[rustango(index)]` on fields are
8116    /// accumulated here during field collection.
8117    indexes: Vec<IndexAttr>,
8118    /// Table-level CHECK constraints declared via
8119    /// `#[rustango(check(name = "…", expr = "…"))]`.
8120    checks: Vec<CheckAttr>,
8121    /// Table-level PG `EXCLUDE` constraints declared via
8122    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8123    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8124    /// migration writer renders them on Postgres and skips with a
8125    /// warning on MySQL/SQLite. Issue #319.
8126    excludes: Vec<ExcludeAttr>,
8127    /// Composite (multi-column) FKs declared via
8128    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8129    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8130    composite_fks: Vec<CompositeFkAttr>,
8131    /// Generic ("any model") FKs declared via
8132    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8133    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8134    generic_fks: Vec<GenericFkAttr>,
8135    /// Where this model lives in a tenancy deployment, declared via
8136    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8137    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8138    /// to partition diff output between registry-scoped and
8139    /// tenant-scoped migration files.
8140    scope: Option<String>,
8141    /// Custom-Manager extension-trait name from
8142    /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8143    /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8144    /// adjacent to the model so users can write
8145    /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8146    /// and discover the convention from the model definition.
8147    manager_ext: Option<syn::Ident>,
8148    /// Extra QuerySet accessor names from
8149    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8150    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8151    /// the default `Self::objects()`. Multiple attributes allowed.
8152    manager_fns: Vec<syn::Ident>,
8153    /// Default ordering declared via `#[rustango(default_order =
8154    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8155    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8156    /// `-` prefix means descending; the `+` prefix or no prefix means
8157    /// ascending.
8158    default_order: Vec<(String, bool, proc_macro2::Span)>,
8159    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8160    /// Routes the emitted schema's `is_view = true` so the migration
8161    /// snapshot skips this model (its underlying SQL view is operator-
8162    /// managed, not rustango-managed).
8163    is_view: bool,
8164    /// Django-shape `Meta.managed` from `#[rustango(managed = false)]`.
8165    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8166    /// the migration snapshot skips this model so `makemigrations` /
8167    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8168    /// TABLE` against it (operator-managed schema).
8169    managed: bool,
8170    /// Django-shape `Meta.base_manager_name` from
8171    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8172    /// `ModelSchema::base_manager_name`. Declarative-only today.
8173    base_manager_name: Option<String>,
8174    /// Django-shape `Meta.order_with_respect_to = "parent_fk"` from
8175    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8176    /// field this model's instances are ordered relative to.
8177    /// Declarative-only today; threaded onto
8178    /// `ModelSchema::order_with_respect_to`.
8179    order_with_respect_to: Option<String>,
8180    /// Django-shape `Meta.proxy = True` from `#[rustango(proxy)]` /
8181    /// `#[rustango(proxy = true)]`. Marks the model as a proxy that
8182    /// shares its DB table with another struct. Threaded into
8183    /// `ModelSchema::proxy` so future codegen can skip table-owning
8184    /// behavior for proxies.
8185    proxy: bool,
8186    /// Django-shape `Meta.required_db_features` from
8187    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8188    /// Each comma-separated capability token surfaces on
8189    /// `ModelSchema::required_db_features` so `manage check --deploy`
8190    /// can warn when the active dialect lacks one.
8191    required_db_features: Vec<String>,
8192    /// Django-shape `Meta.required_db_vendor` from
8193    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8194    /// Normalized to the dialect name `manage check --deploy`
8195    /// compares against `Settings.database.backend`. Aliases
8196    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8197    /// stored under the canonical name.
8198    required_db_vendor: Option<String>,
8199    /// Django-shape `Meta.default_related_name` from
8200    /// `#[rustango(default_related_name = "...")]`. Threaded into
8201    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8202    /// name to use when an FK / M2M field doesn't override it.
8203    /// Today rustango doesn't auto-emit reverse managers; the
8204    /// metadata is the foundation for that work.
8205    default_related_name: Option<String>,
8206    /// Django-shape `Meta.db_table_comment` (4.2+) from
8207    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8208    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8209    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8210    /// inline `COMMENT='...'`, SQLite: no-op).
8211    db_table_comment: Option<String>,
8212    /// Django-shape `Meta.get_latest_by` from
8213    /// `#[rustango(get_latest_by = "created_at")]` /
8214    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8215    /// `(column, descending)` where `descending = true` when the
8216    /// attribute value starts with `-`. Threaded into
8217    /// `ModelSchema::get_latest_by`.
8218    get_latest_by: Option<(String, bool)>,
8219    /// Django-shape `Meta.permissions = [(codename, name), ...]`
8220    /// from `#[rustango(extra_permissions = "approve:Can approve,
8221    /// archive:Can archive")]`. Comma-separated `codename:label`
8222    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8223    extra_permissions: Vec<(String, String)>,
8224    /// Django-shape `Meta.default_permissions` — which CRUD codenames
8225    /// (`"add"` / `"change"` / `"delete"` / `"view"`) the framework
8226    /// auto-creates. Empty `Vec` (default) means **all four** — matches
8227    /// Django's behavior when the operator omits the option. Set via
8228    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8229    /// Validated at parse time; unknown actions fail with a span-pointing
8230    /// error.
8231    default_permissions: Vec<String>,
8232    /// `#[rustango(verbose_name = "blog post")]` — Django-shape
8233    /// human-readable singular label for the model. Threaded into
8234    /// `ModelSchema::verbose_name` so admin section headers /
8235    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8236    /// over the Rust struct identifier.
8237    verbose_name: Option<String>,
8238    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8239    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8240    /// When unset, `display_label_plural()` auto-suffixes `s`.
8241    verbose_name_plural: Option<String>,
8242    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8243    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8244    /// a name (used by `QuerySet::without_global_scope`) with a
8245    /// `fn() -> WhereExpr` path that the macro emits into
8246    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8247    global_scopes: Vec<GlobalScopeAttr>,
8248    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8249    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8250    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8251    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8252    /// entry emits an inherent `<name>_through(&self)` accessor that
8253    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8254    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8255    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8256    through_relations: Vec<ThroughAttr>,
8257    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8258    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8259    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8260    /// Each entry emits two associated functions on the parent —
8261    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8262    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8263    /// over a correlated subquery against the child table. Users
8264    /// drop the result into `QuerySet::where_raw(...)`.
8265    reverse_has_relations: Vec<ReverseHasAttr>,
8266    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8267    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8268    /// entry so the relation-existence family resolves polymorphic
8269    /// children by name.
8270    generic_has_relations: Vec<GenericHasAttr>,
8271}
8272
8273/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8274/// declaration. Each entry becomes one `core::GlobalScope` in the
8275/// emitted schema literal; `apply` resolves at macro-expand time
8276/// against the consumer's scope so the function must be in scope at
8277/// the use site. Issue #820.
8278struct GlobalScopeAttr {
8279    name: String,
8280    apply: syn::Path,
8281}
8282
8283/// Parsed `#[rustango(through(...))]` declaration. Issue
8284/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8285/// `hasManyThrough` / `hasOneThrough` parity.
8286///
8287/// `Country hasManyThrough Post via User` declares as:
8288///
8289/// ```ignore
8290/// #[rustango(through(
8291///     name                   = "posts",
8292///     far                    = "Post",
8293///     far_fk_column          = "author_id",
8294///     intermediate           = "User",
8295///     intermediate_fk_column = "country_id",
8296/// ))]
8297/// struct Country { ... }
8298/// ```
8299///
8300/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8301/// which returns a queryset filtered via a correlated subquery —
8302/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8303/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8304/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8305/// because the subquery lives inside a `WhereExpr::InSubquery` node
8306/// and the outer queryset's pending list stays empty.
8307///
8308/// All four required arguments use **SQL column / table names**
8309/// (not Rust field names) to sidestep the multi-hop-filter substrate
8310/// gap. Once that substrate lands, a higher-level Rust-field-name
8311/// shorthand can be added without breaking this surface.
8312struct ThroughAttr {
8313    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8314    name: String,
8315    /// Far model type identifier. `far = "Post"` → returns
8316    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8317    /// the derive expands.
8318    far: syn::Ident,
8319    /// SQL column on the far model's table that references the
8320    /// intermediate model's primary key. For `Post`'s
8321    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8322    /// default `<field>_id` convention) or whatever the user
8323    /// declared via `#[rustango(db_column = "...")]`.
8324    far_fk_column: String,
8325    /// Intermediate model type identifier. Needed to look up its
8326    /// `SCHEMA` so the subquery's `FROM` clause points at the
8327    /// intermediate table. `intermediate = "User"`.
8328    intermediate: syn::Ident,
8329    /// SQL column on the intermediate's table that references the
8330    /// source (this) model's primary key. For `User`'s
8331    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8332    intermediate_fk_column: String,
8333    /// SQL primary-key column on the intermediate's table — the
8334    /// column the subquery projects. Optional; defaults to `"id"`
8335    /// (rustango's default PK column name). Override when the
8336    /// intermediate declares a custom PK column.
8337    intermediate_pk_column: String,
8338}
8339
8340/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8341/// child_fk_column = "..."))]` declaration. Issue
8342/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8343/// `whereHas` / `whereDoesntHave` parity.
8344///
8345/// `Post hasMany Comment` declares as:
8346///
8347/// ```ignore
8348/// #[rustango(reverse_has(
8349///     name             = "comments",
8350///     child            = "Comment",
8351///     child_fk_column  = "post_id",
8352/// ))]
8353/// struct Post { ... }
8354/// ```
8355///
8356/// The macro emits two associated functions on `Post`:
8357///
8358/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8359///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8360///   <outer>.<self_pk_column>)`.
8361/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8362///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8363///
8364/// User code:
8365///
8366/// ```ignore
8367/// // Posts with at least one comment:
8368/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8369/// // Posts with no comments:
8370/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8371/// ```
8372///
8373/// As with #817, all column / table identifiers are **SQL names**
8374/// (not Rust field names) so the substrate is independent of the
8375/// outstanding multi-hop filter resolver gap. The emitted
8376/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8377/// SQL-emit time via the writer's scope stack.
8378struct ReverseHasAttr {
8379    /// Accessor name. `name = "comments"` → emits
8380    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8381    name: String,
8382    /// Child model type identifier. `child = "Comment"` — needed to
8383    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8384    /// points at the child table.
8385    child: syn::Ident,
8386    /// SQL column on the child's table that references this model's
8387    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8388    /// column is `"post_id"`.
8389    child_fk_column: String,
8390    /// SQL primary-key column on **this** model's table — the column
8391    /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8392    /// (rustango's default PK column name).
8393    self_pk_column: String,
8394}
8395
8396/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8397/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8398/// relation-existence family (issue #830). The child is a polymorphic,
8399/// content-type-discriminated model; emits a `Model::
8400/// generic_reverse_relations()` entry the queryset resolves by name.
8401struct GenericHasAttr {
8402    /// Accessor name, e.g. `name = "tags"`.
8403    name: String,
8404    /// Child model type identifier (`child = "Tag"`) — looked up for its
8405    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8406    child: syn::Ident,
8407    /// Column on the child table holding the parent's content-type id.
8408    /// Optional; defaults to `"content_type_id"`.
8409    ct_column: String,
8410    /// Column on the child table holding the parent's PK value.
8411    /// Optional; defaults to `"object_pk"`.
8412    pk_column: String,
8413    /// SQL primary-key column on **this** (parent) model's table.
8414    /// Optional; defaults to `"id"`.
8415    self_pk_column: String,
8416}
8417
8418/// Parsed form of one index declaration (field-level or container-level).
8419struct IndexAttr {
8420    /// Index name; auto-derived when `None` at parse time.
8421    name: Option<String>,
8422    /// Column names in the index.
8423    columns: Vec<String>,
8424    /// `true` for `CREATE UNIQUE INDEX`.
8425    unique: bool,
8426    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8427    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8428    /// `"btree"` when the attribute is absent — the DDL writer omits
8429    /// the `USING` clause and the backend uses its own default
8430    /// (btree on every supported dialect).
8431    method: String,
8432    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8433    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8434    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8435    where_clause: Option<String>,
8436    /// Django `Index(fields=..., include=[...])` covering-index
8437    /// columns (PG 11+ `INCLUDE (...)` clause). Empty `Vec` (the
8438    /// default) means "no covering columns".
8439    include: Vec<String>,
8440}
8441
8442/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8443struct CheckAttr {
8444    name: String,
8445    expr: String,
8446}
8447
8448/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8449/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8450/// PG-only — surfaced on every backend in the macro emit; the migration
8451/// writer skips the op on MySQL/SQLite. Issue #319.
8452struct ExcludeAttr {
8453    /// Constraint name (free-form Rust identifier).
8454    name: String,
8455    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8456    using: String,
8457    /// Comma-separated `(column, operator)` pairs, in declaration
8458    /// order. Parsed from `"col WITH op, col WITH op"`.
8459    elements: Vec<(String, String)>,
8460    /// Optional `WHERE` predicate (raw SQL).
8461    where_clause: Option<String>,
8462}
8463
8464/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8465/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8466/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8467/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8468/// the model, not the field.
8469struct CompositeFkAttr {
8470    /// Logical relation name (free-form Rust identifier).
8471    name: String,
8472    /// SQL table name of the target.
8473    to: String,
8474    /// Source-side column names, in declaration order.
8475    from: Vec<String>,
8476    /// Target-side column names, same length / order as `from`.
8477    on: Vec<String>,
8478}
8479
8480/// Parsed form of one `#[rustango(generic_fk(name = "target",
8481/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8482/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8483/// generic ("any model") FKs live on the model, not the field.
8484struct GenericFkAttr {
8485    /// Logical relation name (free-form Rust identifier).
8486    name: String,
8487    /// Source-side column carrying the `content_type_id` value.
8488    ct_column: String,
8489    /// Source-side column carrying the target row's primary key.
8490    pk_column: String,
8491}
8492
8493/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8494struct M2MAttr {
8495    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8496    name: String,
8497    /// Target table (e.g. `"app_tags"`).
8498    to: String,
8499    /// Junction table (e.g. `"post_tags"`).
8500    through: String,
8501    /// Source FK column in the junction table (e.g. `"post_id"`).
8502    src: String,
8503    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8504    dst: String,
8505    /// Whether the migration writer should auto-create the junction
8506    /// table. Default `true`. Set `auto_create = false` (#324) when
8507    /// the operator declares the through table as its own
8508    /// `#[derive(Model)]` struct with extra columns.
8509    auto_create: bool,
8510}
8511
8512/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8513/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8514/// junction carries a ContentType discriminator so unrelated models
8515/// share one pivot + related set.
8516struct GenericM2MAttr {
8517    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8518    name: String,
8519    /// Polymorphic junction table (e.g. `"taggables"`).
8520    through: String,
8521    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8522    pk_column: String,
8523    /// Junction column holding the owning model's `content_type_id`
8524    /// discriminator (e.g. `"taggable_type"`).
8525    ct_column: String,
8526    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8527    related_column: String,
8528}
8529
8530/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8531/// "user"))]`. `track` is a comma-separated list of field names whose
8532/// before/after values land in the JSONB `changes` column. `source`
8533/// is informational only — it pins a default source when the model
8534/// is written outside any `audit::with_source(...)` scope (rare).
8535#[derive(Default)]
8536struct AuditAttrs {
8537    /// Field names to capture in the `changes` JSONB. Validated
8538    /// against declared scalar fields at compile time. Empty means
8539    /// "track every scalar field" — Django's audit-everything default.
8540    track: Option<(Vec<String>, proc_macro2::Span)>,
8541}
8542
8543/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8544/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8545/// comma-separated strings; we validate each ident against the model's
8546/// declared fields at compile time.
8547#[derive(Default)]
8548struct AdminAttrs {
8549    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8550    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8551    list_per_page: Option<usize>,
8552    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8553    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8554    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8555    /// Bulk action names. No field-validation against model fields —
8556    /// these are action handlers, not column references.
8557    actions: Option<(Vec<String>, proc_macro2::Span)>,
8558    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8559    /// sections, comma-separated fields per section, optional
8560    /// `Title:` prefix. Empty title omits the `<legend>`.
8561    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8562    /// `admin(list_display_links = "title")` — Django-shape. Names
8563    /// from `list_display` whose cells should link to detail/edit.
8564    /// Issue #350.
8565    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8566    /// `admin(search_help_text = "...")` — Django-shape. Short
8567    /// caption rendered beside the admin list view's search box.
8568    /// Issue #353.
8569    search_help_text: Option<String>,
8570    /// `admin(actions_on_top = false)` — Django-shape. Hides the
8571    /// action-bar above the table. Default `true`. Issue #354.
8572    actions_on_top: Option<bool>,
8573    /// `admin(actions_on_bottom = true)` — Django-shape. Renders an
8574    /// additional action-bar below the table. Default `false`.
8575    /// Issue #354.
8576    actions_on_bottom: Option<bool>,
8577    /// `admin(date_hierarchy = "created_at")` — Django-shape. Name of
8578    /// a date / datetime field whose values render as a clickable
8579    /// year / month / day drill-down strip above the list table.
8580    /// Empty / unset disables the strip. Issue #355.
8581    date_hierarchy: Option<String>,
8582    /// `admin(prepopulated_fields = "slug:title")` — Django-shape.
8583    /// Each entry is `target:source[+source2]`; multiple entries are
8584    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8585    /// The admin change-form emits JS that slugifies the source values
8586    /// into the target field on every keystroke. Issue #356.
8587    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8588    /// `admin(raw_id_fields = "parent, owner")` — Django-shape. Names
8589    /// of FK fields whose change-form widget renders a lookup link
8590    /// next to the input. Issue #357.
8591    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8592    /// `admin(autocomplete_fields = "author_id")` — Django-shape.
8593    /// Names of FK fields whose change-form widget renders an
8594    /// Ajax-driven typeahead populated from a `__autocomplete`
8595    /// endpoint on the target model. Issue #358.
8596    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8597    /// `admin(list_select_related = "all" | "none" | "author, …")`
8598    /// — Django-shape. Tunes the admin list view's FK auto-JOIN
8599    /// policy. Default `"all"` matches rustango's join-everything
8600    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8601    list_select_related: Option<String>,
8602    /// `admin(formfield_overrides = "field:widget, field2:widget2")` —
8603    /// Django-shape. Each entry is `field_name:widget_name`; multiple
8604    /// entries comma-separated. Empty / unset → no overrides. The
8605    /// list of widget names supported is documented on
8606    /// `AdminConfig::formfield_overrides`. Issue #359.
8607    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8608}
8609
8610fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8611    let mut out = ContainerAttrs {
8612        table: None,
8613        display: None,
8614        app: None,
8615        admin: None,
8616        audit: None,
8617        // Default `permissions = true` so every `#[derive(Model)]`
8618        // gets the four CRUD codenames seeded by `auto_create_permissions`
8619        // and is visible to non-superusers in the tenant admin without
8620        // manual per-model annotation. Models that intentionally don't
8621        // want permission rows (registry-internal types, framework
8622        // tables operators shouldn't manage directly) opt out via
8623        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8624        // out-of-the-box admin invisibility regression (#62).
8625        permissions: true,
8626        m2m: Vec::new(),
8627        generic_m2m: Vec::new(),
8628        indexes: Vec::new(),
8629        checks: Vec::new(),
8630        excludes: Vec::new(),
8631        composite_fks: Vec::new(),
8632        generic_fks: Vec::new(),
8633        scope: None,
8634        manager_ext: None,
8635        manager_fns: Vec::new(),
8636        default_order: Vec::new(),
8637        is_view: false,
8638        managed: true,
8639        verbose_name: None,
8640        verbose_name_plural: None,
8641        base_manager_name: None,
8642        order_with_respect_to: None,
8643        proxy: false,
8644        required_db_features: Vec::new(),
8645        required_db_vendor: None,
8646        default_related_name: None,
8647        db_table_comment: None,
8648        get_latest_by: None,
8649        extra_permissions: Vec::new(),
8650        default_permissions: Vec::new(),
8651        global_scopes: Vec::new(),
8652        through_relations: Vec::new(),
8653        reverse_has_relations: Vec::new(),
8654        generic_has_relations: Vec::new(),
8655    };
8656    for attr in &input.attrs {
8657        if !attr.path().is_ident("rustango") {
8658            continue;
8659        }
8660        attr.parse_nested_meta(|meta| {
8661            if meta.path.is_ident("table") {
8662                let s: LitStr = meta.value()?.parse()?;
8663                let name = s.value();
8664                // v0.27.3 (#65) — macro-time guard against table names
8665                // that compile but break SQL downstream. Hyphens are
8666                // the common footgun: PostgreSQL accepts a quoted
8667                // `"intermediate-region"` in CREATE TABLE, but the
8668                // FK / index name derivation in `migrate::ddl`
8669                // emits `intermediate-region_field_fkey` unquoted,
8670                // which then fails the SQL parser. Same shape rule
8671                // as Postgres regular identifiers so the safe path
8672                // is the only path.
8673                validate_table_name(&name, s.span())?;
8674                out.table = Some(name);
8675                return Ok(());
8676            }
8677            if meta.path.is_ident("display") {
8678                let s: LitStr = meta.value()?.parse()?;
8679                out.display = Some((s.value(), s.span()));
8680                return Ok(());
8681            }
8682            if meta.path.is_ident("app") {
8683                let s: LitStr = meta.value()?.parse()?;
8684                out.app = Some(s.value());
8685                return Ok(());
8686            }
8687            if meta.path.is_ident("scope") {
8688                let s: LitStr = meta.value()?.parse()?;
8689                let val = s.value();
8690                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8691                    return Err(meta.error(format!(
8692                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8693                    )));
8694                }
8695                out.scope = Some(val);
8696                return Ok(());
8697            }
8698            if meta.path.is_ident("admin") {
8699                let mut admin = AdminAttrs::default();
8700                meta.parse_nested_meta(|inner| {
8701                    if inner.path.is_ident("list_display") {
8702                        let s: LitStr = inner.value()?.parse()?;
8703                        admin.list_display =
8704                            Some((split_field_list(&s.value()), s.span()));
8705                        return Ok(());
8706                    }
8707                    if inner.path.is_ident("search_fields") {
8708                        let s: LitStr = inner.value()?.parse()?;
8709                        admin.search_fields =
8710                            Some((split_field_list(&s.value()), s.span()));
8711                        return Ok(());
8712                    }
8713                    if inner.path.is_ident("readonly_fields") {
8714                        let s: LitStr = inner.value()?.parse()?;
8715                        admin.readonly_fields =
8716                            Some((split_field_list(&s.value()), s.span()));
8717                        return Ok(());
8718                    }
8719                    if inner.path.is_ident("list_per_page") {
8720                        let lit: syn::LitInt = inner.value()?.parse()?;
8721                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8722                        return Ok(());
8723                    }
8724                    if inner.path.is_ident("ordering") {
8725                        let s: LitStr = inner.value()?.parse()?;
8726                        admin.ordering = Some((
8727                            parse_ordering_list(&s.value()),
8728                            s.span(),
8729                        ));
8730                        return Ok(());
8731                    }
8732                    if inner.path.is_ident("list_filter") {
8733                        let s: LitStr = inner.value()?.parse()?;
8734                        admin.list_filter =
8735                            Some((split_field_list(&s.value()), s.span()));
8736                        return Ok(());
8737                    }
8738                    if inner.path.is_ident("actions") {
8739                        let s: LitStr = inner.value()?.parse()?;
8740                        admin.actions =
8741                            Some((split_field_list(&s.value()), s.span()));
8742                        return Ok(());
8743                    }
8744                    if inner.path.is_ident("fieldsets") {
8745                        let s: LitStr = inner.value()?.parse()?;
8746                        admin.fieldsets =
8747                            Some((parse_fieldset_list(&s.value()), s.span()));
8748                        return Ok(());
8749                    }
8750                    if inner.path.is_ident("list_display_links") {
8751                        let s: LitStr = inner.value()?.parse()?;
8752                        admin.list_display_links =
8753                            Some((split_field_list(&s.value()), s.span()));
8754                        return Ok(());
8755                    }
8756                    if inner.path.is_ident("search_help_text") {
8757                        let s: LitStr = inner.value()?.parse()?;
8758                        admin.search_help_text = Some(s.value());
8759                        return Ok(());
8760                    }
8761                    if inner.path.is_ident("actions_on_top") {
8762                        let lit: syn::LitBool = inner.value()?.parse()?;
8763                        admin.actions_on_top = Some(lit.value);
8764                        return Ok(());
8765                    }
8766                    if inner.path.is_ident("actions_on_bottom") {
8767                        let lit: syn::LitBool = inner.value()?.parse()?;
8768                        admin.actions_on_bottom = Some(lit.value);
8769                        return Ok(());
8770                    }
8771                    if inner.path.is_ident("date_hierarchy") {
8772                        let s: LitStr = inner.value()?.parse()?;
8773                        admin.date_hierarchy = Some(s.value());
8774                        return Ok(());
8775                    }
8776                    if inner.path.is_ident("prepopulated_fields") {
8777                        let s: LitStr = inner.value()?.parse()?;
8778                        admin.prepopulated_fields =
8779                            Some((parse_prepopulated_list(&s.value()), s.span()));
8780                        return Ok(());
8781                    }
8782                    if inner.path.is_ident("raw_id_fields") {
8783                        let s: LitStr = inner.value()?.parse()?;
8784                        admin.raw_id_fields =
8785                            Some((split_field_list(&s.value()), s.span()));
8786                        return Ok(());
8787                    }
8788                    if inner.path.is_ident("autocomplete_fields") {
8789                        let s: LitStr = inner.value()?.parse()?;
8790                        admin.autocomplete_fields =
8791                            Some((split_field_list(&s.value()), s.span()));
8792                        return Ok(());
8793                    }
8794                    if inner.path.is_ident("list_select_related") {
8795                        let s: LitStr = inner.value()?.parse()?;
8796                        admin.list_select_related = Some(s.value());
8797                        return Ok(());
8798                    }
8799                    if inner.path.is_ident("formfield_overrides") {
8800                        let s: LitStr = inner.value()?.parse()?;
8801                        admin.formfield_overrides =
8802                            Some((parse_formfield_overrides(&s.value()), s.span()));
8803                        return Ok(());
8804                    }
8805                    Err(inner.error(
8806                        "unknown admin attribute (supported: \
8807                         `list_display`, `list_display_links`, \
8808                         `search_fields`, `search_help_text`, \
8809                         `readonly_fields`, \
8810                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8811                         `actions_on_top`, `actions_on_bottom`, \
8812                         `date_hierarchy`, \
8813                         `prepopulated_fields`, \
8814                         `raw_id_fields`, \
8815                         `autocomplete_fields`, \
8816                         `list_select_related`, \
8817                         `formfield_overrides`, \
8818                         `fieldsets`)",
8819                    ))
8820                })?;
8821                out.admin = Some(admin);
8822                return Ok(());
8823            }
8824            if meta.path.is_ident("manager") {
8825                // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8826                // Stretch `from_queryset = "..."` (Django Manager.from_queryset
8827                // shape) is left as a follow-up — the issue's primary
8828                // acceptance is the `ext = ...` trait emission.
8829                meta.parse_nested_meta(|inner| {
8830                    if inner.path.is_ident("ext") {
8831                        let s: LitStr = inner.value()?.parse()?;
8832                        let name = s.value();
8833                        if name.is_empty() {
8834                            return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8835                        }
8836                        out.manager_ext =
8837                            Some(syn::Ident::new(&name, s.span()));
8838                        return Ok(());
8839                    }
8840                    Err(inner.error(
8841                        "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8842                    ))
8843                })?;
8844                return Ok(());
8845            }
8846            if meta.path.is_ident("manager_fn") {
8847                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8848                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8849                // next to the default `Self::objects()`. Multiple
8850                // attributes accumulate.
8851                let s: LitStr = meta.value()?.parse()?;
8852                let name = s.value();
8853                if name.is_empty() {
8854                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8855                }
8856                if name == "objects" {
8857                    return Err(meta.error(
8858                        "`manager_fn = \"objects\"` collides with the default \
8859                         accessor — pick a different name",
8860                    ));
8861                }
8862                let ident = syn::Ident::new(&name, s.span());
8863                if out.manager_fns.iter().any(|prev| *prev == ident) {
8864                    return Err(meta.error(format!(
8865                        "duplicate `manager_fn = \"{name}\"`"
8866                    )));
8867                }
8868                out.manager_fns.push(ident);
8869                return Ok(());
8870            }
8871            if meta.path.is_ident("default_order") {
8872                // `#[rustango(default_order = "-created_at, status")]`
8873                // — issue #291 / T2.5. Comma-separated list; `-prefix`
8874                // means descending, `+prefix` or bare name means ascending.
8875                // Per-query opt-in via `QuerySet::with_default_order()`.
8876                let s: LitStr = meta.value()?.parse()?;
8877                let raw = s.value();
8878                let span = s.span();
8879                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8880                    Vec::new();
8881                for entry in raw.split(',') {
8882                    let trimmed = entry.trim();
8883                    if trimmed.is_empty() {
8884                        return Err(syn::Error::new(
8885                            span,
8886                            "`default_order = \"...\"` has an empty entry — \
8887                             check for a stray comma",
8888                        ));
8889                    }
8890                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8891                        (true, rest.trim().to_owned())
8892                    } else if let Some(rest) = trimmed.strip_prefix('+') {
8893                        (false, rest.trim().to_owned())
8894                    } else {
8895                        (false, trimmed.to_owned())
8896                    };
8897                    if name.is_empty() {
8898                        return Err(syn::Error::new(
8899                            span,
8900                            "`default_order` entry has no column name after the prefix",
8901                        ));
8902                    }
8903                    if parsed.iter().any(|(n, _, _)| *n == name) {
8904                        return Err(syn::Error::new(
8905                            span,
8906                            format!("duplicate column `{name}` in `default_order`"),
8907                        ));
8908                    }
8909                    parsed.push((name, desc, span));
8910                }
8911                if parsed.is_empty() {
8912                    return Err(syn::Error::new(
8913                        span,
8914                        "`default_order = \"...\"` cannot be empty",
8915                    ));
8916                }
8917                out.default_order = parsed;
8918                return Ok(());
8919            }
8920            if meta.path.is_ident("global_scope") {
8921                // `#[rustango(global_scope(name = "active", apply =
8922                //  path::to::fn))]` — issue #820. The apply function
8923                // path resolves at macro-expand time in the consumer's
8924                // scope; the macro re-emits it verbatim into the
8925                // `ModelSchema::global_scopes` slice literal.
8926                let span = meta.path.span();
8927                let mut scope_name: Option<String> = None;
8928                let mut apply_path: Option<syn::Path> = None;
8929                meta.parse_nested_meta(|inner| {
8930                    if inner.path.is_ident("name") {
8931                        let s: LitStr = inner.value()?.parse()?;
8932                        let raw = s.value();
8933                        if raw.trim().is_empty() {
8934                            return Err(syn::Error::new(
8935                                s.span(),
8936                                "`global_scope(name = \"...\")` must not be empty",
8937                            ));
8938                        }
8939                        scope_name = Some(raw);
8940                        return Ok(());
8941                    }
8942                    if inner.path.is_ident("apply") {
8943                        let p: syn::Path = inner.value()?.parse()?;
8944                        apply_path = Some(p);
8945                        return Ok(());
8946                    }
8947                    Err(inner.error(
8948                        "unknown `global_scope` attribute (supported: \
8949                         `name`, `apply`)",
8950                    ))
8951                })?;
8952                let Some(name) = scope_name else {
8953                    return Err(syn::Error::new(
8954                        span,
8955                        "`global_scope` requires `name = \"...\"`",
8956                    ));
8957                };
8958                let Some(apply) = apply_path else {
8959                    return Err(syn::Error::new(
8960                        span,
8961                        "`global_scope` requires `apply = fn_path`",
8962                    ));
8963                };
8964                if out.global_scopes.iter().any(|s| s.name == name) {
8965                    return Err(syn::Error::new(
8966                        span,
8967                        format!(
8968                            "duplicate global scope name `{name}` — \
8969                             pick a unique identifier so \
8970                             `QuerySet::without_global_scope(\"{name}\")` \
8971                             is unambiguous"
8972                        ),
8973                    ));
8974                }
8975                out.global_scopes.push(GlobalScopeAttr { name, apply });
8976                return Ok(());
8977            }
8978            if meta.path.is_ident("through") {
8979                // `#[rustango(through(name = "posts", far = "Post",
8980                //  far_fk_column = "author_id", intermediate = "User",
8981                //  intermediate_fk_column = "country_id"
8982                //  [, intermediate_pk_column = "id"]))]` — issue #817.
8983                let span = meta.path.span();
8984                let mut accessor_name: Option<String> = None;
8985                let mut far_ident: Option<syn::Ident> = None;
8986                let mut far_fk_column: Option<String> = None;
8987                let mut intermediate_ident: Option<syn::Ident> = None;
8988                let mut intermediate_fk_column: Option<String> = None;
8989                let mut intermediate_pk_column: Option<String> = None;
8990                fn parse_nonempty_string(
8991                    inner: &syn::meta::ParseNestedMeta<'_>,
8992                    field: &str,
8993                ) -> syn::Result<String> {
8994                    let s: LitStr = inner.value()?.parse()?;
8995                    let raw = s.value();
8996                    let trimmed = raw.trim();
8997                    if trimmed.is_empty() {
8998                        return Err(syn::Error::new(
8999                            s.span(),
9000                            format!("`through({field} = \"...\")` must not be empty"),
9001                        ));
9002                    }
9003                    Ok(trimmed.to_owned())
9004                }
9005                meta.parse_nested_meta(|inner| {
9006                    if inner.path.is_ident("name") {
9007                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9008                        return Ok(());
9009                    }
9010                    if inner.path.is_ident("far") {
9011                        // Far model name accepted as a string literal
9012                        // so the attribute fits inside the existing
9013                        // `parse_nested_meta` shape; parsed back into a
9014                        // `syn::Ident` so the emitted accessor can
9015                        // reference the type directly.
9016                        let s: LitStr = inner.value()?.parse()?;
9017                        let raw = s.value();
9018                        let trimmed = raw.trim();
9019                        if trimmed.is_empty() {
9020                            return Err(syn::Error::new(
9021                                s.span(),
9022                                "`through(far = \"...\")` must not be empty",
9023                            ));
9024                        }
9025                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9026                        return Ok(());
9027                    }
9028                    if inner.path.is_ident("far_fk_column") {
9029                        far_fk_column =
9030                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9031                        return Ok(());
9032                    }
9033                    if inner.path.is_ident("intermediate") {
9034                        let s: LitStr = inner.value()?.parse()?;
9035                        let raw = s.value();
9036                        let trimmed = raw.trim();
9037                        if trimmed.is_empty() {
9038                            return Err(syn::Error::new(
9039                                s.span(),
9040                                "`through(intermediate = \"...\")` must not be empty",
9041                            ));
9042                        }
9043                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9044                        return Ok(());
9045                    }
9046                    if inner.path.is_ident("intermediate_fk_column") {
9047                        intermediate_fk_column =
9048                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9049                        return Ok(());
9050                    }
9051                    if inner.path.is_ident("intermediate_pk_column") {
9052                        intermediate_pk_column =
9053                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9054                        return Ok(());
9055                    }
9056                    Err(inner.error(
9057                        "unknown `through` attribute (supported: \
9058                         `name`, `far`, `far_fk_column`, \
9059                         `intermediate`, `intermediate_fk_column`, \
9060                         `intermediate_pk_column`)",
9061                    ))
9062                })?;
9063                let Some(name) = accessor_name else {
9064                    return Err(syn::Error::new(
9065                        span,
9066                        "`through` requires `name = \"...\"`",
9067                    ));
9068                };
9069                let Some(far) = far_ident else {
9070                    return Err(syn::Error::new(
9071                        span,
9072                        "`through` requires `far = \"FarModelType\"`",
9073                    ));
9074                };
9075                let Some(far_fk_column) = far_fk_column else {
9076                    return Err(syn::Error::new(
9077                        span,
9078                        "`through` requires `far_fk_column = \"<column>\"`",
9079                    ));
9080                };
9081                let Some(intermediate) = intermediate_ident else {
9082                    return Err(syn::Error::new(
9083                        span,
9084                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9085                    ));
9086                };
9087                let Some(intermediate_fk_column) = intermediate_fk_column else {
9088                    return Err(syn::Error::new(
9089                        span,
9090                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9091                    ));
9092                };
9093                let intermediate_pk_column =
9094                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9095                if out.through_relations.iter().any(|t| t.name == name) {
9096                    return Err(syn::Error::new(
9097                        span,
9098                        format!(
9099                            "duplicate `through(name = \"{name}\")` — \
9100                             pick a unique accessor name"
9101                        ),
9102                    ));
9103                }
9104                out.through_relations.push(ThroughAttr {
9105                    name,
9106                    far,
9107                    far_fk_column,
9108                    intermediate,
9109                    intermediate_fk_column,
9110                    intermediate_pk_column,
9111                });
9112                return Ok(());
9113            }
9114            if meta.path.is_ident("reverse_has") {
9115                // `#[rustango(reverse_has(name = "comments",
9116                //  child = "Comment", child_fk_column = "post_id"
9117                //  [, self_pk_column = "id"]))]` — issue #830.
9118                let span = meta.path.span();
9119                let mut accessor_name: Option<String> = None;
9120                let mut child_ident: Option<syn::Ident> = None;
9121                let mut child_fk_column: Option<String> = None;
9122                let mut self_pk_column: Option<String> = None;
9123                meta.parse_nested_meta(|inner| {
9124                    if inner.path.is_ident("name") {
9125                        let s: LitStr = inner.value()?.parse()?;
9126                        let raw = s.value();
9127                        if raw.trim().is_empty() {
9128                            return Err(syn::Error::new(
9129                                s.span(),
9130                                "`reverse_has(name = \"...\")` must not be empty",
9131                            ));
9132                        }
9133                        accessor_name = Some(raw);
9134                        return Ok(());
9135                    }
9136                    if inner.path.is_ident("child") {
9137                        let s: LitStr = inner.value()?.parse()?;
9138                        let raw = s.value();
9139                        let trimmed = raw.trim();
9140                        if trimmed.is_empty() {
9141                            return Err(syn::Error::new(
9142                                s.span(),
9143                                "`reverse_has(child = \"...\")` must not be empty",
9144                            ));
9145                        }
9146                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9147                        return Ok(());
9148                    }
9149                    if inner.path.is_ident("child_fk_column") {
9150                        let s: LitStr = inner.value()?.parse()?;
9151                        let raw = s.value();
9152                        let trimmed = raw.trim();
9153                        if trimmed.is_empty() {
9154                            return Err(syn::Error::new(
9155                                s.span(),
9156                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9157                            ));
9158                        }
9159                        child_fk_column = Some(trimmed.to_owned());
9160                        return Ok(());
9161                    }
9162                    if inner.path.is_ident("self_pk_column") {
9163                        let s: LitStr = inner.value()?.parse()?;
9164                        let raw = s.value();
9165                        let trimmed = raw.trim();
9166                        if trimmed.is_empty() {
9167                            return Err(syn::Error::new(
9168                                s.span(),
9169                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9170                            ));
9171                        }
9172                        self_pk_column = Some(trimmed.to_owned());
9173                        return Ok(());
9174                    }
9175                    Err(inner.error(
9176                        "unknown `reverse_has` attribute (supported: \
9177                         `name`, `child`, `child_fk_column`, \
9178                         `self_pk_column`)",
9179                    ))
9180                })?;
9181                let Some(name) = accessor_name else {
9182                    return Err(syn::Error::new(
9183                        span,
9184                        "`reverse_has` requires `name = \"...\"`",
9185                    ));
9186                };
9187                let Some(child) = child_ident else {
9188                    return Err(syn::Error::new(
9189                        span,
9190                        "`reverse_has` requires `child = \"ChildModelType\"`",
9191                    ));
9192                };
9193                let Some(child_fk_column) = child_fk_column else {
9194                    return Err(syn::Error::new(
9195                        span,
9196                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9197                    ));
9198                };
9199                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9200                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9201                    return Err(syn::Error::new(
9202                        span,
9203                        format!(
9204                            "duplicate `reverse_has(name = \"{name}\")` — \
9205                             pick a unique accessor name"
9206                        ),
9207                    ));
9208                }
9209                out.reverse_has_relations.push(ReverseHasAttr {
9210                    name,
9211                    child,
9212                    child_fk_column,
9213                    self_pk_column,
9214                });
9215                return Ok(());
9216            }
9217            if meta.path.is_ident("generic_has") {
9218                // `#[rustango(generic_has(name = "tags",
9219                //  child = "Tag", ct_column = "content_type_id",
9220                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9221                //  — issue #830, the reverse generic-FK (GFK) arm of the
9222                //  relation-existence family.
9223                let span = meta.path.span();
9224                let mut accessor_name: Option<String> = None;
9225                let mut child_ident: Option<syn::Ident> = None;
9226                let mut ct_column: Option<String> = None;
9227                let mut pk_column: Option<String> = None;
9228                let mut self_pk_column: Option<String> = None;
9229                meta.parse_nested_meta(|inner| {
9230                    if inner.path.is_ident("name") {
9231                        let s: LitStr = inner.value()?.parse()?;
9232                        let raw = s.value();
9233                        if raw.trim().is_empty() {
9234                            return Err(syn::Error::new(
9235                                s.span(),
9236                                "`generic_has(name = \"...\")` must not be empty",
9237                            ));
9238                        }
9239                        accessor_name = Some(raw);
9240                        return Ok(());
9241                    }
9242                    if inner.path.is_ident("child") {
9243                        let s: LitStr = inner.value()?.parse()?;
9244                        let trimmed = s.value().trim().to_owned();
9245                        if trimmed.is_empty() {
9246                            return Err(syn::Error::new(
9247                                s.span(),
9248                                "`generic_has(child = \"...\")` must not be empty",
9249                            ));
9250                        }
9251                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9252                        return Ok(());
9253                    }
9254                    if inner.path.is_ident("ct_column") {
9255                        let s: LitStr = inner.value()?.parse()?;
9256                        let trimmed = s.value().trim().to_owned();
9257                        if trimmed.is_empty() {
9258                            return Err(syn::Error::new(
9259                                s.span(),
9260                                "`generic_has(ct_column = \"...\")` must not be empty",
9261                            ));
9262                        }
9263                        ct_column = Some(trimmed);
9264                        return Ok(());
9265                    }
9266                    if inner.path.is_ident("pk_column") {
9267                        let s: LitStr = inner.value()?.parse()?;
9268                        let trimmed = s.value().trim().to_owned();
9269                        if trimmed.is_empty() {
9270                            return Err(syn::Error::new(
9271                                s.span(),
9272                                "`generic_has(pk_column = \"...\")` must not be empty",
9273                            ));
9274                        }
9275                        pk_column = Some(trimmed);
9276                        return Ok(());
9277                    }
9278                    if inner.path.is_ident("self_pk_column") {
9279                        let s: LitStr = inner.value()?.parse()?;
9280                        let trimmed = s.value().trim().to_owned();
9281                        if trimmed.is_empty() {
9282                            return Err(syn::Error::new(
9283                                s.span(),
9284                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9285                            ));
9286                        }
9287                        self_pk_column = Some(trimmed);
9288                        return Ok(());
9289                    }
9290                    Err(inner.error(
9291                        "unknown `generic_has` attribute (supported: \
9292                         `name`, `child`, `ct_column`, `pk_column`, \
9293                         `self_pk_column`)",
9294                    ))
9295                })?;
9296                let Some(name) = accessor_name else {
9297                    return Err(syn::Error::new(
9298                        span,
9299                        "`generic_has` requires `name = \"...\"`",
9300                    ));
9301                };
9302                let Some(child) = child_ident else {
9303                    return Err(syn::Error::new(
9304                        span,
9305                        "`generic_has` requires `child = \"ChildModelType\"`",
9306                    ));
9307                };
9308                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9309                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9310                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9311                if out.generic_has_relations.iter().any(|r| r.name == name) {
9312                    return Err(syn::Error::new(
9313                        span,
9314                        format!(
9315                            "duplicate `generic_has(name = \"{name}\")` — \
9316                             pick a unique accessor name"
9317                        ),
9318                    ));
9319                }
9320                out.generic_has_relations.push(GenericHasAttr {
9321                    name,
9322                    child,
9323                    ct_column,
9324                    pk_column,
9325                    self_pk_column,
9326                });
9327                return Ok(());
9328            }
9329            if meta.path.is_ident("audit") {
9330                let mut audit = AuditAttrs::default();
9331                meta.parse_nested_meta(|inner| {
9332                    if inner.path.is_ident("track") {
9333                        let s: LitStr = inner.value()?.parse()?;
9334                        audit.track =
9335                            Some((split_field_list(&s.value()), s.span()));
9336                        return Ok(());
9337                    }
9338                    Err(inner.error(
9339                        "unknown audit attribute (supported: `track`)",
9340                    ))
9341                })?;
9342                out.audit = Some(audit);
9343                return Ok(());
9344            }
9345            if meta.path.is_ident("permissions") {
9346                // Two forms accepted:
9347                //   #[rustango(permissions)]          — flag form, true
9348                //   #[rustango(permissions = false)]  — explicit opt-out
9349                //   #[rustango(permissions = true)]   — explicit opt-in
9350                if let Ok(v) = meta.value() {
9351                    let lit: syn::LitBool = v.parse()?;
9352                    out.permissions = lit.value;
9353                } else {
9354                    out.permissions = true;
9355                }
9356                return Ok(());
9357            }
9358            if meta.path.is_ident("view") {
9359                // Issue #293 / T2.10. Two forms accepted, matching
9360                // the `permissions` flag pattern:
9361                //   #[rustango(view)]          — flag form, true
9362                //   #[rustango(view = false)]  — explicit opt-out
9363                //   #[rustango(view = true)]   — explicit opt-in
9364                if let Ok(v) = meta.value() {
9365                    let lit: syn::LitBool = v.parse()?;
9366                    out.is_view = lit.value;
9367                } else {
9368                    out.is_view = true;
9369                }
9370                return Ok(());
9371            }
9372            if meta.path.is_ident("managed") {
9373                // Django-shape Meta.managed. Issue #321.
9374                //   #[rustango(managed = false)]  — operator-managed table
9375                //   #[rustango(managed = true)]   — rustango-managed (the default)
9376                // Bare-flag form is intentionally not accepted: writing
9377                // `#[rustango(managed)]` reads as "yes please manage it"
9378                // which is already the default. The opt-out is the only
9379                // useful state, so it must be explicit.
9380                let v = meta.value()?;
9381                let lit: syn::LitBool = v.parse()?;
9382                out.managed = lit.value;
9383                return Ok(());
9384            }
9385            if meta.path.is_ident("verbose_name") {
9386                let s: LitStr = meta.value()?.parse()?;
9387                out.verbose_name = Some(s.value());
9388                return Ok(());
9389            }
9390            if meta.path.is_ident("verbose_name_plural") {
9391                let s: LitStr = meta.value()?.parse()?;
9392                out.verbose_name_plural = Some(s.value());
9393                return Ok(());
9394            }
9395            if meta.path.is_ident("db_table_comment") {
9396                // Django-shape `Meta.db_table_comment` (4.2+) — free-form
9397                // table-level comment attached to the DB catalog.
9398                let s: LitStr = meta.value()?.parse()?;
9399                out.db_table_comment = Some(s.value());
9400                return Ok(());
9401            }
9402            if meta.path.is_ident("proxy") {
9403                // Django-shape `Meta.proxy = True` — declarative flag
9404                // marking the struct as a proxy of another model that
9405                // shares its DB table. Stored on `ModelSchema::proxy`
9406                // so future codegen can skip `CreateTable` emission
9407                // for proxies (parent owns the table) and route
9408                // per-instance method resolution to the proxy class.
9409                //
9410                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9411                let value = if meta.input.peek(syn::Token![=]) {
9412                    meta.value()?.parse::<syn::LitBool>()?.value
9413                } else {
9414                    true
9415                };
9416                out.proxy = value;
9417                return Ok(());
9418            }
9419            if meta.path.is_ident("order_with_respect_to") {
9420                // Django-shape `Meta.order_with_respect_to = "parent_fk"` —
9421                // the model's instances are intrinsically ordered
9422                // relative to their parent FK. Django auto-generates
9423                // a `_order` integer column + admin reordering UI.
9424                //
9425                // rustango stores the FK field name on
9426                // `ModelSchema::order_with_respect_to`. Declarative-only
9427                // today: the migration writer + admin surfaces still
9428                // treat every model identically. Future codegen will
9429                // key off the metadata to auto-emit the `_order`
9430                // column and reorder helpers.
9431                //
9432                // Validated as a Rust-shape identifier so the macro
9433                // can reject typos at derive time.
9434                let s: LitStr = meta.value()?.parse()?;
9435                let raw = s.value();
9436                if raw.is_empty() {
9437                    return Err(syn::Error::new(
9438                        s.span(),
9439                        "`order_with_respect_to` must be a non-empty FK field name",
9440                    ));
9441                }
9442                let valid = raw
9443                    .chars()
9444                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9445                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9446                if !valid {
9447                    return Err(syn::Error::new(
9448                        s.span(),
9449                        format!(
9450                            "`order_with_respect_to` must be a valid Rust \
9451                             identifier (letters / digits / underscores, \
9452                             not starting with a digit); got `{raw}`"
9453                        ),
9454                    ));
9455                }
9456                out.order_with_respect_to = Some(raw);
9457                return Ok(());
9458            }
9459            if meta.path.is_ident("required_db_features") {
9460                // Django-shape `Meta.required_db_features` — capability
9461                // tokens the model needs (e.g. `"json_extract"`,
9462                // `"window_functions"`, `"row_security"`). Comma-separated.
9463                // `manage check --deploy` walks every model and warns
9464                // when the active backend doesn't advertise the
9465                // capability.
9466                //
9467                // rustango ships a small registry of capability tokens
9468                // each dialect supports — see `Dialect::supports`.
9469                // Unknown tokens still parse (they end up on the
9470                // schema and show up in the warning) so projects can
9471                // declare aspirational capabilities and the check
9472                // verb will keep nagging until the dialect implements
9473                // them.
9474                let s: LitStr = meta.value()?.parse()?;
9475                let raw = s.value();
9476                let features: Vec<String> = raw
9477                    .split(',')
9478                    .map(str::trim)
9479                    .filter(|s| !s.is_empty())
9480                    .map(str::to_owned)
9481                    .collect();
9482                if features.is_empty() {
9483                    return Err(syn::Error::new(
9484                        s.span(),
9485                        "`required_db_features` must list at least one \
9486                         comma-separated capability token",
9487                    ));
9488                }
9489                out.required_db_features = features;
9490                return Ok(());
9491            }
9492            if meta.path.is_ident("required_db_vendor") {
9493                // Django-shape `Meta.required_db_vendor` — the model
9494                // is only meant to run against the named DB backend.
9495                // `manage check --deploy` flags a mismatch so
9496                // ops catches "I forgot to switch DATABASE_URL" at
9497                // deploy time rather than runtime.
9498                //
9499                // Django spells it as a free-form string; rustango
9500                // restricts to the three backends it ships dialects
9501                // for so the check verb can compare reliably.
9502                let s: LitStr = meta.value()?.parse()?;
9503                let raw = s.value().to_ascii_lowercase();
9504                match raw.as_str() {
9505                    "postgresql" | "postgres" | "pg" => {
9506                        out.required_db_vendor = Some("postgres".to_owned());
9507                    }
9508                    "mysql" | "mariadb" => {
9509                        out.required_db_vendor = Some("mysql".to_owned());
9510                    }
9511                    "sqlite" | "sqlite3" => {
9512                        out.required_db_vendor = Some("sqlite".to_owned());
9513                    }
9514                    _ => {
9515                        return Err(syn::Error::new(
9516                            s.span(),
9517                            format!(
9518                                "unknown required_db_vendor `{raw}` — \
9519                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9520                                 `mysql` (alias: `mariadb`), or `sqlite` \
9521                                 (alias: `sqlite3`)"
9522                            ),
9523                        ));
9524                    }
9525                }
9526                return Ok(());
9527            }
9528            if meta.path.is_ident("base_manager_name") {
9529                // Django-shape `Meta.base_manager_name` — name of the
9530                // Manager subclass that `<instance>.<relation>_set`
9531                // uses when resolving reverse-relation managers.
9532                // Distinct from `default_manager_name` (what
9533                // `Model.objects` returns at the class level).
9534                // Stored on `ModelSchema::base_manager_name`.
9535                //
9536                // Validated as a Rust identifier so it stays safe to
9537                // re-emit as code in future reverse-manager codegen.
9538                let s: LitStr = meta.value()?.parse()?;
9539                let raw = s.value();
9540                if raw.is_empty() {
9541                    return Err(syn::Error::new(
9542                        s.span(),
9543                        "`base_manager_name` must be a non-empty string",
9544                    ));
9545                }
9546                let valid = raw
9547                    .chars()
9548                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9549                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9550                if !valid {
9551                    return Err(syn::Error::new(
9552                        s.span(),
9553                        format!(
9554                            "`base_manager_name` must be a valid Rust \
9555                             identifier (letters / digits / underscores, \
9556                             not starting with a digit); got `{raw}`"
9557                        ),
9558                    ));
9559                }
9560                out.base_manager_name = Some(raw);
9561                return Ok(());
9562            }
9563            if meta.path.is_ident("default_related_name") {
9564                // Django-shape `Meta.default_related_name` — the name
9565                // reverse-relation accessors use when callers don't
9566                // override `related_name=...` on the FK / M2M field.
9567                // Stored on `ModelSchema::default_related_name` so
9568                // future reverse-manager codegen / DRF schema emit /
9569                // admin templates can pick the right accessor name
9570                // (today rustango doesn't auto-emit reverse managers;
9571                // the metadata is the foundation for that work).
9572                //
9573                // Django requires snake_case + no `+` suffix; we
9574                // enforce non-empty + ASCII identifier-shape so the
9575                // string is safe to use as a Rust ident later.
9576                let s: LitStr = meta.value()?.parse()?;
9577                let raw = s.value();
9578                if raw.is_empty() {
9579                    return Err(syn::Error::new(
9580                        s.span(),
9581                        "`default_related_name` must be a non-empty string",
9582                    ));
9583                }
9584                let valid = raw
9585                    .chars()
9586                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9587                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9588                if !valid {
9589                    return Err(syn::Error::new(
9590                        s.span(),
9591                        format!(
9592                            "`default_related_name` must be snake_case ASCII \
9593                             (lowercase letters / digits / underscores, not \
9594                             starting with a digit); got `{raw}`"
9595                        ),
9596                    ));
9597                }
9598                out.default_related_name = Some(raw);
9599                return Ok(());
9600            }
9601            if meta.path.is_ident("extra_permissions") {
9602                // Django-shape `Meta.permissions = [(codename, name), ...]`.
9603                // Comma-separated `codename:label` pairs.
9604                let s: LitStr = meta.value()?.parse()?;
9605                let raw = s.value();
9606                let mut pairs = Vec::new();
9607                for entry in raw.split(',') {
9608                    let entry = entry.trim();
9609                    if entry.is_empty() {
9610                        continue;
9611                    }
9612                    let (codename, label) = match entry.split_once(':') {
9613                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9614                        None => (entry.to_owned(), entry.to_owned()),
9615                    };
9616                    if codename.is_empty() {
9617                        return Err(meta.error(
9618                            "`extra_permissions` entries must be `codename:label` pairs",
9619                        ));
9620                    }
9621                    pairs.push((codename, label));
9622                }
9623                if pairs.is_empty() {
9624                    return Err(meta
9625                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9626                }
9627                out.extra_permissions = pairs;
9628                return Ok(());
9629            }
9630            if meta.path.is_ident("default_permissions") {
9631                // Django-shape `Meta.default_permissions = ('view',
9632                // 'change')`. Comma-separated subset of the CRUD
9633                // action set. Empty means all four (the framework
9634                // default — matches Django when the option is
9635                // omitted).
9636                let s: LitStr = meta.value()?.parse()?;
9637                let raw = s.value();
9638                let mut actions: Vec<String> = Vec::new();
9639                for entry in raw.split(',') {
9640                    let action = entry.trim().to_ascii_lowercase();
9641                    if action.is_empty() {
9642                        continue;
9643                    }
9644                    match action.as_str() {
9645                        "add" | "change" | "delete" | "view" => {}
9646                        other => {
9647                            return Err(syn::Error::new(
9648                                s.span(),
9649                                format!(
9650                                    "unknown default_permissions action `{other}` — \
9651                                     expected one of `add`, `change`, `delete`, `view`"
9652                                ),
9653                            ));
9654                        }
9655                    }
9656                    if !actions.contains(&action) {
9657                        actions.push(action);
9658                    }
9659                }
9660                if actions.is_empty() {
9661                    return Err(syn::Error::new(
9662                        s.span(),
9663                        "`default_permissions = \"…\"` must list at least one action; \
9664                         use `permissions = false` on the container if you want NO \
9665                         permissions seeded for this model.",
9666                    ));
9667                }
9668                out.default_permissions = actions;
9669                return Ok(());
9670            }
9671            if meta.path.is_ident("get_latest_by") {
9672                // Django-shape `Meta.get_latest_by`. The `-` prefix
9673                // selects descending order (Django muscle memory).
9674                let s: LitStr = meta.value()?.parse()?;
9675                let raw = s.value();
9676                let trimmed = raw.trim();
9677                if trimmed.is_empty() {
9678                    return Err(meta.error("`get_latest_by` must name a column"));
9679                }
9680                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9681                    (stripped.to_owned(), true)
9682                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9683                    (stripped.to_owned(), false)
9684                } else {
9685                    (trimmed.to_owned(), false)
9686                };
9687                if col.is_empty() {
9688                    return Err(meta.error("`get_latest_by` must name a column"));
9689                }
9690                out.get_latest_by = Some((col, desc));
9691                return Ok(());
9692            }
9693            if meta.path.is_ident("unique_together") {
9694                // Django-shape composite UNIQUE index. Two syntaxes:
9695                //
9696                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9697                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9698                //
9699                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9700                // (col1, col2)`, where <name> defaults to
9701                // `<table>_<col1>_<col2>_uq` when not supplied.
9702                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9703                out.indexes.push(IndexAttr {
9704                    name,
9705                    columns,
9706                    unique: true,
9707                    method: "btree".to_owned(),
9708                    where_clause: None,
9709                include: Vec::new(),
9710                });
9711                return Ok(());
9712            }
9713            if meta.path.is_ident("index_together") {
9714                // Django-shape composite (non-unique) index. Two syntaxes
9715                // mirroring `unique_together`.
9716                //
9717                //   #[rustango(index_together = "created_at, status")]
9718                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9719                let (columns, name) = parse_together_attr(&meta, "index_together")?;
9720                out.indexes.push(IndexAttr {
9721                    name,
9722                    columns,
9723                    unique: false,
9724                    method: "btree".to_owned(),
9725                    where_clause: None,
9726                include: Vec::new(),
9727                });
9728                return Ok(());
9729            }
9730            if meta.path.is_ident("unique_when") {
9731                // Django 4.0+ `UniqueConstraint(condition=Q(...))` —
9732                // partial unique index. Issue #265 / T1.3.
9733                //
9734                //   #[rustango(unique_when(
9735                //       columns   = "email",
9736                //       condition = "deleted_at IS NULL",
9737                //       name      = "unique_active_email"
9738                //   ))]
9739                //
9740                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9741                // on PG / SQLite (both ship partial indexes natively).
9742                // MySQL falls back to a plain UNIQUE index — the
9743                // condition is lost; document the limitation in the
9744                // generated migration.
9745                let mut columns: Option<Vec<String>> = None;
9746                let mut condition: Option<String> = None;
9747                let mut name: Option<String> = None;
9748                let mut include: Vec<String> = Vec::new();
9749                meta.parse_nested_meta(|inner| {
9750                    if inner.path.is_ident("columns") {
9751                        let s: LitStr = inner.value()?.parse()?;
9752                        columns = Some(split_field_list(&s.value()));
9753                        return Ok(());
9754                    }
9755                    if inner.path.is_ident("condition") {
9756                        let s: LitStr = inner.value()?.parse()?;
9757                        condition = Some(s.value());
9758                        return Ok(());
9759                    }
9760                    if inner.path.is_ident("name") {
9761                        let s: LitStr = inner.value()?.parse()?;
9762                        name = Some(s.value());
9763                        return Ok(());
9764                    }
9765                    if inner.path.is_ident("include") {
9766                        // Django `UniqueConstraint(include=[...])` — PG
9767                        // 11+ covering-index columns. Non-key columns
9768                        // travel with the index leaf for index-only
9769                        // scans. Dropped on MySQL/SQLite by the writer.
9770                        let s: LitStr = inner.value()?.parse()?;
9771                        include = split_field_list(&s.value());
9772                        return Ok(());
9773                    }
9774                    Err(inner.error(
9775                        "unknown unique_when attribute (supported: \
9776                         `columns = \"...\"`, `condition = \"...\"`, \
9777                         `name = \"...\"`, `include = \"...\"`)",
9778                    ))
9779                })?;
9780                let columns = columns.ok_or_else(|| {
9781                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9782                })?;
9783                let condition = condition.ok_or_else(|| {
9784                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9785                })?;
9786                if columns.is_empty() {
9787                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9788                }
9789                out.indexes.push(IndexAttr {
9790                    name,
9791                    columns,
9792                    unique: true,
9793                    method: "btree".to_owned(),
9794                    where_clause: Some(condition),
9795                    include,
9796                });
9797                return Ok(());
9798            }
9799            if meta.path.is_ident("index_when") {
9800                // Django `Index(fields=..., condition=Q(...))` parity —
9801                // non-unique partial index. Sibling of `unique_when`
9802                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9803                //
9804                //   #[rustango(index_when(
9805                //       columns   = "status, created_at",
9806                //       condition = "deleted_at IS NULL",
9807                //       name      = "active_status_created_idx"
9808                //   ))]
9809                //
9810                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9811                // on PG / SQLite (both ship partial indexes natively).
9812                // MySQL has no native partial-index support — the writer
9813                // emits a plain CREATE INDEX and the condition is lost;
9814                // operators wanting that selectivity on MySQL should
9815                // declare a covering index plus an application-level
9816                // filter.
9817                let mut columns: Option<Vec<String>> = None;
9818                let mut condition: Option<String> = None;
9819                let mut name: Option<String> = None;
9820                let mut method: String = "btree".to_owned();
9821                let mut include: Vec<String> = Vec::new();
9822                meta.parse_nested_meta(|inner| {
9823                    if inner.path.is_ident("columns") {
9824                        let s: LitStr = inner.value()?.parse()?;
9825                        columns = Some(split_field_list(&s.value()));
9826                        return Ok(());
9827                    }
9828                    if inner.path.is_ident("condition") {
9829                        let s: LitStr = inner.value()?.parse()?;
9830                        condition = Some(s.value());
9831                        return Ok(());
9832                    }
9833                    if inner.path.is_ident("name") {
9834                        let s: LitStr = inner.value()?.parse()?;
9835                        name = Some(s.value());
9836                        return Ok(());
9837                    }
9838                    if inner.path.is_ident("method") {
9839                        let s: LitStr = inner.value()?.parse()?;
9840                        method = s.value();
9841                        return Ok(());
9842                    }
9843                    if inner.path.is_ident("include") {
9844                        // Django `Index(include=[...])` — PG 11+
9845                        // covering-index columns; non-key columns
9846                        // travel with the index leaf. Dropped on
9847                        // MySQL/SQLite.
9848                        let s: LitStr = inner.value()?.parse()?;
9849                        include = split_field_list(&s.value());
9850                        return Ok(());
9851                    }
9852                    Err(inner.error(
9853                        "unknown index_when attribute (supported: \
9854                         `columns = \"...\"`, `condition = \"...\"`, \
9855                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9856                         `include = \"...\"`)",
9857                    ))
9858                })?;
9859                let columns = columns
9860                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9861                let condition = condition.ok_or_else(|| {
9862                    meta.error("`index_when(...)` requires `condition = \"...\"`")
9863                })?;
9864                if columns.is_empty() {
9865                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
9866                }
9867                out.indexes.push(IndexAttr {
9868                    name,
9869                    columns,
9870                    unique: false,
9871                    method,
9872                    where_clause: Some(condition),
9873                    include,
9874                });
9875                return Ok(());
9876            }
9877            if meta.path.is_ident("index") {
9878                // Container-level composite index. Two syntaxes:
9879                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
9880                //   #[rustango(index("col1, col2"))]    — call form (same result)
9881                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9882                // The call form takes the column list as a leading string
9883                // literal, then optional `unique` / `name = "..."` /
9884                // `method = "..."` flags (a leading literal can't compose
9885                // under `parse_nested_meta`, so the paren body is parsed by
9886                // hand). `unique_together` / `index_together` remain the
9887                // Django-shape aliases for the same feature.
9888                let cols_lit: LitStr;
9889                let mut unique = false;
9890                let mut name: Option<String> = None;
9891                let mut method = "btree".to_owned();
9892                if meta.input.peek(syn::token::Paren) {
9893                    let content;
9894                    syn::parenthesized!(content in meta.input);
9895                    cols_lit = content.parse()?;
9896                    while content.peek(syn::Token![,]) {
9897                        content.parse::<syn::Token![,]>()?;
9898                        if content.is_empty() {
9899                            break;
9900                        }
9901                        let flag: syn::Ident = content.parse()?;
9902                        if flag == "unique" {
9903                            unique = true;
9904                        } else if flag == "name" {
9905                            content.parse::<syn::Token![=]>()?;
9906                            let s: LitStr = content.parse()?;
9907                            name = Some(s.value());
9908                        } else if flag == "method" {
9909                            content.parse::<syn::Token![=]>()?;
9910                            let s: LitStr = content.parse()?;
9911                            let v = s.value();
9912                            match v.as_str() {
9913                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9914                                | "bloom" => method = v,
9915                                other => {
9916                                    return Err(syn::Error::new(
9917                                        s.span(),
9918                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9919                                    ));
9920                                }
9921                            }
9922                        } else {
9923                            return Err(syn::Error::new(
9924                                flag.span(),
9925                                "unknown index flag (supported: `unique`, `name`, `method`)",
9926                            ));
9927                        }
9928                    }
9929                } else {
9930                    cols_lit = meta.value()?.parse()?;
9931                }
9932                let columns = split_field_list(&cols_lit.value());
9933                out.indexes.push(IndexAttr {
9934                    name,
9935                    columns,
9936                    unique,
9937                    method,
9938                    where_clause: None,
9939                    include: Vec::new(),
9940                });
9941                return Ok(());
9942            }
9943            if meta.path.is_ident("check") {
9944                // #[rustango(check(name = "…", expr = "…"))]
9945                let mut name: Option<String> = None;
9946                let mut expr: Option<String> = None;
9947                meta.parse_nested_meta(|inner| {
9948                    if inner.path.is_ident("name") {
9949                        let s: LitStr = inner.value()?.parse()?;
9950                        name = Some(s.value());
9951                        return Ok(());
9952                    }
9953                    if inner.path.is_ident("expr") {
9954                        let s: LitStr = inner.value()?.parse()?;
9955                        expr = Some(s.value());
9956                        return Ok(());
9957                    }
9958                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9959                })?;
9960                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9961                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9962                out.checks.push(CheckAttr { name, expr });
9963                return Ok(());
9964            }
9965            if meta.path.is_ident("exclude") {
9966                // #[rustango(exclude(name = "…", using = "gist",
9967                //                    elements = "col WITH op, col WITH op",
9968                //                    where = "…"))]
9969                let mut name: Option<String> = None;
9970                let mut using: Option<String> = None;
9971                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9972                let mut where_clause: Option<String> = None;
9973                meta.parse_nested_meta(|inner| {
9974                    if inner.path.is_ident("name") {
9975                        let s: LitStr = inner.value()?.parse()?;
9976                        name = Some(s.value());
9977                        return Ok(());
9978                    }
9979                    if inner.path.is_ident("using") {
9980                        let s: LitStr = inner.value()?.parse()?;
9981                        using = Some(s.value());
9982                        return Ok(());
9983                    }
9984                    if inner.path.is_ident("elements") {
9985                        let s: LitStr = inner.value()?.parse()?;
9986                        elements_raw = Some((s.value(), s.span()));
9987                        return Ok(());
9988                    }
9989                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9990                        let s: LitStr = inner.value()?.parse()?;
9991                        where_clause = Some(s.value());
9992                        return Ok(());
9993                    }
9994                    Err(inner.error(
9995                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9996                    ))
9997                })?;
9998                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9999                let using = using.unwrap_or_else(|| "gist".to_owned());
10000                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10001                    meta.error(
10002                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
10003                    )
10004                })?;
10005                // Parse `col WITH op` pairs separated by commas.
10006                let mut elements: Vec<(String, String)> = Vec::new();
10007                for pair in elements_str.split(',') {
10008                    let pair = pair.trim();
10009                    if pair.is_empty() {
10010                        continue;
10011                    }
10012                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10013                    let col = split.next().unwrap_or("").trim();
10014                    let rest = split.next().unwrap_or("").trim();
10015                    // `WITH op` — case-insensitive on `WITH`, then op.
10016                    let rest_lc = rest.to_ascii_lowercase();
10017                    let op = rest_lc
10018                        .strip_prefix("with")
10019                        .map(|r| r.trim_start())
10020                        .filter(|r| !r.is_empty())
10021                        .map(|_| {
10022                            // Pull the original-case op from `rest` after the
10023                            // `WITH ` token (5 chars).
10024                            rest[4..].trim_start().to_owned()
10025                        });
10026                    let Some(op) = op else {
10027                        return Err(syn::Error::new(
10028                            elements_span,
10029                            format!(
10030                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10031                                 (e.g. `room_id WITH =` or `during WITH &&`)"
10032                            ),
10033                        ));
10034                    };
10035                    if col.is_empty() || op.is_empty() {
10036                        return Err(syn::Error::new(
10037                            elements_span,
10038                            format!(
10039                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10040                                 (both sides non-empty)"
10041                            ),
10042                        ));
10043                    }
10044                    elements.push((col.to_owned(), op));
10045                }
10046                if elements.is_empty() {
10047                    return Err(syn::Error::new(
10048                        elements_span,
10049                        "exclude requires at least one `col WITH op` element",
10050                    ));
10051                }
10052                out.excludes.push(ExcludeAttr {
10053                    name,
10054                    using,
10055                    elements,
10056                    where_clause,
10057                });
10058                return Ok(());
10059            }
10060            if meta.path.is_ident("generic_fk") {
10061                let mut gfk = GenericFkAttr {
10062                    name: String::new(),
10063                    ct_column: String::new(),
10064                    pk_column: String::new(),
10065                };
10066                meta.parse_nested_meta(|inner| {
10067                    if inner.path.is_ident("name") {
10068                        let s: LitStr = inner.value()?.parse()?;
10069                        gfk.name = s.value();
10070                        return Ok(());
10071                    }
10072                    if inner.path.is_ident("ct_column") {
10073                        let s: LitStr = inner.value()?.parse()?;
10074                        gfk.ct_column = s.value();
10075                        return Ok(());
10076                    }
10077                    if inner.path.is_ident("pk_column") {
10078                        let s: LitStr = inner.value()?.parse()?;
10079                        gfk.pk_column = s.value();
10080                        return Ok(());
10081                    }
10082                    Err(inner.error(
10083                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10084                    ))
10085                })?;
10086                if gfk.name.is_empty() {
10087                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10088                }
10089                if gfk.ct_column.is_empty() {
10090                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10091                }
10092                if gfk.pk_column.is_empty() {
10093                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10094                }
10095                out.generic_fks.push(gfk);
10096                return Ok(());
10097            }
10098            if meta.path.is_ident("fk_composite") {
10099                let mut fk = CompositeFkAttr {
10100                    name: String::new(),
10101                    to: String::new(),
10102                    from: Vec::new(),
10103                    on: Vec::new(),
10104                };
10105                meta.parse_nested_meta(|inner| {
10106                    if inner.path.is_ident("name") {
10107                        let s: LitStr = inner.value()?.parse()?;
10108                        fk.name = s.value();
10109                        return Ok(());
10110                    }
10111                    if inner.path.is_ident("to") {
10112                        let s: LitStr = inner.value()?.parse()?;
10113                        fk.to = s.value();
10114                        return Ok(());
10115                    }
10116                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10117                    // comma-list of string literals.
10118                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10119                        let value = inner.value()?;
10120                        let content;
10121                        syn::parenthesized!(content in value);
10122                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10123                            content.parse_terminated(
10124                                |p| p.parse::<syn::LitStr>(),
10125                                syn::Token![,],
10126                            )?;
10127                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10128                        if inner.path.is_ident("on") {
10129                            fk.on = cols;
10130                        } else {
10131                            fk.from = cols;
10132                        }
10133                        return Ok(());
10134                    }
10135                    Err(inner.error(
10136                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10137                    ))
10138                })?;
10139                if fk.name.is_empty() {
10140                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10141                }
10142                if fk.to.is_empty() {
10143                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10144                }
10145                if fk.from.is_empty() || fk.on.is_empty() {
10146                    return Err(meta.error(
10147                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10148                    ));
10149                }
10150                if fk.from.len() != fk.on.len() {
10151                    return Err(meta.error(format!(
10152                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10153                        fk.from.len(),
10154                        fk.on.len(),
10155                    )));
10156                }
10157                out.composite_fks.push(fk);
10158                return Ok(());
10159            }
10160            if meta.path.is_ident("m2m") {
10161                let mut m2m = M2MAttr {
10162                    name: String::new(),
10163                    to: String::new(),
10164                    through: String::new(),
10165                    src: String::new(),
10166                    dst: String::new(),
10167                    auto_create: true,
10168                };
10169                meta.parse_nested_meta(|inner| {
10170                    if inner.path.is_ident("name") {
10171                        let s: LitStr = inner.value()?.parse()?;
10172                        m2m.name = s.value();
10173                        return Ok(());
10174                    }
10175                    if inner.path.is_ident("to") {
10176                        let s: LitStr = inner.value()?.parse()?;
10177                        m2m.to = s.value();
10178                        return Ok(());
10179                    }
10180                    if inner.path.is_ident("through") {
10181                        let s: LitStr = inner.value()?.parse()?;
10182                        m2m.through = s.value();
10183                        return Ok(());
10184                    }
10185                    if inner.path.is_ident("src") {
10186                        let s: LitStr = inner.value()?.parse()?;
10187                        m2m.src = s.value();
10188                        return Ok(());
10189                    }
10190                    if inner.path.is_ident("dst") {
10191                        let s: LitStr = inner.value()?.parse()?;
10192                        m2m.dst = s.value();
10193                        return Ok(());
10194                    }
10195                    if inner.path.is_ident("auto_create") {
10196                        let lit: syn::LitBool = inner.value()?.parse()?;
10197                        m2m.auto_create = lit.value;
10198                        return Ok(());
10199                    }
10200                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10201                })?;
10202                if m2m.name.is_empty() {
10203                    return Err(meta.error("m2m requires `name = \"...\"`"));
10204                }
10205                if m2m.to.is_empty() {
10206                    return Err(meta.error("m2m requires `to = \"...\"`"));
10207                }
10208                if m2m.through.is_empty() {
10209                    return Err(meta.error("m2m requires `through = \"...\"`"));
10210                }
10211                if m2m.src.is_empty() {
10212                    return Err(meta.error("m2m requires `src = \"...\"`"));
10213                }
10214                if m2m.dst.is_empty() {
10215                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10216                }
10217                out.m2m.push(m2m);
10218                return Ok(());
10219            }
10220            if meta.path.is_ident("generic_m2m") {
10221                let mut gm = GenericM2MAttr {
10222                    name: String::new(),
10223                    through: String::new(),
10224                    pk_column: String::new(),
10225                    ct_column: String::new(),
10226                    related_column: String::new(),
10227                };
10228                meta.parse_nested_meta(|inner| {
10229                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10230                        let s: LitStr = inner.value()?.parse()?;
10231                        Ok(s.value())
10232                    };
10233                    if inner.path.is_ident("name") {
10234                        gm.name = field(&inner)?;
10235                        return Ok(());
10236                    }
10237                    if inner.path.is_ident("through") {
10238                        gm.through = field(&inner)?;
10239                        return Ok(());
10240                    }
10241                    if inner.path.is_ident("pk_column") {
10242                        gm.pk_column = field(&inner)?;
10243                        return Ok(());
10244                    }
10245                    if inner.path.is_ident("ct_column") {
10246                        gm.ct_column = field(&inner)?;
10247                        return Ok(());
10248                    }
10249                    if inner.path.is_ident("related_column") {
10250                        gm.related_column = field(&inner)?;
10251                        return Ok(());
10252                    }
10253                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10254                })?;
10255                for (val, label) in [
10256                    (&gm.name, "name"),
10257                    (&gm.through, "through"),
10258                    (&gm.pk_column, "pk_column"),
10259                    (&gm.ct_column, "ct_column"),
10260                    (&gm.related_column, "related_column"),
10261                ] {
10262                    if val.is_empty() {
10263                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10264                    }
10265                }
10266                out.generic_m2m.push(gm);
10267                return Ok(());
10268            }
10269            Err(meta.error("unknown rustango container attribute"))
10270        })?;
10271    }
10272    Ok(out)
10273}
10274
10275/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10276/// owned field names, trimming whitespace and skipping empty entries.
10277/// Field-name validation against the model is done by the caller.
10278fn split_field_list(raw: &str) -> Vec<String> {
10279    raw.split(',')
10280        .map(str::trim)
10281        .filter(|s| !s.is_empty())
10282        .map(str::to_owned)
10283        .collect()
10284}
10285
10286/// Shared parser for `unique_together` and `index_together` container
10287/// attrs. Accepts both shapes:
10288///
10289///   * `attr = "col1, col2"`              — auto-derived index name.
10290///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10291///
10292/// Returns `(columns, name)`.
10293fn parse_together_attr(
10294    meta: &syn::meta::ParseNestedMeta<'_>,
10295    attr: &str,
10296) -> syn::Result<(Vec<String>, Option<String>)> {
10297    // Disambiguate by whether the next token is `=` (key-value) or
10298    // `(` (parenthesized).
10299    if meta.input.peek(syn::Token![=]) {
10300        let cols_lit: LitStr = meta.value()?.parse()?;
10301        let columns = split_field_list(&cols_lit.value());
10302        check_together_columns(meta, attr, &columns)?;
10303        return Ok((columns, None));
10304    }
10305    let mut columns: Option<Vec<String>> = None;
10306    let mut name: Option<String> = None;
10307    meta.parse_nested_meta(|inner| {
10308        if inner.path.is_ident("columns") {
10309            let s: LitStr = inner.value()?.parse()?;
10310            columns = Some(split_field_list(&s.value()));
10311            return Ok(());
10312        }
10313        if inner.path.is_ident("name") {
10314            let s: LitStr = inner.value()?.parse()?;
10315            name = Some(s.value());
10316            return Ok(());
10317        }
10318        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10319    })?;
10320    let columns = columns.ok_or_else(|| {
10321        meta.error(format!(
10322            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10323        ))
10324    })?;
10325    check_together_columns(meta, attr, &columns)?;
10326    Ok((columns, name))
10327}
10328
10329fn check_together_columns(
10330    meta: &syn::meta::ParseNestedMeta<'_>,
10331    attr: &str,
10332    columns: &[String],
10333) -> syn::Result<()> {
10334    if columns.len() < 2 {
10335        let single = if attr == "unique_together" {
10336            "#[rustango(unique)] on the field"
10337        } else {
10338            "#[rustango(index)] on the field"
10339        };
10340        return Err(meta.error(format!(
10341            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10342        )));
10343    }
10344    Ok(())
10345}
10346
10347/// Parse the fieldsets DSL: pipe-separated sections, optional
10348/// `"Title:"` prefix on each, comma-separated field names after.
10349/// Examples:
10350/// * `"name, office"` → one untitled section with two fields
10351/// * `"Identity: name, office | Metadata: created_at"` → two titled
10352///   sections
10353///
10354/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10355fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10356    raw.split('|')
10357        .map(str::trim)
10358        .filter(|s| !s.is_empty())
10359        .map(|section| {
10360            // Split off an optional `Title:` prefix (first colon).
10361            let (title, rest) = match section.split_once(':') {
10362                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10363                _ => (String::new(), section),
10364            };
10365            let fields = split_field_list(rest);
10366            (title, fields)
10367        })
10368        .collect()
10369}
10370
10371/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10372/// comma-separated entry maps a target field to one or more source
10373/// fields joined with `+`. Whitespace around tokens is trimmed.
10374/// Entries missing `:` or with empty target/source lists are dropped.
10375fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10376    raw.split(',')
10377        .map(str::trim)
10378        .filter(|s| !s.is_empty())
10379        .filter_map(|entry| {
10380            let (target, sources_raw) = entry.split_once(':')?;
10381            let target = target.trim().to_owned();
10382            if target.is_empty() {
10383                return None;
10384            }
10385            let sources: Vec<String> = sources_raw
10386                .split('+')
10387                .map(|s| s.trim().to_owned())
10388                .filter(|s| !s.is_empty())
10389                .collect();
10390            if sources.is_empty() {
10391                return None;
10392            }
10393            Some((target, sources))
10394        })
10395        .collect()
10396}
10397
10398/// Parse Django-shape `formfield_overrides` — `"field:widget,field2:widget2"`
10399/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10400/// and empty halves drop silently — the macro layer only enforces shape,
10401/// not field-name vs. widget-name validity (those checks happen at
10402/// `AdminConfig` consumption time). Issue #359.
10403fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10404    raw.split(',')
10405        .map(str::trim)
10406        .filter(|s| !s.is_empty())
10407        .filter_map(|entry| {
10408            let (field, widget) = entry.split_once(':')?;
10409            let field = field.trim().to_owned();
10410            let widget = widget.trim().to_owned();
10411            if field.is_empty() || widget.is_empty() {
10412                return None;
10413            }
10414            Some((field, widget))
10415        })
10416        .collect()
10417}
10418
10419/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
10420/// Returns `(field_name, desc)` pairs in the same order as the input.
10421fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10422    raw.split(',')
10423        .map(str::trim)
10424        .filter(|s| !s.is_empty())
10425        .map(|spec| {
10426            spec.strip_prefix('-')
10427                .map_or((spec.to_owned(), false), |rest| {
10428                    (rest.trim().to_owned(), true)
10429                })
10430        })
10431        .collect()
10432}
10433
10434struct FieldAttrs {
10435    column: Option<String>,
10436    primary_key: bool,
10437    fk: Option<String>,
10438    o2o: Option<String>,
10439    on: Option<String>,
10440    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10441    /// "set_default" | "no_action")]` — Django-shape
10442    /// `ForeignKey(on_delete=…)`. Only meaningful when `fk` / `o2o` is
10443    /// also set; the macro errors at compile time if applied to a
10444    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10445    /// DDL writer renders `ON DELETE <action>` after the constraint
10446    /// clause when this is `Some`; `None` falls back to the database
10447    /// default (NO ACTION on every backend rustango supports).
10448    on_delete: Option<String>,
10449    /// `#[rustango(related_name = "...")]` — Django-shape per-FK
10450    /// reverse-accessor override. When set, the derive emits
10451    /// `Parent::<related_name>[_pool]` instead of the container-level
10452    /// `default_related_name` or the `<child_snake>_set[_pool]`
10453    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10454    /// silently ignored on non-FK fields. Follow-up to #816.
10455    related_name: Option<String>,
10456    max_length: Option<u32>,
10457    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10458    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10459    /// unconstrained `vector` column.
10460    vector_dims: Option<u32>,
10461    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10462    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10463    /// unconstrained `geometry(Point)` column (SRID 0).
10464    geometry_srid: Option<u32>,
10465    min: Option<i64>,
10466    max: Option<i64>,
10467    default: Option<String>,
10468    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10469    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10470    /// "gen_random_uuid()"`. The Rust field type must be
10471    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10472    /// INSERTs so the DB DEFAULT fires.
10473    auto_uuid: bool,
10474    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10475    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10476    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10477    /// the field is `Auto::Unset`, then bound as a normal parameter
10478    /// rather than relying on a per-dialect DB function. Issue #823
10479    /// (Eloquent `HasUuids`).
10480    ///
10481    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10482    /// Composes with every backend (PG / MySQL / SQLite) — no
10483    /// `gen_random_uuid()` requirement on the database.
10484    default_uuid_v7: bool,
10485    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10486    /// Server-set on insert, immutable from app code afterwards.
10487    /// Implies `auto + default = "now()"`. Field type must be
10488    /// `DateTime<Utc>`.
10489    auto_now_add: bool,
10490    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10491    /// every insert AND every update. Implies `auto + default =
10492    /// "now()"`; the macro additionally rewrites `update_on` /
10493    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10494    /// field value.
10495    auto_now: bool,
10496    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10497    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10498    /// `soft_delete_on(executor)` and `restore_on(executor)`
10499    /// methods on the model.
10500    soft_delete: bool,
10501    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10502    /// the column in the generated DDL.
10503    unique: bool,
10504    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10505    /// generates a `CREATE INDEX` for this column. `unique` here means
10506    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10507    index: bool,
10508    index_unique: bool,
10509    index_name: Option<String>,
10510    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10511    /// `"btree"`. Issue #34.
10512    index_method: String,
10513    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10514    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10515    /// the macro skips this column from every INSERT and UPDATE
10516    /// path, so the database always recomputes the value from
10517    /// `EXPR`. Backlog item #35.
10518    generated_as: Option<String>,
10519    /// `#[rustango(help_text = "…")]` — Django-shape help text
10520    /// rendered below the admin form's input. Threaded into
10521    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10522    /// layers can read it.
10523    help_text: Option<String>,
10524    /// `#[rustango(choices = "value:Label, value:Label")]` — Django-shape
10525    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10526    /// as a `&'static [(&'static str, &'static str)]` slice. When
10527    /// present, the admin form renders a `<select>` instead of `<input>`
10528    /// and the validator rejects values not in the list. Only meaningful
10529    /// for `FieldType::String`; the macro errors at compile time if
10530    /// applied to a non-string field.
10531    choices: Option<Vec<(String, String)>>,
10532    /// `#[rustango(db_comment = "…")]` — Django-shape DB-side column
10533    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10534    /// the comment in CREATE TABLE; Postgres emits a separate
10535    /// `COMMENT ON COLUMN` statement after the table is created;
10536    /// SQLite silently drops the value (no native column comments).
10537    db_comment: Option<String>,
10538    /// `#[rustango(verbose_name = "…")]` — Django-shape human-readable
10539    /// label for the field. Threaded into `FieldSchema::verbose_name`
10540    /// so admin column headers, form labels, and other display
10541    /// surfaces can prefer the friendly caption over the Rust
10542    /// identifier. `None` means renderers fall back to the field name.
10543    verbose_name: Option<String>,
10544    /// `#[rustango(editable = false)]` — Django-shape opt-out from
10545    /// auto-generated form rendering. Defaults to `true` so existing
10546    /// fields keep their current admin / form behavior; setting
10547    /// `false` removes the field from the admin change-form entirely
10548    /// (the value is still visible on detail / list views, just not
10549    /// editable).
10550    editable: bool,
10551    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` — Django-shape
10552    /// "form may submit empty even when DB is NOT NULL". Threaded into
10553    /// `FieldSchema::blank`. Defaults to `false`.
10554    blank: bool,
10555    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10556    /// Django-shape `CITextField`. Threaded into
10557    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10558    /// fields; the macro errors at derive time if applied elsewhere.
10559    case_insensitive: bool,
10560    /// `#[rustango(validators = "email,url")]` — Django-shape
10561    /// model-level validator chain. Comma-separated names that
10562    /// dispatch to the `validators::*` family in `validate_value`.
10563    /// Empty by default; fires on every typed INSERT/UPDATE.
10564    validators: Vec<String>,
10565}
10566
10567fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10568    let mut out = FieldAttrs {
10569        column: None,
10570        primary_key: false,
10571        fk: None,
10572        o2o: None,
10573        on: None,
10574        on_delete: None,
10575        related_name: None,
10576        max_length: None,
10577        vector_dims: None,
10578        geometry_srid: None,
10579        min: None,
10580        max: None,
10581        default: None,
10582        auto_uuid: false,
10583        default_uuid_v7: false,
10584        auto_now_add: false,
10585        auto_now: false,
10586        soft_delete: false,
10587        unique: false,
10588        index: false,
10589        index_unique: false,
10590        index_name: None,
10591        index_method: "btree".to_owned(),
10592        generated_as: None,
10593        help_text: None,
10594        choices: None,
10595        db_comment: None,
10596        verbose_name: None,
10597        editable: true,
10598        blank: false,
10599        case_insensitive: false,
10600        validators: Vec::new(),
10601    };
10602    for attr in &field.attrs {
10603        if !attr.path().is_ident("rustango") {
10604            continue;
10605        }
10606        attr.parse_nested_meta(|meta| {
10607            if meta.path.is_ident("column") {
10608                let s: LitStr = meta.value()?.parse()?;
10609                let name = s.value();
10610                validate_sql_identifier(&name, "column", s.span())?;
10611                out.column = Some(name);
10612                return Ok(());
10613            }
10614            if meta.path.is_ident("primary_key") {
10615                out.primary_key = true;
10616                return Ok(());
10617            }
10618            if meta.path.is_ident("fk") {
10619                let s: LitStr = meta.value()?.parse()?;
10620                out.fk = Some(s.value());
10621                return Ok(());
10622            }
10623            if meta.path.is_ident("o2o") {
10624                let s: LitStr = meta.value()?.parse()?;
10625                out.o2o = Some(s.value());
10626                return Ok(());
10627            }
10628            if meta.path.is_ident("on") {
10629                let s: LitStr = meta.value()?.parse()?;
10630                out.on = Some(s.value());
10631                return Ok(());
10632            }
10633            if meta.path.is_ident("on_delete") {
10634                let s: LitStr = meta.value()?.parse()?;
10635                let raw = s.value();
10636                let normalized = raw.trim().to_ascii_lowercase();
10637                // Validate at parse time so the user gets a clear span
10638                // rather than a downstream compile error in the emit.
10639                match normalized.as_str() {
10640                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10641                    _ => {
10642                        return Err(syn::Error::new(
10643                            s.span(),
10644                            format!(
10645                                "unknown on_delete action `{raw}`; expected one of \
10646                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10647                            ),
10648                        ));
10649                    }
10650                }
10651                out.on_delete = Some(normalized);
10652                return Ok(());
10653            }
10654            if meta.path.is_ident("related_name") {
10655                let s: LitStr = meta.value()?.parse()?;
10656                let raw = s.value();
10657                if raw.trim().is_empty() {
10658                    return Err(syn::Error::new(
10659                        s.span(),
10660                        "`related_name` must be a non-empty identifier",
10661                    ));
10662                }
10663                // Validate as a Rust-ident shape — the value becomes
10664                // a method name on the parent type. Same rule as
10665                // `default_related_name` so the two surfaces match.
10666                if !raw
10667                    .chars()
10668                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10669                    || raw.starts_with(char::is_numeric)
10670                {
10671                    return Err(syn::Error::new(
10672                        s.span(),
10673                        "`related_name` must be snake_case ASCII (lowercase letters, \
10674                         digits, underscores; no leading digit)",
10675                    ));
10676                }
10677                out.related_name = Some(raw);
10678                return Ok(());
10679            }
10680            if meta.path.is_ident("max_length") {
10681                let lit: syn::LitInt = meta.value()?.parse()?;
10682                out.max_length = Some(lit.base10_parse::<u32>()?);
10683                return Ok(());
10684            }
10685            // `#[rustango(vector(dims = N))]` — pgvector column
10686            // dimension (#824). Nested-meta form so it reads like the
10687            // other typed-column attrs.
10688            if meta.path.is_ident("vector") {
10689                meta.parse_nested_meta(|inner| {
10690                    if inner.path.is_ident("dims") {
10691                        let lit: syn::LitInt = inner.value()?.parse()?;
10692                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10693                        return Ok(());
10694                    }
10695                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10696                })?;
10697                return Ok(());
10698            }
10699            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10700            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10701            if meta.path.is_ident("geometry") {
10702                meta.parse_nested_meta(|inner| {
10703                    if inner.path.is_ident("srid") {
10704                        let lit: syn::LitInt = inner.value()?.parse()?;
10705                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10706                        return Ok(());
10707                    }
10708                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10709                })?;
10710                return Ok(());
10711            }
10712            if meta.path.is_ident("min") {
10713                out.min = Some(parse_signed_i64(&meta)?);
10714                return Ok(());
10715            }
10716            if meta.path.is_ident("max") {
10717                out.max = Some(parse_signed_i64(&meta)?);
10718                return Ok(());
10719            }
10720            if meta.path.is_ident("default") {
10721                let s: LitStr = meta.value()?.parse()?;
10722                out.default = Some(s.value());
10723                return Ok(());
10724            }
10725            if meta.path.is_ident("generated_as") {
10726                let s: LitStr = meta.value()?.parse()?;
10727                out.generated_as = Some(s.value());
10728                return Ok(());
10729            }
10730            if meta.path.is_ident("help_text") {
10731                let s: LitStr = meta.value()?.parse()?;
10732                out.help_text = Some(s.value());
10733                return Ok(());
10734            }
10735            if meta.path.is_ident("choices") {
10736                let s: LitStr = meta.value()?.parse()?;
10737                let raw = s.value();
10738                let mut pairs: Vec<(String, String)> = Vec::new();
10739                for chunk in raw.split(',') {
10740                    let chunk = chunk.trim();
10741                    if chunk.is_empty() {
10742                        continue;
10743                    }
10744                    let (value, label) = match chunk.split_once(':') {
10745                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10746                        None => (chunk.to_owned(), chunk.to_owned()),
10747                    };
10748                    if value.is_empty() {
10749                        return Err(syn::Error::new(
10750                            s.span(),
10751                            "`choices` entry has empty value before `:`",
10752                        ));
10753                    }
10754                    pairs.push((value, label));
10755                }
10756                if pairs.is_empty() {
10757                    return Err(syn::Error::new(
10758                        s.span(),
10759                        "`choices = \"…\"` must contain at least one value",
10760                    ));
10761                }
10762                out.choices = Some(pairs);
10763                return Ok(());
10764            }
10765            if meta.path.is_ident("db_comment") {
10766                let s: LitStr = meta.value()?.parse()?;
10767                out.db_comment = Some(s.value());
10768                return Ok(());
10769            }
10770            if meta.path.is_ident("verbose_name") {
10771                let s: LitStr = meta.value()?.parse()?;
10772                out.verbose_name = Some(s.value());
10773                return Ok(());
10774            }
10775            if meta.path.is_ident("editable") {
10776                // Two forms accepted:
10777                //   #[rustango(editable = false)] / true — explicit
10778                //   #[rustango(editable)] — flag form (= true, the
10779                //   default, so harmless; included for symmetry)
10780                if let Ok(v) = meta.value() {
10781                    let lit: syn::LitBool = v.parse()?;
10782                    out.editable = lit.value;
10783                } else {
10784                    out.editable = true;
10785                }
10786                return Ok(());
10787            }
10788            if meta.path.is_ident("blank") {
10789                // Two forms accepted:
10790                //   #[rustango(blank)] — flag form, true
10791                //   #[rustango(blank = false)] / true — explicit
10792                if let Ok(v) = meta.value() {
10793                    let lit: syn::LitBool = v.parse()?;
10794                    out.blank = lit.value;
10795                } else {
10796                    out.blank = true;
10797                }
10798                return Ok(());
10799            }
10800            if meta.path.is_ident("citext") {
10801                // Django-parity CITextField (#344). Two forms:
10802                //   #[rustango(citext)]          — flag form, true
10803                //   #[rustango(citext = true)]   — explicit
10804                //   #[rustango(citext = false)]  — explicit opt-out
10805                // String-only validation lives in the field-type
10806                // emitter (the FieldType discriminant is computed in
10807                // `detect_type`); the macro records the flag and the
10808                // DDL writer emits dialect-specific COLLATE / CITEXT.
10809                if let Ok(v) = meta.value() {
10810                    let lit: syn::LitBool = v.parse()?;
10811                    out.case_insensitive = lit.value;
10812                } else {
10813                    out.case_insensitive = true;
10814                }
10815                return Ok(());
10816            }
10817            if meta.path.is_ident("validators") {
10818                let s: LitStr = meta.value()?.parse()?;
10819                let raw = s.value();
10820                out.validators = raw
10821                    .split(',')
10822                    .map(str::trim)
10823                    .filter(|s| !s.is_empty())
10824                    .map(str::to_owned)
10825                    .collect();
10826                if out.validators.is_empty() {
10827                    return Err(syn::Error::new(
10828                        s.span(),
10829                        "`validators = \"…\"` must list at least one name",
10830                    ));
10831                }
10832                return Ok(());
10833            }
10834            if meta.path.is_ident("auto_uuid") {
10835                out.auto_uuid = true;
10836                // Implied: PK + auto + DEFAULT gen_random_uuid().
10837                // Each is also explicitly settable; the explicit
10838                // value wins if conflicting.
10839                out.primary_key = true;
10840                if out.default.is_none() {
10841                    out.default = Some("gen_random_uuid()".into());
10842                }
10843                return Ok(());
10844            }
10845            if meta.path.is_ident("default_uuid_v7") {
10846                // Backend-neutral counterpart of `auto_uuid` — issue #823.
10847                // No SQL DEFAULT (the macro fills the value Rust-side
10848                // before binding); just mark the field as PK + Auto
10849                // so the insert path is the `Auto::Unset → generate`
10850                // branch.
10851                out.default_uuid_v7 = true;
10852                out.primary_key = true;
10853                return Ok(());
10854            }
10855            if meta.path.is_ident("auto_now_add") {
10856                out.auto_now_add = true;
10857                if out.default.is_none() {
10858                    out.default = Some("now()".into());
10859                }
10860                return Ok(());
10861            }
10862            if meta.path.is_ident("auto_now") {
10863                out.auto_now = true;
10864                if out.default.is_none() {
10865                    out.default = Some("now()".into());
10866                }
10867                return Ok(());
10868            }
10869            if meta.path.is_ident("soft_delete") {
10870                out.soft_delete = true;
10871                return Ok(());
10872            }
10873            if meta.path.is_ident("unique") {
10874                out.unique = true;
10875                return Ok(());
10876            }
10877            if meta.path.is_ident("index") {
10878                out.index = true;
10879                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10880                if meta.input.peek(syn::token::Paren) {
10881                    meta.parse_nested_meta(|inner| {
10882                        if inner.path.is_ident("unique") {
10883                            out.index_unique = true;
10884                            return Ok(());
10885                        }
10886                        if inner.path.is_ident("name") {
10887                            let s: LitStr = inner.value()?.parse()?;
10888                            out.index_name = Some(s.value());
10889                            return Ok(());
10890                        }
10891                        if inner.path.is_ident("method") {
10892                            let s: LitStr = inner.value()?.parse()?;
10893                            let v = s.value();
10894                            match v.as_str() {
10895                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10896                                    out.index_method = v;
10897                                }
10898                                other => {
10899                                    return Err(inner.error(format!(
10900                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10901                                    )));
10902                                }
10903                            }
10904                            return Ok(());
10905                        }
10906                        Err(inner.error(
10907                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10908                        ))
10909                    })?;
10910                }
10911                return Ok(());
10912            }
10913            Err(meta.error("unknown rustango field attribute"))
10914        })?;
10915    }
10916    Ok(out)
10917}
10918
10919/// Parse a signed integer literal, accepting optional leading `-`.
10920fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10921    let expr: syn::Expr = meta.value()?.parse()?;
10922    match expr {
10923        syn::Expr::Lit(syn::ExprLit {
10924            lit: syn::Lit::Int(lit),
10925            ..
10926        }) => lit.base10_parse::<i64>(),
10927        syn::Expr::Unary(syn::ExprUnary {
10928            op: syn::UnOp::Neg(_),
10929            expr,
10930            ..
10931        }) => {
10932            if let syn::Expr::Lit(syn::ExprLit {
10933                lit: syn::Lit::Int(lit),
10934                ..
10935            }) = *expr
10936            {
10937                let v: i64 = lit.base10_parse()?;
10938                Ok(-v)
10939            } else {
10940                Err(syn::Error::new_spanned(expr, "expected integer literal"))
10941            }
10942        }
10943        other => Err(syn::Error::new_spanned(
10944            other,
10945            "expected integer literal (signed)",
10946        )),
10947    }
10948}
10949
10950struct FieldInfo<'a> {
10951    ident: &'a syn::Ident,
10952    column: String,
10953    primary_key: bool,
10954    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10955    /// skip this column when `Auto::Unset` and emit it under
10956    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10957    auto: bool,
10958    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10959    /// the `Column::Value` associated type for typed-column tokens.
10960    value_ty: &'a Type,
10961    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10962    field_type_tokens: TokenStream2,
10963    schema: TokenStream2,
10964    from_row_init: TokenStream2,
10965    /// Variant of [`Self::from_row_init`] that reads the column via
10966    /// `format!("{prefix}__{col}")` so a model can be decoded out of
10967    /// the aliased columns of a JOINed row. Drives slice 9.0d's
10968    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10969    /// helper that `select_related` calls when stitching loaded FKs.
10970    from_aliased_row_init: TokenStream2,
10971    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10972    /// relation helper emit (`Author::<child>_set`) needs to know `T`
10973    /// to point the generated method at the right child model.
10974    fk_inner: Option<Type>,
10975    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10976    /// `kind` (since ForeignKey detection sets `kind` to K's
10977    /// underlying type) but stored separately for clarity at the
10978    /// `FkRelation` construction site, which only sees the FK's
10979    /// surface fields.
10980    fk_pk_kind: DetectedKind,
10981    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10982    /// the bare `ForeignKey<T, K>`. Routes the load_related and
10983    /// fk_pk_access emitters to wrap assignments / accessors in
10984    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10985    /// FK column compiles end-to-end. The DDL writer reads this off
10986    /// the field schema (`nullable` flag); the macro just needs to
10987    /// keep the Rust-side codegen consistent.
10988    nullable: bool,
10989    /// `true` when this column was marked `#[rustango(auto_now)]` —
10990    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10991    /// column instead of the user-supplied value, so `updated_at`
10992    /// always reflects the latest write without the caller having
10993    /// to remember to set it.
10994    auto_now: bool,
10995    /// `true` when this column was marked `#[rustango(auto_now_add)]`
10996    /// — the column is server-set on INSERT (DB DEFAULT) and
10997    /// **immutable** afterwards. `update_on` / `save_on` skip the
10998    /// column entirely so a stale `created_at` value in memory never
10999    /// rewrites the persisted timestamp.
11000    auto_now_add: bool,
11001    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11002    /// Triggers emission of `soft_delete_on(executor)` and
11003    /// `restore_on(executor)` on the model's inherent impl. There is
11004    /// at most one such column per model — emission asserts this.
11005    soft_delete: bool,
11006    /// `Some` when this column was marked
11007    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11008    /// every INSERT and UPDATE path; the database recomputes the
11009    /// value from `EXPR`. Backlog item #35.
11010    generated_as: Option<String>,
11011    /// `true` when this column was marked
11012    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11013    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11014    /// with `Uuid::now_v7()` before binding, so the PK is generated
11015    /// Rust-side and the column is always present in the INSERT
11016    /// statement (no DB DEFAULT requirement). Issue #823.
11017    default_uuid_v7: bool,
11018    /// `Some` when this FK field carried `#[rustango(related_name =
11019    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11020    /// consumed by [`reverse_helper_tokens`] to override the default
11021    /// `<child_snake>_set` accessor name. Follow-up to #816.
11022    related_name: Option<String>,
11023}
11024
11025/// Reject table names that won't survive SQL identifier
11026/// derivation downstream. Postgres' regular-identifier rule
11027/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11028/// through the framework's unquoted FK / index / constraint name
11029/// emission without surprises. We also disallow leading-digit and
11030/// the empty string for clarity.
11031///
11032/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11033/// here — those produce a runtime error from the SQL parser,
11034/// which is loud enough; statically enumerating reserved words
11035/// across the three supported dialects is more friction than help.
11036///
11037/// Backlog item #65.
11038fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11039    validate_sql_identifier(name, "table", span)
11040}
11041
11042/// Reject SQL identifiers that compile but break downstream SQL
11043/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11044/// `kind` is "table" / "column" — used for the error message so users
11045/// see which attribute caused the failure.
11046fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11047    if name.is_empty() {
11048        return Err(syn::Error::new(
11049            span,
11050            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11051        ));
11052    }
11053    let mut chars = name.chars();
11054    let first = chars.next().unwrap();
11055    if !(first.is_ascii_alphabetic() || first == '_') {
11056        return Err(syn::Error::new(
11057            span,
11058            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11059        ));
11060    }
11061    for c in chars {
11062        if !(c.is_ascii_alphanumeric() || c == '_') {
11063            return Err(syn::Error::new(
11064                span,
11065                format!(
11066                    "{kind} name `{name}` contains invalid character {c:?} — \
11067                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11068                     Hyphens in particular break FK / index name derivation \
11069                     downstream; use underscores instead (e.g. `{}`)",
11070                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11071                ),
11072            ));
11073        }
11074    }
11075    Ok(())
11076}
11077
11078fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11079    let root = rustango_root();
11080    let attrs = parse_field_attrs(field)?;
11081    let ident = field
11082        .ident
11083        .as_ref()
11084        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11085    let name = ident.to_string();
11086    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11087    let primary_key = attrs.primary_key;
11088    let DetectedType {
11089        kind,
11090        nullable,
11091        auto: detected_auto,
11092        fk_inner,
11093    } = detect_type(&field.ty)?;
11094    check_bound_compatibility(field, &attrs, kind)?;
11095    let auto = detected_auto;
11096    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11097    // INSERT path: the user must wrap the field in `Auto<T>`, which
11098    // marks the column as DB-default-supplied. The mixin attrs then
11099    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11100    // for `auto_now`, force the value on UPDATE too.
11101    if attrs.auto_uuid {
11102        if kind != DetectedKind::Uuid {
11103            return Err(syn::Error::new_spanned(
11104                field,
11105                "`#[rustango(auto_uuid)]` requires the field type to be \
11106                 `Auto<uuid::Uuid>`",
11107            ));
11108        }
11109        if !detected_auto {
11110            return Err(syn::Error::new_spanned(
11111                field,
11112                "`#[rustango(auto_uuid)]` requires the field type to be \
11113                 wrapped in `Auto<...>` so the macro skips the column on \
11114                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11115            ));
11116        }
11117    }
11118    if attrs.default_uuid_v7 {
11119        if kind != DetectedKind::Uuid {
11120            return Err(syn::Error::new_spanned(
11121                field,
11122                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11123                 `Auto<uuid::Uuid>`",
11124            ));
11125        }
11126        if !detected_auto {
11127            return Err(syn::Error::new_spanned(
11128                field,
11129                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11130                 wrapped in `Auto<...>` so the macro can detect the \
11131                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11132            ));
11133        }
11134        if attrs.auto_uuid {
11135            return Err(syn::Error::new_spanned(
11136                field,
11137                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11138                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11139                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11140                 Pick one.",
11141            ));
11142        }
11143    }
11144    if attrs.auto_now_add || attrs.auto_now {
11145        if kind != DetectedKind::DateTime {
11146            return Err(syn::Error::new_spanned(
11147                field,
11148                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11149                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11150            ));
11151        }
11152        if !detected_auto {
11153            return Err(syn::Error::new_spanned(
11154                field,
11155                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11156                 the field type to be wrapped in `Auto<...>` so the macro skips \
11157                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11158            ));
11159        }
11160    }
11161    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11162        return Err(syn::Error::new_spanned(
11163            field,
11164            "`#[rustango(soft_delete)]` requires the field type to be \
11165             `Option<chrono::DateTime<chrono::Utc>>`",
11166        ));
11167    }
11168    let is_mixin_auto =
11169        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11170    if detected_auto && !primary_key && !is_mixin_auto {
11171        return Err(syn::Error::new_spanned(
11172            field,
11173            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11174             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11175             `auto_now`",
11176        ));
11177    }
11178    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11179        return Err(syn::Error::new_spanned(
11180            field,
11181            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11182             SERIAL / BIGSERIAL already supplies a default sequence.",
11183        ));
11184    }
11185    if fk_inner.is_some() && primary_key {
11186        return Err(syn::Error::new_spanned(
11187            field,
11188            "`ForeignKey<T>` is not allowed on a primary-key field — \
11189             a row's PK is its own identity, not a reference to a parent.",
11190        ));
11191    }
11192    if attrs.generated_as.is_some() {
11193        if primary_key {
11194            return Err(syn::Error::new_spanned(
11195                field,
11196                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11197                 primary-key field — a PK must be writable so the row \
11198                 has an identity at INSERT time.",
11199            ));
11200        }
11201        if attrs.default.is_some() {
11202            return Err(syn::Error::new_spanned(
11203                field,
11204                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11205                 `default = \"…\"` — Postgres rejects DEFAULT on \
11206                 generated columns. The expression IS the default.",
11207            ));
11208        }
11209        if detected_auto {
11210            return Err(syn::Error::new_spanned(
11211                field,
11212                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11213                 an `Auto<T>` field — generated columns are computed \
11214                 by the DB, not server-assigned via a sequence. Use a \
11215                 plain Rust type (e.g. `f64`).",
11216            ));
11217        }
11218        if fk_inner.is_some() {
11219            return Err(syn::Error::new_spanned(
11220                field,
11221                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11222                 ForeignKey field.",
11223            ));
11224        }
11225    }
11226    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11227    let column_lit = column.as_str();
11228    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11229    // dimension that the bare `Vector` Rust type can't carry, so emit
11230    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11231    // fallback of `Vector(0)`.
11232    let field_type_tokens = if kind == DetectedKind::Vector {
11233        let root = rustango_root();
11234        let dims = attrs.vector_dims.unwrap_or(0);
11235        quote!(#root::core::FieldType::Vector(#dims))
11236    } else if kind == DetectedKind::Geometry {
11237        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11238        // SRID that the bare `Point` Rust type can't carry, so emit
11239        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11240        let root = rustango_root();
11241        let srid = attrs.geometry_srid.unwrap_or(0);
11242        quote!(#root::core::FieldType::Geometry(#srid))
11243    } else {
11244        kind.variant_tokens()
11245    };
11246    let max_length = optional_u32(attrs.max_length);
11247    let min = optional_i64(attrs.min);
11248    let max = optional_i64(attrs.max);
11249    let default = optional_str(attrs.default.as_deref());
11250
11251    let unique = attrs.unique;
11252    let generated_as = optional_str(attrs.generated_as.as_deref());
11253    let help_text = optional_str(attrs.help_text.as_deref());
11254    let choices = optional_choices(attrs.choices.as_deref());
11255    let db_comment = optional_str(attrs.db_comment.as_deref());
11256    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11257    let editable = attrs.editable;
11258    let blank = attrs.blank;
11259    let case_insensitive = attrs.case_insensitive;
11260    let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11261    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11262        return Err(syn::Error::new_spanned(
11263            field,
11264            "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11265             or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11266             non-FK column.",
11267        ));
11268    }
11269    let fk_on_delete = match attrs.on_delete.as_deref() {
11270        None => quote!(::core::option::Option::None),
11271        Some(action) => {
11272            let variant = match action {
11273                "cascade" => quote!(Cascade),
11274                "restrict" => quote!(Restrict),
11275                "set_null" => quote!(SetNull),
11276                "set_default" => quote!(SetDefault),
11277                "no_action" => quote!(NoAction),
11278                // parse_field_attrs already validated this — guard against future drift.
11279                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11280            };
11281            quote!(::core::option::Option::Some(
11282                #root::core::OnDeleteAction::#variant
11283            ))
11284        }
11285    };
11286    let schema = quote! {
11287        #root::core::FieldSchema {
11288            name: #name,
11289            column: #column_lit,
11290            ty: #field_type_tokens,
11291            nullable: #nullable,
11292            primary_key: #primary_key,
11293            relation: #relation,
11294            max_length: #max_length,
11295            min: #min,
11296            max: #max,
11297            default: #default,
11298            auto: #auto,
11299            unique: #unique,
11300            generated_as: #generated_as,
11301            help_text: #help_text,
11302            choices: #choices,
11303            db_comment: #db_comment,
11304            verbose_name: #verbose_name,
11305            editable: #editable,
11306            blank: #blank,
11307            case_insensitive: #case_insensitive,
11308            fk_on_delete: #fk_on_delete,
11309            validators: &[ #(#validators_lits),* ],
11310        }
11311    };
11312
11313    let from_row_init = quote! {
11314        #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11315    };
11316    let from_aliased_row_init = quote! {
11317        #ident: #root::sql::sqlx::Row::try_get(
11318            row,
11319            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11320        )?
11321    };
11322
11323    Ok(FieldInfo {
11324        ident,
11325        column,
11326        primary_key,
11327        auto,
11328        value_ty: &field.ty,
11329        field_type_tokens,
11330        schema,
11331        from_row_init,
11332        from_aliased_row_init,
11333        fk_inner: fk_inner.cloned(),
11334        fk_pk_kind: kind,
11335        nullable,
11336        auto_now: attrs.auto_now,
11337        auto_now_add: attrs.auto_now_add,
11338        soft_delete: attrs.soft_delete,
11339        generated_as: attrs.generated_as.clone(),
11340        default_uuid_v7: attrs.default_uuid_v7,
11341        related_name: attrs.related_name.clone(),
11342    })
11343}
11344
11345fn check_bound_compatibility(
11346    field: &syn::Field,
11347    attrs: &FieldAttrs,
11348    kind: DetectedKind,
11349) -> syn::Result<()> {
11350    if attrs.max_length.is_some() && kind != DetectedKind::String {
11351        return Err(syn::Error::new_spanned(
11352            field,
11353            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11354        ));
11355    }
11356    if attrs.choices.is_some() && kind != DetectedKind::String {
11357        return Err(syn::Error::new_spanned(
11358            field,
11359            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11360             integer-valued enumerations should be modeled with a Rust enum and \
11361             custom (de)serializer for now",
11362        ));
11363    }
11364    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11365        return Err(syn::Error::new_spanned(
11366            field,
11367            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11368        ));
11369    }
11370    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11371        if min > max {
11372            return Err(syn::Error::new_spanned(
11373                field,
11374                format!("`min` ({min}) is greater than `max` ({max})"),
11375            ));
11376        }
11377    }
11378    Ok(())
11379}
11380
11381fn optional_u32(value: Option<u32>) -> TokenStream2 {
11382    if let Some(v) = value {
11383        quote!(::core::option::Option::Some(#v))
11384    } else {
11385        quote!(::core::option::Option::None)
11386    }
11387}
11388
11389fn optional_i64(value: Option<i64>) -> TokenStream2 {
11390    if let Some(v) = value {
11391        quote!(::core::option::Option::Some(#v))
11392    } else {
11393        quote!(::core::option::Option::None)
11394    }
11395}
11396
11397fn optional_str(value: Option<&str>) -> TokenStream2 {
11398    if let Some(v) = value {
11399        quote!(::core::option::Option::Some(#v))
11400    } else {
11401        quote!(::core::option::Option::None)
11402    }
11403}
11404
11405fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11406    let Some(pairs) = pairs else {
11407        return quote!(::core::option::Option::None);
11408    };
11409    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11410    quote!(::core::option::Option::Some(&[#(#entries),*]))
11411}
11412
11413fn relation_tokens(
11414    field: &syn::Field,
11415    attrs: &FieldAttrs,
11416    fk_inner: Option<&syn::Type>,
11417    table: &str,
11418) -> syn::Result<TokenStream2> {
11419    let root = rustango_root();
11420    if let Some(inner) = fk_inner {
11421        if attrs.fk.is_some() || attrs.o2o.is_some() {
11422            return Err(syn::Error::new_spanned(
11423                field,
11424                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11425                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11426            ));
11427        }
11428        let on = attrs.on.as_deref().unwrap_or("id");
11429        return Ok(quote! {
11430            ::core::option::Option::Some(#root::core::Relation::Fk {
11431                to: <#inner as #root::core::Model>::SCHEMA.table,
11432                on: #on,
11433            })
11434        });
11435    }
11436    match (&attrs.fk, &attrs.o2o) {
11437        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11438            field,
11439            "`fk` and `o2o` are mutually exclusive",
11440        )),
11441        (Some(to), None) => {
11442            let on = attrs.on.as_deref().unwrap_or("id");
11443            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11444            // the model's own table. Threaded as a literal string at
11445            // macro-expansion time to sidestep the const-eval cycle
11446            // that `Self::SCHEMA.table` would create when referenced
11447            // inside Self::SCHEMA's own initializer.
11448            let resolved = if to == "self" { table } else { to };
11449            Ok(quote! {
11450                ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11451            })
11452        }
11453        (None, Some(to)) => {
11454            let on = attrs.on.as_deref().unwrap_or("id");
11455            let resolved = if to == "self" { table } else { to };
11456            Ok(quote! {
11457                ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11458            })
11459        }
11460        (None, None) => {
11461            if attrs.on.is_some() {
11462                return Err(syn::Error::new_spanned(
11463                    field,
11464                    "`on` requires `fk` or `o2o`",
11465                ));
11466            }
11467            Ok(quote!(::core::option::Option::None))
11468        }
11469    }
11470}
11471
11472/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11473/// about kinds without depending on `rustango-core` (which would require a
11474/// proc-macro/normal split it doesn't have today).
11475#[derive(Clone, Copy, PartialEq, Eq)]
11476enum DetectedKind {
11477    I16,
11478    I32,
11479    I64,
11480    F32,
11481    F64,
11482    Bool,
11483    String,
11484    DateTime,
11485    Date,
11486    Time,
11487    Uuid,
11488    Json,
11489    Decimal,
11490    Binary,
11491    /// `Array<String>` → PG `text[]` (#341).
11492    ArrayText,
11493    /// `Array<i32>` → PG `integer[]` (#341).
11494    ArrayInt,
11495    /// `Array<i64>` → PG `bigint[]` (#341).
11496    ArrayBigInt,
11497    /// `Range<i32>` → PG `int4range` (#343).
11498    RangeInt,
11499    /// `Range<i64>` → PG `int8range` (#343).
11500    RangeBigInt,
11501    /// `Range<Decimal>` → PG `numrange` (#343).
11502    RangeNumeric,
11503    /// `Range<NaiveDate>` → PG `daterange` (#343).
11504    RangeDate,
11505    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11506    RangeDateTime,
11507    /// `HStore` → PG `hstore` (#342).
11508    HStore,
11509    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11510    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11511    /// in at the `FieldType` emission site (not carried on this enum).
11512    Vector,
11513    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11514    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11515    /// threaded in at the `FieldType` emission site.
11516    Geometry,
11517}
11518
11519impl DetectedKind {
11520    fn variant_tokens(self) -> TokenStream2 {
11521        let root = rustango_root();
11522        match self {
11523            Self::I16 => quote!(#root::core::FieldType::I16),
11524            Self::I32 => quote!(#root::core::FieldType::I32),
11525            Self::I64 => quote!(#root::core::FieldType::I64),
11526            Self::F32 => quote!(#root::core::FieldType::F32),
11527            Self::F64 => quote!(#root::core::FieldType::F64),
11528            Self::Bool => quote!(#root::core::FieldType::Bool),
11529            Self::String => quote!(#root::core::FieldType::String),
11530            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11531            Self::Date => quote!(#root::core::FieldType::Date),
11532            Self::Time => quote!(#root::core::FieldType::Time),
11533            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11534            Self::Json => quote!(#root::core::FieldType::Json),
11535            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11536            Self::Binary => quote!(#root::core::FieldType::Binary),
11537            Self::ArrayText => {
11538                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11539            }
11540            Self::ArrayInt => {
11541                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11542            }
11543            Self::ArrayBigInt => {
11544                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11545            }
11546            Self::RangeInt => {
11547                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11548            }
11549            Self::RangeBigInt => {
11550                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11551            }
11552            Self::RangeNumeric => {
11553                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11554            }
11555            Self::RangeDate => {
11556                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11557            }
11558            Self::RangeDateTime => {
11559                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11560            }
11561            Self::HStore => quote!(#root::core::FieldType::HStore),
11562            // Dimension comes from the `vector(dims = N)` attribute,
11563            // applied at the emission site; `0` here is just a fallback.
11564            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11565            // SRID comes from the `geometry(srid = N)` attribute, applied
11566            // at the emission site; `0` here is just a fallback.
11567            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11568        }
11569    }
11570
11571    fn is_integer(self) -> bool {
11572        matches!(self, Self::I16 | Self::I32 | Self::I64)
11573    }
11574
11575    /// `(SqlValue::<Variant>, default expr)` for emitting the
11576    /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11577    /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11578    /// fires only when the parent's `__rustango_pk_value` returns a
11579    /// different variant than expected, which is a compile-time bug —
11580    /// but we still need a value-typed fallback to keep the match
11581    /// total.
11582    fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11583        let root = rustango_root();
11584        match self {
11585            Self::I16 => (quote!(I16), quote!(0i16)),
11586            Self::I32 => (quote!(I32), quote!(0i32)),
11587            Self::I64 => (quote!(I64), quote!(0i64)),
11588            Self::F32 => (quote!(F32), quote!(0f32)),
11589            Self::F64 => (quote!(F64), quote!(0f64)),
11590            Self::Bool => (quote!(Bool), quote!(false)),
11591            Self::String => (quote!(String), quote!(::std::string::String::new())),
11592            Self::DateTime => (
11593                quote!(DateTime),
11594                quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11595            ),
11596            Self::Date => (
11597                quote!(Date),
11598                quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11599            ),
11600            Self::Time => (
11601                quote!(Time),
11602                quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11603            ),
11604            Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11605            Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11606            Self::Decimal => (
11607                quote!(Decimal),
11608                quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11609            ),
11610            Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11611            // Arrays (#341) can never be a foreign-key primary key, so
11612            // this arm is never reached at runtime — but the match must
11613            // stay total. A bare empty `SqlValue::Array` is the dummy.
11614            Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11615                (quote!(Array), quote!(::std::vec::Vec::new()))
11616            }
11617            // Ranges (#343) likewise can't be a FK PK — never reached.
11618            Self::RangeInt
11619            | Self::RangeBigInt
11620            | Self::RangeNumeric
11621            | Self::RangeDate
11622            | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11623            // HStore (#342) can't be a FK PK — never reached.
11624            Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11625            // Vector (#824) can't be a FK PK — never reached.
11626            Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11627            // Geometry (#443) can't be a FK PK — never reached (arm is
11628            // exhaustiveness-only; never interpolated into emitted code).
11629            Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11630        }
11631    }
11632}
11633
11634/// Result of walking a field's Rust type. `kind` is the underlying
11635/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11636/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11637/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11638/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11639#[derive(Clone, Copy)]
11640struct DetectedType<'a> {
11641    kind: DetectedKind,
11642    nullable: bool,
11643    auto: bool,
11644    fk_inner: Option<&'a syn::Type>,
11645}
11646
11647/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11648/// non-`Auto` types — the caller should already have routed Auto-only
11649/// codegen through this helper, so a `None` indicates a macro-internal
11650/// invariant break.
11651fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11652    let Type::Path(TypePath { path, qself: None }) = ty else {
11653        return None;
11654    };
11655    let last = path.segments.last()?;
11656    if last.ident != "Auto" {
11657        return None;
11658    }
11659    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11660        return None;
11661    };
11662    args.args.iter().find_map(|a| match a {
11663        syn::GenericArgument::Type(t) => Some(t),
11664        _ => None,
11665    })
11666}
11667
11668fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11669    let Type::Path(TypePath { path, qself: None }) = ty else {
11670        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11671    };
11672    let last = path
11673        .segments
11674        .last()
11675        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11676
11677    if last.ident == "Option" {
11678        let inner = generic_inner(ty, &last.arguments, "Option")?;
11679        let inner_det = detect_type(inner)?;
11680        if inner_det.nullable {
11681            return Err(syn::Error::new_spanned(
11682                ty,
11683                "nested Option is not supported",
11684            ));
11685        }
11686        if inner_det.auto {
11687            return Err(syn::Error::new_spanned(
11688                ty,
11689                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11690            ));
11691        }
11692        return Ok(DetectedType {
11693            nullable: true,
11694            ..inner_det
11695        });
11696    }
11697
11698    if last.ident == "Auto" {
11699        let inner = generic_inner(ty, &last.arguments, "Auto")?;
11700        let inner_det = detect_type(inner)?;
11701        if inner_det.auto {
11702            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11703        }
11704        if inner_det.nullable {
11705            return Err(syn::Error::new_spanned(
11706                ty,
11707                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11708            ));
11709        }
11710        if inner_det.fk_inner.is_some() {
11711            return Err(syn::Error::new_spanned(
11712                ty,
11713                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11714            ));
11715        }
11716        if !matches!(
11717            inner_det.kind,
11718            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11719        ) {
11720            return Err(syn::Error::new_spanned(
11721                ty,
11722                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11723                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11724                 (DEFAULT now())",
11725            ));
11726        }
11727        return Ok(DetectedType {
11728            auto: true,
11729            ..inner_det
11730        });
11731    }
11732
11733    if last.ident == "ForeignKey" {
11734        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11735        // Resolve the FK column's underlying SQL type from `K`. When the
11736        // user wrote `ForeignKey<T>` without a key parameter, the type
11737        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11738        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11739        // recurse into K so the column DDL emits the right SQL type
11740        // (VARCHAR for String, UUID for Uuid, …) and the load_related
11741        // emitter knows which `SqlValue` variant to match.
11742        let kind = match key_ty {
11743            Some(k) => detect_type(k)?.kind,
11744            None => DetectedKind::I64,
11745        };
11746        return Ok(DetectedType {
11747            kind,
11748            nullable: false,
11749            auto: false,
11750            fk_inner: Some(inner),
11751        });
11752    }
11753
11754    let kind = match last.ident.to_string().as_str() {
11755        "i16" => DetectedKind::I16,
11756        "i32" => DetectedKind::I32,
11757        "i64" => DetectedKind::I64,
11758        "f32" => DetectedKind::F32,
11759        "f64" => DetectedKind::F64,
11760        "bool" => DetectedKind::Bool,
11761        "String" => DetectedKind::String,
11762        "DateTime" => DetectedKind::DateTime,
11763        "NaiveDate" => DetectedKind::Date,
11764        "NaiveTime" => DetectedKind::Time,
11765        "Uuid" => DetectedKind::Uuid,
11766        "Value" => DetectedKind::Json,
11767        "Decimal" => DetectedKind::Decimal,
11768        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11769        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11770        // — that would emit Binary DDL and decode-fail at runtime.
11771        "Vec" => {
11772            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11773            if let Type::Path(TypePath { path, qself: None }) = inner {
11774                if let Some(seg) = path.segments.last() {
11775                    if seg.ident == "u8" && seg.arguments.is_empty() {
11776                        return Ok(DetectedType {
11777                            kind: DetectedKind::Binary,
11778                            nullable: false,
11779                            auto: false,
11780                            fk_inner: None,
11781                        });
11782                    }
11783                }
11784            }
11785            return Err(syn::Error::new_spanned(
11786                ty,
11787                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11788                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11789            ));
11790        }
11791        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11792        // `integer[]` / `bigint[]` (Django `ArrayField`, #341).
11793        "Array" => {
11794            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11795            let elem = match inner {
11796                Type::Path(TypePath { path, qself: None }) => {
11797                    path.segments.last().map(|s| s.ident.to_string())
11798                }
11799                _ => None,
11800            };
11801            let kind = match elem.as_deref() {
11802                Some("String") => DetectedKind::ArrayText,
11803                Some("i32") => DetectedKind::ArrayInt,
11804                Some("i64") => DetectedKind::ArrayBigInt,
11805                _ => {
11806                    return Err(syn::Error::new_spanned(
11807                        ty,
11808                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11809                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11810                    ));
11811                }
11812            };
11813            return Ok(DetectedType {
11814                kind,
11815                nullable: false,
11816                auto: false,
11817                fk_inner: None,
11818            });
11819        }
11820        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11821        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11822        // `int8range` / `numrange` / `daterange` / `tstzrange` (Django
11823        // `RangeField` family, #343).
11824        "Range" => {
11825            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11826            let elem = match inner {
11827                Type::Path(TypePath { path, qself: None }) => {
11828                    path.segments.last().map(|s| s.ident.to_string())
11829                }
11830                _ => None,
11831            };
11832            let kind = match elem.as_deref() {
11833                Some("i32") => DetectedKind::RangeInt,
11834                Some("i64") => DetectedKind::RangeBigInt,
11835                Some("Decimal") => DetectedKind::RangeNumeric,
11836                Some("NaiveDate") => DetectedKind::RangeDate,
11837                Some("DateTime") => DetectedKind::RangeDateTime,
11838                _ => {
11839                    return Err(syn::Error::new_spanned(
11840                        ty,
11841                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11842                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11843                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11844                         (→ tstzrange) are supported (#343)",
11845                    ));
11846                }
11847            };
11848            return Ok(DetectedType {
11849                kind,
11850                nullable: false,
11851                auto: false,
11852                fk_inner: None,
11853            });
11854        }
11855        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11856        // (the `CastValue` impl bridges logical↔stored); the field's own
11857        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11858        // schema just needs `FieldType::String`.
11859        "Cast" => {
11860            return Ok(DetectedType {
11861                kind: DetectedKind::String,
11862                nullable: false,
11863                auto: false,
11864                fk_inner: None,
11865            });
11866        }
11867        // `HStore` → PG `hstore` (Django `HStoreField`, #342). No generic
11868        // parameter — always a string→string map.
11869        "HStore" => {
11870            return Ok(DetectedType {
11871                kind: DetectedKind::HStore,
11872                nullable: false,
11873                auto: false,
11874                fk_inner: None,
11875            });
11876        }
11877        // `Vector` → pgvector `vector(N)` (#824). The dimension is
11878        // supplied by `#[rustango(vector(dims = N))]`, not the type.
11879        "Vector" => {
11880            return Ok(DetectedType {
11881                kind: DetectedKind::Vector,
11882                nullable: false,
11883                auto: false,
11884                fk_inner: None,
11885            });
11886        }
11887        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11888        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11889        "Point" => {
11890            return Ok(DetectedType {
11891                kind: DetectedKind::Geometry,
11892                nullable: false,
11893                auto: false,
11894                fk_inner: None,
11895            });
11896        }
11897        other => {
11898            return Err(syn::Error::new_spanned(
11899                ty,
11900                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)"),
11901            ));
11902        }
11903    };
11904    Ok(DetectedType {
11905        kind,
11906        nullable: false,
11907        auto: false,
11908        fk_inner: None,
11909    })
11910}
11911
11912fn generic_inner<'a>(
11913    ty: &'a Type,
11914    arguments: &'a PathArguments,
11915    wrapper: &str,
11916) -> syn::Result<&'a Type> {
11917    let PathArguments::AngleBracketed(args) = arguments else {
11918        return Err(syn::Error::new_spanned(
11919            ty,
11920            format!("{wrapper} requires a generic argument"),
11921        ));
11922    };
11923    args.args
11924        .iter()
11925        .find_map(|a| match a {
11926            GenericArgument::Type(t) => Some(t),
11927            _ => None,
11928        })
11929        .ok_or_else(|| {
11930            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11931        })
11932}
11933
11934/// Like [`generic_inner`] but pulls *two* type args — the first is
11935/// required, the second is optional. Used by the `ForeignKey<T, K>`
11936/// detection where K defaults to `i64` when omitted.
11937fn generic_pair<'a>(
11938    ty: &'a Type,
11939    arguments: &'a PathArguments,
11940    wrapper: &str,
11941) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11942    let PathArguments::AngleBracketed(args) = arguments else {
11943        return Err(syn::Error::new_spanned(
11944            ty,
11945            format!("{wrapper} requires a generic argument"),
11946        ));
11947    };
11948    let mut types = args.args.iter().filter_map(|a| match a {
11949        GenericArgument::Type(t) => Some(t),
11950        _ => None,
11951    });
11952    let first = types.next().ok_or_else(|| {
11953        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11954    })?;
11955    let second = types.next();
11956    Ok((first, second))
11957}
11958
11959fn to_snake_case(s: &str) -> String {
11960    let mut out = String::with_capacity(s.len() + 4);
11961    for (i, ch) in s.chars().enumerate() {
11962        if ch.is_ascii_uppercase() {
11963            if i > 0 {
11964                out.push('_');
11965            }
11966            out.push(ch.to_ascii_lowercase());
11967        } else {
11968            out.push(ch);
11969        }
11970    }
11971    out
11972}
11973
11974// ============================================================
11975//  #[derive(Form)]  —  slice 8.4B
11976// ============================================================
11977
11978/// Per-field `#[form(...)]` attributes recognised by the derive.
11979#[derive(Default)]
11980struct FormFieldAttrs {
11981    min: Option<i64>,
11982    max: Option<i64>,
11983    min_length: Option<u32>,
11984    max_length: Option<u32>,
11985    /// `#[form(clean = "fn_name")]` — Django-shape `clean_<field>` hook.
11986    /// The named static method on the form struct is called after the
11987    /// field's typed parse + length/range checks; it gets the parsed
11988    /// value by reference and returns `Result<<FieldType>, String>`.
11989    /// On Ok, the returned value replaces the parsed one; on Err, the
11990    /// message is attached to the field error list. Issue #372.
11991    clean: Option<syn::Ident>,
11992}
11993
11994/// Container-level `#[form(...)]` attributes. Currently only the
11995/// Django-shape cross-field `validate` hook (issue #373).
11996#[derive(Default)]
11997struct FormContainerAttrs {
11998    /// `#[form(validate = "fn_name")]` — Django-shape `clean()` hook.
11999    /// After every per-field parse succeeds, the named method on the
12000    /// form struct is called with `&self` and may return
12001    /// `Result<(), FormErrors>`. Errors merge into the field error
12002    /// list. Issue #373.
12003    validate: Option<syn::Ident>,
12004}
12005
12006/// Detected shape of a form field's Rust type.
12007#[derive(Clone, Copy)]
12008enum FormFieldKind {
12009    String,
12010    I16,
12011    I32,
12012    I64,
12013    F32,
12014    F64,
12015    Bool,
12016}
12017
12018impl FormFieldKind {
12019    fn parse_method(self) -> &'static str {
12020        match self {
12021            Self::I16 => "i16",
12022            Self::I32 => "i32",
12023            Self::I64 => "i64",
12024            Self::F32 => "f32",
12025            Self::F64 => "f64",
12026            // String + Bool don't go through `str::parse`; the codegen
12027            // handles them inline.
12028            Self::String | Self::Bool => "",
12029        }
12030    }
12031}
12032
12033fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12034    let root = rustango_root();
12035    let struct_name = &input.ident;
12036
12037    let Data::Struct(data) = &input.data else {
12038        return Err(syn::Error::new_spanned(
12039            struct_name,
12040            "Form can only be derived on structs",
12041        ));
12042    };
12043    let Fields::Named(named) = &data.fields else {
12044        return Err(syn::Error::new_spanned(
12045            struct_name,
12046            "Form requires a struct with named fields",
12047        ));
12048    };
12049
12050    // #373 — container-level `#[form(validate = "fn")]` hook.
12051    let container = parse_form_container_attrs(input)?;
12052    let post_field_clean: Vec<TokenStream2> = Vec::new();
12053    let _ = post_field_clean;
12054
12055    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12056    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12057
12058    for field in &named.named {
12059        let ident = field
12060            .ident
12061            .as_ref()
12062            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12063        let attrs = parse_form_field_attrs(field)?;
12064        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12065
12066        let name_lit = ident.to_string();
12067        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12068        // #372 — append the per-field `clean_<field>` call right after
12069        // the parse block when the attribute is set. The clean fn
12070        // takes &T and returns Result<T, String>; on Err we attach
12071        // the message to the field error list without aborting
12072        // (matches Django's "collect all field errors" shape).
12073        let clean_block = if let Some(clean_fn) = &attrs.clean {
12074            quote! {
12075                if __errors.fields().get(#name_lit).is_none() {
12076                    match Self::#clean_fn(&#ident) {
12077                        ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12078                        ::core::result::Result::Err(__msg) => {
12079                            __errors.add(#name_lit, __msg);
12080                        }
12081                    }
12082                }
12083            }
12084        } else {
12085            quote! {}
12086        };
12087        field_blocks.push(quote! {
12088            #parse_block
12089            #clean_block
12090        });
12091        field_idents.push(ident);
12092    }
12093
12094    // #373 — after every per-field parse + clean succeeds, call the
12095    // cross-field validator if declared. Errors merge into the
12096    // outgoing FormErrors via the existing `FormErrors::merge` helper
12097    // (same primitive the DRF serializer cross-field hook uses).
12098    let cross_field_call = if let Some(validate_fn) = &container.validate {
12099        quote! {
12100            if __errors.is_empty() {
12101                let __candidate = Self { #( #field_idents ),* };
12102                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12103                    __errors.merge(__other);
12104                }
12105                if !__errors.is_empty() {
12106                    return ::core::result::Result::Err(__errors);
12107                }
12108                return ::core::result::Result::Ok(__candidate);
12109            }
12110        }
12111    } else {
12112        quote! {}
12113    };
12114
12115    Ok(quote! {
12116        impl #root::forms::Form for #struct_name {
12117            fn parse(
12118                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12119            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12120                let mut __errors = #root::forms::FormErrors::default();
12121                #( #field_blocks )*
12122                #cross_field_call
12123                if !__errors.is_empty() {
12124                    return ::core::result::Result::Err(__errors);
12125                }
12126                ::core::result::Result::Ok(Self {
12127                    #( #field_idents ),*
12128                })
12129            }
12130        }
12131    })
12132}
12133
12134fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12135    let mut out = FormContainerAttrs::default();
12136    for attr in &input.attrs {
12137        if !attr.path().is_ident("form") {
12138            continue;
12139        }
12140        attr.parse_nested_meta(|meta| {
12141            if meta.path.is_ident("validate") {
12142                let s: LitStr = meta.value()?.parse()?;
12143                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12144                return Ok(());
12145            }
12146            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12147        })?;
12148    }
12149    Ok(out)
12150}
12151
12152fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12153    let mut out = FormFieldAttrs::default();
12154    for attr in &field.attrs {
12155        if !attr.path().is_ident("form") {
12156            continue;
12157        }
12158        attr.parse_nested_meta(|meta| {
12159            if meta.path.is_ident("min") {
12160                let lit: syn::LitInt = meta.value()?.parse()?;
12161                out.min = Some(lit.base10_parse::<i64>()?);
12162                return Ok(());
12163            }
12164            if meta.path.is_ident("max") {
12165                let lit: syn::LitInt = meta.value()?.parse()?;
12166                out.max = Some(lit.base10_parse::<i64>()?);
12167                return Ok(());
12168            }
12169            if meta.path.is_ident("min_length") {
12170                let lit: syn::LitInt = meta.value()?.parse()?;
12171                out.min_length = Some(lit.base10_parse::<u32>()?);
12172                return Ok(());
12173            }
12174            if meta.path.is_ident("max_length") {
12175                let lit: syn::LitInt = meta.value()?.parse()?;
12176                out.max_length = Some(lit.base10_parse::<u32>()?);
12177                return Ok(());
12178            }
12179            if meta.path.is_ident("clean") {
12180                let s: LitStr = meta.value()?.parse()?;
12181                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12182                return Ok(());
12183            }
12184            Err(meta.error(
12185                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12186            ))
12187        })?;
12188    }
12189    Ok(out)
12190}
12191
12192fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12193    let Type::Path(TypePath { path, qself: None }) = ty else {
12194        return Err(syn::Error::new(
12195            span,
12196            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12197        ));
12198    };
12199    let last = path
12200        .segments
12201        .last()
12202        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12203
12204    if last.ident == "Option" {
12205        let inner = generic_inner(ty, &last.arguments, "Option")?;
12206        let (kind, nested) = detect_form_field(inner, span)?;
12207        if nested {
12208            return Err(syn::Error::new(
12209                span,
12210                "nested Option in Form fields is not supported",
12211            ));
12212        }
12213        return Ok((kind, true));
12214    }
12215
12216    let kind = match last.ident.to_string().as_str() {
12217        "String" => FormFieldKind::String,
12218        "i16" => FormFieldKind::I16,
12219        "i32" => FormFieldKind::I32,
12220        "i64" => FormFieldKind::I64,
12221        "f32" => FormFieldKind::F32,
12222        "f64" => FormFieldKind::F64,
12223        "bool" => FormFieldKind::Bool,
12224        other => {
12225            return Err(syn::Error::new(
12226                span,
12227                format!(
12228                    "Form field type `{other}` is not supported in v0.8 — use String / \
12229                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12230                ),
12231            ));
12232        }
12233    };
12234    Ok((kind, false))
12235}
12236
12237#[allow(clippy::too_many_lines)]
12238fn render_form_field_parse(
12239    ident: &syn::Ident,
12240    name_lit: &str,
12241    kind: FormFieldKind,
12242    nullable: bool,
12243    attrs: &FormFieldAttrs,
12244) -> TokenStream2 {
12245    // Pull the raw &str from the payload. Uses variable name `data` to
12246    // match the new `Form::parse(data: &HashMap<…>)` signature.
12247    let lookup = quote! {
12248        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12249    };
12250
12251    let parsed_value = match kind {
12252        FormFieldKind::Bool => quote! {
12253            let __v: bool = match __raw {
12254                ::core::option::Option::None => false,
12255                ::core::option::Option::Some(__s) => !matches!(
12256                    __s.to_ascii_lowercase().as_str(),
12257                    "" | "false" | "0" | "off" | "no"
12258                ),
12259            };
12260        },
12261        FormFieldKind::String => {
12262            if nullable {
12263                quote! {
12264                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12265                        ::core::option::Option::None => ::core::option::Option::None,
12266                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12267                            ::core::option::Option::None
12268                        }
12269                        ::core::option::Option::Some(__s) => {
12270                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12271                        }
12272                    };
12273                }
12274            } else {
12275                quote! {
12276                    let __v: ::std::string::String = match __raw {
12277                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12278                            ::core::clone::Clone::clone(__s)
12279                        }
12280                        _ => {
12281                            __errors.add(#name_lit, "This field is required.");
12282                            ::std::string::String::new()
12283                        }
12284                    };
12285                }
12286            }
12287        }
12288        FormFieldKind::I16
12289        | FormFieldKind::I32
12290        | FormFieldKind::I64
12291        | FormFieldKind::F32
12292        | FormFieldKind::F64 => {
12293            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12294            let ty_lit = kind.parse_method();
12295            let default_val = match kind {
12296                FormFieldKind::I16 => quote! { 0i16 },
12297                FormFieldKind::I32 => quote! { 0i32 },
12298                FormFieldKind::I64 => quote! { 0i64 },
12299                FormFieldKind::F32 => quote! { 0f32 },
12300                FormFieldKind::F64 => quote! { 0f64 },
12301                _ => quote! { Default::default() },
12302            };
12303            if nullable {
12304                quote! {
12305                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12306                        ::core::option::Option::None => ::core::option::Option::None,
12307                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12308                            ::core::option::Option::None
12309                        }
12310                        ::core::option::Option::Some(__s) => {
12311                            match __s.parse::<#parse_ty>() {
12312                                ::core::result::Result::Ok(__n) => {
12313                                    ::core::option::Option::Some(__n)
12314                                }
12315                                ::core::result::Result::Err(__e) => {
12316                                    __errors.add(
12317                                        #name_lit,
12318                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12319                                    );
12320                                    ::core::option::Option::None
12321                                }
12322                            }
12323                        }
12324                    };
12325                }
12326            } else {
12327                quote! {
12328                    let __v: #parse_ty = match __raw {
12329                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12330                            match __s.parse::<#parse_ty>() {
12331                                ::core::result::Result::Ok(__n) => __n,
12332                                ::core::result::Result::Err(__e) => {
12333                                    __errors.add(
12334                                        #name_lit,
12335                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12336                                    );
12337                                    #default_val
12338                                }
12339                            }
12340                        }
12341                        _ => {
12342                            __errors.add(#name_lit, "This field is required.");
12343                            #default_val
12344                        }
12345                    };
12346                }
12347            }
12348        }
12349    };
12350
12351    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12352
12353    quote! {
12354        // `mut` so the per-field `clean` hook (#372) can rewrite the
12355        // parsed value in-place when it returns Ok with a normalized
12356        // form (e.g. trim / lowercase).
12357        let mut #ident = {
12358            #lookup
12359            #parsed_value
12360            #validators
12361            __v
12362        };
12363    }
12364}
12365
12366fn render_form_validators(
12367    name_lit: &str,
12368    kind: FormFieldKind,
12369    nullable: bool,
12370    attrs: &FormFieldAttrs,
12371) -> TokenStream2 {
12372    let mut checks: Vec<TokenStream2> = Vec::new();
12373
12374    let val_ref = if nullable {
12375        quote! { __v.as_ref() }
12376    } else {
12377        quote! { ::core::option::Option::Some(&__v) }
12378    };
12379
12380    let is_string = matches!(kind, FormFieldKind::String);
12381    let is_numeric = matches!(
12382        kind,
12383        FormFieldKind::I16
12384            | FormFieldKind::I32
12385            | FormFieldKind::I64
12386            | FormFieldKind::F32
12387            | FormFieldKind::F64
12388    );
12389
12390    if is_string {
12391        if let Some(min_len) = attrs.min_length {
12392            let min_len_usize = min_len as usize;
12393            checks.push(quote! {
12394                if let ::core::option::Option::Some(__s) = #val_ref {
12395                    if __s.len() < #min_len_usize {
12396                        __errors.add(
12397                            #name_lit,
12398                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12399                        );
12400                    }
12401                }
12402            });
12403        }
12404        if let Some(max_len) = attrs.max_length {
12405            let max_len_usize = max_len as usize;
12406            checks.push(quote! {
12407                if let ::core::option::Option::Some(__s) = #val_ref {
12408                    if __s.len() > #max_len_usize {
12409                        __errors.add(
12410                            #name_lit,
12411                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12412                        );
12413                    }
12414                }
12415            });
12416        }
12417    }
12418
12419    if is_numeric {
12420        if let Some(min) = attrs.min {
12421            checks.push(quote! {
12422                if let ::core::option::Option::Some(__n) = #val_ref {
12423                    if (*__n as f64) < (#min as f64) {
12424                        __errors.add(
12425                            #name_lit,
12426                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12427                        );
12428                    }
12429                }
12430            });
12431        }
12432        if let Some(max) = attrs.max {
12433            checks.push(quote! {
12434                if let ::core::option::Option::Some(__n) = #val_ref {
12435                    if (*__n as f64) > (#max as f64) {
12436                        __errors.add(
12437                            #name_lit,
12438                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12439                        );
12440                    }
12441                }
12442            });
12443        }
12444    }
12445
12446    quote! { #( #checks )* }
12447}
12448
12449// ============================================================
12450//  #[derive(ViewSet)]
12451// ============================================================
12452
12453struct ViewSetAttrs {
12454    model: syn::Path,
12455    fields: Option<Vec<String>>,
12456    filter_fields: Vec<String>,
12457    search_fields: Vec<String>,
12458    /// (field_name, desc)
12459    ordering: Vec<(String, bool)>,
12460    page_size: Option<usize>,
12461    read_only: bool,
12462    perms: ViewSetPermsAttrs,
12463    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12464    /// retrieve / create responses through this `#[derive(Serializer)]`
12465    /// type instead of the default field-level projection (requires the
12466    /// `serializer` feature). Tri-dialect.
12467    serializer: Option<syn::Path>,
12468}
12469
12470#[derive(Default)]
12471struct ViewSetPermsAttrs {
12472    list: Vec<String>,
12473    retrieve: Vec<String>,
12474    create: Vec<String>,
12475    update: Vec<String>,
12476    destroy: Vec<String>,
12477}
12478
12479fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12480    let root = rustango_root();
12481    let struct_name = &input.ident;
12482
12483    // Must be a unit struct or an empty named struct.
12484    match &input.data {
12485        Data::Struct(s) => match &s.fields {
12486            Fields::Unit | Fields::Named(_) => {}
12487            Fields::Unnamed(_) => {
12488                return Err(syn::Error::new_spanned(
12489                    struct_name,
12490                    "ViewSet can only be derived on a unit struct or an empty named struct",
12491                ));
12492            }
12493        },
12494        _ => {
12495            return Err(syn::Error::new_spanned(
12496                struct_name,
12497                "ViewSet can only be derived on a struct",
12498            ));
12499        }
12500    }
12501
12502    let attrs = parse_viewset_attrs(input)?;
12503    let model_path = &attrs.model;
12504
12505    // `.fields(&[...])` call — None means skip (use all scalar fields).
12506    let fields_call = if let Some(ref fields) = attrs.fields {
12507        let lits = fields.iter().map(|f| f.as_str());
12508        quote!(.fields(&[ #(#lits),* ]))
12509    } else {
12510        quote!()
12511    };
12512
12513    let filter_fields_call = if attrs.filter_fields.is_empty() {
12514        quote!()
12515    } else {
12516        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12517        quote!(.filter_fields(&[ #(#lits),* ]))
12518    };
12519
12520    let search_fields_call = if attrs.search_fields.is_empty() {
12521        quote!()
12522    } else {
12523        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12524        quote!(.search_fields(&[ #(#lits),* ]))
12525    };
12526
12527    let ordering_call = if attrs.ordering.is_empty() {
12528        quote!()
12529    } else {
12530        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12531            let f = f.as_str();
12532            quote!((#f, #desc))
12533        });
12534        quote!(.ordering(&[ #(#pairs),* ]))
12535    };
12536
12537    let page_size_call = if let Some(n) = attrs.page_size {
12538        quote!(.page_size(#n))
12539    } else {
12540        quote!()
12541    };
12542
12543    let read_only_call = if attrs.read_only {
12544        quote!(.read_only())
12545    } else {
12546        quote!()
12547    };
12548
12549    // `.serializer::<S>()` — reshape responses through a derived
12550    // serializer. Requires the downstream crate to enable the
12551    // `serializer` feature (the method is gated on it).
12552    let serializer_call = if let Some(ref ser) = attrs.serializer {
12553        quote!(.serializer::<#ser>())
12554    } else {
12555        quote!()
12556    };
12557
12558    let perms = &attrs.perms;
12559    let perms_call = if perms.list.is_empty()
12560        && perms.retrieve.is_empty()
12561        && perms.create.is_empty()
12562        && perms.update.is_empty()
12563        && perms.destroy.is_empty()
12564    {
12565        quote!()
12566    } else {
12567        let list_lits = perms.list.iter().map(|s| s.as_str());
12568        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12569        let create_lits = perms.create.iter().map(|s| s.as_str());
12570        let update_lits = perms.update.iter().map(|s| s.as_str());
12571        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12572        quote! {
12573            .permissions(#root::viewset::ViewSetPerms {
12574                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
12575                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12576                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
12577                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
12578                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
12579            })
12580        }
12581    };
12582
12583    Ok(quote! {
12584        impl #struct_name {
12585            /// Build an `axum::Router` with the six standard REST endpoints
12586            /// for this ViewSet, mounted at `prefix`.
12587            ///
12588            /// Accepts any backend's pool — a `sqlx::PgPool`,
12589            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12590            /// enum — because they all `Into<Pool>` (#1273). This is why
12591            /// the derive no longer names `PgPool`, which would not
12592            /// exist on a project built without the `postgres` feature.
12593            pub fn router(
12594                prefix: &str,
12595                pool: impl ::core::convert::Into<#root::sql::Pool>,
12596            ) -> #root::__axum::Router {
12597                #root::viewset::ViewSet::for_model(
12598                    <#model_path as #root::core::Model>::SCHEMA
12599                )
12600                    #fields_call
12601                    #filter_fields_call
12602                    #search_fields_call
12603                    #ordering_call
12604                    #page_size_call
12605                    #perms_call
12606                    #read_only_call
12607                    #serializer_call
12608                    .router_pool(prefix, pool.into())
12609            }
12610        }
12611    })
12612}
12613
12614fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12615    let mut model: Option<syn::Path> = None;
12616    let mut fields: Option<Vec<String>> = None;
12617    let mut filter_fields: Vec<String> = Vec::new();
12618    let mut search_fields: Vec<String> = Vec::new();
12619    let mut ordering: Vec<(String, bool)> = Vec::new();
12620    let mut page_size: Option<usize> = None;
12621    let mut read_only = false;
12622    let mut perms = ViewSetPermsAttrs::default();
12623    let mut serializer: Option<syn::Path> = None;
12624
12625    for attr in &input.attrs {
12626        if !attr.path().is_ident("viewset") {
12627            continue;
12628        }
12629        attr.parse_nested_meta(|meta| {
12630            if meta.path.is_ident("model") {
12631                let path: syn::Path = meta.value()?.parse()?;
12632                model = Some(path);
12633                return Ok(());
12634            }
12635            if meta.path.is_ident("serializer") {
12636                let path: syn::Path = meta.value()?.parse()?;
12637                serializer = Some(path);
12638                return Ok(());
12639            }
12640            if meta.path.is_ident("fields") {
12641                let s: LitStr = meta.value()?.parse()?;
12642                fields = Some(split_field_list(&s.value()));
12643                return Ok(());
12644            }
12645            if meta.path.is_ident("filter_fields") {
12646                let s: LitStr = meta.value()?.parse()?;
12647                filter_fields = split_field_list(&s.value());
12648                return Ok(());
12649            }
12650            if meta.path.is_ident("search_fields") {
12651                let s: LitStr = meta.value()?.parse()?;
12652                search_fields = split_field_list(&s.value());
12653                return Ok(());
12654            }
12655            if meta.path.is_ident("ordering") {
12656                let s: LitStr = meta.value()?.parse()?;
12657                ordering = parse_ordering_list(&s.value());
12658                return Ok(());
12659            }
12660            if meta.path.is_ident("page_size") {
12661                let lit: syn::LitInt = meta.value()?.parse()?;
12662                page_size = Some(lit.base10_parse::<usize>()?);
12663                return Ok(());
12664            }
12665            if meta.path.is_ident("read_only") {
12666                read_only = true;
12667                return Ok(());
12668            }
12669            if meta.path.is_ident("permissions") {
12670                meta.parse_nested_meta(|inner| {
12671                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12672                        let s: LitStr = inner.value()?.parse()?;
12673                        Ok(split_field_list(&s.value()))
12674                    };
12675                    if inner.path.is_ident("list") {
12676                        perms.list = parse_codenames(&inner)?;
12677                    } else if inner.path.is_ident("retrieve") {
12678                        perms.retrieve = parse_codenames(&inner)?;
12679                    } else if inner.path.is_ident("create") {
12680                        perms.create = parse_codenames(&inner)?;
12681                    } else if inner.path.is_ident("update") {
12682                        perms.update = parse_codenames(&inner)?;
12683                    } else if inner.path.is_ident("destroy") {
12684                        perms.destroy = parse_codenames(&inner)?;
12685                    } else {
12686                        return Err(inner.error(
12687                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12688                        ));
12689                    }
12690                    Ok(())
12691                })?;
12692                return Ok(());
12693            }
12694            Err(meta.error(
12695                "unknown viewset attribute (supported: model, fields, filter_fields, \
12696                 search_fields, ordering, page_size, read_only, serializer, permissions(...))",
12697            ))
12698        })?;
12699    }
12700
12701    let model = model.ok_or_else(|| {
12702        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12703    })?;
12704
12705    Ok(ViewSetAttrs {
12706        model,
12707        fields,
12708        filter_fields,
12709        search_fields,
12710        ordering,
12711        page_size,
12712        read_only,
12713        perms,
12714        serializer,
12715    })
12716}
12717
12718// ============================================================ #[derive(Serializer)]
12719
12720struct SerializerContainerAttrs {
12721    model: syn::Path,
12722    /// `#[serializer(validate = "fn_name")]` on the struct — DRF-shape
12723    /// cross-field validation hook (#436). The named inherent method
12724    /// must take `&self` and return
12725    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12726    /// `validate()` runs every per-field validator first; then calls
12727    /// the cross-field method if declared; aggregates all errors into
12728    /// one `FormErrors`.
12729    cross_validate: Option<syn::Ident>,
12730}
12731
12732#[derive(Default)]
12733struct SerializerFieldAttrs {
12734    read_only: bool,
12735    write_only: bool,
12736    source: Option<String>,
12737    skip: bool,
12738    /// `#[serializer(method = "fn_name")]` — DRF SerializerMethodField
12739    /// analog. The macro emits `from_model` initializer that calls
12740    /// `Self::fn_name(&model)` and stores the return value.
12741    method: Option<String>,
12742    /// `#[serializer(validate = "fn_name")]` — per-field validator
12743    /// callable run by `Self::validate(&self)`. Must return
12744    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12745    /// the field name.
12746    validate: Option<String>,
12747    /// `#[serializer(nested)]` on a field whose type is another
12748    /// `Serializer` — the macro emits `from_model` initializer that
12749    /// reads the parent via `model.<source>.value()` then calls the
12750    /// child serializer's `from_model(parent)`. When the FK is
12751    /// unloaded the field falls back to `Default::default()` (does
12752    /// NOT panic) so a missing prefetch in prod degrades gracefully.
12753    /// Source field on the model defaults to the field name; override
12754    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12755    /// panic-on-unloaded behavior for tests.
12756    nested: bool,
12757    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12758    /// strict behavior: panic when the FK isn't loaded. Useful in
12759    /// test code where forgetting select_related must trip a hard
12760    /// failure rather than render a blank nested object.
12761    nested_strict: bool,
12762    /// `#[serializer(many = TagSerializer)]` — declare the field as
12763    /// a list of nested serializers. Field type must be `Vec<S>`
12764    /// where `S` is the inner serializer. The macro initializes the
12765    /// field to `Vec::new()` in `from_model` and emits a typed
12766    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12767    /// maps each model row through `S::from_model`. Auto-load isn't
12768    /// possible (the M2M / one-to-many accessor is async); callers
12769    /// fetch the children + call the setter post-from_model.
12770    many: Option<syn::Type>,
12771    /// `#[serializer(slug = "name")]` — DRF `SlugRelatedField` analog.
12772    /// Source field on the model must be a `ForeignKey<T>`; the
12773    /// macro emits `from_model` glue that walks
12774    /// `model.<source>.value()?.<slug>` and clones it. Field type on
12775    /// the serializer is typically `String` (whatever type the slug
12776    /// column has). When the FK is unloaded the field falls back to
12777    /// `Default::default()`, same graceful-degrade contract as
12778    /// `nested`. Source defaults to the field name; override with
12779    /// `source = "..."`. v0.44.
12780    slug: Option<String>,
12781    /// `#[serializer(max_length = N)]` — DRF `MaxLengthValidator`. Caps
12782    /// the character count of a string field on write. Overrides the
12783    /// model's `max_length`; when absent the model value is inherited.
12784    max_length: Option<u64>,
12785    /// `#[serializer(min_length = N)]` — DRF `MinLengthValidator`.
12786    /// Serializer-only (the model has no `min_length` column).
12787    min_length: Option<u64>,
12788    /// `#[serializer(min = N)]` — DRF `MinValueValidator`. Inclusive
12789    /// integer lower bound; overrides the model's `min` when given.
12790    min: Option<i64>,
12791    /// `#[serializer(max = N)]` — DRF `MaxValueValidator`. Inclusive
12792    /// integer upper bound; overrides the model's `max` when given.
12793    max: Option<i64>,
12794}
12795
12796fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12797    let mut model: Option<syn::Path> = None;
12798    let mut cross_validate: Option<syn::Ident> = None;
12799    for attr in &input.attrs {
12800        if !attr.path().is_ident("serializer") {
12801            continue;
12802        }
12803        attr.parse_nested_meta(|meta| {
12804            if meta.path.is_ident("model") {
12805                let _eq: syn::Token![=] = meta.input.parse()?;
12806                model = Some(meta.input.parse()?);
12807                return Ok(());
12808            }
12809            if meta.path.is_ident("validate") {
12810                // #436 — container-level `validate = "fn_name"` for the
12811                // DRF cross-field-validation shape. Field-level
12812                // `#[serializer(validate = "...")]` on a field is
12813                // parsed separately in `parse_serializer_field_attrs`.
12814                let s: LitStr = meta.value()?.parse()?;
12815                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12816                return Ok(());
12817            }
12818            Err(meta.error(
12819                "unknown serializer container attribute \
12820                 (supported: `model`, `validate`)",
12821            ))
12822        })?;
12823    }
12824    let model = model.ok_or_else(|| {
12825        syn::Error::new_spanned(
12826            &input.ident,
12827            "`#[serializer(model = SomeModel)]` is required",
12828        )
12829    })?;
12830    Ok(SerializerContainerAttrs {
12831        model,
12832        cross_validate,
12833    })
12834}
12835
12836fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12837    let mut out = SerializerFieldAttrs::default();
12838    for attr in &field.attrs {
12839        if !attr.path().is_ident("serializer") {
12840            continue;
12841        }
12842        attr.parse_nested_meta(|meta| {
12843            if meta.path.is_ident("read_only") {
12844                out.read_only = true;
12845                return Ok(());
12846            }
12847            if meta.path.is_ident("write_only") {
12848                out.write_only = true;
12849                return Ok(());
12850            }
12851            if meta.path.is_ident("skip") {
12852                out.skip = true;
12853                return Ok(());
12854            }
12855            if meta.path.is_ident("source") {
12856                let s: LitStr = meta.value()?.parse()?;
12857                out.source = Some(s.value());
12858                return Ok(());
12859            }
12860            if meta.path.is_ident("method") {
12861                let s: LitStr = meta.value()?.parse()?;
12862                out.method = Some(s.value());
12863                return Ok(());
12864            }
12865            if meta.path.is_ident("validate") {
12866                let s: LitStr = meta.value()?.parse()?;
12867                out.validate = Some(s.value());
12868                return Ok(());
12869            }
12870            if meta.path.is_ident("many") {
12871                let _eq: syn::Token![=] = meta.input.parse()?;
12872                out.many = Some(meta.input.parse()?);
12873                return Ok(());
12874            }
12875            if meta.path.is_ident("nested") {
12876                out.nested = true;
12877                // Optional strict flag inside parentheses:
12878                //   #[serializer(nested(strict))]
12879                if meta.input.peek(syn::token::Paren) {
12880                    meta.parse_nested_meta(|inner| {
12881                        if inner.path.is_ident("strict") {
12882                            out.nested_strict = true;
12883                            return Ok(());
12884                        }
12885                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12886                    })?;
12887                }
12888                return Ok(());
12889            }
12890            if meta.path.is_ident("slug") {
12891                let s: LitStr = meta.value()?.parse()?;
12892                out.slug = Some(s.value());
12893                return Ok(());
12894            }
12895            if meta.path.is_ident("max_length") {
12896                let lit: syn::LitInt = meta.value()?.parse()?;
12897                out.max_length = Some(lit.base10_parse::<u64>()?);
12898                return Ok(());
12899            }
12900            if meta.path.is_ident("min_length") {
12901                let lit: syn::LitInt = meta.value()?.parse()?;
12902                out.min_length = Some(lit.base10_parse::<u64>()?);
12903                return Ok(());
12904            }
12905            if meta.path.is_ident("min") {
12906                let lit: syn::LitInt = meta.value()?.parse()?;
12907                out.min = Some(lit.base10_parse::<i64>()?);
12908                return Ok(());
12909            }
12910            if meta.path.is_ident("max") {
12911                let lit: syn::LitInt = meta.value()?.parse()?;
12912                out.max = Some(lit.base10_parse::<i64>()?);
12913                return Ok(());
12914            }
12915            Err(meta.error(
12916                "unknown serializer field attribute (supported: \
12917                 `read_only`, `write_only`, `source`, `skip`, `method`, \
12918                 `validate`, `nested`, `many`, `slug`, `max_length`, \
12919                 `min_length`, `min`, `max`)",
12920            ))
12921        })?;
12922    }
12923    // Validate: read_only + write_only is nonsensical
12924    if out.read_only && out.write_only {
12925        return Err(syn::Error::new_spanned(
12926            field,
12927            "a field cannot be both `read_only` and `write_only`",
12928        ));
12929    }
12930    if out.method.is_some() && out.source.is_some() {
12931        return Err(syn::Error::new_spanned(
12932            field,
12933            "`method` and `source` are mutually exclusive — `method` computes \
12934             the value from a method, `source` reads it from a different model field",
12935        ));
12936    }
12937    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12938        return Err(syn::Error::new_spanned(
12939            field,
12940            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12941             — pick one strategy for populating the field",
12942        ));
12943    }
12944    Ok(out)
12945}
12946
12947fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12948    let root = rustango_root();
12949    let struct_name = &input.ident;
12950    let struct_name_lit = struct_name.to_string();
12951
12952    let Data::Struct(data) = &input.data else {
12953        return Err(syn::Error::new_spanned(
12954            struct_name,
12955            "Serializer can only be derived on structs",
12956        ));
12957    };
12958    let Fields::Named(named) = &data.fields else {
12959        return Err(syn::Error::new_spanned(
12960            struct_name,
12961            "Serializer requires a struct with named fields",
12962        ));
12963    };
12964
12965    let container = parse_serializer_container_attrs(input)?;
12966    let model_path = &container.model;
12967
12968    // Classify each field. `ty` is only consumed by the
12969    // `#[cfg(feature = "openapi")]` block below, but we always
12970    // capture it to keep the field-info build a single pass.
12971    #[allow(dead_code)]
12972    struct FieldInfo {
12973        ident: syn::Ident,
12974        ty: syn::Type,
12975        attrs: SerializerFieldAttrs,
12976    }
12977    let mut fields_info: Vec<FieldInfo> = Vec::new();
12978    for field in &named.named {
12979        let ident = field.ident.clone().expect("named field has ident");
12980        let attrs = parse_serializer_field_attrs(field)?;
12981        fields_info.push(FieldInfo {
12982            ident,
12983            ty: field.ty.clone(),
12984            attrs,
12985        });
12986    }
12987
12988    // Generate from_model body: struct literal with each field assigned.
12989    let from_model_fields = fields_info.iter().map(|fi| {
12990        let ident = &fi.ident;
12991        let ty = &fi.ty;
12992        if let Some(_inner) = &fi.attrs.many {
12993            // Many — collection field. Initialize empty; caller
12994            // populates via the macro-emitted set_<field> helper
12995            // after fetching the M2M children.
12996            quote! { #ident: ::std::vec::Vec::new() }
12997        } else if let Some(method) = &fi.attrs.method {
12998            // SerializerMethodField: call Self::<method>(&model) to
12999            // compute the value. Method signature must be
13000            // `fn <method>(model: &T) -> <field type>`.
13001            let method_ident = syn::Ident::new(method, ident.span());
13002            quote! { #ident: Self::#method_ident(model) }
13003        } else if let Some(slug_field) = &fi.attrs.slug {
13004            // v0.44 — SlugRelatedField. Source defaults to the field
13005            // name on this struct; override via `source = "..."`. The
13006            // source field on the model is expected to be a
13007            // `ForeignKey<T>`; the slug field on the parent is named
13008            // by the attribute value. When the FK is unloaded the
13009            // field falls back to `Default::default()` — same
13010            // graceful-degrade contract as `nested`.
13011            let src_name = fi
13012                .attrs
13013                .source
13014                .as_deref()
13015                .unwrap_or(&fi.ident.to_string())
13016                .to_owned();
13017            let src_ident = syn::Ident::new(&src_name, ident.span());
13018            let slug_ident = syn::Ident::new(slug_field, ident.span());
13019            quote! {
13020                #ident: match model.#src_ident.value() {
13021                    ::core::option::Option::Some(__loaded) =>
13022                        ::core::clone::Clone::clone(&__loaded.#slug_ident),
13023                    ::core::option::Option::None =>
13024                        ::core::default::Default::default(),
13025                }
13026            }
13027        } else if fi.attrs.nested {
13028            // Nested serializer. Source defaults to the field name on
13029            // this struct; override via `source = "..."`. The source
13030            // field on the model is expected to be a `ForeignKey<T>`
13031            // whose `.value()` returns `Option<&T>` after lazy-load.
13032            //
13033            // Behavior matrix (tweakable per-field):
13034            //   * FK loaded   → nested object materializes via
13035            //                   ChildSerializer::from_model(parent).
13036            //   * FK unloaded → fall back to ChildSerializer::default()
13037            //                   (so prod doesn't crash on a missing
13038            //                   prefetch — just renders a blank nested
13039            //                   object). Add `#[serializer(nested,
13040            //                   strict)]` to keep the v0.18.1
13041            //                   panic-on-unloaded behavior for tests
13042            //                   that want hard guardrails.
13043            let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
13044            let src_ident = syn::Ident::new(&src_name, ident.span());
13045            if fi.attrs.nested_strict {
13046                let panic_msg = format!(
13047                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13048                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13049                );
13050                quote! {
13051                    #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13052                        model.#src_ident.value().expect(#panic_msg),
13053                    )
13054                }
13055            } else {
13056                quote! {
13057                    #ident: match model.#src_ident.value() {
13058                        ::core::option::Option::Some(__loaded) =>
13059                            <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13060                        ::core::option::Option::None =>
13061                            ::core::default::Default::default(),
13062                    }
13063                }
13064            }
13065        } else if fi.attrs.write_only || fi.attrs.skip {
13066            // Not read from model — use default
13067            quote! { #ident: ::core::default::Default::default() }
13068        } else if let Some(src) = &fi.attrs.source {
13069            let src_ident = syn::Ident::new(src, ident.span());
13070            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13071        } else {
13072            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13073        }
13074    });
13075
13076    // is_writable predicate (also used by writable_lits /
13077    // writable_source_lits below).
13078    let is_writable = |fi: &&FieldInfo| {
13079        !fi.attrs.read_only
13080            && !fi.attrs.skip
13081            && fi.attrs.method.is_none()
13082            && !fi.attrs.nested
13083            && fi.attrs.many.is_none()
13084            && fi.attrs.slug.is_none()
13085    };
13086
13087    // Declarative field constraints (DRF `validators=[...]`): one block
13088    // per writable field, run inside `validate()`. Each resolves its
13089    // bounds as the serializer attr when given (`#[serializer(max_length
13090    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13091    // `max` / `choices`), then dispatches on the field's JSON value via
13092    // `forms::validators::check_value` (string → length/choices, integer
13093    // → min/max).
13094    let opt_usize = |v: Option<u64>| match v {
13095        Some(n) => {
13096            let n = n as usize;
13097            quote!(::core::option::Option::Some(#n))
13098        }
13099        None => quote!(::core::option::Option::None),
13100    };
13101    let opt_i64 = |v: Option<i64>| match v {
13102        Some(n) => quote!(::core::option::Option::Some(#n)),
13103        None => quote!(::core::option::Option::None),
13104    };
13105    let constraint_blocks: Vec<_> = fields_info
13106        .iter()
13107        .filter(is_writable)
13108        .map(|fi| {
13109            let ident = &fi.ident;
13110            let fname = ident.to_string();
13111            let mname = fi
13112                .attrs
13113                .source
13114                .clone()
13115                .unwrap_or_else(|| fi.ident.to_string());
13116            let attr_max_len = opt_usize(fi.attrs.max_length);
13117            let attr_min_len = opt_usize(fi.attrs.min_length);
13118            let attr_min = opt_i64(fi.attrs.min);
13119            let attr_max = opt_i64(fi.attrs.max);
13120            quote! {
13121                {
13122                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13123                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13124                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13125                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13126                    let __min: ::core::option::Option<i64> =
13127                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13128                    let __max: ::core::option::Option<i64> =
13129                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13130                    let __choices = __sf.and_then(|__f| __f.choices);
13131                    let __v = #root::__serde_json::to_value(&self.#ident)
13132                        .unwrap_or(#root::__serde_json::Value::Null);
13133                    #root::forms::validators::check_value(
13134                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13135                        &mut __errors,
13136                    );
13137                }
13138            }
13139        })
13140        .collect();
13141    let has_constraints = !constraint_blocks.is_empty();
13142
13143    // Per-field validators (DRF-shape `validators=[...]`). Emit a
13144    // `validate(&self)` method that runs each user-defined validator
13145    // and aggregates errors into `FormErrors`.
13146    let validator_calls: Vec<_> = fields_info
13147        .iter()
13148        .filter_map(|fi| {
13149            let ident = &fi.ident;
13150            let name_lit = ident.to_string();
13151            let method = fi.attrs.validate.as_ref()?;
13152            let method_ident = syn::Ident::new(method, ident.span());
13153            Some(quote! {
13154                if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13155                    __errors.add(#name_lit.to_owned(), __e);
13156                }
13157            })
13158        })
13159        .collect();
13160    // #436 — DRF cross-field `validate(self)` shape. If the
13161    // container declared `#[serializer(validate = "fn_name")]`,
13162    // the macro-generated `validate(&self)` runs every per-field
13163    // validator first, then calls the user's cross-field method,
13164    // merging its `FormErrors` into the per-field errors. Either
13165    // alone is enough to emit the wrapper.
13166    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13167        quote! {
13168            // Merge cross-field errors into the per-field bucket so
13169            // a single .validate() call surfaces both layers.
13170            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13171                __errors.merge(__cross);
13172            }
13173        }
13174    });
13175    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13176    // Anything to run? Declarative constraints (for any writable field) +
13177    // per-field validators + cross-field hook.
13178    let has_run_validations = has_validators || has_constraints;
13179    // Shared body: declarative field constraints first (length / range /
13180    // choices, serializer-attr-or-model), then per-field validators, then
13181    // the cross-field hook.
13182    let validate_body = quote! {
13183        let mut __errors = #root::forms::FormErrors::default();
13184        #( #constraint_blocks )*
13185        #( #validator_calls )*
13186        #cross_validate_call
13187        if __errors.is_empty() {
13188            ::core::result::Result::Ok(())
13189        } else {
13190            ::core::result::Result::Err(__errors)
13191        }
13192    };
13193    // Inherent `validate(&self)` — kept for back-compat with direct
13194    // `serializer.validate()` calls that don't import `ModelSerializer`.
13195    // Emitted only when the serializer declares per-field/cross-field
13196    // validators (unchanged rule) so it never collides with a
13197    // hand-written inherent `validate`.
13198    let validate_method = if has_validators {
13199        quote! {
13200            impl #struct_name {
13201                /// Run the declarative field constraints, every
13202                /// `#[serializer(validate = "...")]` per-field validator,
13203                /// and (when declared) the container-level cross-field
13204                /// validator. Aggregates errors into `FormErrors` keyed by
13205                /// the field name. Returns `Ok(())` when all pass.
13206                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13207                    #validate_body
13208                }
13209            }
13210        }
13211    } else {
13212        quote! {}
13213    };
13214    // Trait-method override so the type-erased ViewSet write path can
13215    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13216    // there's anything to run (declarative constraints inherited from the
13217    // model count); otherwise the trait's default no-op is used.
13218    let trait_validate_override = if has_run_validations {
13219        quote! {
13220            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13221                #validate_body
13222            }
13223        }
13224    } else {
13225        quote! {}
13226    };
13227
13228    // For every `#[serializer(many = S)]` field, emit a
13229    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13230    // helper that maps the parents through `S::from_model`.
13231    let many_setters: Vec<_> = fields_info
13232        .iter()
13233        .filter_map(|fi| {
13234            let many_ty = fi.attrs.many.as_ref()?;
13235            let ident = &fi.ident;
13236            let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
13237            Some(quote! {
13238                /// Populate this `many` field by mapping each parent model
13239                /// through the inner serializer's `from_model`. Call after
13240                /// fetching the M2M / one-to-many children since
13241                /// `from_model` itself can't await an SQL query.
13242                pub fn #setter(
13243                    &mut self,
13244                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13245                ) -> &mut Self {
13246                    self.#ident = models.iter()
13247                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13248                        .collect();
13249                    self
13250                }
13251            })
13252        })
13253        .collect();
13254    let many_setters_impl = if many_setters.is_empty() {
13255        quote! {}
13256    } else {
13257        quote! {
13258            impl #struct_name {
13259                #( #many_setters )*
13260            }
13261        }
13262    };
13263
13264    // Generate custom Serialize: skip write_only fields
13265    let output_fields: Vec<_> = fields_info
13266        .iter()
13267        .filter(|fi| !fi.attrs.write_only)
13268        .collect();
13269    let output_field_count = output_fields.len();
13270    let serialize_fields = output_fields.iter().map(|fi| {
13271        let ident = &fi.ident;
13272        let name_lit = ident.to_string();
13273        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13274    });
13275
13276    // writable_fields: normal + write_only.
13277    // Exclude:
13278    //   - `read_only` — server-computed.
13279    //   - `skip` — caller sets manually post-from_model.
13280    //   - `method` — computed from a Self::fn(&model) call; accepting
13281    //     it on write is meaningless.
13282    //   - `nested` / `many` — populated from related-model data, not
13283    //     from a field on the wire body.
13284    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13285    // `many` in `writable_fields()`, which made the ViewSet write
13286    // path accept those fields from the JSON body and try to bind
13287    // them to the SQL UPDATE — a silent no-op at best, a type
13288    // mismatch at worst.
13289    // (`is_writable` is defined above, near the constraint codegen.)
13290    let writable_lits: Vec<_> = fields_info
13291        .iter()
13292        .filter(is_writable)
13293        .map(|fi| fi.ident.to_string())
13294        .collect();
13295
13296    // `writable_source_fields`: the MODEL field names of writable
13297    // serializer fields (source-resolved). The ViewSet write path skips
13298    // every model column NOT in this set, so `read_only` / `method` /
13299    // computed fields a client posts are ignored instead of written.
13300    let writable_source_lits: Vec<String> = fields_info
13301        .iter()
13302        .filter(is_writable)
13303        .map(|fi| {
13304            fi.attrs
13305                .source
13306                .clone()
13307                .unwrap_or_else(|| fi.ident.to_string())
13308        })
13309        .collect();
13310
13311    // `from_writable_json`: build a partial instance for input
13312    // validation. Writable fields are parsed from the JSON body (keyed
13313    // by serializer field name); every other field defaults. Per-field
13314    // type errors collect into `FormErrors` keyed by the field name.
13315    // Construction is field-by-field (not `Self::default()`) so it works
13316    // for serializers regardless of a struct-level `Default`.
13317    let from_writable_json_inits: Vec<_> = fields_info
13318        .iter()
13319        .map(|fi| {
13320            let ident = &fi.ident;
13321            let fname = ident.to_string();
13322            let ty = &fi.ty;
13323            if is_writable(&fi) {
13324                quote! {
13325                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13326                        ::core::option::Option::Some(__v) => {
13327                            match #root::__serde_json::from_value::<#ty>(
13328                                ::core::clone::Clone::clone(__v),
13329                            ) {
13330                                ::core::result::Result::Ok(__x) => __x,
13331                                ::core::result::Result::Err(__e) => {
13332                                    __errors.add(#fname.to_owned(), __e.to_string());
13333                                    ::core::default::Default::default()
13334                                }
13335                            }
13336                        }
13337                        ::core::option::Option::None => ::core::default::Default::default(),
13338                    }
13339                }
13340            } else {
13341                quote! { #ident: ::core::default::Default::default() }
13342            }
13343        })
13344        .collect();
13345
13346    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13347    // Only includes fields shown in JSON output (skips write_only). For each
13348    // `Option<T>` field, omit from `required` and add `.nullable()`.
13349    let openapi_impl = {
13350        #[cfg(feature = "openapi")]
13351        {
13352            let property_calls = output_fields.iter().map(|fi| {
13353                let ident = &fi.ident;
13354                let name_lit = ident.to_string();
13355                let ty = &fi.ty;
13356                let nullable_call = if is_option(ty) {
13357                    quote! { .nullable() }
13358                } else {
13359                    quote! {}
13360                };
13361                quote! {
13362                    .property(
13363                        #name_lit,
13364                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13365                            #nullable_call,
13366                    )
13367                }
13368            });
13369            let required_lits: Vec<_> = output_fields
13370                .iter()
13371                .filter(|fi| !is_option(&fi.ty))
13372                .map(|fi| fi.ident.to_string())
13373                .collect();
13374            quote! {
13375                impl #root::openapi::OpenApiSchema for #struct_name {
13376                    fn openapi_schema() -> #root::openapi::Schema {
13377                        #root::openapi::Schema::object()
13378                            #( #property_calls )*
13379                            .required([ #( #required_lits ),* ])
13380                    }
13381                }
13382            }
13383        }
13384        #[cfg(not(feature = "openapi"))]
13385        {
13386            quote! {}
13387        }
13388    };
13389
13390    Ok(quote! {
13391        impl #root::serializer::ModelSerializer for #struct_name {
13392            type Model = #model_path;
13393
13394            fn from_model(model: &Self::Model) -> Self {
13395                Self {
13396                    #( #from_model_fields ),*
13397                }
13398            }
13399
13400            fn writable_fields() -> &'static [&'static str] {
13401                &[ #( #writable_lits ),* ]
13402            }
13403
13404            fn writable_source_fields() -> &'static [&'static str] {
13405                &[ #( #writable_source_lits ),* ]
13406            }
13407
13408            fn from_writable_json(
13409                __body: &#root::__serde_json::Value,
13410            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13411                let mut __errors = #root::forms::FormErrors::default();
13412                let __obj = __body.as_object();
13413                let __out = Self {
13414                    #( #from_writable_json_inits ),*
13415                };
13416                if __errors.is_empty() {
13417                    ::core::result::Result::Ok(__out)
13418                } else {
13419                    ::core::result::Result::Err(__errors)
13420                }
13421            }
13422
13423            #trait_validate_override
13424        }
13425
13426        impl #root::__serde::Serialize for #struct_name {
13427            fn serialize<S>(&self, serializer: S)
13428                -> ::core::result::Result<S::Ok, S::Error>
13429            where
13430                S: #root::__serde::Serializer,
13431            {
13432                use #root::__serde::ser::SerializeStruct;
13433                let mut __state = serializer.serialize_struct(
13434                    #struct_name_lit,
13435                    #output_field_count,
13436                )?;
13437                #( #serialize_fields )*
13438                __state.end()
13439            }
13440        }
13441
13442        #openapi_impl
13443
13444        #validate_method
13445
13446        #many_setters_impl
13447    })
13448}
13449
13450/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13451/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13452/// when the feature is off.
13453#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13454fn is_option(ty: &syn::Type) -> bool {
13455    if let syn::Type::Path(p) = ty {
13456        if let Some(last) = p.path.segments.last() {
13457            return last.ident == "Option";
13458        }
13459    }
13460    false
13461}