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")]` — computed field: 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")]` — render a relation as one of its fields: 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 Rust web 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!()` — `Model.field__lookup = value` filter syntax, resolved at
210/// compile time against 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/// filters keyed by a plain string.
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 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/// `logging = false` suppresses the default subscriber, for apps
286/// that install their own. Needed whenever the body reaches
287/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
288/// macro installs gets there first, and `tracing` keeps the first
289/// one, so the `[logging]` section would otherwise be read and
290/// discarded (#1465).
291///
292/// ```ignore
293/// #[rustango::main(logging = false)]
294/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
295///     rustango::manage::Cli::new()
296///         .with_settings_from_env()
297///         .with_logging()          // now the first installer
298///         .run().await
299/// }
300/// ```
301///
302/// Pulls `tracing-subscriber` into the rustango crate behind the
303/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
304/// out get plain `#[tokio::main]` ergonomics without the dependency.
305#[proc_macro_attribute]
306pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
307    expand_main(args.into(), item.into())
308        .unwrap_or_else(syn::Error::into_compile_error)
309        .into()
310}
311
312fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
313    let mut input: syn::ItemFn = syn::parse2(item)?;
314    if input.sig.asyncness.is_none() {
315        return Err(syn::Error::new(
316            input.sig.ident.span(),
317            "`#[rustango::main]` must wrap an `async fn`",
318        ));
319    }
320
321    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
322    // to `#[tokio::main]`. Tokio's proc-macro internally emits
323    // `::tokio::*` paths that resolve against the user crate's deps,
324    // so calling it through the rustango re-export still requires the
325    // user to add tokio to their own Cargo.toml. Building the
326    // runtime ourselves keeps the dep transitive through the
327    // `runtime` feature on rustango.
328    //
329    // Parse optional `flavor = "current_thread"` / `flavor =
330    // "multi_thread"` from the attribute args. Unknown args are
331    // tolerated (forward-compat with tokio's own arg surface).
332    let root = rustango_root();
333    let flavor = parse_flavor(&args);
334    let builder_call = match flavor {
335        Flavor::CurrentThread => quote! {
336            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
337        },
338        Flavor::MultiThread => quote! {
339            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
340        },
341    };
342
343    // Detach the user body and rewrite `main` as a sync fn that
344    // builds the runtime and blocks on the async body.
345    let user_body = input.block.clone();
346    input.sig.asyncness = None;
347
348    // `logging = false` drops the block entirely, so a later
349    // `Cli::with_logging()` is the first installer and its
350    // `[logging]` settings actually take effect (#1465).
351    let logging_prologue = if parse_logging(&args) {
352        quote! {
353            use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
354            // Colour only when stdout is a terminal, and never under
355            // `NO_COLOR` — `Color::Auto`'s rule, called rather than
356            // copied.
357            //
358            // This was inlined at first, on a comment claiming
359            // `rustango::logging` is gated on `admin` + `tenancy`. It is
360            // not: `pub mod logging` is ungated and its contents are
361            // `runtime`-gated — the same feature that gates this macro
362            // (`pub use rustango_macros::main` is `#[cfg(feature =
363            // "runtime")]`). So wherever this expands, `Color` is
364            // reachable, and the copy was justified by a gate that does
365            // not exist. `should_colour` carries a deliberate
366            // `NO_COLOR`-set-but-empty subtlety that the inline happened
367            // to match today and nothing kept matching tomorrow.
368            //
369            // Without any of it the subscriber inherits
370            // tracing-subscriber's default, `cfg!(feature = "ansi")` —
371            // true since the framework turned that feature on — so
372            // `./app > app.log` and every container redirecting stdout
373            // collected escape codes. `Setup::install` has its own
374            // check; this is the default entrypoint and had none.
375            let __ansi = #root::logging::Color::Auto.should_colour();
376            // `try_init` so duplicate installers (e.g. tests already
377            // holding a subscriber) don't panic.
378            let _ = tracing_subscriber::fmt()
379                .with_env_filter(
380                    EnvFilter::try_from_default_env()
381                        .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
382                )
383                .with_ansi(__ansi)
384                .try_init();
385        }
386    } else {
387        quote! {}
388    };
389
390    input.block = syn::parse2(quote! {{
391        { #logging_prologue }
392        let __rt = #builder_call
393            .enable_all()
394            .build()
395            .expect("failed to build tokio runtime");
396        __rt.block_on(async move #user_body)
397    }})?;
398
399    Ok(quote! {
400        #input
401    })
402}
403
404enum Flavor {
405    MultiThread,
406    CurrentThread,
407}
408
409fn parse_flavor(args: &TokenStream2) -> Flavor {
410    // Cheap parser: look for the literal token sequence
411    // `flavor = "current_thread"`. Everything else (including
412    // bare `multi_thread` or no args) defaults to multi-thread.
413    let s = args.to_string();
414    if s.contains("current_thread") {
415        Flavor::CurrentThread
416    } else {
417        Flavor::MultiThread
418    }
419}
420
421/// `false` when the attribute carries `logging = false` — the caller
422/// installs their own subscriber, usually via `Cli::with_logging`
423/// (#1465). Same cheap token match as `parse_flavor`.
424fn parse_logging(args: &TokenStream2) -> bool {
425    !args.to_string().replace(' ', "").contains("logging=false")
426}
427
428/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
429struct QInput {
430    base_path: syn::Path,
431    field: syn::Ident,
432    value: syn::Expr,
433}
434
435impl syn::parse::Parse for QInput {
436    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
437        let base_path: syn::Path = input.parse()?;
438        input.parse::<syn::Token![.]>()?;
439        let field: syn::Ident = input.parse()?;
440        input.parse::<syn::Token![=]>()?;
441        let value: syn::Expr = input.parse()?;
442        Ok(QInput {
443            base_path,
444            field,
445            value,
446        })
447    }
448}
449
450fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
451    let q: QInput = syn::parse2(input)?;
452    let root = rustango_root();
453    let field_str = q.field.to_string();
454    let field_span = q.field.span();
455    let (base, suffix) = match field_str.find("__") {
456        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
457        None => (field_str.as_str(), ""),
458    };
459    if base.is_empty() {
460        return Err(syn::Error::new(
461            field_span,
462            "Q!(): field name is empty before `__` suffix",
463        ));
464    }
465    let base_ident = syn::Ident::new(base, field_span);
466    let value = &q.value;
467    let path = &q.base_path;
468
469    // Most suffixes map directly to a Column method with the value
470    // forwarded unchanged. Some need value-shape massaging (wildcards
471    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
472    // ISNULL). Unknown suffixes fail the build.
473    let expanded = match suffix {
474        "" | "exact" => quote! {
475            #root::core::Column::eq(#path::#base_ident, #value)
476        },
477        "ne" => quote! {
478            #root::core::Column::ne(#path::#base_ident, #value)
479        },
480        "gt" => quote! {
481            #root::core::Column::gt(#path::#base_ident, #value)
482        },
483        "gte" => quote! {
484            #root::core::Column::gte(#path::#base_ident, #value)
485        },
486        "lt" => quote! {
487            #root::core::Column::lt(#path::#base_ident, #value)
488        },
489        "lte" => quote! {
490            #root::core::Column::lte(#path::#base_ident, #value)
491        },
492        // The escaped-LIKE lookups delegate to the typed Column
493        // methods, which escape the value and pair it with the
494        // *Escaped ops so `%` / `_` match literally on every dialect
495        // (#1257) — the macro no longer owns any pattern wrapping.
496        "iexact" => quote! {
497            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
498        },
499        "contains" => quote! {
500            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
501        },
502        "icontains" => quote! {
503            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
504        },
505        "startswith" => quote! {
506            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
507        },
508        "istartswith" => quote! {
509            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
510        },
511        "endswith" => quote! {
512            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
513        },
514        "iendswith" => quote! {
515            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
516        },
517        "in" => quote! {
518            #root::core::Column::is_in(#path::#base_ident, #value)
519        },
520        "not_in" => quote! {
521            #root::core::Column::not_in(#path::#base_ident, #value)
522        },
523        "isnull" => {
524            // Must be a bool literal at macro time so we can route to
525            // is_null vs is_not_null without a runtime branch.
526            let b = match value {
527                syn::Expr::Lit(syn::ExprLit {
528                    lit: syn::Lit::Bool(b),
529                    ..
530                }) => b.value(),
531                _ => {
532                    return Err(syn::Error::new_spanned(
533                        value,
534                        "Q!(): `__isnull` requires a `true` or `false` literal",
535                    ));
536                }
537            };
538            if b {
539                quote! { #root::core::Column::is_null(#path::#base_ident) }
540            } else {
541                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
542            }
543        }
544        "between" => {
545            // Accept a tuple literal `(lo, hi)`.
546            let tuple = match value {
547                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
548                _ => {
549                    return Err(syn::Error::new_spanned(
550                        value,
551                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
552                    ));
553                }
554            };
555            let lo = &tuple.elems[0];
556            let hi = &tuple.elems[1];
557            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
558        }
559        "regex" => quote! {
560            #root::core::Column::regex(#path::#base_ident, #value)
561        },
562        "iregex" => quote! {
563            #root::core::Column::iregex(#path::#base_ident, #value)
564        },
565        _ => {
566            return Err(syn::Error::new(
567                field_span,
568                format!(
569                    "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",
570                    suffix
571                ),
572            ));
573        }
574    };
575    Ok(expanded)
576}
577
578fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
579    // Default to "./migrations" if invoked without args.
580    let path_str = if input.is_empty() {
581        "./migrations".to_string()
582    } else {
583        let lit: LitStr = syn::parse2(input)?;
584        lit.value()
585    };
586
587    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
588        syn::Error::new(
589            proc_macro2::Span::call_site(),
590            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
591        )
592    })?;
593    let abs = std::path::Path::new(&manifest).join(&path_str);
594
595    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
596    if abs.is_dir() {
597        let read = std::fs::read_dir(&abs).map_err(|e| {
598            syn::Error::new(
599                proc_macro2::Span::call_site(),
600                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
601            )
602        })?;
603        for entry in read.flatten() {
604            let path = entry.path();
605            if !path.is_file() {
606                continue;
607            }
608            if path.extension().and_then(|s| s.to_str()) != Some("json") {
609                continue;
610            }
611            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
612                continue;
613            };
614            entries.push((stem.to_owned(), path));
615        }
616    }
617    entries.sort_by(|a, b| a.0.cmp(&b.0));
618
619    // Compile-time chain validation: read each migration's JSON,
620    // pull `name` and `prev` (file-stem-keyed for the chain check),
621    // and verify every `prev` points to another migration in the
622    // slice. Mismatches between the file stem and the embedded
623    // `name` field — and broken `prev` chains — fail at MACRO
624    // EXPANSION time so a misshapen migration set never compiles.
625    //
626    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
627    // migrations + a Rust proc-macro that reads them is a combo no
628    // other Rust web framework matches (Cot's are imperative Rust
629    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
630    // a static chain to validate).
631    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
632    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
633    for (stem, path) in &entries {
634        let raw = std::fs::read_to_string(path).map_err(|e| {
635            syn::Error::new(
636                proc_macro2::Span::call_site(),
637                format!(
638                    "embed_migrations!: cannot read {} for chain validation: {e}",
639                    path.display()
640                ),
641            )
642        })?;
643        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
644            syn::Error::new(
645                proc_macro2::Span::call_site(),
646                format!(
647                    "embed_migrations!: {} is not valid JSON: {e}",
648                    path.display()
649                ),
650            )
651        })?;
652        let name = json
653            .get("name")
654            .and_then(|v| v.as_str())
655            .ok_or_else(|| {
656                syn::Error::new(
657                    proc_macro2::Span::call_site(),
658                    format!(
659                        "embed_migrations!: {} is missing the `name` field",
660                        path.display()
661                    ),
662                )
663            })?
664            .to_owned();
665        if name != *stem {
666            return Err(syn::Error::new(
667                proc_macro2::Span::call_site(),
668                format!(
669                    "embed_migrations!: file stem `{stem}` does not match the migration's \
670                     `name` field `{name}` — rename the file or fix the JSON",
671                ),
672            ));
673        }
674        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
675        chain_names.push(name.clone());
676        prev_refs.push((name, prev));
677    }
678
679    let name_set: std::collections::HashSet<&str> =
680        chain_names.iter().map(String::as_str).collect();
681    for (name, prev) in &prev_refs {
682        if let Some(p) = prev {
683            if !name_set.contains(p.as_str()) {
684                return Err(syn::Error::new(
685                    proc_macro2::Span::call_site(),
686                    format!(
687                        "embed_migrations!: broken migration chain — `{name}` declares \
688                         prev=`{p}` but no migration with that name exists in {}",
689                        abs.display()
690                    ),
691                ));
692            }
693        }
694    }
695
696    let pairs: Vec<TokenStream2> = entries
697        .iter()
698        .map(|(name, path)| {
699            let path_lit = path.display().to_string();
700            quote! { (#name, ::core::include_str!(#path_lit)) }
701        })
702        .collect();
703
704    Ok(quote! {
705        {
706            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
707            __RUSTANGO_EMBEDDED
708        }
709    })
710}
711
712fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
713    let root = rustango_root();
714    let struct_name = &input.ident;
715
716    let Data::Struct(data) = &input.data else {
717        return Err(syn::Error::new_spanned(
718            struct_name,
719            "Model can only be derived on structs",
720        ));
721    };
722    let Fields::Named(named) = &data.fields else {
723        return Err(syn::Error::new_spanned(
724            struct_name,
725            "Model requires a struct with named fields",
726        ));
727    };
728
729    let container = parse_container_attrs(input)?;
730    let table = container
731        .table
732        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
733    let model_name = struct_name.to_string();
734
735    let collected = collect_fields(named, &table)?;
736
737    // Validate that #[rustango(display = "…")] names a real field.
738    if let Some((ref display, span)) = container.display {
739        if !collected.field_names.iter().any(|n| n == display) {
740            return Err(syn::Error::new(
741                span,
742                format!("`display = \"{display}\"` does not match any field on this struct"),
743            ));
744        }
745    }
746    let display = container.display.map(|(name, _)| name);
747    let app_label = container.app.clone();
748
749    // Validate admin field-name lists against declared field names.
750    // Note: `list_display` is intentionally NOT validated here. As of
751    // v0.32 it may also reference inventory-registered computed
752    // fields (via `register_admin_computed!`) whose existence the
753    // macro can't see at compile time — they're submitted from any
754    // crate that depends on rustango. The runtime list-view resolves
755    // unknown names against the inventory + silently drops the
756    // truly-bogus ones, which is the cheaper trade-off versus
757    // forcing a per-Model attr to opt out.
758    if let Some(admin) = &container.admin {
759        for (label, list) in [
760            ("search_fields", &admin.search_fields),
761            ("readonly_fields", &admin.readonly_fields),
762            ("list_filter", &admin.list_filter),
763        ] {
764            if let Some((names, span)) = list {
765                for name in names {
766                    if !collected.field_names.iter().any(|n| n == name) {
767                        return Err(syn::Error::new(
768                            *span,
769                            format!(
770                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
771                            ),
772                        ));
773                    }
774                }
775            }
776        }
777        if let Some((pairs, span)) = &admin.ordering {
778            for (name, _) in pairs {
779                if !collected.field_names.iter().any(|n| n == name) {
780                    return Err(syn::Error::new(
781                        *span,
782                        format!(
783                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
784                        ),
785                    ));
786                }
787            }
788        }
789        if let Some((groups, span)) = &admin.fieldsets {
790            for (_, fields) in groups {
791                for name in fields {
792                    if !collected.field_names.iter().any(|n| n == name) {
793                        return Err(syn::Error::new(
794                            *span,
795                            format!(
796                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
797                            ),
798                        ));
799                    }
800                }
801            }
802        }
803    }
804    if let Some(audit) = &container.audit {
805        if let Some((names, span)) = &audit.track {
806            for name in names {
807                if !collected.field_names.iter().any(|n| n == name) {
808                    return Err(syn::Error::new(
809                        *span,
810                        format!(
811                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
812                        ),
813                    ));
814                }
815            }
816        }
817    }
818
819    // Issue #291 / T2.5 — validate each `default_order` column name
820    // against the model's collected fields. Typos fail at macro-expand
821    // time, not at the database.
822    for (col, _desc, span) in &container.default_order {
823        if !collected.field_names.iter().any(|n| n == col) {
824            return Err(syn::Error::new(
825                *span,
826                format!(
827                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
828                ),
829            ));
830        }
831    }
832
833    // Build the audit_track list for ModelSchema: None when no audit attr,
834    // Some(empty) when audit present without track, Some(names) when explicit.
835    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
836        audit
837            .track
838            .as_ref()
839            .map(|(names, _)| names.clone())
840            .unwrap_or_default()
841    });
842
843    // Merge field-level indexes into the container's index list.
844    let mut all_indexes: Vec<IndexAttr> = container.indexes;
845    for field in &named.named {
846        let ident = field.ident.as_ref().expect("named");
847        let col = to_snake_case(&ident.to_string()); // column name fallback
848                                                     // Re-parse field attrs to check for index flag
849        if let Ok(fa) = parse_field_attrs(field) {
850            if fa.index {
851                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
852                let auto_name = if fa.index_unique {
853                    format!("{table}_{col_name}_uq_idx")
854                } else {
855                    format!("{table}_{col_name}_idx")
856                };
857                all_indexes.push(IndexAttr {
858                    name: fa.index_name.or(Some(auto_name)),
859                    columns: vec![col_name],
860                    unique: fa.index_unique,
861                    method: fa.index_method,
862                    where_clause: None,
863                    include: Vec::new(),
864                });
865            }
866        }
867    }
868
869    let model_impl = model_impl_tokens(
870        struct_name,
871        &model_name,
872        &table,
873        display.as_deref(),
874        app_label.as_deref(),
875        container.admin.as_ref(),
876        &container.default_order,
877        &collected.field_schemas,
878        collected.soft_delete_column.as_deref(),
879        container.permissions,
880        audit_track_names.as_deref(),
881        &container.m2m,
882        &all_indexes,
883        &container.checks,
884        &container.excludes,
885        &container.composite_fks,
886        &container.generic_fks,
887        container.scope.as_deref(),
888        container.is_view,
889        container.verbose_name.as_deref(),
890        container.verbose_name_plural.as_deref(),
891        container.managed,
892        container.base_manager_name.as_deref(),
893        container.order_with_respect_to.as_deref(),
894        container.proxy,
895        &container.required_db_features,
896        container.required_db_vendor.as_deref(),
897        container.default_related_name.as_deref(),
898        container.db_table_comment.as_deref(),
899        container
900            .get_latest_by
901            .as_ref()
902            .map(|(c, d)| (c.as_str(), *d)),
903        &container.extra_permissions,
904        &container.default_permissions,
905        &container.global_scopes,
906        &container.reverse_has_relations,
907        &container.generic_has_relations,
908    );
909    let module_ident = column_module_ident(struct_name);
910    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
911    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
912        let track_set: Option<std::collections::HashSet<&str>> = audit
913            .track
914            .as_ref()
915            .map(|(names, _)| names.iter().map(String::as_str).collect());
916        collected
917            .column_entries
918            .iter()
919            .filter(|c| {
920                track_set
921                    .as_ref()
922                    .map_or(true, |s| s.contains(c.name.as_str()))
923            })
924            .collect()
925    });
926    let inherent_impl = inherent_impl_tokens(
927        struct_name,
928        &collected,
929        collected.primary_key.as_ref(),
930        &column_consts,
931        audited_fields.as_deref(),
932        &all_indexes,
933        &container.manager_fns,
934    );
935    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
936    let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
937    let reverse_helpers = reverse_helper_tokens(
938        struct_name,
939        &collected.fk_relations,
940        container.default_related_name.as_deref(),
941    );
942    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
943    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
944    // Issue #817 — `#[rustango(through(...))]` accessors.
945    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
946    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
947    let reverse_has_accessors =
948        reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
949    let generic_fk_accessors = generic_fk_accessor_tokens(
950        struct_name,
951        &container.generic_fks,
952        &collected.column_entries,
953    );
954
955    // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
956    // emits an empty extension trait so users can add methods via
957    // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
958    // trait declaration. See `crates/rustango/src/manager.rs` for the
959    // pattern this replaces.
960    let manager_trait = container.manager_ext.as_ref().map(|name| {
961        let model_name_str = struct_name.to_string();
962        let doc = format!(
963            "Custom-Manager extension trait for [`{model_name_str}`]. \
964             Generated by `#[rustango(manager(ext = ...))]`. Add methods \
965             via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
966        );
967        quote! {
968            #[doc = #doc]
969            pub trait #name: ::core::marker::Sized {}
970        }
971    });
972
973    Ok(quote! {
974        #model_impl
975        #inherent_impl
976        #from_row_impl
977        #column_module
978        #reverse_helpers
979        #m2m_accessors
980        #generic_m2m_accessors
981        #through_accessors
982        #reverse_has_accessors
983        #generic_fk_accessors
984        #manager_trait
985
986        #root::core::inventory::submit! {
987            #root::core::ModelEntry {
988                schema: <#struct_name as #root::core::Model>::SCHEMA,
989                // `module_path!()` evaluates at the registration site,
990                // so a Model declared in `crate::blog::models` records
991                // `"<crate>::blog::models"` and `resolved_app_label()`
992                // can infer "blog" without an explicit attribute.
993                module_path: ::core::module_path!(),
994            }
995        }
996    })
997}
998
999/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1000/// matches `field_name` against the model's FK fields and, for a
1001/// match, decodes the FK target via the parent's macro-generated
1002/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
1003/// `ForeignKey::Loaded` on `self`.
1004///
1005/// Always emitted (with empty arms for FK-less models, which
1006/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1007/// trait bound on `fetch_on` is universally satisfied — users
1008/// never have to think about implementing it.
1009fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1010    let root = rustango_root();
1011    let arms = fk_relations.iter().map(|rel| {
1012        let parent_ty = &rel.parent_type;
1013        let fk_col = rel.fk_column.as_str();
1014        // FK field's Rust ident matches its SQL column name in v0.8
1015        // (no `column = "..."` rename ships on FK fields).
1016        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1017        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1018        let assign = if rel.nullable {
1019            quote! {
1020                self.#field_ident = ::core::option::Option::Some(
1021                    #root::sql::ForeignKey::loaded(_pk, _parent),
1022                );
1023            }
1024        } else {
1025            quote! {
1026                self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1027            }
1028        };
1029        quote! {
1030            #fk_col => {
1031                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1032                // Audit #451 — multi-hop `select_related("a__b__c")`:
1033                // stitch the deeper relation onto this parent first,
1034                // decoding it at the accumulated `__next_alias`. The
1035                // parent type also impls `LoadRelated`, so this recurses
1036                // the FK chain to arbitrary depth.
1037                if let ::core::option::Option::Some(__r) = __rest {
1038                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1039                        &mut _parent, row, __r, &__next_alias,
1040                    )?;
1041                }
1042                // Loud-in-debug, default-in-release: a divergence
1043                // between the FK field's declared `K` (drives the
1044                // expected `SqlValue::<Variant>`) and the parent's
1045                // `__rustango_pk_value` output is a macro-internal
1046                // invariant break — surfacing the panic in dev
1047                // catches it before users hit silent PK=0 corruption.
1048                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1049                    #root::core::SqlValue::#variant_ident(v) => v,
1050                    _other => {
1051                        ::core::debug_assert!(
1052                            false,
1053                            "rustango macro bug: load_related on FK `{}` expected \
1054                             SqlValue::{} from parent's __rustango_pk_value but got \
1055                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1056                            #fk_col,
1057                            ::core::stringify!(#variant_ident),
1058                            _other,
1059                        );
1060                        #default_expr
1061                    }
1062                };
1063                #assign
1064                ::core::result::Result::Ok(true)
1065            }
1066        }
1067    });
1068    quote! {
1069        #[cfg(feature = "postgres")]
1070        impl #root::sql::LoadRelated for #struct_name {
1071            #[allow(unused_variables)]
1072            fn __rustango_load_related(
1073                &mut self,
1074                row: &#root::sql::sqlx::postgres::PgRow,
1075                field_name: &str,
1076                alias: &str,
1077            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1078                // Audit #451 — split the multi-hop path: `base` is the FK
1079                // on THIS model, `__rest` (if any) is the remaining chain
1080                // to stitch onto the loaded parent. `__next_alias` is the
1081                // accumulated join alias the parent's columns live under
1082                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1083                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1084                    match field_name.split_once("__") {
1085                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1086                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1087                    };
1088                let __next_alias: ::std::string::String = match __rest {
1089                    ::core::option::Option::Some(__r) => {
1090                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1091                        ::std::format!("{}__{}", alias, __rb)
1092                    }
1093                    ::core::option::Option::None => ::std::string::String::new(),
1094                };
1095                match __base {
1096                    #( #arms )*
1097                    _ => ::core::result::Result::Ok(false),
1098                }
1099            }
1100        }
1101    }
1102}
1103
1104/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1105/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1106/// which expands to a `LoadRelatedMy` impl when rustango is built with
1107/// the `mysql` feature, and to nothing otherwise. The decoded parent
1108/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1109/// decoder, also batch8) so the dual emission is symmetric across
1110/// backends.
1111fn load_related_impl_my_tokens(
1112    struct_name: &syn::Ident,
1113    fk_relations: &[FkRelation],
1114) -> TokenStream2 {
1115    let root = rustango_root();
1116    let arms = fk_relations.iter().map(|rel| {
1117        let parent_ty = &rel.parent_type;
1118        let fk_col = rel.fk_column.as_str();
1119        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1120        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1121        let assign = if rel.nullable {
1122            quote! {
1123                __self.#field_ident = ::core::option::Option::Some(
1124                    #root::sql::ForeignKey::loaded(_pk, _parent),
1125                );
1126            }
1127        } else {
1128            quote! {
1129                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1130            }
1131        };
1132        // `self` IS hygiene-tracked through macro_rules — emitted from
1133        // a different context than the `&mut self` parameter inside
1134        // the macro_rules-expanded fn. Pass it through as `__self`
1135        // and let the macro_rules rebind it to the receiver.
1136        quote! {
1137            #fk_col => {
1138                let mut _parent: #parent_ty =
1139                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1140                // Audit #451 — multi-hop: stitch the deeper relation onto
1141                // the parent at the accumulated alias (see PG twin).
1142                if let ::core::option::Option::Some(__r) = __rest {
1143                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1144                        &mut _parent, row, __r, &__next_alias,
1145                    )?;
1146                }
1147                // See note in `load_related_impl_tokens` (PG twin) —
1148                // the same loud-in-debug invariant guard.
1149                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1150                    #root::core::SqlValue::#variant_ident(v) => v,
1151                    _other => {
1152                        ::core::debug_assert!(
1153                            false,
1154                            "rustango macro bug: load_related on FK `{}` expected \
1155                             SqlValue::{} from parent's __rustango_pk_value but got \
1156                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1157                            #fk_col,
1158                            ::core::stringify!(#variant_ident),
1159                            _other,
1160                        );
1161                        #default_expr
1162                    }
1163                };
1164                #assign
1165                ::core::result::Result::Ok(true)
1166            }
1167        }
1168    });
1169    quote! {
1170        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1171            #( #arms )*
1172        });
1173    }
1174}
1175
1176/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1177/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1178/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1179fn load_related_impl_sqlite_tokens(
1180    struct_name: &syn::Ident,
1181    fk_relations: &[FkRelation],
1182) -> TokenStream2 {
1183    let root = rustango_root();
1184    let arms = fk_relations.iter().map(|rel| {
1185        let parent_ty = &rel.parent_type;
1186        let fk_col = rel.fk_column.as_str();
1187        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1188        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1189        let assign = if rel.nullable {
1190            quote! {
1191                __self.#field_ident = ::core::option::Option::Some(
1192                    #root::sql::ForeignKey::loaded(_pk, _parent),
1193                );
1194            }
1195        } else {
1196            quote! {
1197                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1198            }
1199        };
1200        quote! {
1201            #fk_col => {
1202                let mut _parent: #parent_ty =
1203                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1204                // Audit #451 — multi-hop: stitch the deeper relation onto
1205                // the parent at the accumulated alias (see PG twin).
1206                if let ::core::option::Option::Some(__r) = __rest {
1207                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1208                        &mut _parent, row, __r, &__next_alias,
1209                    )?;
1210                }
1211                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1212                    #root::core::SqlValue::#variant_ident(v) => v,
1213                    _other => {
1214                        ::core::debug_assert!(
1215                            false,
1216                            "rustango macro bug: load_related on FK `{}` expected \
1217                             SqlValue::{} from parent's __rustango_pk_value but got \
1218                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1219                            #fk_col,
1220                            ::core::stringify!(#variant_ident),
1221                            _other,
1222                        );
1223                        #default_expr
1224                    }
1225                };
1226                #assign
1227                ::core::result::Result::Ok(true)
1228            }
1229        }
1230    });
1231    quote! {
1232        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1233            #( #arms )*
1234        });
1235    }
1236}
1237
1238/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1239/// matches `field_name` against the model's FK fields and returns
1240/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1241/// group children by parent PK.
1242///
1243/// Always emitted (with `_ => None` for FK-less models) so the
1244/// trait bound on `fetch_with_prefetch` is universally satisfied.
1245fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1246    let root = rustango_root();
1247    let arms = fk_relations.iter().map(|rel| {
1248        let fk_col = rel.fk_column.as_str();
1249        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1250        if rel.pk_kind == DetectedKind::I64 {
1251            // i64 FK — return the stored PK so prefetch_related can
1252            // group children by it. Nullable variant unwraps via
1253            // `as_ref().map(...)`: an unset (NULL) FK column yields
1254            // `None` and that child sits out of the grouping (correct
1255            // semantics — it has no parent to attach to).
1256            if rel.nullable {
1257                quote! {
1258                    #fk_col => self.#field_ident
1259                        .as_ref()
1260                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1261                }
1262            } else {
1263                quote! {
1264                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1265                }
1266            }
1267        } else {
1268            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1269            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1270            // i64-keyed grouping path — the trait signature is
1271            // `Option<i64>` and a non-i64 PK can't lower into it.
1272            // The FK still works for everything else (CRUD, lazy
1273            // load via `.get()`, select_related JOINs); only the
1274            // bulk prefetch grouper needs the integer key.
1275            quote! {
1276                #fk_col => ::core::option::Option::None,
1277            }
1278        }
1279    });
1280    // PK-type-agnostic version: every FK arm emits an
1281    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1282    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1283    // opt OUT of the legacy i64 method (it returns None) but opt IN
1284    // here.
1285    let value_arms = fk_relations.iter().map(|rel| {
1286        let fk_col = rel.fk_column.as_str();
1287        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1288        if rel.nullable {
1289            quote! {
1290                #fk_col => self.#field_ident
1291                    .as_ref()
1292                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1293                        #root::sql::ForeignKey::pk(fk)
1294                    )),
1295            }
1296        } else {
1297            quote! {
1298                #fk_col => ::core::option::Option::Some(
1299                    ::core::convert::Into::<#root::core::SqlValue>::into(
1300                        self.#field_ident.pk()
1301                    )
1302                ),
1303            }
1304        }
1305    });
1306    quote! {
1307        impl #root::sql::FkPkAccess for #struct_name {
1308            #[allow(unused_variables)]
1309            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1310                match field_name {
1311                    #( #arms )*
1312                    _ => ::core::option::Option::None,
1313                }
1314            }
1315            #[allow(unused_variables)]
1316            fn __rustango_fk_pk_value(
1317                &self,
1318                field_name: &str,
1319            ) -> ::core::option::Option<#root::core::SqlValue> {
1320                match field_name {
1321                    #( #value_arms )*
1322                    _ => ::core::option::Option::None,
1323                }
1324            }
1325        }
1326    }
1327}
1328
1329/// For every `ForeignKey<Parent>` field on `Child`, emit
1330/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1331/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1332/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1333/// — the canonical reverse-FK fetch. One round trip, no N+1.
1334///
1335/// **PG-only emission**: the accessor is bounded on
1336/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1337/// both of which are gated behind the `postgres` cargo feature. The
1338/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1339/// model derive itself compiles on tri-dialect / sqlite-only
1340/// downstream builds — the accessor just isn't materialised. A tri-
1341/// dialect `_set_pool` variant is a separate follow-up.
1342fn reverse_helper_tokens(
1343    child_ident: &syn::Ident,
1344    fk_relations: &[FkRelation],
1345    default_related_name: Option<&str>,
1346) -> TokenStream2 {
1347    let root = rustango_root();
1348    if fk_relations.is_empty() {
1349        return TokenStream2::new();
1350    }
1351    // Method-name resolution per FK (issue #816 + follow-up):
1352    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1353    //      itself — wins over everything else.
1354    //   2. Container-level `default_related_name = "..."` on the
1355    //      child. Applies to every FK on this model that didn't
1356    //      override.
1357    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1358    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1359    //      edge cases.
1360    //
1361    // The PG-on-executor variant keeps the resolved name; the
1362    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1363    // framework's convention for the `&Pool` flavor of every helper).
1364    let default_pg_suffix = default_related_name
1365        .map(str::to_owned)
1366        .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1367    let impls = fk_relations.iter().map(|rel| {
1368        let pg_suffix = rel
1369            .related_name
1370            .clone()
1371            .unwrap_or_else(|| default_pg_suffix.clone());
1372        let pool_suffix = format!("{}_pool", pg_suffix);
1373        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1374        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1375        let parent_ty = &rel.parent_type;
1376        let fk_col = rel.fk_column.as_str();
1377        let doc = format!(
1378            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1379             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1380             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1381             further `{child_ident}::objects()` filters via direct queryset use."
1382        );
1383        let pool_doc = format!(
1384            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1385             [`#root::sql::Pool`] and dispatches per backend so the \
1386             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1387             Use this from framework code that holds a `&Pool` (admin, \
1388             tenancy resolver, viewset handlers); reach for the executor- \
1389             bound variant when you already have a typed `sqlx::Executor`."
1390        );
1391        quote! {
1392            #[cfg(feature = "postgres")]
1393            impl #parent_ty {
1394                #[doc = #doc]
1395                ///
1396                /// # Errors
1397                /// Returns [`#root::sql::ExecError`] for SQL-writing
1398                /// or driver failures.
1399                pub async fn #pg_method_ident<'_c, _E>(
1400                    &self,
1401                    _executor: _E,
1402                ) -> ::core::result::Result<
1403                    ::std::vec::Vec<#child_ident>,
1404                    #root::sql::ExecError,
1405                >
1406                where
1407                    _E: #root::sql::sqlx::Executor<
1408                        '_c,
1409                        Database = #root::sql::sqlx::Postgres,
1410                    >,
1411                {
1412                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1413                    #root::query::QuerySet::<#child_ident>::new()
1414                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1415                        .fetch_on(_executor)
1416                        .await
1417                }
1418            }
1419
1420            impl #parent_ty {
1421                #[doc = #pool_doc]
1422                ///
1423                /// # Errors
1424                /// Returns [`#root::sql::ExecError`] for SQL-writing
1425                /// or driver failures.
1426                pub async fn #pool_method_ident(
1427                    &self,
1428                    pool: &#root::sql::Pool,
1429                ) -> ::core::result::Result<
1430                    ::std::vec::Vec<#child_ident>,
1431                    #root::sql::ExecError,
1432                > {
1433                    use #root::sql::FetcherPool as _;
1434                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1435                    #root::query::QuerySet::<#child_ident>::new()
1436                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1437                        .fetch(pool)
1438                        .await
1439                }
1440            }
1441        }
1442    });
1443    quote! { #( #impls )* }
1444}
1445
1446/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1447/// relation declared on the model.
1448/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1449/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1450///
1451/// Closes #239 + #240 — `comment.content_object` /
1452/// `comment.content_object = post` ergonomics on top of the existing
1453/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1454///
1455/// `column_entries` is passed so we can resolve each `ct_column` /
1456/// `pk_column` SQL name back to its Rust field ident — the macro
1457/// only sees the column-side strings in the attribute, but the
1458/// emitted accessor needs to read the actual struct field.
1459fn generic_fk_accessor_tokens(
1460    struct_name: &syn::Ident,
1461    generic_fks: &[GenericFkAttr],
1462    column_entries: &[ColumnEntry],
1463) -> TokenStream2 {
1464    let root = rustango_root();
1465    if generic_fks.is_empty() {
1466        return TokenStream2::new();
1467    }
1468    let methods = generic_fks.iter().filter_map(|gfk| {
1469        // Resolve `ct_column` + `pk_column` to the struct's Rust
1470        // field idents. A typo (column name doesn't match any field)
1471        // emits no method for that registration — the user will see
1472        // the compiler reject the SCHEMA literal anyway, so there's
1473        // a clear error path without us double-reporting.
1474        let ct_ident = column_entries
1475            .iter()
1476            .find(|c| c.column == gfk.ct_column)
1477            .map(|c| c.ident.clone())?;
1478        let pk_ident = column_entries
1479            .iter()
1480            .find(|c| c.column == gfk.pk_column)
1481            .map(|c| c.ident.clone())?;
1482
1483        let accessor_ident =
1484            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1485        let setter_ident =
1486            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1487        let name_literal = gfk.name.as_str();
1488
1489        Some(quote! {
1490            #[doc = concat!(
1491                "Resolve the polymorphic `",
1492                #name_literal,
1493                "` relation. Reads `self.",
1494                stringify!(#ct_ident),
1495                "` + `self.",
1496                stringify!(#pk_ident),
1497                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1498                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1499                #name_literal,
1500                "\", ...))]`."
1501            )]
1502            pub async fn #accessor_ident(
1503                &self,
1504                pool: &#root::sql::Pool,
1505            ) -> ::core::result::Result<
1506                ::core::option::Option<#root::__serde_json::Value>,
1507                #root::sql::ExecError,
1508            > {
1509                let gfk = #root::contenttypes::GenericForeignKey::new(
1510                    self.#ct_ident as i64,
1511                    self.#pk_ident as i64,
1512                );
1513                gfk.get_object(pool).await
1514            }
1515
1516            #[doc = concat!(
1517                "Set the polymorphic `",
1518                #name_literal,
1519                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1520                stringify!(#ct_ident),
1521                "` and `self.",
1522                stringify!(#pk_ident),
1523                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1524                #name_literal,
1525                "\", ...))]`."
1526            )]
1527            pub async fn #setter_ident<T: #root::core::Model>(
1528                &mut self,
1529                pool: &#root::sql::Pool,
1530                target_pk: i64,
1531            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1532                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1533                    pool,
1534                    target_pk,
1535                ).await?;
1536                self.#ct_ident = gfk.content_type_id as _;
1537                self.#pk_ident = gfk.object_pk as _;
1538                ::core::result::Result::Ok(())
1539            }
1540        })
1541    });
1542    quote! {
1543        impl #struct_name {
1544            #( #methods )*
1545        }
1546    }
1547}
1548
1549fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1550    let root = rustango_root();
1551    if m2m_relations.is_empty() {
1552        return TokenStream2::new();
1553    }
1554    let methods = m2m_relations.iter().map(|rel| {
1555        let method_name = format!("{}_m2m", rel.name);
1556        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1557        let through = rel.through.as_str();
1558        let src_col = rel.src.as_str();
1559        let dst_col = rel.dst.as_str();
1560        quote! {
1561            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1562                #root::sql::M2MManager {
1563                    src_pk: self.__rustango_pk_value(),
1564                    through: #through,
1565                    src_col: #src_col,
1566                    dst_col: #dst_col,
1567                }
1568            }
1569        }
1570    });
1571    quote! {
1572        impl #struct_name {
1573            #( #methods )*
1574        }
1575    }
1576}
1577
1578/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1579/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1580fn generic_m2m_accessor_tokens(
1581    struct_name: &syn::Ident,
1582    relations: &[GenericM2MAttr],
1583) -> TokenStream2 {
1584    let root = rustango_root();
1585    if relations.is_empty() {
1586        return TokenStream2::new();
1587    }
1588    let methods = relations.iter().map(|rel| {
1589        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1590        let through = rel.through.as_str();
1591        let pk_col = rel.pk_column.as_str();
1592        let ct_col = rel.ct_column.as_str();
1593        let related_col = rel.related_column.as_str();
1594        quote! {
1595            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1596                #root::sql::GenericM2MManager {
1597                    src_pk: self.__rustango_pk_value(),
1598                    src_schema: <Self as #root::core::Model>::SCHEMA,
1599                    through: #through,
1600                    pk_col: #pk_col,
1601                    ct_col: #ct_col,
1602                    dst_col: #related_col,
1603                }
1604            }
1605        }
1606    });
1607    quote! {
1608        impl #struct_name {
1609            #( #methods )*
1610        }
1611    }
1612}
1613
1614/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1615/// associated functions for each `#[rustango(reverse_has(...))]`
1616/// attribute. Issue #830.
1617///
1618/// The two emitted functions return ready-to-use `WhereExpr` nodes
1619/// that downstream callers drop into
1620/// `QuerySet::<Self>::where_raw(...)`:
1621///
1622/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1623///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1624///   `whereHas` parity (without the closure-style sub-predicate
1625///   refinement — that's a follow-up; users can layer additional
1626///   predicates by constructing the SelectQuery themselves and
1627///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1628/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1629///   `whereDoesntHave` parity.
1630///
1631/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1632/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1633/// machinery threads the outer-table reference through automatically
1634/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1635fn reverse_has_accessor_tokens(
1636    struct_name: &syn::Ident,
1637    reverse_has_relations: &[ReverseHasAttr],
1638) -> TokenStream2 {
1639    let root = rustango_root();
1640    if reverse_has_relations.is_empty() {
1641        return TokenStream2::new();
1642    }
1643    let methods = reverse_has_relations.iter().map(|rel| {
1644        let exists_name = format!("{}_exists_expr", rel.name);
1645        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1646        let count_name = format!("{}_count", rel.name);
1647        let fetch_name = format!("{}_fetch", rel.name);
1648        let first_name = format!("{}_first", rel.name);
1649        let pluck_name = format!("{}_pluck", rel.name);
1650        let accessor_name = rel.name.as_str();
1651        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1652        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1653        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1654        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1655        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1656        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1657        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1658        let child = &rel.child;
1659        let child_fk_column = rel.child_fk_column.as_str();
1660        let self_pk_column = rel.self_pk_column.as_str();
1661        let exists_doc = format!(
1662            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1663             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1664             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1665             filter to {struct_name}s with at least one matching child.",
1666        );
1667        let not_exists_doc = format!(
1668            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1669             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1670             <outer>.{self_pk_column})`. Drop into \
1671             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1672             {struct_name}s with **no** matching child.",
1673        );
1674        let count_doc = format!(
1675            "Eloquent `$model->{name}->count()` analog — returns \
1676             the number of `{child}` rows whose `{child_fk_column}` \
1677             matches this `{struct_name}` instance's primary key. \
1678             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1679             {child_fk_column} = <self.pk>`.",
1680            name = rel.name,
1681        );
1682        let accessor_doc = format!(
1683            "Eloquent `$model->{name}` accessor — returns a \
1684             `QuerySet<{child}>` filtered to rows whose \
1685             `{child_fk_column}` matches this `{struct_name}` \
1686             instance's primary key. **Chainable**: compose `.filter()` \
1687             / `.order_by()` / `.limit()` etc. on top, then call \
1688             `.fetch(&pool)` (the QuerySet trait method) when \
1689             done. For the simple \"fetch all\" hot path with no \
1690             further composition, prefer the bare-name \
1691             `{name}_fetch(&pool)` companion.",
1692            name = rel.name,
1693        );
1694        let fetch_doc = format!(
1695            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1696             over `{name}(&self).fetch(&pool)`. Use this when \
1697             you don't need further `.filter()` / `.order_by()` \
1698             composition; falls back to the chainable accessor when \
1699             you do. Avoids the `_pool` suffix on the most common \
1700             call-site shape.",
1701            name = rel.name,
1702        );
1703        quote! {
1704            #[doc = #accessor_doc]
1705            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1706                #root::query::QuerySet::<#child>::new()
1707                    .filter(#child_fk_column, self.__rustango_pk_value())
1708            }
1709
1710            #[doc = #fetch_doc]
1711            pub async fn #fetch_ident(
1712                &self,
1713                pool: &#root::sql::Pool,
1714            ) -> ::core::result::Result<
1715                ::std::vec::Vec<#child>,
1716                #root::sql::ExecError,
1717            > {
1718                use #root::sql::FetcherPool as _;
1719                self.#accessor_ident().fetch(pool).await
1720            }
1721
1722            /// Eloquent `$model->relation->first()` / `hasOne`
1723            /// semantics — bare-name shortcut over
1724            /// `self.<name>().first(&pool)`. Returns `None` when no
1725            /// child rows match. Useful when the relation is
1726            /// nominally many-to-one in shape but at most one row is
1727            /// expected (latest comment, primary tag, etc.).
1728            pub async fn #first_ident(
1729                &self,
1730                pool: &#root::sql::Pool,
1731            ) -> ::core::result::Result<
1732                ::core::option::Option<#child>,
1733                #root::sql::ExecError,
1734            > {
1735                self.#accessor_ident().first(pool).await
1736            }
1737
1738            /// Eloquent `$model->relation->pluck($col)` — project a
1739            /// single column from the child rows into a `Vec<U>`.
1740            /// Skips the typed `Child` decode, which is cheaper when
1741            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1742            pub async fn #pluck_ident<U>(
1743                &self,
1744                col: &'static str,
1745                pool: &#root::sql::Pool,
1746            ) -> ::core::result::Result<
1747                ::std::vec::Vec<U>,
1748                #root::sql::ExecError,
1749            >
1750            where
1751                U: #root::sql::MaybePgScalar
1752                    + #root::sql::MaybeMyScalar
1753                    + #root::sql::MaybeSqliteScalar
1754                    + ::core::marker::Send
1755                    + ::core::marker::Unpin,
1756            {
1757                self.#accessor_ident()
1758                    .values_list_flat(col)
1759                    .fetch::<U>(pool)
1760                    .await
1761            }
1762
1763            #[doc = #exists_doc]
1764            pub fn #exists_ident() -> #root::core::WhereExpr {
1765                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1766                let child_schema =
1767                    <#child as #root::core::Model>::SCHEMA;
1768                let inner = SelectQuery {
1769                    where_clause: WhereExpr::ExprCompare {
1770                        lhs: Expr::Column(#child_fk_column),
1771                        op: Op::Eq,
1772                        rhs: Expr::OuterRef(#self_pk_column),
1773                    },
1774                    ..SelectQuery::new(child_schema)
1775                };
1776                WhereExpr::Exists(::std::boxed::Box::new(inner))
1777            }
1778
1779            #[doc = #not_exists_doc]
1780            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1781                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1782                let child_schema =
1783                    <#child as #root::core::Model>::SCHEMA;
1784                let inner = SelectQuery {
1785                    where_clause: WhereExpr::ExprCompare {
1786                        lhs: Expr::Column(#child_fk_column),
1787                        op: Op::Eq,
1788                        rhs: Expr::OuterRef(#self_pk_column),
1789                    },
1790                    ..SelectQuery::new(child_schema)
1791                };
1792                WhereExpr::NotExists(::std::boxed::Box::new(inner))
1793            }
1794
1795            #[doc = #count_doc]
1796            pub async fn #count_ident(
1797                &self,
1798                pool: &#root::sql::Pool,
1799            ) -> ::core::result::Result<
1800                i64,
1801                #root::sql::ExecError,
1802            > {
1803                use #root::sql::CounterPool as _;
1804                #root::query::QuerySet::<#child>::new()
1805                    .filter(#child_fk_column, self.__rustango_pk_value())
1806                    .count(pool)
1807                    .await
1808            }
1809        }
1810    });
1811    quote! {
1812        impl #struct_name {
1813            #( #methods )*
1814        }
1815    }
1816}
1817
1818/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1819/// `#[rustango(through(...))]` attribute. Issue #817.
1820///
1821/// Each accessor builds a correlated subquery via
1822/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1823/// intermediate table, filters on the FK column pointing at this
1824/// model, and projects the intermediate PK. The outer queryset
1825/// filters the far model by its FK-to-intermediate column being in
1826/// that set.
1827///
1828/// The returned `QuerySet<Far>` is **chainable** — the subquery
1829/// lives inside a `where_raw` clause so the user's later
1830/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1831/// disturb it.
1832///
1833/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1834/// SQLite — no LATERAL or backend-specific syntax involved.
1835fn through_accessor_tokens(
1836    struct_name: &syn::Ident,
1837    through_relations: &[ThroughAttr],
1838) -> TokenStream2 {
1839    let root = rustango_root();
1840    if through_relations.is_empty() {
1841        return TokenStream2::new();
1842    }
1843    let methods = through_relations.iter().map(|rel| {
1844        let method_name = format!("{}_through", rel.name);
1845        let count_name = format!("{}_through_count", rel.name);
1846        let fetch_name = format!("{}_through_fetch", rel.name);
1847        let first_name = format!("{}_through_first", rel.name);
1848        let pluck_name = format!("{}_through_pluck", rel.name);
1849        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1850        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1851        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1852        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1853        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1854        let far = &rel.far;
1855        let intermediate = &rel.intermediate;
1856        let far_fk_column = rel.far_fk_column.as_str();
1857        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1858        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1859        let doc = format!(
1860            "Eloquent `hasManyThrough` accessor — returns a \
1861             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1862             instance through the intermediate `{intermediate}` table. \
1863             Generated SQL shape: \
1864             `… WHERE {far_fk_column} IN (SELECT \
1865             {intermediate_pk_column} FROM <{intermediate}> WHERE \
1866             {intermediate_fk_column} = self.pk)`. Chainable like any \
1867             other QuerySet — compose `.filter()` / `.order_by()` / \
1868             `.limit()` etc. on top.",
1869        );
1870        let count_doc = format!(
1871            "Eloquent `$model->{name}->count()` analog for the \
1872             through-relation — returns the number of `{far}` rows \
1873             reachable through `{intermediate}`. Equivalent to \
1874             `self.{name}_through().count(pool)` but spelled \
1875             as a bare instance method for parity with the \
1876             `reverse_has` `<name>_count` shape.",
1877            name = rel.name,
1878        );
1879        quote! {
1880            #[doc = #doc]
1881            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1882                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1883                let intermediate_schema =
1884                    <#intermediate as #root::core::Model>::SCHEMA;
1885                let sub = SelectQuery {
1886                    where_clause: WhereExpr::Predicate(Filter {
1887                        column: #intermediate_fk_column,
1888                        op: Op::Eq,
1889                        value: self.__rustango_pk_value(),
1890                    }),
1891                    projection: ::core::option::Option::Some(
1892                        ::std::vec![#intermediate_pk_column],
1893                    ),
1894                    ..SelectQuery::new(intermediate_schema)
1895                };
1896                #root::query::QuerySet::<#far>::new().where_raw(
1897                    WhereExpr::InSubquery {
1898                        column: #far_fk_column,
1899                        negated: false,
1900                        subquery: ::std::boxed::Box::new(sub),
1901                    },
1902                )
1903            }
1904
1905            #[doc = #count_doc]
1906            pub async fn #count_ident(
1907                &self,
1908                pool: &#root::sql::Pool,
1909            ) -> ::core::result::Result<
1910                i64,
1911                #root::sql::ExecError,
1912            > {
1913                use #root::sql::CounterPool as _;
1914                self.#method_ident().count(pool).await
1915            }
1916
1917            /// Eloquent `$model->relation->get()` for the
1918            /// through-relation — bare-name hot-path over
1919            /// `self.<name>_through().fetch(&pool)`. Use this
1920            /// when you don't need further `.filter()` /
1921            /// `.order_by()` composition; falls back to the
1922            /// chainable accessor when you do. Avoids the `_pool`
1923            /// suffix on the most common call-site shape.
1924            pub async fn #fetch_ident(
1925                &self,
1926                pool: &#root::sql::Pool,
1927            ) -> ::core::result::Result<
1928                ::std::vec::Vec<#far>,
1929                #root::sql::ExecError,
1930            > {
1931                use #root::sql::FetcherPool as _;
1932                self.#method_ident().fetch(pool).await
1933            }
1934
1935            /// Eloquent `hasOneThrough` analog — bare-name shortcut
1936            /// over `self.<name>_through().first(&pool)`. Returns
1937            /// `None` when no far rows are reachable through the
1938            /// intermediate. Useful when at most one row is
1939            /// expected (latest comment by country, primary tag,
1940            /// etc.).
1941            pub async fn #first_ident(
1942                &self,
1943                pool: &#root::sql::Pool,
1944            ) -> ::core::result::Result<
1945                ::core::option::Option<#far>,
1946                #root::sql::ExecError,
1947            > {
1948                self.#method_ident().first(pool).await
1949            }
1950
1951            /// Pluck a single column from the far rows into `Vec<U>`
1952            /// — cheaper than the typed `<Far>` decode when only one
1953            /// scalar column is needed.
1954            pub async fn #pluck_ident<U>(
1955                &self,
1956                col: &'static str,
1957                pool: &#root::sql::Pool,
1958            ) -> ::core::result::Result<
1959                ::std::vec::Vec<U>,
1960                #root::sql::ExecError,
1961            >
1962            where
1963                U: #root::sql::MaybePgScalar
1964                    + #root::sql::MaybeMyScalar
1965                    + #root::sql::MaybeSqliteScalar
1966                    + ::core::marker::Send
1967                    + ::core::marker::Unpin,
1968            {
1969                self.#method_ident()
1970                    .values_list_flat(col)
1971                    .fetch::<U>(pool)
1972                    .await
1973            }
1974        }
1975    });
1976    quote! {
1977        impl #struct_name {
1978            #( #methods )*
1979        }
1980    }
1981}
1982
1983struct ColumnEntry {
1984    /// The struct field ident, used both for the inherent const name on
1985    /// the model and for the inner column type's name.
1986    ident: syn::Ident,
1987    /// The struct's field type, used as `Column::Value`.
1988    value_ty: Type,
1989    /// Rust-side field name (e.g. `"id"`).
1990    name: String,
1991    /// SQL-side column name (e.g. `"user_id"`).
1992    column: String,
1993    /// `#root::core::FieldType::I64` etc.
1994    field_type_tokens: TokenStream2,
1995}
1996
1997struct CollectedFields {
1998    field_schemas: Vec<TokenStream2>,
1999    from_row_inits: Vec<TokenStream2>,
2000    /// Aliased counterparts of `from_row_inits` — read columns via
2001    /// `format!("{prefix}__{col}")` aliases so a Model can be
2002    /// decoded from a JOINed row's projected target columns.
2003    from_aliased_row_inits: Vec<TokenStream2>,
2004    /// Static column-name list — used by the simple insert path
2005    /// (no `Auto<T>` fields). Aligned with `insert_values`.
2006    insert_columns: Vec<TokenStream2>,
2007    /// Static `Into<SqlValue>` expressions, one per field. Aligned
2008    /// with `insert_columns`. Used by the simple insert path only.
2009    insert_values: Vec<TokenStream2>,
2010    /// Per-field push expressions for the dynamic (Auto-aware)
2011    /// insert path. Each statement either unconditionally pushes
2012    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2013    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2014    insert_pushes: Vec<TokenStream2>,
2015    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2016    /// when `has_auto == false`.
2017    returning_cols: Vec<TokenStream2>,
2018    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2019    /// field. Run after `insert_returning` to populate the model.
2020    auto_assigns: Vec<TokenStream2>,
2021    /// `(ident, column_literal)` pairs for every Auto field. Used by
2022    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2023    /// instead of `self`.
2024    auto_field_idents: Vec<(syn::Ident, String)>,
2025    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2026    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2027    /// back into the struct after insert (MySQL has no RETURNING → the
2028    /// field stays at its placeholder until the next read).
2029    generated_field_idents: Vec<(syn::Ident, String)>,
2030    /// Inner `T` of the first `Auto<T>` field, for the MySQL
2031    /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
2032    first_auto_value_ty: Option<Type>,
2033    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2034    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2035    /// `columns`).
2036    bulk_pushes_no_auto: Vec<TokenStream2>,
2037    /// Bulk-insert per-row pushes for **all fields including Auto**.
2038    /// Used by the all-Auto-Set bulk path (Auto col included with the
2039    /// caller-supplied value).
2040    bulk_pushes_all: Vec<TokenStream2>,
2041    /// Column-name literals for non-Auto fields only (paired with
2042    /// `bulk_pushes_no_auto`).
2043    bulk_columns_no_auto: Vec<TokenStream2>,
2044    /// Column-name literals for every field including Auto (paired
2045    /// with `bulk_pushes_all`).
2046    bulk_columns_all: Vec<TokenStream2>,
2047    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2048    /// + the loop that asserts every row matches. One pair per Auto
2049    /// field. Empty when `has_auto == false`.
2050    bulk_auto_uniformity: Vec<TokenStream2>,
2051    /// Identifier of the first Auto field, used as the witness for
2052    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2053    first_auto_ident: Option<syn::Ident>,
2054    /// `true` if any field on the struct is `Auto<T>`.
2055    has_auto: bool,
2056    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2057    /// Gates `save()` codegen — only Auto PKs let us infer
2058    /// insert-vs-update from the in-memory value.
2059    pk_is_auto: bool,
2060    /// `Assignment` constructors for every non-PK column. Drives the
2061    /// UPDATE branch of `save()`.
2062    update_assignments: Vec<TokenStream2>,
2063    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2064    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2065    upsert_update_columns: Vec<TokenStream2>,
2066    primary_key: Option<(syn::Ident, String)>,
2067    column_entries: Vec<ColumnEntry>,
2068    /// Rust-side field names, in declaration order. Used to validate
2069    /// container attributes like `display = "…"`.
2070    field_names: Vec<String>,
2071    /// FK fields on this child model. Drives the reverse-relation
2072    /// helper emit — for each FK, the macro adds an inherent
2073    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2074    /// method on the parent type.
2075    fk_relations: Vec<FkRelation>,
2076    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2077    /// the model has one. Drives emission of the `soft_delete_on` /
2078    /// `restore_on` inherent methods. At most one such column per
2079    /// model is allowed; collect_fields rejects duplicates.
2080    soft_delete_column: Option<String>,
2081    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2082    /// companion to `soft_delete_column` for emitting predicates
2083    /// that need to read the field off `&self` (e.g. `trashed()`).
2084    soft_delete_field_ident: Option<syn::Ident>,
2085}
2086
2087#[derive(Clone)]
2088struct FkRelation {
2089    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2090    /// helper is emitted as `impl <ParentType> { … }`.
2091    parent_type: Type,
2092    /// SQL column name on the child table for this FK (e.g. `"author"`).
2093    /// Used in the generated `WHERE <fk_column> = $1` clause.
2094    fk_column: String,
2095    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2096    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2097    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2098    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2099    pk_kind: DetectedKind,
2100    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2101    /// FK column). Drives the `Some(...)` wrapping in load_related
2102    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2103    /// the codegen matches the field's declared shape.
2104    nullable: bool,
2105    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2106    /// override. When set, the reverse helper picks this name instead
2107    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2108    /// #816 (issue's "Related" note re: per-FK override).
2109    related_name: Option<String>,
2110}
2111
2112fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2113    let root = rustango_root();
2114    let cap = named.named.len();
2115    let mut out = CollectedFields {
2116        field_schemas: Vec::with_capacity(cap),
2117        from_row_inits: Vec::with_capacity(cap),
2118        from_aliased_row_inits: Vec::with_capacity(cap),
2119        insert_columns: Vec::with_capacity(cap),
2120        insert_values: Vec::with_capacity(cap),
2121        insert_pushes: Vec::with_capacity(cap),
2122        returning_cols: Vec::new(),
2123        auto_assigns: Vec::new(),
2124        auto_field_idents: Vec::new(),
2125        generated_field_idents: Vec::new(),
2126        first_auto_value_ty: None,
2127        bulk_pushes_no_auto: Vec::with_capacity(cap),
2128        bulk_pushes_all: Vec::with_capacity(cap),
2129        bulk_columns_no_auto: Vec::with_capacity(cap),
2130        bulk_columns_all: Vec::with_capacity(cap),
2131        bulk_auto_uniformity: Vec::new(),
2132        first_auto_ident: None,
2133        has_auto: false,
2134        pk_is_auto: false,
2135        update_assignments: Vec::with_capacity(cap),
2136        upsert_update_columns: Vec::with_capacity(cap),
2137        primary_key: None,
2138        column_entries: Vec::with_capacity(cap),
2139        field_names: Vec::with_capacity(cap),
2140        fk_relations: Vec::new(),
2141        soft_delete_column: None,
2142        soft_delete_field_ident: None,
2143    };
2144
2145    for field in &named.named {
2146        let info = process_field(field, table)?;
2147        out.field_names.push(info.ident.to_string());
2148        out.field_schemas.push(info.schema);
2149        out.from_row_inits.push(info.from_row_init);
2150        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2151        if let Some(parent_ty) = info.fk_inner.clone() {
2152            out.fk_relations.push(FkRelation {
2153                parent_type: parent_ty,
2154                fk_column: info.column.clone(),
2155                pk_kind: info.fk_pk_kind,
2156                nullable: info.nullable,
2157                related_name: info.related_name.clone(),
2158            });
2159        }
2160        if info.soft_delete {
2161            if out.soft_delete_column.is_some() {
2162                return Err(syn::Error::new_spanned(
2163                    field,
2164                    "only one field may be marked `#[rustango(soft_delete)]`",
2165                ));
2166            }
2167            out.soft_delete_column = Some(info.column.clone());
2168            out.soft_delete_field_ident = Some(info.ident.clone());
2169        }
2170        let column = info.column.as_str();
2171        let ident = info.ident;
2172        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2173        // skip every write path — Postgres recomputes the value
2174        // from EXPR. Push only the column-entry record (so typed
2175        // column constants still exist for filtering / projection)
2176        // and the schema literal (already pushed above) and move
2177        // on. No insert_columns/values, no insert_pushes, no
2178        // bulk_*, no update_assignments, no upsert_update_columns,
2179        // no returning_cols.
2180        if info.generated_as.is_some() {
2181            out.column_entries.push(ColumnEntry {
2182                ident: ident.clone(),
2183                value_ty: info.value_ty.clone(),
2184                name: ident.to_string(),
2185                column: info.column.clone(),
2186                field_type_tokens: info.field_type_tokens,
2187            });
2188            // #1028 — refresh the DB-computed value after insert on
2189            // RETURNING-capable backends. The column joins `returning_cols`
2190            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2191            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2192            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2193            // it keeps the placeholder until the next read.
2194            out.returning_cols.push(quote!(#column));
2195            out.generated_field_idents
2196                .push((ident.clone(), info.column.clone()));
2197            continue;
2198        }
2199        out.insert_columns.push(quote!(#column));
2200        out.insert_values.push(quote! {
2201            ::core::convert::Into::<#root::core::SqlValue>::into(
2202                ::core::clone::Clone::clone(&self.#ident)
2203            )
2204        });
2205        if info.auto {
2206            out.has_auto = true;
2207            if out.first_auto_ident.is_none() {
2208                out.first_auto_ident = Some(ident.clone());
2209                out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2210            }
2211            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2212            // before binding, so the value is already in
2213            // `self.#ident` after the insert_push — RETURNING is
2214            // redundant. Skip adding this column to returning_cols /
2215            // auto_assigns / auto_field_idents to avoid (a) an
2216            // unnecessary RETURNING column on every dialect, and (b)
2217            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2218            // integer PK.
2219            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2220            // for the same reason: they are filled Rust-side below and
2221            // bound, so the value is already in `self` and RETURNING
2222            // would be a redundant column on every dialect.
2223            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2224                out.returning_cols.push(quote!(#column));
2225                out.auto_field_idents
2226                    .push((ident.clone(), info.column.clone()));
2227                out.auto_assigns.push(quote! {
2228                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2229                });
2230            }
2231            if info.default_uuid_v7 {
2232                // Rust-side UUIDv7 generation (issue #823, Eloquent
2233                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2234                // and bind; Auto::Set → bind the user's value. The
2235                // column is ALWAYS present in the INSERT statement —
2236                // no RETURNING / no DB DEFAULT needed.
2237                out.insert_pushes.push(quote! {
2238                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2239                        self.#ident = #root::sql::Auto::Set(
2240                            #root::__uuid::Uuid::now_v7(),
2241                        );
2242                    }
2243                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2244                        _columns.push(#column);
2245                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2246                            ::core::clone::Clone::clone(_v)
2247                        ));
2248                    }
2249                });
2250            } else if info.auto_now_add || info.auto_now {
2251                // Rust-side write timestamp (#1464), mirroring
2252                // `default_uuid_v7` above. `auto_now` has always bound
2253                // `Utc::now()` on UPDATE but not on INSERT, so the
2254                // first write of an `updated_at` took the DB default
2255                // while every later one took the clock — the same
2256                // column, filled two different ways.
2257                //
2258                // The default is the problem. `CREATE TABLE IF NOT
2259                // EXISTS` leaves an upgraded table's
2260                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2261                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2262                // statement that changes it — so every insert relying
2263                // on that default wrote the legacy shape forever, while
2264                // the migrate sweep converted the rows around it. The
2265                // column went permanently mixed and `ORDER BY` inverted
2266                // the admin audit log (#1616 rework review,
2267                // correctness-001 / dialects-005 / security-002).
2268                //
2269                // Binding it here makes the stale default unreachable
2270                // rather than trying to migrate it: the timestamp comes
2271                // from the application, not the database.
2272                out.insert_pushes.push(quote! {
2273                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2274                        self.#ident = #root::sql::Auto::Set(
2275                            #root::__chrono::Utc::now(),
2276                        );
2277                    }
2278                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2279                        _columns.push(#column);
2280                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2281                            ::core::clone::Clone::clone(_v)
2282                        ));
2283                    }
2284                });
2285            } else {
2286                out.insert_pushes.push(quote! {
2287                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2288                        _columns.push(#column);
2289                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2290                            ::core::clone::Clone::clone(_v)
2291                        ));
2292                    }
2293                });
2294            }
2295            // Bulk: Auto fields appear only in the all-Set path,
2296            // never in the Unset path (we drop them from `columns`).
2297            out.bulk_columns_all.push(quote!(#column));
2298            out.bulk_pushes_all.push(quote! {
2299                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2300                    ::core::clone::Clone::clone(&_row.#ident)
2301                ));
2302            });
2303            // …except the timestamp columns, which must appear in BOTH
2304            // paths (#1464). The path is chosen by the *first* Auto
2305            // field — in practice the PK — so a bulk insert of rows
2306            // with an unset PK took the no-auto branch and dropped the
2307            // timestamp column, falling back to the DB default that
2308            // this change exists to stop depending on.
2309            //
2310            // `rows` is borrowed immutably here, so this fills per row
2311            // at push time rather than writing back into the struct. An
2312            // explicitly-Set value is still honoured; only `Unset`
2313            // takes the clock.
2314            if info.auto_now_add || info.auto_now {
2315                out.bulk_columns_no_auto.push(quote!(#column));
2316                out.bulk_pushes_no_auto.push(quote! {
2317                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2318                        match &_row.#ident {
2319                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2320                            #root::sql::Auto::Unset => #root::__chrono::Utc::now(),
2321                        }
2322                    ));
2323                });
2324            }
2325            // Uniformity check: every row's Auto state must match the
2326            // first row's. Mixed Set/Unset within one bulk_insert is
2327            // rejected here so the column list stays consistent.
2328            let ident_clone = ident.clone();
2329            out.bulk_auto_uniformity.push(quote! {
2330                for _r in rows.iter().skip(1) {
2331                    if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2332                        return ::core::result::Result::Err(
2333                            #root::sql::ExecError::Sql(
2334                                #root::sql::SqlError::BulkAutoMixed
2335                            )
2336                        );
2337                    }
2338                }
2339            });
2340        } else {
2341            out.insert_pushes.push(quote! {
2342                _columns.push(#column);
2343                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2344                    ::core::clone::Clone::clone(&self.#ident)
2345                ));
2346            });
2347            // Bulk: non-Auto fields appear in BOTH paths.
2348            out.bulk_columns_no_auto.push(quote!(#column));
2349            out.bulk_columns_all.push(quote!(#column));
2350            let push_expr = quote! {
2351                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2352                    ::core::clone::Clone::clone(&_row.#ident)
2353                ));
2354            };
2355            out.bulk_pushes_no_auto.push(push_expr.clone());
2356            out.bulk_pushes_all.push(push_expr);
2357        }
2358        if info.primary_key {
2359            if out.primary_key.is_some() {
2360                return Err(syn::Error::new_spanned(
2361                    field,
2362                    "only one field may be marked `#[rustango(primary_key)]`",
2363                ));
2364            }
2365            out.primary_key = Some((ident.clone(), info.column.clone()));
2366            if info.auto {
2367                out.pk_is_auto = true;
2368            }
2369        } else if info.auto_now_add {
2370            // Immutable post-insert: skip from UPDATE entirely.
2371        } else if info.auto_now {
2372            // `auto_now` columns: bind `chrono::Utc::now()` on every
2373            // UPDATE so the column is always overridden with the
2374            // wall-clock at write time, regardless of what value the
2375            // user left in the struct field.
2376            out.update_assignments.push(quote! {
2377                #root::core::Assignment {
2378                    column: #column,
2379                    value: ::core::convert::Into::<#root::core::Expr>::into(
2380                        ::core::convert::Into::<#root::core::SqlValue>::into(
2381                            #root::__chrono::Utc::now()
2382                        )
2383                    ),
2384                }
2385            });
2386            out.upsert_update_columns.push(quote!(#column));
2387        } else {
2388            out.update_assignments.push(quote! {
2389                #root::core::Assignment {
2390                    column: #column,
2391                    value: ::core::convert::Into::<#root::core::Expr>::into(
2392                        ::core::convert::Into::<#root::core::SqlValue>::into(
2393                            ::core::clone::Clone::clone(&self.#ident)
2394                        )
2395                    ),
2396                }
2397            });
2398            out.upsert_update_columns.push(quote!(#column));
2399        }
2400        out.column_entries.push(ColumnEntry {
2401            ident: ident.clone(),
2402            value_ty: info.value_ty.clone(),
2403            name: ident.to_string(),
2404            column: info.column.clone(),
2405            field_type_tokens: info.field_type_tokens,
2406        });
2407    }
2408    Ok(out)
2409}
2410
2411fn model_impl_tokens(
2412    struct_name: &syn::Ident,
2413    model_name: &str,
2414    table: &str,
2415    display: Option<&str>,
2416    app_label: Option<&str>,
2417    admin: Option<&AdminAttrs>,
2418    default_order: &[(String, bool, proc_macro2::Span)],
2419    field_schemas: &[TokenStream2],
2420    soft_delete_column: Option<&str>,
2421    permissions: bool,
2422    audit_track: Option<&[String]>,
2423    m2m_relations: &[M2MAttr],
2424    indexes: &[IndexAttr],
2425    checks: &[CheckAttr],
2426    excludes: &[ExcludeAttr],
2427    composite_fks: &[CompositeFkAttr],
2428    generic_fks: &[GenericFkAttr],
2429    scope: Option<&str>,
2430    is_view: bool,
2431    verbose_name: Option<&str>,
2432    verbose_name_plural: Option<&str>,
2433    managed: bool,
2434    base_manager_name: Option<&str>,
2435    order_with_respect_to: Option<&str>,
2436    proxy: bool,
2437    required_db_features: &[String],
2438    required_db_vendor: Option<&str>,
2439    default_related_name: Option<&str>,
2440    db_table_comment: Option<&str>,
2441    get_latest_by: Option<(&str, bool)>,
2442    extra_permissions: &[(String, String)],
2443    default_permissions: &[String],
2444    global_scopes: &[GlobalScopeAttr],
2445    reverse_has_relations: &[ReverseHasAttr],
2446    generic_has_relations: &[GenericHasAttr],
2447) -> TokenStream2 {
2448    let root = rustango_root();
2449    let display_tokens = if let Some(name) = display {
2450        quote!(::core::option::Option::Some(#name))
2451    } else {
2452        quote!(::core::option::Option::None)
2453    };
2454    let app_label_tokens = if let Some(name) = app_label {
2455        quote!(::core::option::Option::Some(#name))
2456    } else {
2457        quote!(::core::option::Option::None)
2458    };
2459    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2460        quote!(::core::option::Option::Some(#col))
2461    } else {
2462        quote!(::core::option::Option::None)
2463    };
2464    let audit_track_tokens = match audit_track {
2465        None => quote!(::core::option::Option::None),
2466        Some(names) => {
2467            let lits = names.iter().map(|n| n.as_str());
2468            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2469        }
2470    };
2471    let admin_tokens = admin_config_tokens(admin);
2472    // Default `tenant` so single-tenant projects (no `scope` attr
2473    // anywhere) keep the v0.24.x behavior. Container-attr parser
2474    // already validated the value is "registry" or "tenant".
2475    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2476        Some("registry") => quote!(#root::core::ModelScope::Registry),
2477        _ => quote!(#root::core::ModelScope::Tenant),
2478    };
2479    let verbose_name_tokens = optional_str(verbose_name);
2480    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2481    let base_manager_name_tokens = optional_str(base_manager_name);
2482    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2483    let required_db_features_lits: Vec<&str> =
2484        required_db_features.iter().map(String::as_str).collect();
2485    let required_db_vendor_tokens = optional_str(required_db_vendor);
2486    let default_related_name_tokens = optional_str(default_related_name);
2487    let db_table_comment_tokens = optional_str(db_table_comment);
2488    let get_latest_by_tokens = match get_latest_by {
2489        Some((col, desc)) => {
2490            quote!(::core::option::Option::Some((#col, #desc)))
2491        }
2492        None => quote!(::core::option::Option::None),
2493    };
2494    let extra_permission_tokens: Vec<_> = extra_permissions
2495        .iter()
2496        .map(|(c, l)| quote!((#c, #l)))
2497        .collect();
2498    let default_permission_tokens: Vec<_> = default_permissions
2499        .iter()
2500        .map(|action| quote!(#action))
2501        .collect();
2502    let indexes_tokens = indexes.iter().map(|idx| {
2503        // When no explicit `name = "..."` was given, derive a stable,
2504        // collision-free name from the table + columns
2505        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2506        // would clash the moment a model declares two unnamed indexes.
2507        // Capped at Postgres's 63-char identifier limit.
2508        let derived_name = idx.name.clone().unwrap_or_else(|| {
2509            let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2510            if n.len() > 63 {
2511                n.truncate(63);
2512            }
2513            n
2514        });
2515        let name = derived_name.as_str();
2516        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2517        let unique = idx.unique;
2518        // Map the parsed method string onto the IndexMethod enum
2519        // variant — kept at the codegen layer so the IR doesn't
2520        // carry the string form.
2521        let method_variant = match idx.method.as_str() {
2522            "gin" => quote!(#root::core::IndexMethod::Gin),
2523            "gist" => quote!(#root::core::IndexMethod::Gist),
2524            "brin" => quote!(#root::core::IndexMethod::Brin),
2525            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2526            "hash" => quote!(#root::core::IndexMethod::Hash),
2527            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2528            _ => quote!(#root::core::IndexMethod::BTree),
2529        };
2530        let where_clause = match &idx.where_clause {
2531            Some(s) => quote!(::core::option::Option::Some(#s)),
2532            None => quote!(::core::option::Option::None),
2533        };
2534        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2535        quote! {
2536            #root::core::IndexSchema {
2537                name: #name,
2538                columns: &[ #(#cols),* ],
2539                unique: #unique,
2540                method: #method_variant,
2541                where_clause: #where_clause,
2542                include: &[ #(#include_lits),* ],
2543            }
2544        }
2545    });
2546    let checks_tokens = checks.iter().map(|c| {
2547        let name = c.name.as_str();
2548        let expr = c.expr.as_str();
2549        quote! {
2550            #root::core::CheckConstraint {
2551                name: #name,
2552                expr: #expr,
2553            }
2554        }
2555    });
2556    let excludes_tokens = excludes.iter().map(|e| {
2557        let name = e.name.as_str();
2558        let using = e.using.as_str();
2559        let element_tokens = e.elements.iter().map(|(col, op)| {
2560            let col_s = col.as_str();
2561            let op_s = op.as_str();
2562            quote!((#col_s, #op_s))
2563        });
2564        let where_tokens = match e.where_clause.as_deref() {
2565            Some(w) => quote!(::core::option::Option::Some(#w)),
2566            None => quote!(::core::option::Option::None),
2567        };
2568        quote! {
2569            #root::core::ExclusionConstraint {
2570                name: #name,
2571                using: #using,
2572                elements: &[ #(#element_tokens),* ],
2573                where_clause: #where_tokens,
2574            }
2575        }
2576    });
2577    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2578        let name = rel.name.as_str();
2579        let to = rel.to.as_str();
2580        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2581        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2582        quote! {
2583            #root::core::CompositeFkRelation {
2584                name: #name,
2585                to: #to,
2586                from: &[ #(#from_cols),* ],
2587                on: &[ #(#on_cols),* ],
2588            }
2589        }
2590    });
2591    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2592        let name = rel.name.as_str();
2593        let ct_col = rel.ct_column.as_str();
2594        let pk_col = rel.pk_column.as_str();
2595        quote! {
2596            #root::core::GenericRelation {
2597                name: #name,
2598                ct_column: #ct_col,
2599                pk_column: #pk_col,
2600            }
2601        }
2602    });
2603    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2604    // no `#[rustango(default_order = "...")]` attribute was supplied.
2605    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2606        let col_lit = col.as_str();
2607        quote! { (#col_lit, #desc) }
2608    });
2609
2610    // Issue #820 — `global_scopes` slice literal. Empty when no
2611    // `#[rustango(global_scope(...))]` attribute was supplied. The
2612    // `apply` path is re-emitted verbatim so it resolves in the
2613    // consumer's scope; the name is stored as a string literal.
2614    let global_scope_tokens = global_scopes.iter().map(|s| {
2615        let name = s.name.as_str();
2616        let apply = &s.apply;
2617        quote! {
2618            #root::core::GlobalScope {
2619                name: #name,
2620                apply: #apply,
2621            }
2622        }
2623    });
2624
2625    let m2m_tokens = m2m_relations.iter().map(|rel| {
2626        let name = rel.name.as_str();
2627        let to = rel.to.as_str();
2628        let through = rel.through.as_str();
2629        let src = rel.src.as_str();
2630        let dst = rel.dst.as_str();
2631        let auto_create = rel.auto_create;
2632        quote! {
2633            #root::core::M2MRelation {
2634                name: #name,
2635                to: #to,
2636                through: #through,
2637                src_col: #src,
2638                dst_col: #dst,
2639                auto_create: #auto_create,
2640            }
2641        }
2642    });
2643    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2644    // when the model declares `#[rustango(reverse_has(...))]`. Each
2645    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2646    // a const expression. Models without reverse_has fall through to
2647    // the trait's empty default — no override emitted.
2648    let reverse_relations_override = if reverse_has_relations.is_empty() {
2649        quote!()
2650    } else {
2651        let entries = reverse_has_relations.iter().map(|rel| {
2652            let name = rel.name.as_str();
2653            let child = &rel.child;
2654            let child_fk_column = rel.child_fk_column.as_str();
2655            let self_pk_column = rel.self_pk_column.as_str();
2656            quote! {
2657                #root::core::ReverseRelation {
2658                    name: #name,
2659                    child_schema: <#child as #root::core::Model>::SCHEMA,
2660                    child_fk_column: #child_fk_column,
2661                    self_pk_column: #self_pk_column,
2662                }
2663            }
2664        });
2665        quote! {
2666            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2667                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2668                RELS
2669            }
2670        }
2671    };
2672    // Issue #830 — `Model::generic_reverse_relations()` override for
2673    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2674    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2675        quote!()
2676    } else {
2677        let entries = generic_has_relations.iter().map(|rel| {
2678            let name = rel.name.as_str();
2679            let child = &rel.child;
2680            let ct_column = rel.ct_column.as_str();
2681            let pk_column = rel.pk_column.as_str();
2682            let self_pk_column = rel.self_pk_column.as_str();
2683            quote! {
2684                #root::core::GenericReverseRelation {
2685                    name: #name,
2686                    child_schema: <#child as #root::core::Model>::SCHEMA,
2687                    ct_column: #ct_column,
2688                    pk_column: #pk_column,
2689                    self_pk_column: #self_pk_column,
2690                }
2691            }
2692        });
2693        quote! {
2694            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2695                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2696                RELS
2697            }
2698        }
2699    };
2700    quote! {
2701        impl #root::core::Model for #struct_name {
2702            const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2703                name: #model_name,
2704                table: #table,
2705                fields: &[ #(#field_schemas),* ],
2706                display: #display_tokens,
2707                app_label: #app_label_tokens,
2708                admin: #admin_tokens,
2709                soft_delete_column: #soft_delete_tokens,
2710                permissions: #permissions,
2711                audit_track: #audit_track_tokens,
2712                m2m: &[ #(#m2m_tokens),* ],
2713                indexes: &[ #(#indexes_tokens),* ],
2714                check_constraints: &[ #(#checks_tokens),* ],
2715                exclusion_constraints: &[ #(#excludes_tokens),* ],
2716                composite_relations: &[ #(#composite_fk_tokens),* ],
2717                generic_relations: &[ #(#generic_fk_tokens),* ],
2718                scope: #scope_tokens,
2719                default_order: &[ #(#default_order_tokens),* ],
2720                is_view: #is_view,
2721                verbose_name: #verbose_name_tokens,
2722                verbose_name_plural: #verbose_name_plural_tokens,
2723                managed: #managed,
2724                base_manager_name: #base_manager_name_tokens,
2725                order_with_respect_to: #order_with_respect_to_tokens,
2726                proxy: #proxy,
2727                required_db_features: &[ #(#required_db_features_lits),* ],
2728                required_db_vendor: #required_db_vendor_tokens,
2729                default_related_name: #default_related_name_tokens,
2730                db_table_comment: #db_table_comment_tokens,
2731                get_latest_by: #get_latest_by_tokens,
2732                extra_permissions: &[ #(#extra_permission_tokens),* ],
2733                default_permissions: &[ #(#default_permission_tokens),* ],
2734                global_scopes: &[ #(#global_scope_tokens),* ],
2735            };
2736
2737            #reverse_relations_override
2738            #generic_reverse_relations_override
2739        }
2740    }
2741}
2742
2743/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2744/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2745/// otherwise a static reference to a populated `AdminConfig`.
2746fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2747    let root = rustango_root();
2748    let Some(admin) = admin else {
2749        return quote!(::core::option::Option::None);
2750    };
2751
2752    let list_display = admin
2753        .list_display
2754        .as_ref()
2755        .map(|(v, _)| v.as_slice())
2756        .unwrap_or(&[]);
2757    let list_display_lits = list_display.iter().map(|s| s.as_str());
2758
2759    let search_fields = admin
2760        .search_fields
2761        .as_ref()
2762        .map(|(v, _)| v.as_slice())
2763        .unwrap_or(&[]);
2764    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2765
2766    let readonly_fields = admin
2767        .readonly_fields
2768        .as_ref()
2769        .map(|(v, _)| v.as_slice())
2770        .unwrap_or(&[]);
2771    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2772
2773    let list_filter = admin
2774        .list_filter
2775        .as_ref()
2776        .map(|(v, _)| v.as_slice())
2777        .unwrap_or(&[]);
2778    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2779
2780    let actions = admin
2781        .actions
2782        .as_ref()
2783        .map(|(v, _)| v.as_slice())
2784        .unwrap_or(&[]);
2785    let actions_lits = actions.iter().map(|s| s.as_str());
2786
2787    let fieldsets = admin
2788        .fieldsets
2789        .as_ref()
2790        .map(|(v, _)| v.as_slice())
2791        .unwrap_or(&[]);
2792    let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2793        let title = title.as_str();
2794        let field_lits = fields.iter().map(|s| s.as_str());
2795        quote!(#root::core::Fieldset {
2796            title: #title,
2797            fields: &[ #( #field_lits ),* ],
2798        })
2799    });
2800
2801    let list_display_links = admin
2802        .list_display_links
2803        .as_ref()
2804        .map(|(v, _)| v.as_slice())
2805        .unwrap_or(&[]);
2806    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2807
2808    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2809    let actions_on_top = admin.actions_on_top.unwrap_or(true);
2810    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2811    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2812
2813    let prepopulated = admin
2814        .prepopulated_fields
2815        .as_ref()
2816        .map(|(v, _)| v.as_slice())
2817        .unwrap_or(&[]);
2818    let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2819        let target = target.as_str();
2820        let source_lits = sources.iter().map(|s| s.as_str());
2821        quote!(#root::core::PrepopulatedField {
2822            target: #target,
2823            sources: &[ #( #source_lits ),* ],
2824        })
2825    });
2826
2827    let raw_id_fields = admin
2828        .raw_id_fields
2829        .as_ref()
2830        .map(|(v, _)| v.as_slice())
2831        .unwrap_or(&[]);
2832    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2833
2834    let autocomplete_fields = admin
2835        .autocomplete_fields
2836        .as_ref()
2837        .map(|(v, _)| v.as_slice())
2838        .unwrap_or(&[]);
2839    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2840
2841    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2842    let list_select_related_tokens = match admin.list_select_related.as_deref() {
2843        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2844        Some("none") => quote!(#root::core::ListSelectRelated::None),
2845        Some(raw) => {
2846            let names: Vec<&str> = raw
2847                .split(',')
2848                .map(str::trim)
2849                .filter(|s| !s.is_empty())
2850                .collect();
2851            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2852        }
2853    };
2854
2855    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2856    // a Vec<(String, String)>. Empty / unset → no overrides.
2857    let formfield_pairs: Vec<(&str, &str)> = admin
2858        .formfield_overrides
2859        .as_ref()
2860        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2861        .unwrap_or_default();
2862    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2863        let field = *field;
2864        let widget = *widget;
2865        quote!((#field, #widget))
2866    });
2867
2868    let list_per_page = admin.list_per_page.unwrap_or(0);
2869
2870    let ordering_pairs = admin
2871        .ordering
2872        .as_ref()
2873        .map(|(v, _)| v.as_slice())
2874        .unwrap_or(&[]);
2875    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2876        let name = name.as_str();
2877        let desc = *desc;
2878        quote!((#name, #desc))
2879    });
2880
2881    quote! {
2882        ::core::option::Option::Some(&#root::core::AdminConfig {
2883            list_display: &[ #( #list_display_lits ),* ],
2884            search_fields: &[ #( #search_fields_lits ),* ],
2885            list_per_page: #list_per_page,
2886            ordering: &[ #( #ordering_tokens ),* ],
2887            readonly_fields: &[ #( #readonly_fields_lits ),* ],
2888            list_filter: &[ #( #list_filter_lits ),* ],
2889            actions: &[ #( #actions_lits ),* ],
2890            fieldsets: &[ #( #fieldset_tokens ),* ],
2891            list_display_links: &[ #( #list_display_links_lits ),* ],
2892            search_help_text: #search_help_text,
2893            actions_on_top: #actions_on_top,
2894            actions_on_bottom: #actions_on_bottom,
2895            date_hierarchy: #date_hierarchy,
2896            prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2897            raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2898            autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2899            list_select_related: #list_select_related_tokens,
2900            formfield_overrides: &[ #( #formfield_tokens ),* ],
2901        })
2902    }
2903}
2904
2905fn inherent_impl_tokens(
2906    struct_name: &syn::Ident,
2907    fields: &CollectedFields,
2908    primary_key: Option<&(syn::Ident, String)>,
2909    column_consts: &TokenStream2,
2910    audited_fields: Option<&[&ColumnEntry]>,
2911    indexes: &[IndexAttr],
2912    manager_fns: &[syn::Ident],
2913) -> TokenStream2 {
2914    let root = rustango_root();
2915    // Audit-emit fragments threaded into write paths. Non-empty only
2916    // when the model carries `#[rustango(audit(...))]`. They reborrow
2917    // `_executor` (a `&mut PgConnection` for audited models — the
2918    // macro switches the signature below) so the data write and the
2919    // audit INSERT both run on the same caller-supplied connection.
2920    let executor_passes_to_data_write = if audited_fields.is_some() {
2921        quote!(&mut *_executor)
2922    } else {
2923        quote!(_executor)
2924    };
2925    let executor_param = if audited_fields.is_some() {
2926        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2927    } else {
2928        quote!(_executor: _E)
2929    };
2930    let executor_generics = if audited_fields.is_some() {
2931        quote!()
2932    } else {
2933        quote!(<'_c, _E>)
2934    };
2935    let executor_where = if audited_fields.is_some() {
2936        quote!()
2937    } else {
2938        quote! {
2939            where
2940                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2941        }
2942    };
2943    // For audited models the `_on` methods take `&mut PgConnection`, so
2944    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2945    // must acquire a connection first. Non-audited models keep the
2946    // direct delegation since `&PgPool` IS an Executor.
2947    let pool_to_save_on = if audited_fields.is_some() {
2948        quote! {
2949            let mut _conn = pool.acquire().await?;
2950            self.save_on(&mut *_conn).await
2951        }
2952    } else {
2953        quote!(self.save_on(pool).await)
2954    };
2955    let pool_to_insert_on = if audited_fields.is_some() {
2956        quote! {
2957            let mut _conn = pool.acquire().await?;
2958            self.insert_on(&mut *_conn).await
2959        }
2960    } else {
2961        quote!(self.insert_on(pool).await)
2962    };
2963    let pool_to_delete_on = if audited_fields.is_some() {
2964        quote! {
2965            let mut _conn = pool.acquire().await?;
2966            self.delete_on(&mut *_conn).await
2967        }
2968    } else {
2969        quote!(self.delete_on(pool).await)
2970    };
2971    let pool_to_bulk_insert_on = if audited_fields.is_some() {
2972        quote! {
2973            let mut _conn = pool.acquire().await?;
2974            Self::bulk_insert_on(rows, &mut *_conn).await
2975        }
2976    } else {
2977        quote!(Self::bulk_insert_on(rows, pool).await)
2978    };
2979    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2980    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2981    // directly, but `upsert_on` for audited models takes
2982    // `&mut PgConnection` (the audit emit needs a real connection).
2983    // Add the missing acquire shim to keep audited Auto-PK upsert
2984    // compiling.
2985    let pool_to_upsert_on = if audited_fields.is_some() {
2986        quote! {
2987            let mut _conn = pool.acquire().await?;
2988            self.upsert_on(&mut *_conn).await
2989        }
2990    } else {
2991        quote!(self.upsert_on(pool).await)
2992    };
2993
2994    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2995    // (audit-on-connection over &Pool needs a bi-dialect transaction
2996    // helper, deferred). Two body shapes:
2997    // - has_auto: build InsertQuery skipping Auto::Unset columns,
2998    //   request Auto cols in `returning`, dispatch via
2999    //   `insert_returning_pool`, then on the returned `PgRow` /
3000    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
3001    //   row OR drop the single i64 into the first Auto field on MySQL
3002    //   (multi-Auto models on MySQL error at runtime since
3003    //   `LAST_INSERT_ID()` only reports one)
3004    // - non-Auto: build InsertQuery with explicit columns/values and
3005    //   call `insert_pool` (no returning needed)
3006    // pool_insert_method body for the audited Auto-PK case is moved
3007    // to after audit_pair_tokens / audit_pk_to_string (they live
3008    // ~150 lines below). This block keeps the non-audited and
3009    // non-Auto branches in place — the audited Auto-PK arm is
3010    // computed below and merged via the dispatch helper variable.
3011    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3012        // Audited models with explicit (non-Auto) PKs go through
3013        // the non-Auto insert path below — the audit emit is one
3014        // round-trip after the INSERT inside the same tx via
3015        // audit::save_one_with_audit? No, INSERT semantics
3016        // differ. For non-Auto PK + audited, route through a
3017        // dedicated insert + audit emit on the same tx, but defer
3018        // the macro emission to the audit-bundle-aware block below
3019        // — this `quote!()` placeholder gets overwritten there.
3020        quote!()
3021    } else if audited_fields.is_some() && fields.has_auto {
3022        // Audited Auto-PK insert_pool — assembled after the audit
3023        // bundles. Placeholder; real emission below.
3024        quote!()
3025    } else if fields.has_auto {
3026        let pushes = &fields.insert_pushes;
3027        let returning_cols = &fields.returning_cols;
3028        // When every `Auto<T>` field is filled Rust-side
3029        // (`default_uuid_v7`, issue #823), there is no column to read
3030        // back from the database — `returning_cols` is empty. Route
3031        // through plain `insert_pool` instead of
3032        // `insert_returning_pool` to skip the redundant RETURNING /
3033        // LAST_INSERT_ID round-trip.
3034        if fields.returning_cols.is_empty() {
3035            quote! {
3036                /// Insert this row against either backend. Every
3037                /// `Auto<T>` PK on this model is filled Rust-side
3038                /// (e.g. `default_uuid_v7`) before binding, so no
3039                /// RETURNING round-trip is needed.
3040                ///
3041                /// # Errors
3042                /// As [`Self::insert`].
3043                pub async fn insert_pool(
3044                    &mut self,
3045                    pool: &#root::sql::Pool,
3046                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3047                    let mut _columns: ::std::vec::Vec<&'static str> =
3048                        ::std::vec::Vec::new();
3049                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3050                        ::std::vec::Vec::new();
3051                    #( #pushes )*
3052                    let _query = #root::core::InsertQuery {
3053                        model: <Self as #root::core::Model>::SCHEMA,
3054                        columns: _columns,
3055                        values: _values,
3056                        returning: ::std::vec::Vec::new(),
3057                        on_conflict: ::core::option::Option::None,
3058                    };
3059                    #root::sql::insert_pool(pool, &_query).await
3060                }
3061
3062                /// Eloquent `Model::insertOrIgnore()` — same shape
3063                /// as the auto-PK branch above. Returns `Ok(true)`
3064                /// when inserted, `Ok(false)` when a conflict caused
3065                /// the INSERT to silently skip.
3066                ///
3067                /// # Errors
3068                /// As [`Self::insert`].
3069                pub async fn insert_or_ignore(
3070                    &mut self,
3071                    pool: &#root::sql::Pool,
3072                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3073                    let mut _columns: ::std::vec::Vec<&'static str> =
3074                        ::std::vec::Vec::new();
3075                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3076                        ::std::vec::Vec::new();
3077                    #( #pushes )*
3078                    let _query = #root::core::InsertQuery {
3079                        model: <Self as #root::core::Model>::SCHEMA,
3080                        columns: _columns,
3081                        values: _values,
3082                        returning: ::std::vec::Vec::new(),
3083                        on_conflict: ::core::option::Option::Some(
3084                            #root::core::ConflictClause::DoNothing,
3085                        ),
3086                    };
3087                    let dialect = pool.dialect();
3088                    let stmt = dialect.compile_insert(&_query)?;
3089                    let rows = #root::sql::raw_execute_pool(
3090                        pool, &stmt.sql, stmt.params,
3091                    ).await?;
3092                    ::core::result::Result::Ok(rows > 0)
3093                }
3094            }
3095        } else {
3096            quote! {
3097                /// Insert this row against either backend, populating any
3098                /// `Auto<T>` PK from the auto-assigned value.
3099                ///
3100                /// # Errors
3101                /// As [`Self::insert`].
3102                pub async fn insert_pool(
3103                    &mut self,
3104                    pool: &#root::sql::Pool,
3105                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3106                    let mut _columns: ::std::vec::Vec<&'static str> =
3107                        ::std::vec::Vec::new();
3108                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3109                        ::std::vec::Vec::new();
3110                    #( #pushes )*
3111                    let _query = #root::core::InsertQuery {
3112                        model: <Self as #root::core::Model>::SCHEMA,
3113                        columns: _columns,
3114                        values: _values,
3115                        returning: ::std::vec![ #( #returning_cols ),* ],
3116                        on_conflict: ::core::option::Option::None,
3117                    };
3118                    let _result = #root::sql::insert_returning_pool(
3119                        pool, &_query,
3120                    ).await?;
3121                    #root::sql::apply_auto_pk(_result, self)
3122                }
3123
3124                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3125                /// row or silently skip on unique-constraint
3126                /// violation. Returns `Ok(true)` when a row was
3127                /// inserted, `Ok(false)` when a conflict caused the
3128                /// INSERT to silently skip.
3129                ///
3130                /// **Caveat on auto-PK models**: when the row is
3131                /// skipped (conflict), this instance's `Auto<T>`
3132                /// fields stay `Unset` — no PK is back-populated
3133                /// because the server didn't auto-assign one. For
3134                /// "insert then read back the PK or the existing
3135                /// row's PK", use the `upsert` family or
3136                /// `get_or_create`.
3137                ///
3138                /// # Errors
3139                /// As [`Self::insert`].
3140                pub async fn insert_or_ignore(
3141                    &mut self,
3142                    pool: &#root::sql::Pool,
3143                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3144                    let mut _columns: ::std::vec::Vec<&'static str> =
3145                        ::std::vec::Vec::new();
3146                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3147                        ::std::vec::Vec::new();
3148                    #( #pushes )*
3149                    let _query = #root::core::InsertQuery {
3150                        model: <Self as #root::core::Model>::SCHEMA,
3151                        columns: _columns,
3152                        values: _values,
3153                        returning: ::std::vec::Vec::new(),
3154                        on_conflict: ::core::option::Option::Some(
3155                            #root::core::ConflictClause::DoNothing,
3156                        ),
3157                    };
3158                    let dialect = pool.dialect();
3159                    let stmt = dialect.compile_insert(&_query)?;
3160                    let rows = #root::sql::raw_execute_pool(
3161                        pool, &stmt.sql, stmt.params,
3162                    ).await?;
3163                    ::core::result::Result::Ok(rows > 0)
3164                }
3165            }
3166        }
3167    } else {
3168        let insert_columns = &fields.insert_columns;
3169        let insert_values = &fields.insert_values;
3170        quote! {
3171            /// Insert this row into its table against either backend.
3172            /// Equivalent to [`Self::insert`] but takes
3173            /// [`#root::sql::Pool`].
3174            ///
3175            /// # Errors
3176            /// As [`Self::insert`].
3177            pub async fn insert_pool(
3178                &self,
3179                pool: &#root::sql::Pool,
3180            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3181                let _query = #root::core::InsertQuery {
3182                    model: <Self as #root::core::Model>::SCHEMA,
3183                    columns: ::std::vec![ #( #insert_columns ),* ],
3184                    values: ::std::vec![ #( #insert_values ),* ],
3185                    returning: ::std::vec::Vec::new(),
3186                    on_conflict: ::core::option::Option::None,
3187                };
3188                #root::sql::insert_pool(pool, &_query).await
3189            }
3190
3191            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3192            /// or silently skip on unique-constraint violation. Maps
3193            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3194            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3195            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3196            /// `INSERT IGNORE INTO …`.
3197            ///
3198            /// Returns `Ok(true)` when a row was inserted,
3199            /// `Ok(false)` when a conflict caused the INSERT to
3200            /// silently skip.
3201            ///
3202            /// Use for "create-if-absent" patterns where you don't
3203            /// need the row back. For "find-or-create with the row
3204            /// returned", use the queryset-level
3205            /// `crate::sql::get_or_create` free function.
3206            ///
3207            /// # Errors
3208            /// As [`Self::insert`], plus any dialect-specific
3209            /// translation error from the ConflictClause writer.
3210            pub async fn insert_or_ignore(
3211                &self,
3212                pool: &#root::sql::Pool,
3213            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3214                let _query = #root::core::InsertQuery {
3215                    model: <Self as #root::core::Model>::SCHEMA,
3216                    columns: ::std::vec![ #( #insert_columns ),* ],
3217                    values: ::std::vec![ #( #insert_values ),* ],
3218                    returning: ::std::vec::Vec::new(),
3219                    on_conflict: ::core::option::Option::Some(
3220                        #root::core::ConflictClause::DoNothing,
3221                    ),
3222                };
3223                let dialect = pool.dialect();
3224                let stmt = dialect.compile_insert(&_query)?;
3225                let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3226                ::core::result::Result::Ok(rows > 0)
3227            }
3228        }
3229    };
3230
3231    // pool_save_method moved to after audit_pair_tokens /
3232    // audit_pk_to_string (they live ~70 lines below) — needed for
3233    // the audited branch which builds an UpdateQuery + PendingEntry
3234    // and dispatches via audit::save_one_with_audit.
3235
3236    // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3237    // are computed (they live ~80 lines below).
3238
3239    // Build the (column, JSON value) pair list used by every
3240    // snapshot-style audit emission. Reused across delete_on,
3241    // soft_delete_on, restore_on, and (later) bulk paths. Empty
3242    // when the model isn't audited.
3243    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3244        .map(|tracked| {
3245            tracked
3246                .iter()
3247                .map(|c| {
3248                    let column_lit = c.column.as_str();
3249                    let ident = &c.ident;
3250                    quote! {
3251                        (
3252                            #column_lit,
3253                            #root::__serde_json::to_value(&self.#ident)
3254                                .unwrap_or(#root::__serde_json::Value::Null),
3255                        )
3256                    }
3257                })
3258                .collect()
3259        })
3260        .unwrap_or_default();
3261    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3262        if fields.pk_is_auto {
3263            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3264        } else {
3265            quote!(::std::format!("{}", &self.#pk_ident))
3266        }
3267    } else {
3268        quote!(::std::string::String::new())
3269    };
3270    let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3271        if audited_fields.is_some() {
3272            let pairs = audit_pair_tokens.iter();
3273            let pk_str = audit_pk_to_string.clone();
3274            quote! {
3275                let _audit_entry = #root::audit::PendingEntry {
3276                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3277                    entity_pk: #pk_str,
3278                    operation: #op_path,
3279                    source: #root::audit::current_source(),
3280                    changes: #root::audit::snapshot_changes(&[
3281                        #( #pairs ),*
3282                    ]),
3283                };
3284                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3285            }
3286        } else {
3287            quote!()
3288        }
3289    };
3290    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3291    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3292    let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3293    let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3294
3295    // `save_pool(&Pool)` — emitted for every model with a PK.
3296    // Audited Auto-PK models are deferred (the Auto::Unset →
3297    // insert_pool path needs the audited-insert flow from a future
3298    // batch). Three body shapes:
3299    // - non-audited, plain PK: build UpdateQuery + dispatch through
3300    //   sql::update_pool
3301    // - non-audited, Auto-PK: same, but Auto::Unset routes to
3302    //   self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3303    // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3304    //   through audit::save_one_with_audit (per-backend tx wraps
3305    //   UPDATE + audit emit atomically). Snapshot-style audit (post-
3306    //   write field values) — diff-style audit (with pre-UPDATE
3307    //   SELECT for `before` values) needs per-tracked-column codegen
3308    //   that doesn't fit the runtime-helper pattern; legacy &PgPool
3309    //   `save` keeps the diff for now.
3310    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3311        let pk_column_lit = pk_col.as_str();
3312        let assignments = &fields.update_assignments;
3313        if audited_fields.is_some() {
3314            if fields.pk_is_auto {
3315                // Auto-PK + audited: defer. The Auto::Unset insert
3316                // path needs a transactional INSERT + LAST_INSERT_ID
3317                // + audit emit flow — that's a follow-up batch.
3318                quote!()
3319            } else {
3320                let pairs = audit_pair_tokens.iter();
3321                let pairs2 = audit_pair_tokens.iter();
3322                let pk_str = audit_pk_to_string.clone();
3323                let pk_str2 = audit_pk_to_string.clone();
3324                quote! {
3325                    /// Save (UPDATE) this row against either backend
3326                    /// with audit emission inside the same transaction.
3327                    /// Bi-dialect counterpart of [`Self::save`] for
3328                    /// audited models with non-`Auto<T>` PKs.
3329                    ///
3330                    /// Captures **post-write** field state (snapshot
3331                    /// audit). The legacy &PgPool [`Self::save`]
3332                    /// captures BEFORE+AFTER for true diff audit;
3333                    /// porting that to the &Pool path needs runtime
3334                    /// per-tracked-column decoding and is deferred.
3335                    ///
3336                    /// # Errors
3337                    /// As [`Self::save`].
3338                    pub async fn save_pool(
3339                        &mut self,
3340                        pool: &#root::sql::Pool,
3341                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3342                        let _query = #root::core::UpdateQuery {
3343                            model: <Self as #root::core::Model>::SCHEMA,
3344                            set: ::std::vec![ #( #assignments ),* ],
3345                            where_clause: #root::core::WhereExpr::Predicate(
3346                                #root::core::Filter {
3347                                    column: #pk_column_lit,
3348                                    op: #root::core::Op::Eq,
3349                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3350                                        ::core::clone::Clone::clone(&self.#pk_ident)
3351                                    ),
3352                                }
3353                            ),
3354                        };
3355                        let _audit_entry = #root::audit::PendingEntry {
3356                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3357                            entity_pk: #pk_str,
3358                            operation: #root::audit::AuditOp::Update,
3359                            source: #root::audit::current_source(),
3360                            changes: #root::audit::snapshot_changes(&[
3361                                #( #pairs ),*
3362                            ]),
3363                        };
3364                        let _affected = #root::audit::save_one_with_audit(
3365                            pool, &_query, &_audit_entry,
3366                        ).await?;
3367                        ::core::result::Result::Ok(_affected)
3368                    }
3369
3370                    /// `save_pool` narrowed to a Rust-field allowlist — issue #66.
3371                    /// Audit emission shrinks to the same column set so
3372                    /// the audit log reflects exactly what was written.
3373                    ///
3374                    /// # Errors
3375                    /// As [`Self::save_pool`], plus
3376                    /// [`#root::core::QueryError::UnknownField`] wrapped
3377                    /// in `ExecError::Query` for unknown field names.
3378                    pub async fn save_partial(
3379                        &mut self,
3380                        fields: &[&str],
3381                        pool: &#root::sql::Pool,
3382                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3383                        if fields.is_empty() {
3384                            #root::__tracing::warn!(
3385                                target: "rustango::save_partial",
3386                                model = <Self as #root::core::Model>::SCHEMA.name,
3387                                "save_partial called with empty field list — no-op"
3388                            );
3389                            return ::core::result::Result::Ok(0);
3390                        }
3391                        let _schema = <Self as #root::core::Model>::SCHEMA;
3392                        let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3393                            ::std::collections::HashSet::with_capacity(fields.len());
3394                        for f in fields {
3395                            match _schema.field(f) {
3396                                ::core::option::Option::Some(fs) => {
3397                                    _wanted_cols.insert(fs.column);
3398                                }
3399                                ::core::option::Option::None => {
3400                                    return ::core::result::Result::Err(
3401                                        #root::sql::ExecError::Query(
3402                                            #root::core::QueryError::UnknownField {
3403                                                model: _schema.name,
3404                                                field: (*f).to_owned(),
3405                                            }
3406                                        )
3407                                    );
3408                                }
3409                            }
3410                        }
3411                        let _full: ::std::vec::Vec<#root::core::Assignment> =
3412                            ::std::vec![ #( #assignments ),* ];
3413                        let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3414                            .into_iter()
3415                            .filter(|a| _wanted_cols.contains(a.column))
3416                            .collect();
3417                        if _filtered.is_empty() {
3418                            #root::__tracing::warn!(
3419                                target: "rustango::save_partial",
3420                                model = _schema.name,
3421                                "save_partial: every named field maps to a non-assignable column — no-op"
3422                            );
3423                            return ::core::result::Result::Ok(0);
3424                        }
3425                        let _query = #root::core::UpdateQuery {
3426                            model: _schema,
3427                            set: _filtered,
3428                            where_clause: #root::core::WhereExpr::Predicate(
3429                                #root::core::Filter {
3430                                    column: #pk_column_lit,
3431                                    op: #root::core::Op::Eq,
3432                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3433                                        ::core::clone::Clone::clone(&self.#pk_ident)
3434                                    ),
3435                                }
3436                            ),
3437                        };
3438                        // Narrow the audit snapshot to the same column set.
3439                        let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3440                            ::std::vec![ #( #pairs2 ),* ];
3441                        let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3442                            _all_pairs
3443                                .into_iter()
3444                                .filter(|(col, _)| _wanted_cols.contains(col))
3445                                .collect();
3446                        let _audit_entry = #root::audit::PendingEntry {
3447                            entity_table: _schema.table,
3448                            entity_pk: #pk_str2,
3449                            operation: #root::audit::AuditOp::Update,
3450                            source: #root::audit::current_source(),
3451                            changes: #root::audit::snapshot_changes(&_narrowed),
3452                        };
3453                        let _affected = #root::audit::save_one_with_audit(
3454                            pool, &_query, &_audit_entry,
3455                        ).await?;
3456                        ::core::result::Result::Ok(_affected)
3457                    }
3458
3459                    /// Typed-column counterpart of [`Self::save_partial`] —
3460                    /// issue #67. `fields` is a tuple of [`Column`]
3461                    /// constants whose `Model` matches `Self`; typos and
3462                    /// model mismatches surface at *compile time*
3463                    /// (`Author::name` inside a `Post::save_partial_typed`
3464                    /// call is a type error, no runtime check).
3465                    ///
3466                    /// ```ignore
3467                    /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3468                    /// ```
3469                    ///
3470                    /// Lowers to [`Self::save_partial`] under the hood;
3471                    /// audit narrowing + every other semantic is identical.
3472                    ///
3473                    /// [`Column`]: #root::core::Column
3474                    ///
3475                    /// # Errors
3476                    /// As [`Self::save_partial`].
3477                    pub async fn save_partial_typed<
3478                        L: #root::core::TypedFieldList<Self>,
3479                    >(
3480                        &mut self,
3481                        fields: L,
3482                        pool: &#root::sql::Pool,
3483                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3484                        let _names = fields.rust_field_names();
3485                        let _refs: ::std::vec::Vec<&str> =
3486                            _names.iter().copied().collect();
3487                        self.save_partial(&_refs, pool).await
3488                    }
3489                }
3490            }
3491        } else {
3492            let dispatch_unset = if fields.pk_is_auto {
3493                quote! {
3494                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3495                        return self.insert_pool(pool).await.map(|()| 1u64);
3496                    }
3497                }
3498            } else {
3499                quote!()
3500            };
3501            quote! {
3502                /// Save this row to its table against either backend.
3503                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3504                /// `UPDATE` keyed on the PK.
3505                ///
3506                /// # Errors
3507                /// As [`Self::save`].
3508                pub async fn save_pool(
3509                    &mut self,
3510                    pool: &#root::sql::Pool,
3511                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3512                    #dispatch_unset
3513                    let _query = #root::core::UpdateQuery {
3514                        model: <Self as #root::core::Model>::SCHEMA,
3515                        set: ::std::vec![ #( #assignments ),* ],
3516                        where_clause: #root::core::WhereExpr::Predicate(
3517                            #root::core::Filter {
3518                                column: #pk_column_lit,
3519                                op: #root::core::Op::Eq,
3520                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3521                                    ::core::clone::Clone::clone(&self.#pk_ident)
3522                                ),
3523                            }
3524                        ),
3525                    };
3526                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3527                    ::core::result::Result::Ok(_affected)
3528                }
3529
3530                /// Save (UPDATE) only the listed Rust-side fields,
3531                /// leaving every other column untouched. Issue #66.
3532                ///
3533                /// `fields` are Rust-side struct field names; the macro
3534                /// resolves each to its SQL column. Unknown field
3535                /// names return [`#root::core::QueryError::UnknownField`]
3536                /// wrapped in `ExecError::Query`. An empty list is a
3537                /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3538                ///
3539                /// Use this when:
3540                /// * you only mutated a couple of fields on a wide row
3541                ///   (avoid re-writing every column on every save), or
3542                /// * two writers diverged after their initial read and
3543                ///   you want to preserve the other writer's changes to
3544                ///   columns you didn't touch.
3545                ///
3546                /// Auto-PK models with an unset PK return
3547                /// [`#root::core::QueryError::UnknownField`] with
3548                /// field name `<pk>` — `save_partial` is an
3549                /// UPDATE-only path. Call [`Self::insert_pool`]
3550                /// (or [`Self::save_pool`] which dispatches based on
3551                /// PK state) for the INSERT case.
3552                ///
3553                /// # Errors
3554                /// As [`Self::save_pool`], plus `UnknownField` for
3555                /// unknown / empty / Auto-Unset cases.
3556                pub async fn save_partial(
3557                    &mut self,
3558                    fields: &[&str],
3559                    pool: &#root::sql::Pool,
3560                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3561                    if fields.is_empty() {
3562                        #root::__tracing::warn!(
3563                            target: "rustango::save_partial",
3564                            model = <Self as #root::core::Model>::SCHEMA.name,
3565                            "save_partial called with empty field list — no-op"
3566                        );
3567                        return ::core::result::Result::Ok(0);
3568                    }
3569                    let _schema = <Self as #root::core::Model>::SCHEMA;
3570                    // Validate field names against the schema.
3571                    let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3572                        ::std::collections::HashSet::with_capacity(fields.len());
3573                    for f in fields {
3574                        match _schema.field(f) {
3575                            ::core::option::Option::Some(fs) => {
3576                                _wanted_cols.insert(fs.column);
3577                            }
3578                            ::core::option::Option::None => {
3579                                return ::core::result::Result::Err(
3580                                    #root::sql::ExecError::Query(
3581                                        #root::core::QueryError::UnknownField {
3582                                            model: _schema.name,
3583                                            field: (*f).to_owned(),
3584                                        }
3585                                    )
3586                                );
3587                            }
3588                        }
3589                    }
3590                    // Build the full assignment vec, then keep only the
3591                    // assignments whose column is in `_wanted_cols`.
3592                    let _full: ::std::vec::Vec<#root::core::Assignment> =
3593                        ::std::vec![ #( #assignments ),* ];
3594                    let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3595                        .into_iter()
3596                        .filter(|a| _wanted_cols.contains(a.column))
3597                        .collect();
3598                    if _filtered.is_empty() {
3599                        // All field names valid, but they all map to
3600                        // non-assignable slots (PK column, computed/
3601                        // virtual fields, relations without an
3602                        // assignment). Same no-op as an empty list.
3603                        #root::__tracing::warn!(
3604                            target: "rustango::save_partial",
3605                            model = _schema.name,
3606                            "save_partial: every named field maps to a non-assignable column — no-op"
3607                        );
3608                        return ::core::result::Result::Ok(0);
3609                    }
3610                    let _query = #root::core::UpdateQuery {
3611                        model: _schema,
3612                        set: _filtered,
3613                        where_clause: #root::core::WhereExpr::Predicate(
3614                            #root::core::Filter {
3615                                column: #pk_column_lit,
3616                                op: #root::core::Op::Eq,
3617                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3618                                    ::core::clone::Clone::clone(&self.#pk_ident)
3619                                ),
3620                            }
3621                        ),
3622                    };
3623                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3624                    ::core::result::Result::Ok(_affected)
3625                }
3626
3627                /// Typed-column counterpart of [`Self::save_partial`] —
3628                /// issue #67. `fields` is a tuple of [`Column`]
3629                /// constants whose `Model` matches `Self`; typos and
3630                /// model mismatches surface at *compile time*
3631                /// (`Author::name` inside a `Post::save_partial_typed`
3632                /// call is a type error, no runtime check).
3633                ///
3634                /// ```ignore
3635                /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3636                /// ```
3637                ///
3638                /// Lowers to [`Self::save_partial`] under the hood — the
3639                /// tuple is reduced to a `&[&str]` slice of Rust-side
3640                /// field names and forwarded.
3641                ///
3642                /// [`Column`]: #root::core::Column
3643                ///
3644                /// # Errors
3645                /// As [`Self::save_partial`].
3646                pub async fn save_partial_typed<
3647                    L: #root::core::TypedFieldList<Self>,
3648                >(
3649                    &mut self,
3650                    fields: L,
3651                    pool: &#root::sql::Pool,
3652                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3653                    let _names = fields.rust_field_names();
3654                    let _refs: ::std::vec::Vec<&str> =
3655                        _names.iter().copied().collect();
3656                    self.save_partial(&_refs, pool).await
3657                }
3658            }
3659        }
3660    } else {
3661        quote!()
3662    };
3663
3664    // Audited `insert_pool` (overrides the placeholder set higher up
3665    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3666    // audited models get insert_pool routing through
3667    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3668    // + auto-PK readback + audit emit). Snapshot-style audit (the
3669    // PendingEntry's `changes` carries post-write field values).
3670    let pool_insert_method = if audited_fields.is_some() {
3671        if let Some(_) = primary_key {
3672            let pushes = if fields.has_auto {
3673                fields.insert_pushes.clone()
3674            } else {
3675                // For non-Auto-PK models, the macro normally builds
3676                // {columns, values} from fields.insert_columns +
3677                // fields.insert_values rather than insert_pushes.
3678                // Map those into the pushes shape.
3679                fields
3680                    .insert_columns
3681                    .iter()
3682                    .zip(&fields.insert_values)
3683                    .map(|(col, val)| {
3684                        quote! {
3685                            _columns.push(#col);
3686                            _values.push(#val);
3687                        }
3688                    })
3689                    .collect()
3690            };
3691            let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3692                fields.returning_cols.clone()
3693            } else {
3694                // Non-Auto-PK: still need RETURNING something for the
3695                // audit helper's contract (it errors on empty
3696                // returning). Return the PK column so the audit row
3697                // can carry the assigned PK back. Some non-Auto PKs
3698                // are server-side-default (e.g. UUIDv4 default), so
3699                // RETURNING is genuinely useful.
3700                primary_key
3701                    .map(|(_, col)| {
3702                        let lit = col.as_str();
3703                        vec![quote!(#lit)]
3704                    })
3705                    .unwrap_or_default()
3706            };
3707            let pairs = audit_pair_tokens.iter();
3708            let pk_str = audit_pk_to_string.clone();
3709            quote! {
3710                /// Insert this row against either backend with audit
3711                /// emission inside the same transaction. Bi-dialect
3712                /// counterpart of [`Self::insert`] for audited models.
3713                ///
3714                /// Snapshot-style audit (post-write field values).
3715                ///
3716                /// # Errors
3717                /// As [`Self::insert`].
3718                pub async fn insert_pool(
3719                    &mut self,
3720                    pool: &#root::sql::Pool,
3721                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3722                    let mut _columns: ::std::vec::Vec<&'static str> =
3723                        ::std::vec::Vec::new();
3724                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3725                        ::std::vec::Vec::new();
3726                    #( #pushes )*
3727                    let _query = #root::core::InsertQuery {
3728                        model: <Self as #root::core::Model>::SCHEMA,
3729                        columns: _columns,
3730                        values: _values,
3731                        returning: ::std::vec![ #( #returning_cols ),* ],
3732                        on_conflict: ::core::option::Option::None,
3733                    };
3734                    let _audit_entry = #root::audit::PendingEntry {
3735                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
3736                        entity_pk: #pk_str,
3737                        operation: #root::audit::AuditOp::Create,
3738                        source: #root::audit::current_source(),
3739                        changes: #root::audit::snapshot_changes(&[
3740                            #( #pairs ),*
3741                        ]),
3742                    };
3743                    let _result = #root::audit::insert_one_with_audit(
3744                        pool, &_query, &_audit_entry,
3745                    ).await?;
3746                    #root::sql::apply_auto_pk(_result, self)
3747                }
3748            }
3749        } else {
3750            quote!()
3751        }
3752    } else {
3753        // Keep the non-audited pool_insert_method we built earlier.
3754        pool_insert_method
3755    };
3756
3757    // Update audited save_pool: now that insert_pool is wired for
3758    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3759    // insert_pool. Non-audited save_pool already does this.
3760    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3761    // Replaces the snapshot-only emission with a per-backend transaction
3762    // body that:
3763    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3764    //     column), capturing BEFORE values
3765    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3766    //  3. builds AFTER pairs from &self
3767    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3768    //     AuditOp::Update + diff_changes(...) on the same tx connection
3769    //  5. commits
3770    //
3771    // Per-backend arms inline the SQL string + placeholder shape, then
3772    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3773    // is polymorphic over Row type — the same tokens work against
3774    // PgRow and MySqlRow as long as the field's Rust type implements
3775    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3776    // primitives + chrono/uuid/serde_json::Value all do).
3777    let pool_save_method = if let Some(tracked) = audited_fields {
3778        if let Some((pk_ident, pk_col)) = primary_key {
3779            let pk_column_lit = pk_col.as_str();
3780            // Two iterators — quote!'s `#(#var)*` consumes the
3781            // iterator, and we need to splice the same after-pairs
3782            // sequence into both per-backend arms.
3783            let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3784            let pk_str = audit_pk_to_string.clone();
3785            // Per-tracked-column BEFORE-pair token list. Each entry
3786            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3787            // The Row alias resolves to PgRow / MySqlRow per call site,
3788            // so the same template generates both the PG and MySQL bodies.
3789            let mk_before_pairs =
3790                |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3791                    tracked
3792                        .iter()
3793                        .map(|c| {
3794                            let column_lit = c.column.as_str();
3795                            let value_ty = &c.value_ty;
3796                            quote! {
3797                                (
3798                                    #column_lit,
3799                                    match #getter::<#value_ty>(
3800                                        _audit_before_row, #column_lit,
3801                                    ) {
3802                                        ::core::result::Result::Ok(v) => {
3803                                            #root::__serde_json::to_value(&v)
3804                                                .unwrap_or(#root::__serde_json::Value::Null)
3805                                        }
3806                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3807                                    },
3808                                )
3809                            }
3810                        })
3811                        .collect()
3812                };
3813            let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3814                mk_before_pairs(quote!(#root::sql::try_get_returning));
3815            let before_pairs_my: Vec<proc_macro2::TokenStream> =
3816                mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3817            let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3818                mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3819            let pg_select_cols: String = tracked
3820                .iter()
3821                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3822                .collect::<Vec<_>>()
3823                .join(", ");
3824            let my_select_cols: String = tracked
3825                .iter()
3826                .map(|c| format!("`{}`", c.column.replace('`', "``")))
3827                .collect::<Vec<_>>()
3828                .join(", ");
3829            // SQLite uses double-quote identifier quoting (same as
3830            // Postgres in default config), so the column-list shape
3831            // matches PG.
3832            let sqlite_select_cols: String = pg_select_cols.clone();
3833            let pk_value_for_bind = if fields.pk_is_auto {
3834                quote!(self.#pk_ident.get().copied().unwrap_or_default())
3835            } else {
3836                quote!(::core::clone::Clone::clone(&self.#pk_ident))
3837            };
3838            let assignments = &fields.update_assignments;
3839            let unset_dispatch = if fields.has_auto {
3840                quote! {
3841                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3842                        return self.insert_pool(pool).await.map(|()| 1u64);
3843                    }
3844                }
3845            } else {
3846                quote!()
3847            };
3848            quote! {
3849                /// Save this row against either backend with audit
3850                /// emission (diff-style: BEFORE+AFTER) inside the
3851                /// same transaction. Auto::Unset PK routes to
3852                /// insert_pool. Bi-dialect counterpart of
3853                /// [`Self::save`] for audited models.
3854                ///
3855                /// The audit row's `changes` JSON contains one
3856                /// `{ "field": { "before": …, "after": … } }` entry
3857                /// per tracked column whose value actually changed
3858                /// — same shape as the existing &PgPool save() emits.
3859                ///
3860                /// # Errors
3861                /// As [`Self::save`].
3862                pub async fn save_pool(
3863                    &mut self,
3864                    pool: &#root::sql::Pool,
3865                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3866                    #unset_dispatch
3867                    let _query = #root::core::UpdateQuery {
3868                        model: <Self as #root::core::Model>::SCHEMA,
3869                        set: ::std::vec![ #( #assignments ),* ],
3870                        where_clause: #root::core::WhereExpr::Predicate(
3871                            #root::core::Filter {
3872                                column: #pk_column_lit,
3873                                op: #root::core::Op::Eq,
3874                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3875                                    ::core::clone::Clone::clone(&self.#pk_ident)
3876                                ),
3877                            }
3878                        ),
3879                    };
3880                    let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3881                        ::std::vec![ #( #after_pairs_pg ),* ];
3882                    #root::audit::save_one_with_diff(
3883                        pool,
3884                        &_query,
3885                        #pk_column_lit,
3886                        ::core::convert::Into::<#root::core::SqlValue>::into(
3887                            #pk_value_for_bind,
3888                        ),
3889                        <Self as #root::core::Model>::SCHEMA.table,
3890                        #pk_str,
3891                        _after_pairs,
3892                        #pg_select_cols,
3893                        #my_select_cols,
3894                        #sqlite_select_cols,
3895                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3896                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3897                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3898                    ).await
3899                }
3900            }
3901        } else {
3902            quote!()
3903        }
3904    } else {
3905        pool_save_method
3906    };
3907
3908    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3909    // body shapes:
3910    // - non-audited: simple dispatch through `sql::delete_pool`
3911    // - audited: routes through `audit::delete_one_with_audit`,
3912    //   which opens a per-backend transaction wrapping DELETE +
3913    //   audit emit so the data write and audit row commit atomically.
3914    let pool_delete_method = {
3915        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3916        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3917        if let Some(pk_ident) = pk_ident_for_pool {
3918            if audited_fields.is_some() {
3919                let pairs = audit_pair_tokens.iter();
3920                let pk_str = audit_pk_to_string.clone();
3921                quote! {
3922                    /// Delete this row against either backend with audit
3923                    /// emission inside the same transaction. Bi-dialect
3924                    /// counterpart of [`Self::delete`] for audited models.
3925                    ///
3926                    /// # Errors
3927                    /// As [`Self::delete`].
3928                    pub async fn delete_pool(
3929                        &self,
3930                        pool: &#root::sql::Pool,
3931                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3932                        let _query = #root::core::DeleteQuery {
3933                            model: <Self as #root::core::Model>::SCHEMA,
3934                            where_clause: #root::core::WhereExpr::Predicate(
3935                                #root::core::Filter {
3936                                    column: #pk_column_lit,
3937                                    op: #root::core::Op::Eq,
3938                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3939                                        ::core::clone::Clone::clone(&self.#pk_ident)
3940                                    ),
3941                                }
3942                            ),
3943                        };
3944                        let _audit_entry = #root::audit::PendingEntry {
3945                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3946                            entity_pk: #pk_str,
3947                            operation: #root::audit::AuditOp::Delete,
3948                            source: #root::audit::current_source(),
3949                            changes: #root::audit::snapshot_changes(&[
3950                                #( #pairs ),*
3951                            ]),
3952                        };
3953                        #root::audit::delete_one_with_audit(
3954                            pool, &_query, &_audit_entry,
3955                        ).await
3956                    }
3957                }
3958            } else {
3959                quote! {
3960                    /// Delete the row identified by this instance's primary key
3961                    /// against either backend. Equivalent to [`Self::delete`] but
3962                    /// takes [`#root::sql::Pool`] and dispatches per backend.
3963                    ///
3964                    /// # Errors
3965                    /// As [`Self::delete`].
3966                    pub async fn delete_pool(
3967                        &self,
3968                        pool: &#root::sql::Pool,
3969                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3970                        let _query = #root::core::DeleteQuery {
3971                            model: <Self as #root::core::Model>::SCHEMA,
3972                            where_clause: #root::core::WhereExpr::Predicate(
3973                                #root::core::Filter {
3974                                    column: #pk_column_lit,
3975                                    op: #root::core::Op::Eq,
3976                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3977                                        ::core::clone::Clone::clone(&self.#pk_ident)
3978                                    ),
3979                                }
3980                            ),
3981                        };
3982                        #root::sql::delete_pool(pool, &_query).await
3983                    }
3984                }
3985            }
3986        } else {
3987            quote!()
3988        }
3989    };
3990
3991    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3992    // matching this instance's PK and overwrite the in-memory state
3993    // with the freshly-fetched columns.
3994    // Issue #825. Only emitted when the model declares a PK; non-PK
3995    // models can't address a specific row.
3996    //
3997    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3998    // every field from `self`; resets the PK to `Auto::Unset` when
3999    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4000    // a fresh autoincrement value. Non-Auto PKs preserve the source
4001    // PK — the caller must overwrite before insert. Pure-Rust, no
4002    // I/O, no dialect surface.
4003    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4004        let other_field_clones: Vec<TokenStream2> = fields
4005            .column_entries
4006            .iter()
4007            .filter(|c| &c.ident != pk_ident)
4008            .map(|c| {
4009                let ident = &c.ident;
4010                quote! {
4011                    #ident: ::core::clone::Clone::clone(&self.#ident)
4012                }
4013            })
4014            .collect();
4015        let pk_clone_token = if fields.pk_is_auto {
4016            quote! { #pk_ident: #root::sql::Auto::Unset }
4017        } else {
4018            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4019        };
4020        let replicate_doc = if fields.pk_is_auto {
4021            quote! {
4022                /// Eloquent-style `replicate()` — return a clone of this
4023                /// row with the primary key reset to [`Auto::Unset`] so
4024                /// the copy is ready for `insert_pool` / `save_pool` to
4025                /// allocate a fresh autoincrement value. Every other
4026                /// field is `Clone`d verbatim. Issue #825.
4027                ///
4028                /// `auto_now_add` / `auto_now` timestamp fields are
4029                /// **not** reset (Eloquent's `replicate` doesn't reset
4030                /// them either) — pass them through the normal insert
4031                /// path if you want fresh values, or assign them
4032                /// explicitly after the call.
4033            }
4034        } else {
4035            quote! {
4036                /// Eloquent-style `replicate()` — clone this row
4037                /// verbatim. Because the primary key is **not** an
4038                /// `Auto<T>`, the clone keeps the source PK; the
4039                /// caller must overwrite `copy.<pk>` before inserting
4040                /// to avoid a unique-key violation. Issue #825.
4041            }
4042        };
4043        // 2026-06-07 — field-name / shortcut collision guard.
4044        //
4045        // The macro emits both `pub const <field>: <field>_col = ...`
4046        // (per-field typed-column const, used by the typed-builder
4047        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4048        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4049        // model has a field named e.g. `count`, both items would land
4050        // in the same inherent impl with the same name, and the
4051        // compiler rejects the derive with "duplicate definitions".
4052        //
4053        // Drop the conflicting shortcut for that model. Callers can
4054        // still reach the same behavior via
4055        // `QuerySet::<Model>::default().count(&pool)`.
4056        let column_names: ::std::collections::HashSet<String> = fields
4057            .column_entries
4058            .iter()
4059            .map(|c| c.ident.to_string())
4060            .collect();
4061        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4062            if column_names.contains(name) {
4063                quote! {}
4064            } else {
4065                tokens
4066            }
4067        };
4068        let count_method = emit_if_no_field_collision(
4069            "count",
4070            quote! {
4071                /// Count rows of this model — `SELECT COUNT(*) FROM
4072                /// <table>`. Eloquent `Model::count()` parity.
4073                ///
4074                /// Skipped on models that already declare a field named
4075                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4076                /// in that case.
4077                ///
4078                /// # Errors
4079                /// As [`CounterPool::count`].
4080                ///
4081                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4082                pub async fn count(
4083                    pool: &#root::sql::Pool,
4084                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4085                    use #root::sql::CounterPool as _;
4086                    #root::query::QuerySet::<Self>::default()
4087                        .count(pool)
4088                        .await
4089                }
4090            },
4091        );
4092        let value_method = emit_if_no_field_collision(
4093            "value",
4094            quote! {
4095                /// Pluck a single scalar from the first row.
4096                /// Eloquent `Model::query()->value($col)` parity.
4097                ///
4098                /// Skipped on models that already declare a field named
4099                /// `value`. Drop into
4100                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4101                ///
4102                /// # Errors
4103                /// As `ValuesFlatQuerySet::first`.
4104                pub async fn value<U>(
4105                    col: &str,
4106                    pool: &#root::sql::Pool,
4107                ) -> ::core::result::Result<
4108                    ::core::option::Option<U>,
4109                    #root::sql::ExecError,
4110                >
4111                where
4112                    U: #root::sql::MaybePgScalar
4113                        + #root::sql::MaybeMyScalar
4114                        + #root::sql::MaybeSqliteScalar
4115                        + ::core::marker::Send
4116                        + ::core::marker::Unpin,
4117                {
4118                    let _col_static: &'static str = Self::__resolve_col(col)?;
4119                    #root::query::QuerySet::<Self>::default()
4120                        .values_list_flat(_col_static)
4121                        .first::<U>(pool)
4122                        .await
4123                }
4124            },
4125        );
4126        let sum_method = emit_if_no_field_collision(
4127            "sum",
4128            quote! {
4129                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4130                /// Skipped on models that already declare a field named
4131                /// `sum`.
4132                ///
4133                /// # Errors
4134                /// As [`#root::sql::fetch_aggregate_pool`].
4135                pub async fn sum<U>(
4136                    col: &str,
4137                    pool: &#root::sql::Pool,
4138                ) -> ::core::result::Result<
4139                    ::core::option::Option<U>,
4140                    #root::sql::ExecError,
4141                >
4142                where
4143                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4144                        + #root::sql::MaybeMyFromRow
4145                        + #root::sql::MaybeSqliteFromRow
4146                        + ::core::marker::Send
4147                        + ::core::marker::Unpin,
4148                {
4149                    Self::__aggregate_one_pool::<U>(
4150                        col,
4151                        |c| #root::core::AggregateExpr::Sum(c),
4152                        pool,
4153                    )
4154                    .await
4155                }
4156            },
4157        );
4158        let avg_method = emit_if_no_field_collision(
4159            "avg",
4160            quote! {
4161                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4162                /// models that already declare a field named `avg`.
4163                ///
4164                /// # Errors
4165                /// As [`#root::sql::fetch_aggregate_pool`].
4166                pub async fn avg<U>(
4167                    col: &str,
4168                    pool: &#root::sql::Pool,
4169                ) -> ::core::result::Result<
4170                    ::core::option::Option<U>,
4171                    #root::sql::ExecError,
4172                >
4173                where
4174                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4175                        + #root::sql::MaybeMyFromRow
4176                        + #root::sql::MaybeSqliteFromRow
4177                        + ::core::marker::Send
4178                        + ::core::marker::Unpin,
4179                {
4180                    Self::__aggregate_one_pool::<U>(
4181                        col,
4182                        |c| #root::core::AggregateExpr::Avg(c),
4183                        pool,
4184                    )
4185                    .await
4186                }
4187            },
4188        );
4189        let min_method = emit_if_no_field_collision(
4190            "min",
4191            quote! {
4192                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4193                /// models that already declare a field named `min`.
4194                ///
4195                /// # Errors
4196                /// As [`#root::sql::fetch_aggregate_pool`].
4197                pub async fn min<U>(
4198                    col: &str,
4199                    pool: &#root::sql::Pool,
4200                ) -> ::core::result::Result<
4201                    ::core::option::Option<U>,
4202                    #root::sql::ExecError,
4203                >
4204                where
4205                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4206                        + #root::sql::MaybeMyFromRow
4207                        + #root::sql::MaybeSqliteFromRow
4208                        + ::core::marker::Send
4209                        + ::core::marker::Unpin,
4210                {
4211                    Self::__aggregate_one_pool::<U>(
4212                        col,
4213                        |c| #root::core::AggregateExpr::Min(c),
4214                        pool,
4215                    )
4216                    .await
4217                }
4218            },
4219        );
4220        let max_method = emit_if_no_field_collision(
4221            "max",
4222            quote! {
4223                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4224                /// models that already declare a field named `max`.
4225                ///
4226                /// # Errors
4227                /// As [`#root::sql::fetch_aggregate_pool`].
4228                pub async fn max<U>(
4229                    col: &str,
4230                    pool: &#root::sql::Pool,
4231                ) -> ::core::result::Result<
4232                    ::core::option::Option<U>,
4233                    #root::sql::ExecError,
4234                >
4235                where
4236                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4237                        + #root::sql::MaybeMyFromRow
4238                        + #root::sql::MaybeSqliteFromRow
4239                        + ::core::marker::Send
4240                        + ::core::marker::Unpin,
4241                {
4242                    Self::__aggregate_one_pool::<U>(
4243                        col,
4244                        |c| #root::core::AggregateExpr::Max(c),
4245                        pool,
4246                    )
4247                    .await
4248                }
4249            },
4250        );
4251        let first_method = emit_if_no_field_collision(
4252            "first",
4253            quote! {
4254                /// First row of this model. Eloquent `Model::first()`.
4255                /// Skipped on models that already declare a field named
4256                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4257                ///
4258                /// # Errors
4259                /// As `QuerySet::first`.
4260                pub async fn first(
4261                    pool: &#root::sql::Pool,
4262                ) -> ::core::result::Result<
4263                    ::core::option::Option<Self>,
4264                    #root::sql::ExecError,
4265                > {
4266                    #root::query::QuerySet::<Self>::default()
4267                        .first(pool)
4268                        .await
4269                }
4270            },
4271        );
4272        let last_method = emit_if_no_field_collision(
4273            "last",
4274            quote! {
4275                /// Last row of this model by primary-key DESC.
4276                /// Eloquent `Model::query()->latest('id')->first()`
4277                /// parity — fetches the highest-PK row without
4278                /// requiring the caller to spell the PK column.
4279                /// Returns `None` on an empty table.
4280                ///
4281                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4282                /// Skipped on models that already declare a field
4283                /// named `last`.
4284                ///
4285                /// # Errors
4286                /// As `QuerySet::last`.
4287                pub async fn last(
4288                    pool: &#root::sql::Pool,
4289                ) -> ::core::result::Result<
4290                    ::core::option::Option<Self>,
4291                    #root::sql::ExecError,
4292                > {
4293                    #root::query::QuerySet::<Self>::default()
4294                        .last(pool)
4295                        .await
4296                }
4297            },
4298        );
4299        quote! {
4300            /// Re-SELECT this row by its primary key and overwrite
4301            /// every in-memory field with the freshly-fetched value.
4302            /// Issue #825.
4303            ///
4304            /// Use this when the row may have been modified by another
4305            /// process / connection / job since you read it — e.g. after
4306            /// a queued task callback, or to re-sync stale UI state
4307            /// before re-saving.
4308            ///
4309            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4310            /// when the primary key no longer matches any row (e.g.
4311            /// the row was deleted concurrently).
4312            ///
4313            /// # Errors
4314            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4315            /// the PK no longer exists.
4316            ///
4317            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4318            pub async fn refresh_from_db(
4319                &mut self,
4320                pool: &#root::sql::Pool,
4321            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4322                use #root::sql::FetcherPool as _;
4323                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4324                    ::core::clone::Clone::clone(&self.#pk_ident),
4325                );
4326                let mut _rows: ::std::vec::Vec<Self> =
4327                    #root::query::QuerySet::<Self>::default()
4328                        .filter(::core::stringify!(#pk_ident), _pk_val)
4329                        .limit(1)
4330                        .fetch(pool)
4331                        .await?;
4332                match _rows.into_iter().next() {
4333                    ::core::option::Option::Some(_fresh) => {
4334                        *self = _fresh;
4335                        ::core::result::Result::Ok(())
4336                    }
4337                    ::core::option::Option::None => ::core::result::Result::Err(
4338                        #root::sql::ExecError::Driver(
4339                            #root::sql::sqlx::Error::RowNotFound,
4340                        ),
4341                    ),
4342                }
4343            }
4344
4345            /// Atomically increment the integer column `col` by
4346            /// `by` for this row. Equivalent to
4347            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4348            ///
4349            /// **Doesn't mutate `self`** — the in-memory copy is now
4350            /// stale; call [`Self::refresh_from_db_pool`] /
4351            /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4352            /// affected count (0 when the PK doesn't match any row,
4353            /// 1 on success).
4354            ///
4355            /// `col` is the Rust field name as a string; unknown
4356            /// fields surface as `UnknownField` at runtime. Negative
4357            /// `by` values atomically decrement (see also
4358            /// [`Self::decrement_pool`]).
4359            ///
4360            /// # Errors
4361            /// As [`UpdaterPool::execute_pool`].
4362            ///
4363            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4364            pub async fn increment(
4365                &self,
4366                col: &str,
4367                by: i64,
4368                pool: &#root::sql::Pool,
4369            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4370                Self::__increment_one(self, col, by, pool).await
4371            }
4372
4373            /// Sibling of [`Self::increment`] — atomically
4374            /// decrement this row's `col` by `by`. Eloquent
4375            /// `$model->decrement($col, $by)` parity. Equivalent to
4376            /// `self.increment(col, -by, &pool)`; the separate name
4377            /// keeps call sites readable.
4378            ///
4379            /// # Errors
4380            /// As [`Self::increment`].
4381            pub async fn decrement(
4382                &self,
4383                col: &str,
4384                by: i64,
4385                pool: &#root::sql::Pool,
4386            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4387                Self::__increment_one(self, col, -by, pool).await
4388            }
4389
4390            /// Bulk-increment: add `by` to `col` on every row of the
4391            /// table. Eloquent `Model::query()->increment($col, $by)`
4392            /// parity. Use for counters, score adjustments, view
4393            /// rollups.
4394            ///
4395            /// # Errors
4396            /// As [`UpdaterPool::execute_pool`].
4397            ///
4398            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4399            pub async fn increment_each(
4400                col: &str,
4401                by: i64,
4402                pool: &#root::sql::Pool,
4403            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4404                Self::__increment_all(col, by, pool).await
4405            }
4406
4407            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4408            ///
4409            /// # Errors
4410            /// As [`Self::increment_each`].
4411            pub async fn decrement_each(
4412                col: &str,
4413                by: i64,
4414                pool: &#root::sql::Pool,
4415            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4416                Self::__increment_all(col, -by, pool).await
4417            }
4418
4419            /// Internal: forward to
4420            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4421            /// One-line wrapper kept as a per-Model method so the
4422            /// macro's emitted `increment` / `decrement` instance
4423            /// calls don't have to thread `Self` through manually.
4424            #[doc(hidden)]
4425            pub async fn __increment_one(
4426                this: &Self,
4427                col: &str,
4428                by: i64,
4429                pool: &#root::sql::Pool,
4430            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4431                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4432                    ::core::clone::Clone::clone(&this.#pk_ident),
4433                );
4434                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4435                    ::core::stringify!(#pk_ident),
4436                    _pk_val,
4437                    col,
4438                    by,
4439                    pool,
4440                )
4441                .await
4442            }
4443
4444            /// Internal: forward to
4445            /// [`#root::sql::model_shortcuts::increment_all_pool`].
4446            #[doc(hidden)]
4447            pub async fn __increment_all(
4448                col: &str,
4449                by: i64,
4450                pool: &#root::sql::Pool,
4451            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4452                #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4453            }
4454
4455            /// Internal: forward to
4456            /// [`#root::sql::model_shortcuts::resolve_col`].
4457            #[doc(hidden)]
4458            pub fn __resolve_col(
4459                col: &str,
4460            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4461                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4462            }
4463
4464            /// Internal: forward to
4465            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4466            #[doc(hidden)]
4467            #[must_use]
4468            pub fn __add_signed_expr(
4469                col_static: &'static str,
4470                signed_by: i64,
4471            ) -> #root::core::Expr {
4472                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4473            }
4474
4475            /// Re-SELECT this row by its primary key and return a
4476            /// **new** instance with the freshly-fetched fields.
4477            /// Eloquent `Model::fresh()` parity — non-mutating
4478            /// counterpart of [`Self::refresh_from_db_pool`].
4479            ///
4480            /// Returns `Ok(None)` when the row was deleted
4481            /// concurrently — vs [`Self::refresh_from_db_pool`]
4482            /// which surfaces that as `RowNotFound` because
4483            /// in-place mutation has nothing to write to.
4484            ///
4485            /// Useful when you want to compare the in-memory
4486            /// instance against the persisted state (audit-style
4487            /// diffs, conflict detection) without mutating the
4488            /// reference you already hold.
4489            ///
4490            /// # Errors
4491            /// As [`FetcherPool::fetch`].
4492            ///
4493            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4494            pub async fn fresh(
4495                &self,
4496                pool: &#root::sql::Pool,
4497            ) -> ::core::result::Result<
4498                ::core::option::Option<Self>,
4499                #root::sql::ExecError,
4500            > {
4501                use #root::sql::FetcherPool as _;
4502                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4503                    ::core::clone::Clone::clone(&self.#pk_ident),
4504                );
4505                let _rows: ::std::vec::Vec<Self> =
4506                    #root::query::QuerySet::<Self>::default()
4507                        .filter(::core::stringify!(#pk_ident), _pk_val)
4508                        .limit(1)
4509                        .fetch(pool)
4510                        .await?;
4511                ::core::result::Result::Ok(_rows.into_iter().next())
4512            }
4513
4514            #replicate_doc
4515            #[must_use]
4516            pub fn replicate(&self) -> Self {
4517                Self {
4518                    #pk_clone_token,
4519                    #( #other_field_clones, )*
4520                }
4521            }
4522
4523            #first_method
4524
4525            #last_method
4526
4527            /// Throwing counterpart of [`Self::first_pool`] —
4528            /// errors with `RowNotFound` when the table is empty.
4529            /// Eloquent `Model::firstOrFail()` parity.
4530            ///
4531            /// # Errors
4532            /// As [`Self::first_pool`]; additionally
4533            /// [`sqlx::Error::RowNotFound`] on empty tables.
4534            ///
4535            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4536            pub async fn first_or_fail(
4537                pool: &#root::sql::Pool,
4538            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4539                // Route through the queryset rather than `Self::first`,
4540                // which is suppressed on models with a field named
4541                // `first` (field/shortcut collision guard).
4542                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4543                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4544                    ::core::option::Option::None => ::core::result::Result::Err(
4545                        #root::sql::ExecError::Driver(
4546                            #root::sql::sqlx::Error::RowNotFound,
4547                        ),
4548                    ),
4549                }
4550            }
4551
4552            /// Single-column projection — `SELECT <col> FROM
4553            /// <table>`. Returns `Vec<U>` where each element is the
4554            /// decoded value of the column. Eloquent
4555            /// `Model::pluck($column)` parity.
4556            ///
4557            /// Thin wrapper over `QuerySet::<Self>::default()
4558            /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4559            /// be decodable from the column's SQL type on every
4560            /// dialect the binary targets (common picks: `i64` /
4561            /// `i32` / `String` / `bool` / `f64`).
4562            ///
4563            /// # Errors
4564            /// As `ValuesFlatQuerySet::fetch`.
4565            pub async fn pluck<U>(
4566                col: &'static str,
4567                pool: &#root::sql::Pool,
4568            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4569            where
4570                U: #root::sql::MaybePgScalar
4571                    + #root::sql::MaybeMyScalar
4572                    + #root::sql::MaybeSqliteScalar
4573                    + ::core::marker::Send
4574                    + ::core::marker::Unpin,
4575            {
4576                #root::query::QuerySet::<Self>::default()
4577                    .values_list_flat(col)
4578                    .fetch::<U>(pool)
4579                    .await
4580            }
4581
4582            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4583            /// stream every row of this model in batches of `n`,
4584            /// invoking the callback once per batch. Stable PK-ASC
4585            /// ordering so the LIMIT/OFFSET pagination is
4586            /// deterministic across drivers.
4587            ///
4588            /// The callback is async — it can do further DB work
4589            /// (writes, related-row lookups, queue dispatch) per
4590            /// batch. Return `Err(...)` from the callback to abort
4591            /// the iteration early; the error bubbles up.
4592            ///
4593            /// **When to use**: bulk processing flows that can't
4594            /// fit the whole table in memory — sending newsletters,
4595            /// running data migrations, computing summary
4596            /// statistics. For small / known-bounded tables, plain
4597            /// `Self::all(&pool)` is simpler.
4598            ///
4599            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4600            /// large tables. For multi-million-row scans prefer
4601            /// keyset-by-PK (the standard "WHERE id > last_seen"
4602            /// shape) over `chunk(...)`.
4603            ///
4604            /// Skipped on models without a primary key — chunking
4605            /// needs a stable order to avoid skipping / repeating
4606            /// rows across batches.
4607            pub async fn chunk<F, Fut>(
4608                n: i64,
4609                pool: &#root::sql::Pool,
4610                mut cb: F,
4611            ) -> ::core::result::Result<(), #root::sql::ExecError>
4612            where
4613                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4614                Fut: ::core::future::Future<
4615                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4616                >,
4617            {
4618                use #root::sql::FetcherPool as _;
4619                let pk_col = match Self::primary_key_column() {
4620                    ::core::option::Option::Some(c) => c,
4621                    ::core::option::Option::None => {
4622                        return ::core::result::Result::Ok(());
4623                    }
4624                };
4625                let mut offset: i64 = 0;
4626                loop {
4627                    let rows: ::std::vec::Vec<Self> =
4628                        #root::query::QuerySet::<Self>::default()
4629                            .order_by(&[(pk_col, false)])
4630                            .limit(n)
4631                            .offset(offset)
4632                            .fetch(pool)
4633                            .await?;
4634                    if rows.is_empty() {
4635                        return ::core::result::Result::Ok(());
4636                    }
4637                    let len = rows.len() as i64;
4638                    cb(rows).await?;
4639                    if len < n {
4640                        return ::core::result::Result::Ok(());
4641                    }
4642                    offset += n;
4643                }
4644            }
4645
4646            /// Eloquent `Model::chunkById($n, fn (...))` — same
4647            /// per-batch callback shape as [`Self::chunk`], but uses
4648            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4649            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4650            /// — the right choice for multi-million-row sweeps.
4651            ///
4652            /// Requires the primary key to be a signed integer type
4653            /// (`i64` / `i32`); the keyset comparison rides on
4654            /// `__rustango_pk_value()` lowering through
4655            /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4656            /// non-integer PKs (UUID, String) — those should use
4657            /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4658            /// keyset loop.
4659            ///
4660            /// Callback errors abort iteration; the error bubbles up
4661            /// unchanged. Empty table → callback invoked zero times.
4662            pub async fn chunk_by_id<F, Fut>(
4663                n: i64,
4664                pool: &#root::sql::Pool,
4665                mut cb: F,
4666            ) -> ::core::result::Result<(), #root::sql::ExecError>
4667            where
4668                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4669                Fut: ::core::future::Future<
4670                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4671                >,
4672            {
4673                use #root::sql::FetcherPool as _;
4674                let pk_col = match Self::primary_key_column() {
4675                    ::core::option::Option::Some(c) => c,
4676                    ::core::option::Option::None => {
4677                        return ::core::result::Result::Ok(());
4678                    }
4679                };
4680                // Track the largest PK seen so the next batch picks
4681                // up from there. `i64::MIN` as the sentinel — the
4682                // very first iteration's `> MIN` matches every row,
4683                // so the loop entry is uniform with subsequent
4684                // iterations.
4685                let mut last_seen: i64 = i64::MIN;
4686                loop {
4687                    let key = ::std::format!("{}__gt", pk_col);
4688                    let rows: ::std::vec::Vec<Self> =
4689                        #root::query::QuerySet::<Self>::default()
4690                            .filter(key.as_str(), last_seen)
4691                            .order_by(&[(pk_col, false)])
4692                            .limit(n)
4693                            .fetch(pool)
4694                            .await?;
4695                    if rows.is_empty() {
4696                        return ::core::result::Result::Ok(());
4697                    }
4698                    let len = rows.len() as i64;
4699                    // Capture the last row's PK BEFORE moving rows
4700                    // into the callback.
4701                    let max_pk = match rows
4702                        .last()
4703                        .map(|r| r.__rustango_pk_value())
4704                    {
4705                        ::core::option::Option::Some(
4706                            #root::core::SqlValue::I64(v),
4707                        ) => v,
4708                        ::core::option::Option::Some(
4709                            #root::core::SqlValue::I32(v),
4710                        ) => i64::from(v),
4711                        _ => return ::core::result::Result::Ok(()),
4712                    };
4713                    cb(rows).await?;
4714                    if len < n {
4715                        return ::core::result::Result::Ok(());
4716                    }
4717                    last_seen = max_pk;
4718                }
4719            }
4720
4721            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4722            /// per-row callback companion to [`Self::chunk`].
4723            /// Streams every row in keyset-paginated batches of
4724            /// `batch` size, calling `cb` once per row.
4725            ///
4726            /// Inherits the keyset-paginated scan of
4727            /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4728            /// non-integer PKs are silently a no-op).
4729            ///
4730            /// ```ignore
4731            /// Post::each(500, &pool, |p| async move {
4732            ///     reindex(p).await?;
4733            ///     Ok(())
4734            /// }).await?;
4735            /// ```
4736            ///
4737            /// Return `Err(...)` from the callback to abort
4738            /// iteration; the error bubbles up unchanged.
4739            pub async fn each<F, Fut>(
4740                batch: i64,
4741                pool: &#root::sql::Pool,
4742                mut cb: F,
4743            ) -> ::core::result::Result<(), #root::sql::ExecError>
4744            where
4745                F: ::core::ops::FnMut(Self) -> Fut,
4746                Fut: ::core::future::Future<
4747                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4748                >,
4749            {
4750                use #root::sql::FetcherPool as _;
4751                let pk_col = match Self::primary_key_column() {
4752                    ::core::option::Option::Some(c) => c,
4753                    ::core::option::Option::None => {
4754                        return ::core::result::Result::Ok(());
4755                    }
4756                };
4757                let mut last_seen: i64 = i64::MIN;
4758                loop {
4759                    let key = ::std::format!("{}__gt", pk_col);
4760                    let rows: ::std::vec::Vec<Self> =
4761                        #root::query::QuerySet::<Self>::default()
4762                            .filter(key.as_str(), last_seen)
4763                            .order_by(&[(pk_col, false)])
4764                            .limit(batch)
4765                            .fetch(pool)
4766                            .await?;
4767                    if rows.is_empty() {
4768                        return ::core::result::Result::Ok(());
4769                    }
4770                    let len = rows.len() as i64;
4771                    let max_pk = match rows
4772                        .last()
4773                        .map(|r| r.__rustango_pk_value())
4774                    {
4775                        ::core::option::Option::Some(
4776                            #root::core::SqlValue::I64(v),
4777                        ) => v,
4778                        ::core::option::Option::Some(
4779                            #root::core::SqlValue::I32(v),
4780                        ) => i64::from(v),
4781                        _ => return ::core::result::Result::Ok(()),
4782                    };
4783                    for row in rows {
4784                        cb(row).await?;
4785                    }
4786                    if len < batch {
4787                        return ::core::result::Result::Ok(());
4788                    }
4789                    last_seen = max_pk;
4790                }
4791            }
4792
4793            /// Delete every row of this model — `TRUNCATE TABLE
4794            /// <table> RESTART IDENTITY CASCADE` on Postgres,
4795            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4796            /// support `TRUNCATE` inside foreign-key constraints
4797            /// or — for SQLite — at all). Eloquent `Model::truncate()`
4798            /// parity.
4799            ///
4800            /// **Use only in tests / fixture-reset flows.** Production
4801            /// writes through this would silently bypass the
4802            /// `pre_delete` / `post_delete` signals (no per-row hooks
4803            /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4804            /// row's audit-log entry.
4805            ///
4806            /// # Errors
4807            /// As [`raw_execute_pool`].
4808            ///
4809            /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4810            pub async fn truncate(
4811                pool: &#root::sql::Pool,
4812            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4813                let _table = <Self as #root::core::Model>::SCHEMA.table;
4814                let _dialect = pool.dialect();
4815                let _quoted = _dialect.quote_ident(_table);
4816                let _sql = if _dialect.name() == "postgres" {
4817                    ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4818                } else {
4819                    ::std::format!("DELETE FROM {}", _quoted)
4820                };
4821                #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4822            }
4823
4824            /// Bulk-delete every row whose primary key is in
4825            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4826            /// Returns the affected row count.
4827            ///
4828            /// Eloquent `Model::destroy([1, 2, 3])` parity.
4829            /// Empty `pks` is a no-op (returns 0).
4830            ///
4831            /// Accepts any iterable whose elements are
4832            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4833            /// `[i64; N]`, etc.
4834            ///
4835            /// # Errors
4836            /// As `delete_pool`.
4837            pub async fn destroy<V>(
4838                pks: impl ::core::iter::IntoIterator<Item = V>,
4839                pool: &#root::sql::Pool,
4840            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4841            where
4842                V: ::core::convert::Into<#root::core::SqlValue>,
4843            {
4844                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4845                    pks.into_iter().map(::core::convert::Into::into).collect();
4846                if _values.is_empty() {
4847                    return ::core::result::Result::Ok(0);
4848                }
4849                let _query = #root::core::DeleteQuery {
4850                    model: <Self as #root::core::Model>::SCHEMA,
4851                    where_clause: #root::core::WhereExpr::Predicate(
4852                        #root::core::Filter {
4853                            column: <Self as #root::core::Model>::SCHEMA
4854                                .primary_key()
4855                                .ok_or_else(|| {
4856                                    #root::sql::ExecError::Sql(
4857                                        #root::sql::SqlError::MissingPrimaryKey,
4858                                    )
4859                                })?
4860                                .column,
4861                            op: #root::core::Op::In,
4862                            value: #root::core::SqlValue::List(_values),
4863                        },
4864                    ),
4865                };
4866                #root::sql::delete_pool(pool, &_query).await
4867            }
4868
4869            /// Fetch every row where `<col> = <val>`. Eloquent
4870            /// `Model::where($col, $val)->get()` parity.
4871            ///
4872            /// Thin wrapper over `QuerySet::<Self>::default()
4873            /// .filter(col, val).fetch(pool)`. For one row,
4874            /// use [`Self::first_where_pool`]; for a chain that
4875            /// needs further `.filter()` / `.order_by()` /
4876            /// `.limit()`, drop down to `Self::query().filter(...)`
4877            /// directly.
4878            ///
4879            /// `val` accepts any value `Into<SqlValue>` so plain
4880            /// strings, ints, UUIDs, etc. all work.
4881            ///
4882            /// # Errors
4883            /// As [`FetcherPool::fetch`].
4884            ///
4885            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4886            pub async fn where_(
4887                col: &str,
4888                val: impl ::core::convert::Into<#root::core::SqlValue>,
4889                pool: &#root::sql::Pool,
4890            ) -> ::core::result::Result<
4891                ::std::vec::Vec<Self>,
4892                #root::sql::ExecError,
4893            > {
4894                use #root::sql::FetcherPool as _;
4895                #root::query::QuerySet::<Self>::default()
4896                    .filter(col, val)
4897                    .fetch(pool)
4898                    .await
4899            }
4900
4901            /// Fetch every row where `<col> IN (vals)`. Eloquent
4902            /// `Model::whereIn($col, $vals)->get()` parity. Empty
4903            /// `vals` returns no rows (matches SQL's empty-IN
4904            /// semantics).
4905            ///
4906            /// `vals` accepts any iterable whose items are
4907            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4908            /// etc.
4909            ///
4910            /// # Errors
4911            /// As [`FetcherPool::fetch`].
4912            ///
4913            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4914            pub async fn where_in<V>(
4915                col: &str,
4916                vals: impl ::core::iter::IntoIterator<Item = V>,
4917                pool: &#root::sql::Pool,
4918            ) -> ::core::result::Result<
4919                ::std::vec::Vec<Self>,
4920                #root::sql::ExecError,
4921            >
4922            where
4923                V: ::core::convert::Into<#root::core::SqlValue>,
4924            {
4925                use #root::sql::FetcherPool as _;
4926                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4927                    vals.into_iter().map(::core::convert::Into::into).collect();
4928                if _values.is_empty() {
4929                    return ::core::result::Result::Ok(::std::vec::Vec::new());
4930                }
4931                let _key = ::std::format!("{}__in", col);
4932                #root::query::QuerySet::<Self>::default()
4933                    .filter(&_key, #root::core::SqlValue::List(_values))
4934                    .fetch(pool)
4935                    .await
4936            }
4937
4938            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4939            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4940            /// `vals` returns every row (matches SQL's empty-NOT-IN
4941            /// semantics — vacuously true for every row).
4942            ///
4943            /// # Errors
4944            /// As [`FetcherPool::fetch`].
4945            ///
4946            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4947            pub async fn where_not_in<V>(
4948                col: &str,
4949                vals: impl ::core::iter::IntoIterator<Item = V>,
4950                pool: &#root::sql::Pool,
4951            ) -> ::core::result::Result<
4952                ::std::vec::Vec<Self>,
4953                #root::sql::ExecError,
4954            >
4955            where
4956                V: ::core::convert::Into<#root::core::SqlValue>,
4957            {
4958                use #root::sql::FetcherPool as _;
4959                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4960                    vals.into_iter().map(::core::convert::Into::into).collect();
4961                if _values.is_empty() {
4962                    return #root::query::QuerySet::<Self>::default()
4963                        .fetch(pool)
4964                        .await;
4965                }
4966                let _key = ::std::format!("{}__not_in", col);
4967                #root::query::QuerySet::<Self>::default()
4968                    .filter(&_key, #root::core::SqlValue::List(_values))
4969                    .fetch(pool)
4970                    .await
4971            }
4972
4973            /// Fetch every row where `<col> IS NULL`. Eloquent
4974            /// `Model::whereNull($col)->get()` parity.
4975            ///
4976            /// # Errors
4977            /// As [`FetcherPool::fetch`].
4978            ///
4979            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4980            pub async fn where_null(
4981                col: &str,
4982                pool: &#root::sql::Pool,
4983            ) -> ::core::result::Result<
4984                ::std::vec::Vec<Self>,
4985                #root::sql::ExecError,
4986            > {
4987                use #root::sql::FetcherPool as _;
4988                let _key = ::std::format!("{}__isnull", col);
4989                #root::query::QuerySet::<Self>::default()
4990                    .filter(&_key, #root::core::SqlValue::Bool(true))
4991                    .fetch(pool)
4992                    .await
4993            }
4994
4995            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4996            /// `Model::whereNotNull($col)->get()` parity.
4997            ///
4998            /// # Errors
4999            /// As [`FetcherPool::fetch`].
5000            ///
5001            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5002            pub async fn where_not_null(
5003                col: &str,
5004                pool: &#root::sql::Pool,
5005            ) -> ::core::result::Result<
5006                ::std::vec::Vec<Self>,
5007                #root::sql::ExecError,
5008            > {
5009                use #root::sql::FetcherPool as _;
5010                let _key = ::std::format!("{}__isnull", col);
5011                #root::query::QuerySet::<Self>::default()
5012                    .filter(&_key, #root::core::SqlValue::Bool(false))
5013                    .fetch(pool)
5014                    .await
5015            }
5016
5017            /// Fetch up to `n` rows in random order. Eloquent
5018            /// `Model::inRandomOrder()->limit($n)->get()` /
5019            /// `Model::query()->inRandomOrder()->get()->take($n)`
5020            /// parity. **Performance caveat**: random ordering
5021            /// forces a full table scan + per-row random key sort;
5022            /// the optimizer cannot use an index. Prefer a
5023            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5024            ///
5025            /// # Errors
5026            /// As [`FetcherPool::fetch`].
5027            ///
5028            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5029            pub async fn random_n(
5030                n: i64,
5031                pool: &#root::sql::Pool,
5032            ) -> ::core::result::Result<
5033                ::std::vec::Vec<Self>,
5034                #root::sql::ExecError,
5035            > {
5036                use #root::sql::FetcherPool as _;
5037                #root::query::QuerySet::<Self>::default()
5038                    .order_random()
5039                    .limit(n)
5040                    .fetch(pool)
5041                    .await
5042            }
5043
5044            /// Fetch one row in random order. Eloquent
5045            /// `Model::inRandomOrder()->first()` parity. Same
5046            /// performance caveat as [`Self::random_n_pool`].
5047            ///
5048            /// # Errors
5049            /// As [`FetcherPool::fetch`].
5050            ///
5051            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5052            pub async fn random(
5053                pool: &#root::sql::Pool,
5054            ) -> ::core::result::Result<
5055                ::core::option::Option<Self>,
5056                #root::sql::ExecError,
5057            > {
5058                ::core::result::Result::Ok(
5059                    Self::random_n(1, pool).await?.into_iter().next(),
5060                )
5061            }
5062
5063            /// Fetch every row ordered ASC by `field`. Eloquent
5064            /// `Model::oldest($field)->get()` parity — the multi-row
5065            /// counterpart of [`Self::earliest_pool`].
5066            ///
5067            /// # Errors
5068            /// As [`FetcherPool::fetch`].
5069            ///
5070            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5071            pub async fn oldest(
5072                field: &str,
5073                pool: &#root::sql::Pool,
5074            ) -> ::core::result::Result<
5075                ::std::vec::Vec<Self>,
5076                #root::sql::ExecError,
5077            > {
5078                use #root::sql::FetcherPool as _;
5079                #root::query::QuerySet::<Self>::default()
5080                    .order_by(&[(field, false)])
5081                    .fetch(pool)
5082                    .await
5083            }
5084
5085            /// Fetch every row ordered DESC by `field`. Eloquent
5086            /// `Model::latest($field)->get()` parity — the multi-row
5087            /// counterpart of [`Self::latest_pool`].
5088            ///
5089            /// # Errors
5090            /// As [`FetcherPool::fetch`].
5091            ///
5092            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5093            pub async fn newest(
5094                field: &str,
5095                pool: &#root::sql::Pool,
5096            ) -> ::core::result::Result<
5097                ::std::vec::Vec<Self>,
5098                #root::sql::ExecError,
5099            > {
5100                use #root::sql::FetcherPool as _;
5101                #root::query::QuerySet::<Self>::default()
5102                    .order_by(&[(field, true)])
5103                    .fetch(pool)
5104                    .await
5105            }
5106
5107            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5108            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5109            /// Routes through the existing `__year` lookup suffix
5110            /// (issue #829).
5111            ///
5112            /// # Errors
5113            /// As [`FetcherPool::fetch`].
5114            ///
5115            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5116            pub async fn where_year(
5117                col: &str,
5118                year: i64,
5119                pool: &#root::sql::Pool,
5120            ) -> ::core::result::Result<
5121                ::std::vec::Vec<Self>,
5122                #root::sql::ExecError,
5123            > {
5124                use #root::sql::FetcherPool as _;
5125                let _key = ::std::format!("{}__year", col);
5126                #root::query::QuerySet::<Self>::default()
5127                    .filter(&_key, #root::core::SqlValue::I64(year))
5128                    .fetch(pool)
5129                    .await
5130            }
5131
5132            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5133            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5134            /// `month` is 1–12.
5135            ///
5136            /// # Errors
5137            /// As [`FetcherPool::fetch`].
5138            ///
5139            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5140            pub async fn where_month(
5141                col: &str,
5142                month: i64,
5143                pool: &#root::sql::Pool,
5144            ) -> ::core::result::Result<
5145                ::std::vec::Vec<Self>,
5146                #root::sql::ExecError,
5147            > {
5148                use #root::sql::FetcherPool as _;
5149                let _key = ::std::format!("{}__month", col);
5150                #root::query::QuerySet::<Self>::default()
5151                    .filter(&_key, #root::core::SqlValue::I64(month))
5152                    .fetch(pool)
5153                    .await
5154            }
5155
5156            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5157            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5158            /// `day` is 1–31.
5159            ///
5160            /// # Errors
5161            /// As [`FetcherPool::fetch`].
5162            ///
5163            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5164            pub async fn where_day(
5165                col: &str,
5166                day: i64,
5167                pool: &#root::sql::Pool,
5168            ) -> ::core::result::Result<
5169                ::std::vec::Vec<Self>,
5170                #root::sql::ExecError,
5171            > {
5172                use #root::sql::FetcherPool as _;
5173                let _key = ::std::format!("{}__day", col);
5174                #root::query::QuerySet::<Self>::default()
5175                    .filter(&_key, #root::core::SqlValue::I64(day))
5176                    .fetch(pool)
5177                    .await
5178            }
5179
5180            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5181            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5182            /// `hour` is 0–23.
5183            ///
5184            /// # Errors
5185            /// As [`FetcherPool::fetch`].
5186            ///
5187            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5188            pub async fn where_hour(
5189                col: &str,
5190                hour: i64,
5191                pool: &#root::sql::Pool,
5192            ) -> ::core::result::Result<
5193                ::std::vec::Vec<Self>,
5194                #root::sql::ExecError,
5195            > {
5196                use #root::sql::FetcherPool as _;
5197                let _key = ::std::format!("{}__hour", col);
5198                #root::query::QuerySet::<Self>::default()
5199                    .filter(&_key, #root::core::SqlValue::I64(hour))
5200                    .fetch(pool)
5201                    .await
5202            }
5203
5204            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5205            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5206            /// `minute` is 0–59.
5207            ///
5208            /// # Errors
5209            /// As [`FetcherPool::fetch`].
5210            ///
5211            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5212            pub async fn where_minute(
5213                col: &str,
5214                minute: i64,
5215                pool: &#root::sql::Pool,
5216            ) -> ::core::result::Result<
5217                ::std::vec::Vec<Self>,
5218                #root::sql::ExecError,
5219            > {
5220                use #root::sql::FetcherPool as _;
5221                let _key = ::std::format!("{}__minute", col);
5222                #root::query::QuerySet::<Self>::default()
5223                    .filter(&_key, #root::core::SqlValue::I64(minute))
5224                    .fetch(pool)
5225                    .await
5226            }
5227
5228            /// Fetch every row where `<col> LIKE <pattern>` —
5229            /// caller-supplied pattern (must include `%` / `_`
5230            /// wildcards manually). Eloquent `Model::whereLike`
5231            /// parity.
5232            ///
5233            /// # Errors
5234            /// As [`FetcherPool::fetch`].
5235            ///
5236            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5237            pub async fn where_like(
5238                col: &str,
5239                pattern: impl ::core::convert::Into<::std::string::String>,
5240                pool: &#root::sql::Pool,
5241            ) -> ::core::result::Result<
5242                ::std::vec::Vec<Self>,
5243                #root::sql::ExecError,
5244            > {
5245                use #root::sql::FetcherPool as _;
5246                let _key = ::std::format!("{}__like", col);
5247                #root::query::QuerySet::<Self>::default()
5248                    .filter(
5249                        &_key,
5250                        #root::core::SqlValue::String(pattern.into()),
5251                    )
5252                    .fetch(pool)
5253                    .await
5254            }
5255
5256            /// Fetch every row where `<col> ILIKE <pattern>` —
5257            /// case-insensitive LIKE (PG native, MySQL/SQLite
5258            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5259            ///
5260            /// # Errors
5261            /// As [`FetcherPool::fetch`].
5262            ///
5263            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5264            pub async fn where_ilike(
5265                col: &str,
5266                pattern: impl ::core::convert::Into<::std::string::String>,
5267                pool: &#root::sql::Pool,
5268            ) -> ::core::result::Result<
5269                ::std::vec::Vec<Self>,
5270                #root::sql::ExecError,
5271            > {
5272                use #root::sql::FetcherPool as _;
5273                let _key = ::std::format!("{}__ilike", col);
5274                #root::query::QuerySet::<Self>::default()
5275                    .filter(
5276                        &_key,
5277                        #root::core::SqlValue::String(pattern.into()),
5278                    )
5279                    .fetch(pool)
5280                    .await
5281            }
5282
5283            /// Fetch every row where `<col>` starts with `prefix`
5284            /// (auto-appends `%`). Eloquent
5285            /// `whereLike("col", "$prefix%")` parity.
5286            ///
5287            /// # Errors
5288            /// As [`FetcherPool::fetch`].
5289            ///
5290            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5291            pub async fn where_starts_with(
5292                col: &str,
5293                prefix: impl ::core::convert::Into<::std::string::String>,
5294                pool: &#root::sql::Pool,
5295            ) -> ::core::result::Result<
5296                ::std::vec::Vec<Self>,
5297                #root::sql::ExecError,
5298            > {
5299                use #root::sql::FetcherPool as _;
5300                let _key = ::std::format!("{}__startswith", col);
5301                #root::query::QuerySet::<Self>::default()
5302                    .filter(
5303                        &_key,
5304                        #root::core::SqlValue::String(prefix.into()),
5305                    )
5306                    .fetch(pool)
5307                    .await
5308            }
5309
5310            /// Fetch every row where `<col>` ends with `suffix`
5311            /// (auto-prepends `%`). Eloquent
5312            /// `whereLike("col", "%$suffix")` parity.
5313            ///
5314            /// # Errors
5315            /// As [`FetcherPool::fetch`].
5316            ///
5317            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5318            pub async fn where_ends_with(
5319                col: &str,
5320                suffix: impl ::core::convert::Into<::std::string::String>,
5321                pool: &#root::sql::Pool,
5322            ) -> ::core::result::Result<
5323                ::std::vec::Vec<Self>,
5324                #root::sql::ExecError,
5325            > {
5326                use #root::sql::FetcherPool as _;
5327                let _key = ::std::format!("{}__endswith", col);
5328                #root::query::QuerySet::<Self>::default()
5329                    .filter(
5330                        &_key,
5331                        #root::core::SqlValue::String(suffix.into()),
5332                    )
5333                    .fetch(pool)
5334                    .await
5335            }
5336
5337            /// Fetch every row where `<col>` contains `substr`
5338            /// (auto-wraps with `%`).
5339            /// Eloquent `whereLike("col", "%$substr%")` parity.
5340            ///
5341            /// # Errors
5342            /// As [`FetcherPool::fetch`].
5343            ///
5344            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5345            pub async fn where_contains(
5346                col: &str,
5347                substr: impl ::core::convert::Into<::std::string::String>,
5348                pool: &#root::sql::Pool,
5349            ) -> ::core::result::Result<
5350                ::std::vec::Vec<Self>,
5351                #root::sql::ExecError,
5352            > {
5353                use #root::sql::FetcherPool as _;
5354                let _key = ::std::format!("{}__contains", col);
5355                #root::query::QuerySet::<Self>::default()
5356                    .filter(
5357                        &_key,
5358                        #root::core::SqlValue::String(substr.into()),
5359                    )
5360                    .fetch(pool)
5361                    .await
5362            }
5363
5364            /// Fetch every row where `<col> > val`. Eloquent
5365            /// `Model::where($col, ">", $val)->get()` parity.
5366            ///
5367            /// # Errors
5368            /// As [`FetcherPool::fetch`].
5369            ///
5370            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5371            pub async fn where_gt(
5372                col: &str,
5373                val: impl ::core::convert::Into<#root::core::SqlValue>,
5374                pool: &#root::sql::Pool,
5375            ) -> ::core::result::Result<
5376                ::std::vec::Vec<Self>,
5377                #root::sql::ExecError,
5378            > {
5379                use #root::sql::FetcherPool as _;
5380                let _key = ::std::format!("{}__gt", col);
5381                #root::query::QuerySet::<Self>::default()
5382                    .filter(&_key, val)
5383                    .fetch(pool)
5384                    .await
5385            }
5386
5387            /// Fetch every row where `<col> >= val`. Eloquent
5388            /// `Model::where($col, ">=", $val)->get()` parity.
5389            ///
5390            /// # Errors
5391            /// As [`FetcherPool::fetch`].
5392            ///
5393            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5394            pub async fn where_gte(
5395                col: &str,
5396                val: impl ::core::convert::Into<#root::core::SqlValue>,
5397                pool: &#root::sql::Pool,
5398            ) -> ::core::result::Result<
5399                ::std::vec::Vec<Self>,
5400                #root::sql::ExecError,
5401            > {
5402                use #root::sql::FetcherPool as _;
5403                let _key = ::std::format!("{}__gte", col);
5404                #root::query::QuerySet::<Self>::default()
5405                    .filter(&_key, val)
5406                    .fetch(pool)
5407                    .await
5408            }
5409
5410            /// Fetch every row where `<col> < val`. Eloquent
5411            /// `Model::where($col, "<", $val)->get()` parity.
5412            ///
5413            /// # Errors
5414            /// As [`FetcherPool::fetch`].
5415            ///
5416            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5417            pub async fn where_lt(
5418                col: &str,
5419                val: impl ::core::convert::Into<#root::core::SqlValue>,
5420                pool: &#root::sql::Pool,
5421            ) -> ::core::result::Result<
5422                ::std::vec::Vec<Self>,
5423                #root::sql::ExecError,
5424            > {
5425                use #root::sql::FetcherPool as _;
5426                let _key = ::std::format!("{}__lt", col);
5427                #root::query::QuerySet::<Self>::default()
5428                    .filter(&_key, val)
5429                    .fetch(pool)
5430                    .await
5431            }
5432
5433            /// Fetch every row where `<col> <= val`. Eloquent
5434            /// `Model::where($col, "<=", $val)->get()` parity.
5435            ///
5436            /// # Errors
5437            /// As [`FetcherPool::fetch`].
5438            ///
5439            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5440            pub async fn where_lte(
5441                col: &str,
5442                val: impl ::core::convert::Into<#root::core::SqlValue>,
5443                pool: &#root::sql::Pool,
5444            ) -> ::core::result::Result<
5445                ::std::vec::Vec<Self>,
5446                #root::sql::ExecError,
5447            > {
5448                use #root::sql::FetcherPool as _;
5449                let _key = ::std::format!("{}__lte", col);
5450                #root::query::QuerySet::<Self>::default()
5451                    .filter(&_key, val)
5452                    .fetch(pool)
5453                    .await
5454            }
5455
5456            /// Fetch every row where `<col> <> val`. Eloquent
5457            /// `Model::where($col, "!=", $val)->get()` parity.
5458            ///
5459            /// # Errors
5460            /// As [`FetcherPool::fetch`].
5461            ///
5462            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5463            pub async fn where_ne(
5464                col: &str,
5465                val: impl ::core::convert::Into<#root::core::SqlValue>,
5466                pool: &#root::sql::Pool,
5467            ) -> ::core::result::Result<
5468                ::std::vec::Vec<Self>,
5469                #root::sql::ExecError,
5470            > {
5471                use #root::sql::FetcherPool as _;
5472                let _key = ::std::format!("{}__ne", col);
5473                #root::query::QuerySet::<Self>::default()
5474                    .filter(&_key, val)
5475                    .fetch(pool)
5476                    .await
5477            }
5478
5479            /// Fetch every row matching `<col> = val` for ANY of the
5480            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5481            /// parity. Empty `cols` returns no rows.
5482            ///
5483            /// Resolves each `&str` column to its SCHEMA-registered
5484            /// `&'static str` once and builds a single OR-composed
5485            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5486            ///
5487            /// # Errors
5488            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5489            /// when any column is not declared on the model.
5490            ///
5491            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5492            pub async fn where_any(
5493                cols: &[&str],
5494                val: impl ::core::convert::Into<#root::core::SqlValue>,
5495                pool: &#root::sql::Pool,
5496            ) -> ::core::result::Result<
5497                ::std::vec::Vec<Self>,
5498                #root::sql::ExecError,
5499            > {
5500                Self::__where_multi(cols, val, false, pool).await
5501            }
5502
5503            /// Fetch every row matching `<col> = val` for ALL listed
5504            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5505            /// Empty `cols` returns every row (vacuous AND).
5506            ///
5507            /// # Errors
5508            /// As [`Self::where_any`].
5509            pub async fn where_all(
5510                cols: &[&str],
5511                val: impl ::core::convert::Into<#root::core::SqlValue>,
5512                pool: &#root::sql::Pool,
5513            ) -> ::core::result::Result<
5514                ::std::vec::Vec<Self>,
5515                #root::sql::ExecError,
5516            > {
5517                Self::__where_multi(cols, val, true, pool).await
5518            }
5519
5520            /// Internal: build a Q expression composing `cols` via
5521            /// AND (`all`) or OR (`!all`), then fetch. Backs
5522            /// `where_any` / `where_all`.
5523            #[doc(hidden)]
5524            pub async fn __where_multi(
5525                cols: &[&str],
5526                val: impl ::core::convert::Into<#root::core::SqlValue>,
5527                all: bool,
5528                pool: &#root::sql::Pool,
5529            ) -> ::core::result::Result<
5530                ::std::vec::Vec<Self>,
5531                #root::sql::ExecError,
5532            > {
5533                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5534                    .await
5535            }
5536
5537            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5538            /// parity. PK-ordered
5539            /// is NOT guaranteed without an explicit `order_by` —
5540            /// drop into `Self::query()` for that.
5541            ///
5542            /// # Errors
5543            /// As [`FetcherPool::fetch`].
5544            ///
5545            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5546            pub async fn take(
5547                n: i64,
5548                pool: &#root::sql::Pool,
5549            ) -> ::core::result::Result<
5550                ::std::vec::Vec<Self>,
5551                #root::sql::ExecError,
5552            > {
5553                use #root::sql::FetcherPool as _;
5554                #root::query::QuerySet::<Self>::default()
5555                    .limit(n)
5556                    .fetch(pool)
5557                    .await
5558            }
5559
5560            /// Fetch the page-th window of `per_page` rows
5561            /// (1-indexed). Eloquent
5562            /// `Model::query()->forPage($page, $perPage)->get()`
5563            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5564            /// LIMIT per_page`; for large offsets this is O(N) —
5565            /// prefer keyset pagination via PK on hot paths.
5566            ///
5567            /// # Errors
5568            /// As [`FetcherPool::fetch`].
5569            ///
5570            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5571            pub async fn for_page(
5572                page: i64,
5573                per_page: i64,
5574                pool: &#root::sql::Pool,
5575            ) -> ::core::result::Result<
5576                ::std::vec::Vec<Self>,
5577                #root::sql::ExecError,
5578            > {
5579                use #root::sql::FetcherPool as _;
5580                let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5581                #root::query::QuerySet::<Self>::default()
5582                    .limit(per_page)
5583                    .offset(_offset)
5584                    .fetch(pool)
5585                    .await
5586            }
5587
5588            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5589            /// one page of rows AND the total row count in one
5590            /// call. Returns `(rows, total)`. Two queries: a
5591            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5592            /// for the total.
5593            ///
5594            /// Useful for paginated UIs that need both the visible
5595            /// rows AND a "Page X of Y" / total-count widget. For
5596            /// large tables prefer keyset pagination + cached count;
5597            /// every call to `paginate` re-counts the full table.
5598            ///
5599            /// Same 1-indexed `page` convention as [`Self::for_page`].
5600            ///
5601            /// # Errors
5602            /// As [`Self::for_page`] and [`Self::count`].
5603            pub async fn paginate(
5604                page: i64,
5605                per_page: i64,
5606                pool: &#root::sql::Pool,
5607            ) -> ::core::result::Result<
5608                (::std::vec::Vec<Self>, i64),
5609                #root::sql::ExecError,
5610            > {
5611                // Route through the queryset rather than `Self::count`:
5612                // the `count` inherent method is suppressed on models
5613                // that declare a field named `count` (field/shortcut
5614                // collision guard), and `paginate` must still compile
5615                // for those models.
5616                let total = {
5617                    use #root::sql::CounterPool as _;
5618                    #root::query::QuerySet::<Self>::default()
5619                        .count(pool)
5620                        .await?
5621                };
5622                let rows = Self::for_page(page, per_page, pool).await?;
5623                ::core::result::Result::Ok((rows, total))
5624            }
5625
5626            /// Bulk-update — set `set_col = set_val` on every row
5627            /// matching `where_col = where_val`. Returns affected row
5628            /// count. Eloquent
5629            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5630            /// parity.
5631            ///
5632            /// For multi-column updates drop into the queryset
5633            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5634            ///
5635            /// # Errors
5636            /// As [`UpdaterPool::execute_pool`].
5637            ///
5638            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5639            pub async fn update_where(
5640                where_col: &str,
5641                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5642                set_col: &str,
5643                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5644                pool: &#root::sql::Pool,
5645            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5646                use #root::sql::UpdaterPool as _;
5647                #root::query::QuerySet::<Self>::default()
5648                    .filter(where_col, where_val)
5649                    .update()
5650                    .set(set_col, set_val)
5651                    .execute_pool(pool)
5652                    .await
5653            }
5654
5655            /// Bulk-delete — remove every row matching
5656            /// `where_col = where_val`. Returns affected row count.
5657            /// Eloquent
5658            /// `Model::where($where_col, $where_val)->delete()` parity.
5659            ///
5660            /// For more complex filters drop into the queryset
5661            /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5662            ///
5663            /// # Errors
5664            /// As [`#root::sql::delete_pool`].
5665            pub async fn delete_where(
5666                where_col: &str,
5667                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5668                pool: &#root::sql::Pool,
5669            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5670                let _query = #root::core::DeleteQuery {
5671                    model: <Self as #root::core::Model>::SCHEMA,
5672                    where_clause: #root::core::WhereExpr::Predicate(
5673                        #root::core::Filter {
5674                            column: <Self as #root::core::Model>::SCHEMA
5675                                .field(where_col)
5676                                .ok_or_else(|| {
5677                                    #root::sql::ExecError::Query(
5678                                        #root::core::QueryError::UnknownField {
5679                                            model: <Self as #root::core::Model>::SCHEMA.name,
5680                                            field: ::std::string::ToString::to_string(where_col),
5681                                        },
5682                                    )
5683                                })?
5684                                .column,
5685                            op: #root::core::Op::Eq,
5686                            value: ::core::convert::Into::into(where_val),
5687                        },
5688                    ),
5689                };
5690                #root::sql::delete_pool(pool, &_query).await
5691            }
5692
5693            /// Bulk-update — set `set_col = set_val` on EVERY row of
5694            /// this model's table. **No WHERE clause** — use with
5695            /// care. Eloquent `Model::query()->update([$col => $val])`
5696            /// parity.
5697            ///
5698            /// Use for backfills, one-shot reset flows, etc. The
5699            /// returned count is rows affected.
5700            ///
5701            /// # Errors
5702            /// As [`UpdaterPool::execute_pool`].
5703            ///
5704            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5705            pub async fn update_all(
5706                set_col: &str,
5707                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5708                pool: &#root::sql::Pool,
5709            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5710                use #root::sql::UpdaterPool as _;
5711                #root::query::QuerySet::<Self>::default()
5712                    .update()
5713                    .set(set_col, set_val)
5714                    .execute_pool(pool)
5715                    .await
5716            }
5717
5718            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5719            /// Eloquent `Model::whereNotLike` parity. Pattern is
5720            /// passed verbatim — caller controls `%` / `_`.
5721            ///
5722            /// # Errors
5723            /// As [`FetcherPool::fetch`].
5724            ///
5725            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5726            pub async fn where_not_like(
5727                col: &str,
5728                pattern: impl ::core::convert::Into<::std::string::String>,
5729                pool: &#root::sql::Pool,
5730            ) -> ::core::result::Result<
5731                ::std::vec::Vec<Self>,
5732                #root::sql::ExecError,
5733            > {
5734                use #root::sql::FetcherPool as _;
5735                let _key = ::std::format!("{}__not_like", col);
5736                #root::query::QuerySet::<Self>::default()
5737                    .filter(
5738                        &_key,
5739                        #root::core::SqlValue::String(pattern.into()),
5740                    )
5741                    .fetch(pool)
5742                    .await
5743            }
5744
5745            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5746            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5747            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5748            ///
5749            /// # Errors
5750            /// As [`FetcherPool::fetch`].
5751            ///
5752            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5753            pub async fn where_not_ilike(
5754                col: &str,
5755                pattern: impl ::core::convert::Into<::std::string::String>,
5756                pool: &#root::sql::Pool,
5757            ) -> ::core::result::Result<
5758                ::std::vec::Vec<Self>,
5759                #root::sql::ExecError,
5760            > {
5761                use #root::sql::FetcherPool as _;
5762                let _key = ::std::format!("{}__not_ilike", col);
5763                #root::query::QuerySet::<Self>::default()
5764                    .filter(
5765                        &_key,
5766                        #root::core::SqlValue::String(pattern.into()),
5767                    )
5768                    .fetch(pool)
5769                    .await
5770            }
5771
5772            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5773            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5774            /// parity.
5775            ///
5776            /// # Errors
5777            /// As [`FetcherPool::fetch`].
5778            ///
5779            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5780            pub async fn where_not_between(
5781                col: &str,
5782                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5783                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5784                pool: &#root::sql::Pool,
5785            ) -> ::core::result::Result<
5786                ::std::vec::Vec<Self>,
5787                #root::sql::ExecError,
5788            > {
5789                use #root::sql::FetcherPool as _;
5790                let _key = ::std::format!("{}__not_between", col);
5791                let _vals = #root::core::SqlValue::List(::std::vec![
5792                    ::core::convert::Into::into(lo),
5793                    ::core::convert::Into::into(hi),
5794                ]);
5795                #root::query::QuerySet::<Self>::default()
5796                    .filter(&_key, _vals)
5797                    .fetch(pool)
5798                    .await
5799            }
5800
5801            /// Returns the SQL table name for this model. Eloquent
5802            /// `$model->getTable()` parity.
5803            #[must_use]
5804            pub fn table_name() -> &'static str {
5805                <Self as #root::core::Model>::SCHEMA.table
5806            }
5807
5808            /// Returns the SQL column name of this model's primary
5809            /// key, or `None` when the model has no
5810            /// `#[rustango(primary_key)]`. Eloquent
5811            /// `$model->getKeyName()` parity.
5812            #[must_use]
5813            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5814                <Self as #root::core::Model>::SCHEMA
5815                    .primary_key()
5816                    .map(|f| f.column)
5817            }
5818
5819            /// Fetch every row where `<col> BETWEEN lo AND hi`
5820            /// (inclusive on both ends — same as SQL). Eloquent
5821            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5822            ///
5823            /// # Errors
5824            /// As [`FetcherPool::fetch`].
5825            ///
5826            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5827            pub async fn where_between(
5828                col: &str,
5829                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5830                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5831                pool: &#root::sql::Pool,
5832            ) -> ::core::result::Result<
5833                ::std::vec::Vec<Self>,
5834                #root::sql::ExecError,
5835            > {
5836                use #root::sql::FetcherPool as _;
5837                let _key = ::std::format!("{}__between", col);
5838                let _vals = #root::core::SqlValue::List(::std::vec![
5839                    ::core::convert::Into::into(lo),
5840                    ::core::convert::Into::into(hi),
5841                ]);
5842                #root::query::QuerySet::<Self>::default()
5843                    .filter(&_key, _vals)
5844                    .fetch(pool)
5845                    .await
5846            }
5847
5848            /// Fetch the first row where `<col> = <val>`. Returns
5849            /// `Ok(None)` when no row matches. Eloquent
5850            /// `Model::firstWhere($col, $val)` parity.
5851            ///
5852            /// Thin wrapper over `QuerySet::<Self>::default()
5853            /// .filter(col, val).first(pool)`. Use this when you
5854            /// want one row identified by a non-PK column (e.g.
5855            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5856            ///
5857            /// `val` accepts any value `Into<SqlValue>` so plain
5858            /// strings, ints, UUIDs, etc. all work.
5859            ///
5860            /// # Errors
5861            /// As `QuerySet::first`.
5862            pub async fn first_where(
5863                col: &str,
5864                val: impl ::core::convert::Into<#root::core::SqlValue>,
5865                pool: &#root::sql::Pool,
5866            ) -> ::core::result::Result<
5867                ::core::option::Option<Self>,
5868                #root::sql::ExecError,
5869            > {
5870                #root::query::QuerySet::<Self>::default()
5871                    .filter(col, val)
5872                    .first(pool)
5873                    .await
5874            }
5875
5876            #value_method
5877
5878            /// Fetch the row with the largest `field` value —
5879            /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5880            /// `Ok(None)` for an empty table. Eloquent
5881            /// `Model::latest($field)->first()` parity.
5882            /// Thin wrapper over `QuerySet::<Self>::default()
5883            /// .latest(field, pool)`.
5884            ///
5885            /// **Field name** is the Rust field ident as a string
5886            /// (not the SQL column). Unknown fields surface as
5887            /// `ExecError::Query(QueryError::UnknownField)` at
5888            /// compile time.
5889            ///
5890            /// # Errors
5891            /// As `QuerySet::latest`.
5892            pub async fn latest(
5893                field: &str,
5894                pool: &#root::sql::Pool,
5895            ) -> ::core::result::Result<
5896                ::core::option::Option<Self>,
5897                #root::sql::ExecError,
5898            > {
5899                #root::query::QuerySet::<Self>::default()
5900                    .latest(field, pool)
5901                    .await
5902            }
5903
5904            /// Sibling of [`Self::latest_pool`] — fetches the row
5905            /// with the smallest `field` value (`ORDER BY <field>
5906            /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5907            /// ->first()` parity.
5908            ///
5909            /// # Errors
5910            /// As [`Self::latest_pool`].
5911            pub async fn earliest(
5912                field: &str,
5913                pool: &#root::sql::Pool,
5914            ) -> ::core::result::Result<
5915                ::core::option::Option<Self>,
5916                #root::sql::ExecError,
5917            > {
5918                #root::query::QuerySet::<Self>::default()
5919                    .earliest(field, pool)
5920                    .await
5921            }
5922
5923            #count_method
5924
5925            /// `true` when the table contains at least one row.
5926            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
5927            /// over `QuerySet::<Self>::default().exists(pool)`.
5928            ///
5929            /// # Errors
5930            /// As [`ExistsPool::exists`].
5931            ///
5932            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5933            pub async fn exists(
5934                pool: &#root::sql::Pool,
5935            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5936                use #root::sql::ExistsPool as _;
5937                #root::query::QuerySet::<Self>::default()
5938                    .exists(pool)
5939                    .await
5940            }
5941
5942            /// Inverse of [`Self::exists`] — returns `true` when
5943            /// the table has zero rows. Eloquent
5944            /// `Model::doesntExist()` parity.
5945            ///
5946            /// # Errors
5947            /// As [`Self::exists`].
5948            pub async fn doesnt_exist(
5949                pool: &#root::sql::Pool,
5950            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5951                Self::exists(pool).await.map(|e| !e)
5952            }
5953
5954            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5955            /// `true` when a row with primary key `pk` exists in
5956            /// the table. Sugar over
5957            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5958            ///
5959            /// Differs from [`Self::exists`] (which checks "any row
5960            /// in the table") by checking a specific PK existence.
5961            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5962            /// because the row is never materialized — the SQL is
5963            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5964            ///
5965            /// # Errors
5966            /// As [`#root::sql::ExistsPool::contains_pk`].
5967            pub async fn contains_pk(
5968                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5969                pool: &#root::sql::Pool,
5970            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5971                use #root::sql::ExistsPool as _;
5972                #root::query::QuerySet::<Self>::default()
5973                    .contains_pk(pool, pk)
5974                    .await
5975            }
5976
5977            #sum_method
5978            #avg_method
5979            #min_method
5980            #max_method
5981
5982            /// Internal: forward to
5983            /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5984            /// Backs `sum` / `avg` / `min` / `max`.
5985            #[doc(hidden)]
5986            pub async fn __aggregate_one_pool<U>(
5987                col: &str,
5988                build: fn(&'static str) -> #root::core::AggregateExpr,
5989                pool: &#root::sql::Pool,
5990            ) -> ::core::result::Result<
5991                ::core::option::Option<U>,
5992                #root::sql::ExecError,
5993            >
5994            where
5995                (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5996                    + #root::sql::MaybeMyFromRow
5997                    + #root::sql::MaybeSqliteFromRow
5998                    + ::core::marker::Send
5999                    + ::core::marker::Unpin,
6000            {
6001                #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
6002                    .await
6003            }
6004
6005            /// Fetch every row of this model from `pool`. Eloquent
6006            /// `Model::all()` parity — a thin wrapper over
6007            /// `QuerySet::<Self>::default().fetch(pool)`.
6008            ///
6009            /// **Use with care on large tables** — there's no
6010            /// pagination or limit; the entire table is materialized
6011            /// into memory. For anything beyond fixture / lookup
6012            /// tables, page through `QuerySet::<Self>::default()
6013            /// .order_by(...).limit(N).offset(M).fetch(pool)`
6014            /// or stream via `.iterator(chunk_size)`.
6015            ///
6016            /// # Errors
6017            /// As [`FetcherPool::fetch`].
6018            ///
6019            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6020            pub async fn all(
6021                pool: &#root::sql::Pool,
6022            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6023            {
6024                use #root::sql::FetcherPool as _;
6025                #root::query::QuerySet::<Self>::default()
6026                    .fetch(pool)
6027                    .await
6028            }
6029
6030            /// Look up every row whose primary key is in `pks`.
6031            /// Returns the matching rows in **inventory** order — NOT
6032            /// the order of `pks`. Empty `pks` returns an empty
6033            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6034            /// with a list) parity.
6035            ///
6036            /// Thin wrapper over `QuerySet::<Self>::default()
6037            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6038            /// Caller-supplied PKs that don't match a row are
6039            /// silently skipped (the returned vec is shorter than
6040            /// the input list). For an order-preserving / "fail
6041            /// when any missing" variant, build the queryset
6042            /// explicitly with `in_bulk` instead.
6043            ///
6044            /// Accepts any iterable whose elements are
6045            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6046            /// `Vec<Uuid>`, etc.
6047            ///
6048            /// # Errors
6049            /// As [`FetcherPool::fetch`].
6050            ///
6051            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6052            pub async fn find_many<V>(
6053                pks: impl ::core::iter::IntoIterator<Item = V>,
6054                pool: &#root::sql::Pool,
6055            ) -> ::core::result::Result<
6056                ::std::vec::Vec<Self>,
6057                #root::sql::ExecError,
6058            >
6059            where
6060                V: ::core::convert::Into<#root::core::SqlValue>,
6061            {
6062                use #root::sql::FetcherPool as _;
6063                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6064                    pks.into_iter().map(::core::convert::Into::into).collect();
6065                if _values.is_empty() {
6066                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6067                }
6068                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6069                #root::query::QuerySet::<Self>::default()
6070                    .filter(&_key, #root::core::SqlValue::List(_values))
6071                    .fetch(pool)
6072                    .await
6073            }
6074
6075            /// Look up the row whose primary key equals `pk`. Returns
6076            /// `Ok(None)` when no row matches, rather than erroring.
6077            /// Eloquent `Model::find`
6078            /// shape — accepts any value `Into<SqlValue>`.
6079            ///
6080            /// One-liner shortcut for the common
6081            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6082            /// .limit(1).fetch(pool).await?.into_iter().next()`
6083            /// dance.
6084            ///
6085            /// # Errors
6086            /// As [`FetcherPool::fetch`].
6087            ///
6088            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6089            pub async fn find(
6090                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6091                pool: &#root::sql::Pool,
6092            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6093            {
6094                use #root::sql::FetcherPool as _;
6095                let _pk_val: #root::core::SqlValue = pk.into();
6096                let mut _rows: ::std::vec::Vec<Self> =
6097                    #root::query::QuerySet::<Self>::default()
6098                        .filter(::core::stringify!(#pk_ident), _pk_val)
6099                        .limit(1)
6100                        .fetch(pool)
6101                        .await?;
6102                ::core::result::Result::Ok(_rows.into_iter().next())
6103            }
6104
6105            /// Look up the row whose primary key equals `pk`. Errors
6106            /// when no row matches — the throwing counterpart of
6107            /// [`Self::find_pool`]. Eloquent `Model::findOrFail`
6108            /// parity.
6109            ///
6110            /// Translates the miss into
6111            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6112            /// so callers can `?`-bubble straight through the typical
6113            /// `ExecError` error chain.
6114            ///
6115            /// # Errors
6116            /// As [`Self::find_pool`]; additionally
6117            /// [`sqlx::Error::RowNotFound`] when no row matches.
6118            ///
6119            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6120            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6121            pub async fn find_or_fail(
6122                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6123                pool: &#root::sql::Pool,
6124            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6125                match Self::find(pk, pool).await? {
6126                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6127                    ::core::option::Option::None => ::core::result::Result::Err(
6128                        #root::sql::ExecError::Driver(
6129                            #root::sql::sqlx::Error::RowNotFound,
6130                        ),
6131                    ),
6132                }
6133            }
6134
6135            /// Look up multiple rows by primary key, **failing** if
6136            /// any requested PK is missing. Eloquent
6137            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6138            /// inventory order (NOT request order — same as
6139            /// [`Self::find_many`]).
6140            ///
6141            /// Empty `pks` returns an empty `Vec` (no rows requested
6142            /// → nothing to fail on).
6143            ///
6144            /// Differs from [`Self::find_many`] in that this method
6145            /// requires every PK to resolve. If even one is missing,
6146            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6147            /// in `ExecError::Driver`). Rows are deduped at the SQL
6148            /// layer, so passing the same PK twice in `pks` counts as
6149            /// one expected row.
6150            ///
6151            /// # Errors
6152            /// As [`Self::find_many`], plus `RowNotFound` when the
6153            /// returned row count is less than the count of distinct
6154            /// requested PKs.
6155            pub async fn find_many_or_fail<V>(
6156                pks: impl ::core::iter::IntoIterator<Item = V>,
6157                pool: &#root::sql::Pool,
6158            ) -> ::core::result::Result<
6159                ::std::vec::Vec<Self>,
6160                #root::sql::ExecError,
6161            >
6162            where
6163                V: ::core::convert::Into<#root::core::SqlValue>,
6164            {
6165                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6166                    pks.into_iter().map(::core::convert::Into::into).collect();
6167                if _values.is_empty() {
6168                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6169                }
6170                // Dedup the requested PK list before counting so
6171                // duplicate-PK requests don't false-fail (the SQL
6172                // `IN (...)` clause naturally dedups too).
6173                let mut _seen: ::std::collections::HashSet<
6174                    ::std::string::String,
6175                > = ::std::collections::HashSet::new();
6176                for v in &_values {
6177                    _seen.insert(v.to_display_string());
6178                }
6179                let _expected = _seen.len();
6180                let _rows = Self::find_many(_values, pool).await?;
6181                if _rows.len() < _expected {
6182                    return ::core::result::Result::Err(
6183                        #root::sql::ExecError::Driver(
6184                            #root::sql::sqlx::Error::RowNotFound,
6185                        ),
6186                    );
6187                }
6188                ::core::result::Result::Ok(_rows)
6189            }
6190
6191            /// Find by primary key or run `fallback` to produce a
6192            /// default row to return. Eloquent
6193            /// `Model::findOr($pk, fn() => …)` parity.
6194            ///
6195            /// Unlike [`Self::find_or_fail_pool`] (which raises on
6196            /// miss), this is the "give me something sensible"
6197            /// branch: typical use is "fetch the user's row, else
6198            /// fall back to an anonymous/guest stub".
6199            ///
6200            /// `fallback` runs only when no row matches — the DB
6201            /// round-trip happens unconditionally.
6202            ///
6203            /// # Errors
6204            /// As [`Self::find_pool`].
6205            pub async fn find_or<F>(
6206                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6207                pool: &#root::sql::Pool,
6208                fallback: F,
6209            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6210            where
6211                F: ::core::ops::FnOnce() -> Self,
6212            {
6213                ::core::result::Result::Ok(
6214                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6215                )
6216            }
6217
6218            /// Same as [`Self::find_or`] but also returns a `bool`
6219            /// indicating whether the row was found in the DB
6220            /// (`true`) or freshly built from `fallback` (`false`).
6221            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6222            /// in PHP the returned model also exposes `->exists`;
6223            /// here that's surfaced as the second tuple element.
6224            ///
6225            /// Useful for edit-or-create form handlers where the
6226            /// caller needs to know whether to PATCH or POST when
6227            /// the user submits the form.
6228            ///
6229            /// # Errors
6230            /// As [`Self::find`].
6231            pub async fn find_or_new<F>(
6232                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6233                pool: &#root::sql::Pool,
6234                fallback: F,
6235            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6236            where
6237                F: ::core::ops::FnOnce() -> Self,
6238            {
6239                match Self::find(pk, pool).await? {
6240                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6241                    ::core::option::Option::None => {
6242                        ::core::result::Result::Ok((fallback(), false))
6243                    }
6244                }
6245            }
6246
6247            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6248            /// [`Self::find_or_new`] but **persists** the new row
6249            /// when the PK isn't found. Returns `(row, exists: bool)`
6250            /// — `exists=true` when the PK was found,
6251            /// `false` when a fresh row was inserted.
6252            ///
6253            /// ```ignore
6254            /// let (post, found) = Post::find_or_insert(
6255            ///     pk,
6256            ///     &pool,
6257            ///     || Post { id: Auto::default(), title: "new".into() },
6258            /// ).await?;
6259            /// // `found` true → returned existing row;
6260            /// // `found` false → fallback was inserted, `post.id` populated.
6261            /// ```
6262            ///
6263            /// **Caveat**: two concurrent calls that both miss the
6264            /// find can both INSERT, violating uniqueness. Wrap in a
6265            /// transaction or rely on a UNIQUE constraint + handle
6266            /// the conflict error if you need race-free semantics.
6267            ///
6268            /// # Errors
6269            /// As [`Self::find`] and [`Self::save_pool`].
6270            pub async fn find_or_insert<F>(
6271                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6272                pool: &#root::sql::Pool,
6273                fallback: F,
6274            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6275            where
6276                F: ::core::ops::FnOnce() -> Self,
6277            {
6278                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6279                    return ::core::result::Result::Ok((_row, true));
6280                }
6281                let mut _new = fallback();
6282                _new.save_pool(pool).await?;
6283                ::core::result::Result::Ok((_new, false))
6284            }
6285
6286            /// Fetch the first row of the table, or run `fallback`
6287            /// when the table is empty. Eloquent
6288            /// `Model::firstOr(fn() => …)` parity.
6289            ///
6290            /// # Errors
6291            /// As [`Self::first_pool`].
6292            pub async fn first_or<F>(
6293                pool: &#root::sql::Pool,
6294                fallback: F,
6295            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6296            where
6297                F: ::core::ops::FnOnce() -> Self,
6298            {
6299                // Route through the queryset rather than `Self::first`,
6300                // which is suppressed on models with a field named
6301                // `first` (field/shortcut collision guard).
6302                ::core::result::Result::Ok(
6303                    #root::query::QuerySet::<Self>::default()
6304                        .first(pool)
6305                        .await?
6306                        .unwrap_or_else(fallback),
6307                )
6308            }
6309
6310            /// Fetch exactly one row matching `<col> = <val>`. Errors
6311            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6312            /// or more than one matches
6313            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6314            /// Eloquent `Model::sole($col, $val)` parity.
6315            ///
6316            /// # Errors
6317            /// As [`Self::where_pool`] plus the explicit
6318            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6319            pub async fn sole(
6320                col: &str,
6321                val: impl ::core::convert::Into<#root::core::SqlValue>,
6322                pool: &#root::sql::Pool,
6323            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6324                let mut _rows = Self::where_(col, val, pool).await?;
6325                match _rows.len() {
6326                    0 => ::core::result::Result::Err(
6327                        #root::sql::ExecError::Driver(
6328                            #root::sql::sqlx::Error::RowNotFound,
6329                        ),
6330                    ),
6331                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6332                    n => ::core::result::Result::Err(
6333                        #root::sql::ExecError::MultipleRowsReturned {
6334                            op: "sole",
6335                            table: <Self as #root::core::Model>::SCHEMA.name,
6336                            count: n,
6337                        },
6338                    ),
6339                }
6340            }
6341        }
6342    } else {
6343        quote!()
6344    };
6345
6346    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6347    // the non-audited `_pool` methods but execute against an open
6348    // `PoolTx` so the writes participate in the caller's transaction.
6349    // Auditing inside TX is deferred; these always use the plain
6350    // executor primitives regardless of whether the model is audited.
6351    let tx_insert_method = if fields.has_auto {
6352        let pushes = &fields.insert_pushes;
6353        let returning_cols = &fields.returning_cols;
6354        quote! {
6355            /// Insert this row inside an open transaction, populating
6356            /// any `Auto<T>` PK from the auto-assigned value. Works
6357            /// against any backend that `tx` wraps.
6358            ///
6359            /// # Errors
6360            /// As [`Self::insert_pool`].
6361            pub async fn insert_tx(
6362                &mut self,
6363                tx: &mut #root::sql::PoolTx<'_>,
6364            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6365                let mut _columns: ::std::vec::Vec<&'static str> =
6366                    ::std::vec::Vec::new();
6367                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6368                    ::std::vec::Vec::new();
6369                #( #pushes )*
6370                let _query = #root::core::InsertQuery {
6371                    model: <Self as #root::core::Model>::SCHEMA,
6372                    columns: _columns,
6373                    values: _values,
6374                    returning: ::std::vec![ #( #returning_cols ),* ],
6375                    on_conflict: ::core::option::Option::None,
6376                };
6377                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6378                #root::sql::apply_auto_pk(_result, self)
6379            }
6380        }
6381    } else {
6382        let insert_columns = &fields.insert_columns;
6383        let insert_values = &fields.insert_values;
6384        quote! {
6385            /// Insert this row inside an open transaction.
6386            ///
6387            /// # Errors
6388            /// As [`Self::insert_pool`].
6389            pub async fn insert_tx(
6390                &self,
6391                tx: &mut #root::sql::PoolTx<'_>,
6392            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6393                let _query = #root::core::InsertQuery {
6394                    model: <Self as #root::core::Model>::SCHEMA,
6395                    columns: ::std::vec![ #( #insert_columns ),* ],
6396                    values: ::std::vec![ #( #insert_values ),* ],
6397                    returning: ::std::vec::Vec::new(),
6398                    on_conflict: ::core::option::Option::None,
6399                };
6400                #root::sql::insert_tx(tx, &_query).await
6401            }
6402        }
6403    };
6404
6405    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6406        let pk_column_lit = pk_col.as_str();
6407        let assignments = &fields.update_assignments;
6408        let dispatch_unset = if fields.pk_is_auto {
6409            quote! {
6410                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6411                    return self.insert_tx(tx).await.map(|()| 1u64);
6412                }
6413            }
6414        } else {
6415            quote!()
6416        };
6417        quote! {
6418            /// Save this row inside an open transaction. `INSERT` when
6419            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6420            /// PK. Works against any backend that `tx` wraps.
6421            ///
6422            /// # Errors
6423            /// As [`Self::save_pool`].
6424            pub async fn save_tx(
6425                &mut self,
6426                tx: &mut #root::sql::PoolTx<'_>,
6427            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6428                #dispatch_unset
6429                let _query = #root::core::UpdateQuery {
6430                    model: <Self as #root::core::Model>::SCHEMA,
6431                    set: ::std::vec![ #( #assignments ),* ],
6432                    where_clause: #root::core::WhereExpr::Predicate(
6433                        #root::core::Filter {
6434                            column: #pk_column_lit,
6435                            op: #root::core::Op::Eq,
6436                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
6437                                ::core::clone::Clone::clone(&self.#pk_ident)
6438                            ),
6439                        }
6440                    ),
6441                };
6442                let _affected = #root::sql::update_tx(tx, &_query).await?;
6443                ::core::result::Result::Ok(_affected)
6444            }
6445        }
6446    } else {
6447        quote!()
6448    };
6449
6450    let tx_delete_method = {
6451        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6452        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6453        if let Some(pk_ident) = pk_ident_for_tx {
6454            quote! {
6455                /// Delete the row identified by this instance's PK
6456                /// inside an open transaction. Works against any backend
6457                /// that `tx` wraps.
6458                ///
6459                /// # Errors
6460                /// As [`Self::delete_pool`].
6461                pub async fn delete_tx(
6462                    &self,
6463                    tx: &mut #root::sql::PoolTx<'_>,
6464                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6465                    let _query = #root::core::DeleteQuery {
6466                        model: <Self as #root::core::Model>::SCHEMA,
6467                        where_clause: #root::core::WhereExpr::Predicate(
6468                            #root::core::Filter {
6469                                column: #pk_column_lit,
6470                                op: #root::core::Op::Eq,
6471                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6472                                    ::core::clone::Clone::clone(&self.#pk_ident)
6473                                ),
6474                            }
6475                        ),
6476                    };
6477                    #root::sql::delete_tx(tx, &_query).await
6478                }
6479            }
6480        } else {
6481            quote!()
6482        }
6483    };
6484
6485    // Update emission captures both BEFORE and AFTER state — runs an
6486    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6487    // each tracked field's prior value, then after the UPDATE diffs
6488    // against the in-memory `&self`. `diff_changes` drops unchanged
6489    // columns so the JSON only contains the actual delta.
6490    //
6491    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6492    // and binds `_audit_before_pairs`; `audit_update_post` runs
6493    // after the UPDATE and emits the PendingEntry.
6494    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6495        audited_fields
6496    {
6497        if tracked.is_empty() {
6498            (quote!(), quote!())
6499        } else {
6500            let select_cols: String = tracked
6501                .iter()
6502                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6503                .collect::<Vec<_>>()
6504                .join(", ");
6505            let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6506            let select_cols_lit = select_cols;
6507            let pk_column_lit_for_select = pk_column_for_select;
6508            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6509                if fields.pk_is_auto {
6510                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6511                } else {
6512                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6513                }
6514            } else {
6515                quote!(0_i64)
6516            };
6517            let before_pairs = tracked.iter().map(|c| {
6518                let column_lit = c.column.as_str();
6519                let value_ty = &c.value_ty;
6520                quote! {
6521                    (
6522                        #column_lit,
6523                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6524                            &_audit_before_row, #column_lit,
6525                        ) {
6526                            ::core::result::Result::Ok(v) => {
6527                                #root::__serde_json::to_value(&v)
6528                                    .unwrap_or(#root::__serde_json::Value::Null)
6529                            }
6530                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6531                        },
6532                    )
6533                }
6534            });
6535            let after_pairs = tracked.iter().map(|c| {
6536                let column_lit = c.column.as_str();
6537                let ident = &c.ident;
6538                quote! {
6539                    (
6540                        #column_lit,
6541                        #root::__serde_json::to_value(&self.#ident)
6542                            .unwrap_or(#root::__serde_json::Value::Null),
6543                    )
6544                }
6545            });
6546            let pk_str = audit_pk_to_string.clone();
6547            let pre = quote! {
6548                let _audit_select_sql = ::std::format!(
6549                    r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6550                    #select_cols_lit,
6551                    <Self as #root::core::Model>::SCHEMA.table,
6552                    #pk_column_lit_for_select,
6553                );
6554                let _audit_before_pairs:
6555                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6556                    match #root::sql::sqlx::query(&_audit_select_sql)
6557                        .bind(#pk_value_for_bind)
6558                        .fetch_optional(&mut *_executor)
6559                        .await
6560                    {
6561                        ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6562                            ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6563                        }
6564                        _ => ::core::option::Option::None,
6565                    };
6566            };
6567            let post = quote! {
6568                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6569                    let _audit_after:
6570                        ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6571                        ::std::vec![ #( #after_pairs ),* ];
6572                    let _audit_entry = #root::audit::PendingEntry {
6573                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
6574                        entity_pk: #pk_str,
6575                        operation: #root::audit::AuditOp::Update,
6576                        source: #root::audit::current_source(),
6577                        changes: #root::audit::diff_changes(
6578                            &_audit_before,
6579                            &_audit_after,
6580                        ),
6581                    };
6582                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6583                }
6584            };
6585            (pre, post)
6586        }
6587    } else {
6588        (quote!(), quote!())
6589    };
6590
6591    // Bulk-insert audit: capture every row's tracked fields after the
6592    // RETURNING populates each PK, then push one batched INSERT INTO
6593    // audit_log via `emit_many`. One round-trip regardless of N rows.
6594    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6595        let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6596            if fields.pk_is_auto {
6597                quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6598            } else {
6599                quote!(::std::format!("{}", &_row.#pk_ident))
6600            }
6601        } else {
6602            quote!(::std::string::String::new())
6603        };
6604        let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6605            let column_lit = c.column.as_str();
6606            let ident = &c.ident;
6607            quote! {
6608                (
6609                    #column_lit,
6610                    #root::__serde_json::to_value(&_row.#ident)
6611                        .unwrap_or(#root::__serde_json::Value::Null),
6612                )
6613            }
6614        });
6615        quote! {
6616            let _audit_source = #root::audit::current_source();
6617            let mut _audit_entries:
6618                ::std::vec::Vec<#root::audit::PendingEntry> =
6619                    ::std::vec::Vec::with_capacity(rows.len());
6620            for _row in rows.iter() {
6621                _audit_entries.push(#root::audit::PendingEntry {
6622                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
6623                    entity_pk: #row_pk_str,
6624                    operation: #root::audit::AuditOp::Create,
6625                    source: _audit_source.clone(),
6626                    changes: #root::audit::snapshot_changes(&[
6627                        #( #row_pairs ),*
6628                    ]),
6629                });
6630            }
6631            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6632        }
6633    } else {
6634        quote!()
6635    };
6636
6637    let save_method = if fields.pk_is_auto {
6638        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6639        let pk_column_lit = pk_column.as_str();
6640        let assignments = &fields.update_assignments;
6641        let upsert_cols = &fields.upsert_update_columns;
6642        let upsert_pushes = &fields.insert_pushes;
6643        let upsert_returning = &fields.returning_cols;
6644        let upsert_auto_assigns = &fields.auto_assigns;
6645        // Conflict target: prefer the first declared `unique_together`
6646        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6647        // `BIGSERIAL` and never collide on insert, so a PK-only target
6648        // would silently turn `upsert()` into "always-insert" for
6649        // surrogate-PK models with composite UNIQUE constraints — see
6650        // `RolePermission` / `UserRole` / `UserPermission` in the
6651        // tenancy permission engine. When no `unique_together` is
6652        // declared we keep the PK target (the original behaviour).
6653        let upsert_target_columns: Vec<String> = indexes
6654            .iter()
6655            .find(|i| i.unique && !i.columns.is_empty())
6656            .map(|i| i.columns.clone())
6657            .unwrap_or_else(|| vec![pk_column.clone()]);
6658        let upsert_target_lits = upsert_target_columns
6659            .iter()
6660            .map(String::as_str)
6661            .collect::<Vec<_>>();
6662        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6663            quote!(#root::core::ConflictClause::DoNothing)
6664        } else {
6665            quote!(#root::core::ConflictClause::DoUpdate {
6666                target: ::std::vec![ #( #upsert_target_lits ),* ],
6667                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6668            })
6669        };
6670        Some(quote! {
6671            /// Insert this row if its `Auto<T>` primary key is
6672            /// `Unset`, otherwise update the existing row matching the
6673            /// PK. The caller doesn't need to pick `insert` vs the
6674            /// bulk-update path manually.
6675            ///
6676            /// On the insert branch, populates the PK from `RETURNING`
6677            /// (same behavior as `insert`). On the update branch,
6678            /// writes every non-PK column back; if no row matches the
6679            /// PK, returns `Ok(())` silently.
6680            ///
6681            /// Only generated when the primary key is declared as
6682            /// `Auto<T>`. Models with a manually-managed PK must use
6683            /// `insert` or the QuerySet update builder.
6684            ///
6685            /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6686            /// driver-specific `PgPool`, so it does not exist at all on a
6687            /// build without the `postgres` feature: selecting `sqlite`
6688            /// makes the method vanish rather than fail with anything that
6689            /// names the cause.
6690            ///
6691            /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6692            /// all three backends. The naming is inverted — the short name
6693            /// is the narrow one — and that is tracked in #1293.
6694            ///
6695            /// # Errors
6696            /// Returns [`#root::sql::ExecError`] for SQL-writing
6697            /// or driver failures.
6698            #[cfg(feature = "postgres")]
6699            pub async fn save(
6700                &mut self,
6701                pool: &#root::sql::sqlx::PgPool,
6702            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6703                #pool_to_save_on
6704            }
6705
6706            /// Like [`Self::save`] but accepts any sqlx executor —
6707            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6708            /// escape hatch for tenant-scoped writes: schema-mode
6709            /// tenants share the registry pool but rely on a per-
6710            /// checkout `SET search_path`, so passing `&PgPool` would
6711            /// silently hit the wrong schema. Acquire a connection
6712            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6713            ///
6714            /// # Errors
6715            /// As [`Self::save`].
6716            #[cfg(feature = "postgres")]
6717            pub async fn save_on #executor_generics (
6718                &mut self,
6719                #executor_param,
6720            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6721            #executor_where
6722            {
6723                // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6724                // the rows-affected count (0 when the PK no longer exists).
6725                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6726                    return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6727                }
6728                #audit_update_pre
6729                let _query = #root::core::UpdateQuery {
6730                    model: <Self as #root::core::Model>::SCHEMA,
6731                    set: ::std::vec![ #( #assignments ),* ],
6732                    where_clause: #root::core::WhereExpr::Predicate(
6733                        #root::core::Filter {
6734                            column: #pk_column_lit,
6735                            op: #root::core::Op::Eq,
6736                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
6737                                ::core::clone::Clone::clone(&self.#pk_ident)
6738                            ),
6739                        }
6740                    ),
6741                };
6742                let _affected = #root::sql::__macro_internals::update_on(
6743                    #executor_passes_to_data_write,
6744                    &_query,
6745                ).await?;
6746                #audit_update_post
6747                ::core::result::Result::Ok(_affected)
6748            }
6749
6750            /// Per-call override for the audit source. Runs
6751            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6752            /// scope so the resulting audit entry records `source`
6753            /// instead of the task-local default. Useful for seed
6754            /// scripts and one-off CLI tools that don't sit inside an
6755            /// admin handler. The override applies only to this call;
6756            /// no global state changes.
6757            ///
6758            /// # Errors
6759            /// As [`Self::save_on`].
6760            #[cfg(feature = "postgres")]
6761            pub async fn save_on_with #executor_generics (
6762                &mut self,
6763                #executor_param,
6764                source: #root::audit::AuditSource,
6765            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6766            #executor_where
6767            {
6768                #root::audit::with_source(source, self.save_on(_executor)).await
6769            }
6770
6771            /// Insert this row or update it in-place if the primary key already
6772            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6773            ///
6774            /// With `Auto::Unset` PK the server assigns a new key and no conflict
6775            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6776            /// inserted if absent or all non-PK columns are overwritten if present.
6777            ///
6778            /// # Errors
6779            /// As [`Self::insert_on`].
6780            #[cfg(feature = "postgres")]
6781            pub async fn upsert(
6782                &mut self,
6783                pool: &#root::sql::sqlx::PgPool,
6784            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6785                #pool_to_upsert_on
6786            }
6787
6788            /// Like [`Self::upsert`] but accepts any sqlx executor.
6789            /// See [`Self::save_on`] for tenancy-scoped rationale.
6790            ///
6791            /// # Errors
6792            /// As [`Self::upsert`].
6793            #[cfg(feature = "postgres")]
6794            pub async fn upsert_on #executor_generics (
6795                &mut self,
6796                #executor_param,
6797            ) -> ::core::result::Result<(), #root::sql::ExecError>
6798            #executor_where
6799            {
6800                let mut _columns: ::std::vec::Vec<&'static str> =
6801                    ::std::vec::Vec::new();
6802                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6803                    ::std::vec::Vec::new();
6804                #( #upsert_pushes )*
6805                let query = #root::core::InsertQuery {
6806                    model: <Self as #root::core::Model>::SCHEMA,
6807                    columns: _columns,
6808                    values: _values,
6809                    returning: ::std::vec![ #( #upsert_returning ),* ],
6810                    on_conflict: ::core::option::Option::Some(#conflict_clause),
6811                };
6812                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6813                    #executor_passes_to_data_write,
6814                    &query,
6815                ).await?;
6816                let _returning_row = &_returning_row_v;
6817                #( #upsert_auto_assigns )*
6818                ::core::result::Result::Ok(())
6819            }
6820        })
6821    } else {
6822        None
6823    };
6824
6825    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6826        let pk_column_lit = pk_column.as_str();
6827        // Optional `soft_delete_on` / `restore_on` companions when the
6828        // model has a `#[rustango(soft_delete)]` column. They land
6829        // alongside the regular `delete_on` so callers have both
6830        // options — a hard delete (audit-tracked as a real DELETE) and
6831        // a logical delete (audit-tracked as an UPDATE setting the
6832        // deleted_at column to NOW()).
6833        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6834            let col_lit = col;
6835            let sd_field_ident = fields
6836                .soft_delete_field_ident
6837                .clone()
6838                .expect("soft_delete_column without ident");
6839            quote! {
6840                /// Soft-delete this row by setting its
6841                /// `#[rustango(soft_delete)]` column to `NOW()`.
6842                /// The row stays in the table; query helpers can
6843                /// filter it out by checking the column for `IS NOT
6844                /// NULL`.
6845                ///
6846                /// # Errors
6847                /// As [`Self::delete`].
6848                #[cfg(feature = "postgres")]
6849                pub async fn soft_delete_on #executor_generics (
6850                    &self,
6851                    #executor_param,
6852                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6853                #executor_where
6854                {
6855                    let _query = #root::core::UpdateQuery {
6856                        model: <Self as #root::core::Model>::SCHEMA,
6857                        set: ::std::vec![
6858                            #root::core::Assignment {
6859                                column: #col_lit,
6860                                value: ::core::convert::Into::<#root::core::Expr>::into(
6861                                    ::core::convert::Into::<#root::core::SqlValue>::into(
6862                                        #root::__chrono::Utc::now()
6863                                    )
6864                                ),
6865                            },
6866                        ],
6867                        where_clause: #root::core::WhereExpr::Predicate(
6868                            #root::core::Filter {
6869                                column: #pk_column_lit,
6870                                op: #root::core::Op::Eq,
6871                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6872                                    ::core::clone::Clone::clone(&self.#pk_ident)
6873                                ),
6874                            }
6875                        ),
6876                    };
6877                    let _affected = #root::sql::__macro_internals::update_on(
6878                        #executor_passes_to_data_write,
6879                        &_query,
6880                    ).await?;
6881                    #audit_softdelete_emit
6882                    ::core::result::Result::Ok(_affected)
6883                }
6884
6885                /// Inverse of [`Self::soft_delete_on`] — clears the
6886                /// soft-delete column back to NULL so the row is
6887                /// considered live again.
6888                ///
6889                /// # Errors
6890                /// As [`Self::delete`].
6891                #[cfg(feature = "postgres")]
6892                pub async fn restore_on #executor_generics (
6893                    &self,
6894                    #executor_param,
6895                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6896                #executor_where
6897                {
6898                    let _query = #root::core::UpdateQuery {
6899                        model: <Self as #root::core::Model>::SCHEMA,
6900                        set: ::std::vec![
6901                            #root::core::Assignment {
6902                                column: #col_lit,
6903                                value: ::core::convert::Into::<#root::core::Expr>::into(
6904                                    #root::core::SqlValue::Null
6905                                ),
6906                            },
6907                        ],
6908                        where_clause: #root::core::WhereExpr::Predicate(
6909                            #root::core::Filter {
6910                                column: #pk_column_lit,
6911                                op: #root::core::Op::Eq,
6912                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6913                                    ::core::clone::Clone::clone(&self.#pk_ident)
6914                                ),
6915                            }
6916                        ),
6917                    };
6918                    let _affected = #root::sql::__macro_internals::update_on(
6919                        #executor_passes_to_data_write,
6920                        &_query,
6921                    ).await?;
6922                    #audit_restore_emit
6923                    ::core::result::Result::Ok(_affected)
6924                }
6925
6926                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6927                /// — takes [`#root::sql::Pool`] and dispatches per
6928                /// backend. Eloquent `Model::delete()` semantics on
6929                /// soft-delete-enabled models (closes #821 partial).
6930                ///
6931                /// Sets the `#[rustango(soft_delete)]` column to
6932                /// `NOW()` on every backend. Query helpers
6933                /// (`QuerySet::active()` / `only_trashed()`,
6934                /// `soft_delete::active_filter` /
6935                /// `compose_with_active`) filter trashed rows out by
6936                /// reading `IS NULL` on the same column.
6937                ///
6938                /// # Errors
6939                /// As [`#root::sql::update_pool`].
6940                pub async fn soft_delete(
6941                    &self,
6942                    pool: &#root::sql::Pool,
6943                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6944                    let _query = #root::core::UpdateQuery {
6945                        model: <Self as #root::core::Model>::SCHEMA,
6946                        set: ::std::vec![
6947                            #root::core::Assignment {
6948                                column: #col_lit,
6949                                value: ::core::convert::Into::<#root::core::Expr>::into(
6950                                    ::core::convert::Into::<#root::core::SqlValue>::into(
6951                                        #root::__chrono::Utc::now()
6952                                    )
6953                                ),
6954                            },
6955                        ],
6956                        where_clause: #root::core::WhereExpr::Predicate(
6957                            #root::core::Filter {
6958                                column: #pk_column_lit,
6959                                op: #root::core::Op::Eq,
6960                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6961                                    ::core::clone::Clone::clone(&self.#pk_ident)
6962                                ),
6963                            }
6964                        ),
6965                    };
6966                    #root::sql::update_pool(pool, &_query).await
6967                }
6968
6969                /// Tri-dialect counterpart of [`Self::restore_on`].
6970                /// Clears the `#[rustango(soft_delete)]` column back
6971                /// to `NULL`, marking the row live again. Eloquent
6972                /// `Model::restore()` parity.
6973                ///
6974                /// # Errors
6975                /// As [`#root::sql::update_pool`].
6976                pub async fn restore(
6977                    &self,
6978                    pool: &#root::sql::Pool,
6979                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6980                    let _query = #root::core::UpdateQuery {
6981                        model: <Self as #root::core::Model>::SCHEMA,
6982                        set: ::std::vec![
6983                            #root::core::Assignment {
6984                                column: #col_lit,
6985                                value: ::core::convert::Into::<#root::core::Expr>::into(
6986                                    #root::core::SqlValue::Null
6987                                ),
6988                            },
6989                        ],
6990                        where_clause: #root::core::WhereExpr::Predicate(
6991                            #root::core::Filter {
6992                                column: #pk_column_lit,
6993                                op: #root::core::Op::Eq,
6994                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6995                                    ::core::clone::Clone::clone(&self.#pk_ident)
6996                                ),
6997                            }
6998                        ),
6999                    };
7000                    #root::sql::update_pool(pool, &_query).await
7001                }
7002
7003                /// Hard-delete this row, ignoring the soft-delete
7004                /// column. Eloquent `Model::forceDelete()` parity —
7005                /// the escape hatch when you need to actually purge
7006                /// data (GDPR, fixture cleanup, etc.).
7007                ///
7008                /// Equivalent to [`Self::delete_pool`] (the framework's
7009                /// non-soft delete) — exposed under the Eloquent name
7010                /// for muscle-memory + so soft-delete-enabled models
7011                /// have all three operations (soft / restore / force)
7012                /// in one place.
7013                ///
7014                /// # Errors
7015                /// As [`Self::delete_pool`].
7016                pub async fn force_delete(
7017                    &self,
7018                    pool: &#root::sql::Pool,
7019                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7020                    Self::delete_pool(self, pool).await
7021                }
7022
7023                /// Returns `true` when this row is soft-deleted (its
7024                /// `#[rustango(soft_delete)]` column is currently
7025                /// set — Eloquent `$model->trashed()` parity).
7026                ///
7027                /// Pure in-memory predicate over `&self`; does not
7028                /// hit the database. Useful in admin/template code
7029                /// like `{% if post.trashed() %}…{% endif %}` and in
7030                /// guard clauses on restore/force-delete flows.
7031                pub fn trashed(&self) -> bool {
7032                    ::core::option::Option::is_some(&self.#sd_field_ident)
7033                }
7034
7035                /// Fetch every row whose `#[rustango(soft_delete)]`
7036                /// column is `NULL` (a.k.a. the live, non-trashed
7037                /// rows). Eloquent's default `Model::all()` behavior on
7038                /// a soft-delete model (Eloquent auto-scopes trashed
7039                /// rows out; rustango doesn't have auto-scoping yet —
7040                /// see issue #820 — so this is the explicit shortcut).
7041                ///
7042                /// One-liner over `QuerySet::<Self>::default()
7043                /// .active().fetch(pool)`. Closes #821 partial.
7044                ///
7045                /// # Errors
7046                /// As [`#root::sql::FetcherPool::fetch`].
7047                pub async fn active(
7048                    pool: &#root::sql::Pool,
7049                ) -> ::core::result::Result<
7050                    ::std::vec::Vec<Self>,
7051                    #root::sql::ExecError,
7052                > {
7053                    use #root::sql::FetcherPool as _;
7054                    #root::query::QuerySet::<Self>::default()
7055                        .active()
7056                        .fetch(pool)
7057                        .await
7058                }
7059
7060                /// Fetch ONLY soft-deleted rows. Eloquent
7061                /// `Model::onlyTrashed()->get()` parity — drives the
7062                /// admin "Trash" page, restore flows, GDPR purge
7063                /// scans, etc.
7064                ///
7065                /// One-liner over `QuerySet::<Self>::default()
7066                /// .only_trashed().fetch(pool)`. Closes #821
7067                /// partial.
7068                ///
7069                /// # Errors
7070                /// As [`#root::sql::FetcherPool::fetch`].
7071                pub async fn only_trashed(
7072                    pool: &#root::sql::Pool,
7073                ) -> ::core::result::Result<
7074                    ::std::vec::Vec<Self>,
7075                    #root::sql::ExecError,
7076                > {
7077                    use #root::sql::FetcherPool as _;
7078                    #root::query::QuerySet::<Self>::default()
7079                        .only_trashed()
7080                        .fetch(pool)
7081                        .await
7082                }
7083
7084                /// Fetch every row, both live and soft-deleted.
7085                /// Eloquent `Model::withTrashed()->get()` parity.
7086                ///
7087                /// Today every queryset already includes trashed rows
7088                /// (rustango has no global-scope tracking yet — issue
7089                /// #820), so this is functionally equivalent to
7090                /// [`Self::all_pool`]. Exposed as a named shortcut so
7091                /// soft-delete-aware code reads `Model::with_trashed_pool`
7092                /// rather than `Model::all_pool` — keeps intent visible
7093                /// in callers and stays correct when auto-scoping lands.
7094                ///
7095                /// Closes #821 partial.
7096                ///
7097                /// # Errors
7098                /// As [`#root::sql::FetcherPool::fetch`].
7099                pub async fn with_trashed(
7100                    pool: &#root::sql::Pool,
7101                ) -> ::core::result::Result<
7102                    ::std::vec::Vec<Self>,
7103                    #root::sql::ExecError,
7104                > {
7105                    use #root::sql::FetcherPool as _;
7106                    #root::query::QuerySet::<Self>::default()
7107                        .with_trashed()
7108                        .fetch(pool)
7109                        .await
7110                }
7111
7112            }
7113        } else {
7114            quote!()
7115        };
7116        quote! {
7117            /// Delete the row identified by this instance's primary key.
7118            ///
7119            /// Returns the number of rows affected (0 or 1).
7120            ///
7121            /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7122            /// a driver-specific `PgPool`, so it does not exist on a build
7123            /// without the `postgres` feature. [`Self::delete_pool`] takes
7124            /// `rustango::sql::Pool` and works on all three. The naming is
7125            /// inverted; tracked in #1293.
7126            ///
7127            /// # Errors
7128            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7129            /// driver failures.
7130            #[cfg(feature = "postgres")]
7131            pub async fn delete(
7132                &self,
7133                pool: &#root::sql::sqlx::PgPool,
7134            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7135                #pool_to_delete_on
7136            }
7137
7138            /// Like [`Self::delete`] but accepts any sqlx executor —
7139            /// for tenant-scoped deletes against an explicitly-acquired
7140            /// connection. See [`Self::save_on`] for the rationale.
7141            ///
7142            /// # Errors
7143            /// As [`Self::delete`].
7144            #[cfg(feature = "postgres")]
7145            pub async fn delete_on #executor_generics (
7146                &self,
7147                #executor_param,
7148            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7149            #executor_where
7150            {
7151                let query = #root::core::DeleteQuery {
7152                    model: <Self as #root::core::Model>::SCHEMA,
7153                    where_clause: #root::core::WhereExpr::Predicate(
7154                        #root::core::Filter {
7155                            column: #pk_column_lit,
7156                            op: #root::core::Op::Eq,
7157                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
7158                                ::core::clone::Clone::clone(&self.#pk_ident)
7159                            ),
7160                        }
7161                    ),
7162                };
7163                let _affected = #root::sql::__macro_internals::delete_on(
7164                    #executor_passes_to_data_write,
7165                    &query,
7166                ).await?;
7167                #audit_delete_emit
7168                ::core::result::Result::Ok(_affected)
7169            }
7170
7171            /// Per-call audit-source override for [`Self::delete_on`].
7172            /// See [`Self::save_on_with`] for shape rationale.
7173            ///
7174            /// # Errors
7175            /// As [`Self::delete_on`].
7176            #[cfg(feature = "postgres")]
7177            pub async fn delete_on_with #executor_generics (
7178                &self,
7179                #executor_param,
7180                source: #root::audit::AuditSource,
7181            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7182            #executor_where
7183            {
7184                #root::audit::with_source(source, self.delete_on(_executor)).await
7185            }
7186            #pool_delete_method
7187            #pool_insert_method
7188            #pool_save_method
7189            #refresh_replicate_methods
7190            #tx_delete_method
7191            #tx_insert_method
7192            #tx_save_method
7193            #soft_delete_methods
7194
7195            /// Returns `true` when `other` represents the same DB
7196            /// row as `self` — i.e. their primary keys compare
7197            /// equal. Eloquent `$model->is($other)` parity.
7198            ///
7199            /// Because both arguments are typed `&Self`, the
7200            /// model/table check is automatic — `Post::is` cannot
7201            /// be invoked against a `Comment` at compile time. Only
7202            /// the PK has to be compared at runtime.
7203            pub fn is(&self, other: &Self) -> bool {
7204                self.#pk_ident == other.#pk_ident
7205            }
7206
7207            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7208            /// parity.
7209            pub fn is_not(&self, other: &Self) -> bool {
7210                self.#pk_ident != other.#pk_ident
7211            }
7212
7213            /// Returns this row's primary-key value as an
7214            /// [`#root::core::SqlValue`]. Eloquent
7215            /// `$model->getKey()` parity.
7216            ///
7217            /// Useful when you need to thread the PK through a
7218            /// generic `Into<SqlValue>`-bound API without knowing the
7219            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7220            #[must_use]
7221            pub fn get_key(&self) -> #root::core::SqlValue {
7222                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7223            }
7224
7225        }
7226    });
7227
7228    let insert_method = if fields.has_auto {
7229        let pushes = &fields.insert_pushes;
7230        let returning_cols = &fields.returning_cols;
7231        let auto_assigns = &fields.auto_assigns;
7232        quote! {
7233            /// Insert this row into its table. Skips columns whose
7234            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7235            /// sequence fills them in, then reads each `Auto` column
7236            /// back via `RETURNING` and stores it on `self`.
7237            ///
7238            /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7239            /// a driver-specific `PgPool`, so it does not exist on a build
7240            /// without the `postgres` feature. [`Self::insert_pool`] takes
7241            /// `rustango::sql::Pool` and works on all three. The naming is
7242            /// inverted; tracked in #1293.
7243            ///
7244            /// # Errors
7245            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7246            /// driver failures.
7247            #[cfg(feature = "postgres")]
7248            pub async fn insert(
7249                &mut self,
7250                pool: &#root::sql::sqlx::PgPool,
7251            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7252                #pool_to_insert_on
7253            }
7254
7255            /// Like [`Self::insert`] but accepts any sqlx executor.
7256            /// See [`Self::save_on`] for tenancy-scoped rationale.
7257            ///
7258            /// # Errors
7259            /// As [`Self::insert`].
7260            #[cfg(feature = "postgres")]
7261            pub async fn insert_on #executor_generics (
7262                &mut self,
7263                #executor_param,
7264            ) -> ::core::result::Result<(), #root::sql::ExecError>
7265            #executor_where
7266            {
7267                let mut _columns: ::std::vec::Vec<&'static str> =
7268                    ::std::vec::Vec::new();
7269                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7270                    ::std::vec::Vec::new();
7271                #( #pushes )*
7272                let query = #root::core::InsertQuery {
7273                    model: <Self as #root::core::Model>::SCHEMA,
7274                    columns: _columns,
7275                    values: _values,
7276                    returning: ::std::vec![ #( #returning_cols ),* ],
7277                    on_conflict: ::core::option::Option::None,
7278                };
7279                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7280                    #executor_passes_to_data_write,
7281                    &query,
7282                ).await?;
7283                let _returning_row = &_returning_row_v;
7284                #( #auto_assigns )*
7285                #audit_insert_emit
7286                ::core::result::Result::Ok(())
7287            }
7288
7289            /// Per-call audit-source override for [`Self::insert_on`].
7290            /// See [`Self::save_on_with`] for shape rationale.
7291            ///
7292            /// # Errors
7293            /// As [`Self::insert_on`].
7294            #[cfg(feature = "postgres")]
7295            pub async fn insert_on_with #executor_generics (
7296                &mut self,
7297                #executor_param,
7298                source: #root::audit::AuditSource,
7299            ) -> ::core::result::Result<(), #root::sql::ExecError>
7300            #executor_where
7301            {
7302                #root::audit::with_source(source, self.insert_on(_executor)).await
7303            }
7304        }
7305    } else {
7306        let insert_columns = &fields.insert_columns;
7307        let insert_values = &fields.insert_values;
7308        quote! {
7309            /// Insert this row into its table.
7310            ///
7311            /// # Errors
7312            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7313            /// driver failures.
7314            #[cfg(feature = "postgres")]
7315            pub async fn insert(
7316                &self,
7317                pool: &#root::sql::sqlx::PgPool,
7318            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7319                self.insert_on(pool).await
7320            }
7321
7322            /// Like [`Self::insert`] but accepts any sqlx executor.
7323            /// See [`Self::save_on`] for tenancy-scoped rationale.
7324            ///
7325            /// # Errors
7326            /// As [`Self::insert`].
7327            #[cfg(feature = "postgres")]
7328            pub async fn insert_on<'_c, _E>(
7329                &self,
7330                _executor: _E,
7331            ) -> ::core::result::Result<(), #root::sql::ExecError>
7332            where
7333                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7334            {
7335                let query = #root::core::InsertQuery {
7336                    model: <Self as #root::core::Model>::SCHEMA,
7337                    columns: ::std::vec![ #( #insert_columns ),* ],
7338                    values: ::std::vec![ #( #insert_values ),* ],
7339                    returning: ::std::vec::Vec::new(),
7340                    on_conflict: ::core::option::Option::None,
7341                };
7342                #root::sql::__macro_internals::insert_on(_executor, &query).await
7343            }
7344        }
7345    };
7346
7347    let bulk_insert_method = if fields.has_auto {
7348        let cols_no_auto = &fields.bulk_columns_no_auto;
7349        let cols_all = &fields.bulk_columns_all;
7350        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7351        let pushes_all = &fields.bulk_pushes_all;
7352        let returning_cols = &fields.returning_cols;
7353        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7354        let uniformity = &fields.bulk_auto_uniformity;
7355        let first_auto_ident = fields
7356            .first_auto_ident
7357            .as_ref()
7358            .expect("has_auto implies first_auto_ident is Some");
7359        quote! {
7360            /// Bulk-insert `rows` in a single round-trip. Every row's
7361            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7362            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7363            /// (caller-supplied values). Mixed Set/Unset is rejected
7364            /// — call `insert` per row for that case.
7365            ///
7366            /// Empty slice is a no-op. Each row's `Auto` fields are
7367            /// populated from the `RETURNING` clause in input order
7368            /// before this returns.
7369            ///
7370            /// # Errors
7371            /// Returns [`#root::sql::ExecError`] for validation,
7372            /// SQL-writing, mixed-Auto rejection, or driver failures.
7373            #[cfg(feature = "postgres")]
7374            pub async fn bulk_insert(
7375                rows: &mut [Self],
7376                pool: &#root::sql::sqlx::PgPool,
7377            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7378                #pool_to_bulk_insert_on
7379            }
7380
7381            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7382            /// See [`Self::save_on`] for tenancy-scoped rationale.
7383            ///
7384            /// # Errors
7385            /// As [`Self::bulk_insert`].
7386            #[cfg(feature = "postgres")]
7387            pub async fn bulk_insert_on #executor_generics (
7388                rows: &mut [Self],
7389                #executor_param,
7390            ) -> ::core::result::Result<(), #root::sql::ExecError>
7391            #executor_where
7392            {
7393                if rows.is_empty() {
7394                    return ::core::result::Result::Ok(());
7395                }
7396                let _first_unset = matches!(
7397                    rows[0].#first_auto_ident,
7398                    #root::sql::Auto::Unset
7399                );
7400                #( #uniformity )*
7401
7402                let mut _all_rows: ::std::vec::Vec<
7403                    ::std::vec::Vec<#root::core::SqlValue>,
7404                > = ::std::vec::Vec::with_capacity(rows.len());
7405                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7406                    for _row in rows.iter() {
7407                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7408                            ::std::vec::Vec::new();
7409                        #( #pushes_no_auto )*
7410                        _all_rows.push(_row_vals);
7411                    }
7412                    ::std::vec![ #( #cols_no_auto ),* ]
7413                } else {
7414                    for _row in rows.iter() {
7415                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7416                            ::std::vec::Vec::new();
7417                        #( #pushes_all )*
7418                        _all_rows.push(_row_vals);
7419                    }
7420                    ::std::vec![ #( #cols_all ),* ]
7421                };
7422
7423                let _query = #root::core::BulkInsertQuery {
7424                    model: <Self as #root::core::Model>::SCHEMA,
7425                    columns: _columns,
7426                    rows: _all_rows,
7427                    returning: ::std::vec![ #( #returning_cols ),* ],
7428                    on_conflict: ::core::option::Option::None,
7429                };
7430                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7431                    #executor_passes_to_data_write,
7432                    &_query,
7433                ).await?;
7434                if _returned.len() != rows.len() {
7435                    return ::core::result::Result::Err(
7436                        #root::sql::ExecError::Sql(
7437                            #root::sql::SqlError::BulkInsertReturningMismatch {
7438                                expected: rows.len(),
7439                                actual: _returned.len(),
7440                            }
7441                        )
7442                    );
7443                }
7444                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7445                    #auto_assigns_for_row
7446                }
7447                #audit_bulk_insert_emit
7448                ::core::result::Result::Ok(())
7449            }
7450        }
7451    } else {
7452        let cols_all = &fields.bulk_columns_all;
7453        let pushes_all = &fields.bulk_pushes_all;
7454        quote! {
7455            /// Bulk-insert `rows` in a single round-trip. Every row's
7456            /// fields are written verbatim — there are no `Auto<T>`
7457            /// fields on this model.
7458            ///
7459            /// Empty slice is a no-op.
7460            ///
7461            /// # Errors
7462            /// Returns [`#root::sql::ExecError`] for validation,
7463            /// SQL-writing, or driver failures.
7464            #[cfg(feature = "postgres")]
7465            pub async fn bulk_insert(
7466                rows: &[Self],
7467                pool: &#root::sql::sqlx::PgPool,
7468            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7469                Self::bulk_insert_on(rows, pool).await
7470            }
7471
7472            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7473            /// See [`Self::save_on`] for tenancy-scoped rationale.
7474            ///
7475            /// # Errors
7476            /// As [`Self::bulk_insert`].
7477            #[cfg(feature = "postgres")]
7478            pub async fn bulk_insert_on<'_c, _E>(
7479                rows: &[Self],
7480                _executor: _E,
7481            ) -> ::core::result::Result<(), #root::sql::ExecError>
7482            where
7483                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7484            {
7485                if rows.is_empty() {
7486                    return ::core::result::Result::Ok(());
7487                }
7488                let mut _all_rows: ::std::vec::Vec<
7489                    ::std::vec::Vec<#root::core::SqlValue>,
7490                > = ::std::vec::Vec::with_capacity(rows.len());
7491                for _row in rows.iter() {
7492                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7493                        ::std::vec::Vec::new();
7494                    #( #pushes_all )*
7495                    _all_rows.push(_row_vals);
7496                }
7497                let _query = #root::core::BulkInsertQuery {
7498                    model: <Self as #root::core::Model>::SCHEMA,
7499                    columns: ::std::vec![ #( #cols_all ),* ],
7500                    rows: _all_rows,
7501                    returning: ::std::vec::Vec::new(),
7502                    on_conflict: ::core::option::Option::None,
7503                };
7504                let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7505                ::core::result::Result::Ok(())
7506            }
7507        }
7508    };
7509
7510    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7511    // (no postgres-feature gate); routes through the existing
7512    // `bulk_insert_pool` + per-dialect conflict writer.
7513    //
7514    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7515    // sequence picks the PK for fresh inserts; the UPDATE branch never
7516    // touches the Auto column.
7517    let bulk_upsert_pool_method = {
7518        // Pick the "no Auto" columns when the model has Auto fields,
7519        // else every column.
7520        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7521            (
7522                fields.bulk_columns_no_auto.clone(),
7523                fields.bulk_pushes_no_auto.clone(),
7524            )
7525        } else {
7526            (
7527                fields.bulk_columns_all.clone(),
7528                fields.bulk_pushes_all.clone(),
7529            )
7530        };
7531        quote! {
7532            /// Tri-dialect bulk insert that updates on conflict — the
7533            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7534            ///
7535            /// Per-row values are extracted and lowered into a
7536            /// [`#root::core::BulkInsertQuery`] with
7537            /// `on_conflict = DoUpdate { target, update_columns }`. The
7538            /// writer dispatches per-dialect:
7539            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7540            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7541            ///
7542            /// `target` names the column(s) whose unique constraint
7543            /// defines the conflict (typically a `unique` or
7544            /// `unique_together` natural-key column, NOT the `Auto<T>`
7545            /// PK). `update_cols` names the columns to overwrite on
7546            /// conflict — every other column is left untouched on the
7547            /// existing row.
7548            ///
7549            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7550            /// picks the PK on insert; the update path never touches
7551            /// the Auto column). Auto-set rows trigger a hard error.
7552            /// Empty slice is a no-op.
7553            ///
7554            /// # Errors
7555            /// Returns [`#root::sql::ExecError`] for validation,
7556            /// SQL-writing, or driver failures.
7557            pub async fn bulk_upsert_pool(
7558                rows: &[Self],
7559                target: &[&'static str],
7560                update_cols: &[&'static str],
7561                pool: &#root::sql::Pool,
7562            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7563                if rows.is_empty() {
7564                    return ::core::result::Result::Ok(());
7565                }
7566                let mut _all_rows: ::std::vec::Vec<
7567                    ::std::vec::Vec<#root::core::SqlValue>,
7568                > = ::std::vec::Vec::with_capacity(rows.len());
7569                for _row in rows.iter() {
7570                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7571                        ::std::vec::Vec::new();
7572                    #( #upsert_pushes )*
7573                    _all_rows.push(_row_vals);
7574                }
7575                let _query = #root::core::BulkInsertQuery {
7576                    model: <Self as #root::core::Model>::SCHEMA,
7577                    columns: ::std::vec![ #( #upsert_cols ),* ],
7578                    rows: _all_rows,
7579                    returning: ::std::vec::Vec::new(),
7580                    on_conflict: ::core::option::Option::Some(
7581                        #root::core::ConflictClause::DoUpdate {
7582                            target: target.to_vec(),
7583                            update_columns: update_cols.to_vec(),
7584                        }
7585                    ),
7586                };
7587                #root::sql::bulk_insert_pool(pool, &_query).await
7588            }
7589
7590            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7591            /// skip rows that would violate a unique constraint. Issue
7592            /// #267 / T1.5. Same per-dialect dispatch as
7593            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7594            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7595            /// <pivot> = <pivot>` (MySQL no-op write).
7596            ///
7597            /// # Errors
7598            /// As [`Self::bulk_upsert_pool`].
7599            pub async fn bulk_insert_or_ignore_pool(
7600                rows: &[Self],
7601                pool: &#root::sql::Pool,
7602            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7603                if rows.is_empty() {
7604                    return ::core::result::Result::Ok(());
7605                }
7606                let mut _all_rows: ::std::vec::Vec<
7607                    ::std::vec::Vec<#root::core::SqlValue>,
7608                > = ::std::vec::Vec::with_capacity(rows.len());
7609                for _row in rows.iter() {
7610                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7611                        ::std::vec::Vec::new();
7612                    #( #upsert_pushes )*
7613                    _all_rows.push(_row_vals);
7614                }
7615                let _query = #root::core::BulkInsertQuery {
7616                    model: <Self as #root::core::Model>::SCHEMA,
7617                    columns: ::std::vec![ #( #upsert_cols ),* ],
7618                    rows: _all_rows,
7619                    returning: ::std::vec::Vec::new(),
7620                    on_conflict: ::core::option::Option::Some(
7621                        #root::core::ConflictClause::DoNothing
7622                    ),
7623                };
7624                #root::sql::bulk_insert_pool(pool, &_query).await
7625            }
7626        }
7627    };
7628
7629    // Ergonomic `Model::bulk_update(objs, fields)`.
7630    // The SQL/IR/executor stack
7631    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7632    // `write_bulk_update_*` writers) already existed; what was missing
7633    // was the per-model constructor that maps `&[Self]` + a runtime
7634    // column list into rows of `[pk, col_vals…]` so callers don't
7635    // hand-build the IR. Emitted only when the model has a primary key
7636    // (the PK is the join key and can't itself be updated).
7637    let bulk_update_method = match &fields.primary_key {
7638        None => quote! {},
7639        Some((pk_ident, pk_col)) => {
7640            // One pair of match arms per non-PK column: a validation arm
7641            // resolving the runtime name to its `&'static str` column,
7642            // and a value arm pushing that field off the row.
7643            let mut col_arms: Vec<TokenStream2> = Vec::new();
7644            let mut val_arms: Vec<TokenStream2> = Vec::new();
7645            for entry in &fields.column_entries {
7646                if &entry.column == pk_col {
7647                    continue;
7648                }
7649                let col = &entry.column;
7650                let ident = &entry.ident;
7651                col_arms.push(quote! { #col => #col, });
7652                val_arms.push(quote! {
7653                    #col => _row_vals.push(
7654                        ::core::convert::Into::<#root::core::SqlValue>::into(
7655                            ::core::clone::Clone::clone(&_o.#ident)
7656                        )
7657                    ),
7658                });
7659            }
7660            quote! {
7661                /// Write per-row-different values for the named `fields`
7662                /// across every object in `objs` in a single statement,
7663                /// matched by primary key.
7664                ///
7665                /// `fields` names the **columns** to update. The primary
7666                /// key identifies each row and cannot itself be updated
7667                /// (pass it and you get
7668                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7669                /// Empty `objs` or `fields` is a no-op returning `0`.
7670                /// Objects whose PK matches no row are simply not updated.
7671                /// Returns the number of rows affected.
7672                ///
7673                /// Tri-dialect: lowers to one
7674                /// [`#root::core::BulkUpdateQuery`] and dispatches
7675                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7676                /// a CTE + correlated subquery on SQLite, an inner
7677                /// `JOIN (VALUES …)` on MySQL.
7678                ///
7679                /// # Errors
7680                /// [`#root::core::QueryError::UnknownField`] for a name
7681                /// that isn't a column on this model,
7682                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7683                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7684                /// SQL-writing / driver failures.
7685                pub async fn bulk_update(
7686                    objs: &[Self],
7687                    fields: &[&str],
7688                    pool: &#root::sql::Pool,
7689                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7690                    if objs.is_empty() || fields.is_empty() {
7691                        return ::core::result::Result::Ok(0);
7692                    }
7693                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7694                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7695                        ::std::vec::Vec::with_capacity(fields.len());
7696                    for &_f in fields {
7697                        let _col: &'static str = match _f {
7698                            #pk_col => {
7699                                return ::core::result::Result::Err(
7700                                    ::core::convert::Into::into(
7701                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7702                                            model: _model_name,
7703                                            field: ::std::string::ToString::to_string(_f),
7704                                        }
7705                                    )
7706                                );
7707                            }
7708                            #( #col_arms )*
7709                            _ => {
7710                                return ::core::result::Result::Err(
7711                                    ::core::convert::Into::into(
7712                                        #root::core::QueryError::UnknownField {
7713                                            model: _model_name,
7714                                            field: ::std::string::ToString::to_string(_f),
7715                                        }
7716                                    )
7717                                );
7718                            }
7719                        };
7720                        // A model whose only field is the primary key emits no
7721                        // non-PK arms, so every arm above diverges and this push
7722                        // is genuinely unreachable — correct, since such a model
7723                        // has nothing to bulk-update. Scoped to the statement
7724                        // rather than the fn so a real unreachability elsewhere
7725                        // in this body still warns. Without it every PK-only
7726                        // model warns in the *user's* crate, which they cannot
7727                        // act on (#1290).
7728                        #[allow(unreachable_code)]
7729                        _update_columns.push(_col);
7730                    }
7731                    let mut _rows: ::std::vec::Vec<
7732                        ::std::vec::Vec<#root::core::SqlValue>,
7733                    > = ::std::vec::Vec::with_capacity(objs.len());
7734                    for _o in objs.iter() {
7735                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7736                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7737                        // PK first — the writers expect `[pk, …update cols]`.
7738                        _row_vals.push(
7739                            ::core::convert::Into::<#root::core::SqlValue>::into(
7740                                ::core::clone::Clone::clone(&_o.#pk_ident)
7741                            )
7742                        );
7743                        for &_f in fields {
7744                            match _f {
7745                                #( #val_arms )*
7746                                // Unreachable: every name was validated
7747                                // against the same arm set above.
7748                                _ => {}
7749                            }
7750                        }
7751                        _rows.push(_row_vals);
7752                    }
7753                    let _query = #root::core::BulkUpdateQuery {
7754                        model: <Self as #root::core::Model>::SCHEMA,
7755                        update_columns: _update_columns,
7756                        rows: _rows,
7757                    };
7758                    #root::sql::bulk_update_pool(pool, &_query).await
7759                }
7760            }
7761        }
7762    };
7763
7764    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7765        quote! {
7766            /// Hidden runtime accessor for the primary-key value as a
7767            /// [`SqlValue`]. Used by reverse-relation helpers
7768            /// (`<parent>::<child>_set`) emitted from sibling models'
7769            /// FK fields. Not part of the public API.
7770            #[doc(hidden)]
7771            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7772                ::core::convert::Into::<#root::core::SqlValue>::into(
7773                    ::core::clone::Clone::clone(&self.#pk_ident)
7774                )
7775            }
7776        }
7777    });
7778
7779    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7780        quote! {
7781            impl #root::sql::HasPkValue for #struct_name {
7782                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7783                    ::core::convert::Into::<#root::core::SqlValue>::into(
7784                        ::core::clone::Clone::clone(&self.#pk_ident)
7785                    )
7786                }
7787            }
7788        }
7789    });
7790
7791    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7792
7793    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7794    // dispatch to the right per-backend body without the macro emitting
7795    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7796    // so audited models with non-Auto PKs (which still go through
7797    // `insert_one_with_audit` → `apply_auto_pk`) link.
7798    let assign_auto_pk_pool_impl = {
7799        let auto_assigns = &fields.auto_assigns;
7800        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7801        // so the body is structurally identical to `auto_assigns` —
7802        // only the helper name swaps from `try_get_returning` to
7803        // `try_get_returning_sqlite` so the closure typechecks against
7804        // a `SqliteRow` instead of a `PgRow`.
7805        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7806            .auto_field_idents
7807            .iter()
7808            .map(|(ident, column)| {
7809                quote! {
7810                    self.#ident = #root::sql::try_get_returning_sqlite(
7811                        _returning_row, #column
7812                    )?;
7813                }
7814            })
7815            .collect();
7816        // #1028 — decode each `generated_as` column from the same
7817        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7818        // own type drives the decode (no `Auto<T>` wrapper).
7819        let generated_assigns: Vec<TokenStream2> = fields
7820            .generated_field_idents
7821            .iter()
7822            .map(|(ident, column)| {
7823                quote! {
7824                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7825                }
7826            })
7827            .collect();
7828        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7829            .generated_field_idents
7830            .iter()
7831            .map(|(ident, column)| {
7832                quote! {
7833                    self.#ident = #root::sql::try_get_returning_sqlite(
7834                        _returning_row, #column
7835                    )?;
7836                }
7837            })
7838            .collect();
7839        let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7840            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7841            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7842            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7843            // those models can't use AUTO_INCREMENT). The trait is only
7844            // touched on the MySQL arm at runtime, so PG-only consumers
7845            // never see the bound failure.
7846            //
7847            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7848            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7849            // runtime with "multi-column RETURNING". MySQL has no
7850            // multi-column RETURNING semantic and a follow-up SELECT
7851            // would need cross-trait plumbing. Pragmatic shape: succeed
7852            // with the FIRST Auto field populated from LAST_INSERT_ID();
7853            // any other Auto fields stay `Auto::Unset`. Callers that
7854            // need the DB-defaulted timestamp / UUID can re-fetch the
7855            // row by PK after `save_pool`. Fixes the cookbook chapter
7856            // 12 dialect divergence.
7857            let value_ty = fields
7858                .first_auto_value_ty
7859                .as_ref()
7860                .expect("first_auto_value_ty set whenever first_auto_ident is");
7861            quote! {
7862                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7863                    ::rustango_from_mysql_auto_id(_id)?;
7864                self.#first = #root::sql::Auto::Set(_converted);
7865                ::core::result::Result::Ok(())
7866            }
7867        } else {
7868            quote! {
7869                let _ = _id;
7870                ::core::result::Result::Ok(())
7871            }
7872        };
7873        quote! {
7874            impl #root::sql::AssignAutoPkPool for #struct_name {
7875                fn __rustango_assign_from_pg_row(
7876                    &mut self,
7877                    _returning_row: &#root::sql::PgReturningRow,
7878                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7879                    #( #auto_assigns )*
7880                    #( #generated_assigns )*
7881                    ::core::result::Result::Ok(())
7882                }
7883                fn __rustango_assign_from_mysql_id(
7884                    &mut self,
7885                    _id: i64,
7886                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7887                    #mysql_body
7888                }
7889                fn __rustango_assign_from_sqlite_row(
7890                    &mut self,
7891                    _returning_row: &#root::sql::SqliteReturningRow,
7892                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7893                    #( #auto_assigns_sqlite )*
7894                    #( #generated_assigns_sqlite )*
7895                    ::core::result::Result::Ok(())
7896                }
7897            }
7898        }
7899    };
7900
7901    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7902    let aliased_row_helper = quote! {
7903        /// Decode a row's aliased target columns (produced by
7904        /// `select_related`'s LEFT JOIN) into a fresh instance of
7905        /// this model. Reads each column via
7906        /// `format!("{prefix}__{col}")`, matching the alias the
7907        /// SELECT writer emitted. Slice 9.0d.
7908        #[doc(hidden)]
7909        #[cfg(feature = "postgres")]
7910        pub fn __rustango_from_aliased_row(
7911            row: &#root::sql::sqlx::postgres::PgRow,
7912            prefix: &str,
7913        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7914            ::core::result::Result::Ok(Self {
7915                #( #from_aliased_row_inits ),*
7916            })
7917        }
7918    };
7919    // v0.23.0-batch8 — MySQL counterpart, gated through the
7920    // cfg-aware macro_rules so PG-only builds expand to nothing.
7921    let aliased_row_helper_my = quote! {
7922        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7923            #( #from_aliased_row_inits ),*
7924        });
7925    };
7926
7927    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7928    // pattern + cfg gate on the `sqlite` feature.
7929    let aliased_row_helper_sqlite = quote! {
7930        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7931            #( #from_aliased_row_inits ),*
7932        });
7933    };
7934
7935    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7936    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7937    let load_related_impl_sqlite =
7938        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7939
7940    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7941    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7942    // default `Self::objects()`. Each accessor returns a fresh
7943    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7944    // methods the user defined.
7945    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7946        .iter()
7947        .map(|fn_ident| {
7948            let model_name_str = struct_name.to_string();
7949            let fn_name_str = fn_ident.to_string();
7950            let doc = format!(
7951                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7952                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7953                 equivalent to `Self::objects()`. Chains with any \
7954                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7955                 extension methods."
7956            );
7957            quote! {
7958                #[doc = #doc]
7959                #[must_use]
7960                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7961                    #root::query::QuerySet::new()
7962                }
7963            }
7964        })
7965        .collect();
7966
7967    quote! {
7968        impl #struct_name {
7969            /// Start a new `QuerySet` over this model.
7970            #[must_use]
7971            pub fn objects() -> #root::query::QuerySet<#struct_name> {
7972                #root::query::QuerySet::new()
7973            }
7974
7975            /// Eloquent-shape alias of [`Self::objects`]. Returns
7976            /// a fresh `QuerySet<Self>` ready for `.filter()` /
7977            /// `.where_()` / etc. Matches Laravel muscle-memory:
7978            ///
7979            /// ```ignore
7980            /// // Eloquent:    Post::query()->where('published', true)
7981            /// // rustango:    Post::query().filter("published", true)
7982            /// //         or:  Post::objects().filter("published", true)
7983            /// ```
7984            ///
7985            /// Both names point at the same underlying constructor;
7986            /// neither is preferred.
7987            #[must_use]
7988            pub fn query() -> #root::query::QuerySet<#struct_name> {
7989                #root::query::QuerySet::new()
7990            }
7991
7992            #( #extra_manager_fns )*
7993
7994            #insert_method
7995
7996            #bulk_insert_method
7997
7998            #bulk_upsert_pool_method
7999
8000            #bulk_update_method
8001
8002            #save_method
8003
8004            #pk_methods
8005
8006            #pk_value_helper
8007
8008            #aliased_row_helper
8009
8010            #column_consts
8011        }
8012
8013        #aliased_row_helper_my
8014
8015        #aliased_row_helper_sqlite
8016
8017        #load_related_impl
8018
8019        #load_related_impl_my
8020
8021        #load_related_impl_sqlite
8022
8023        #has_pk_value_impl
8024
8025        #fk_pk_access_impl
8026
8027        #assign_auto_pk_pool_impl
8028    }
8029}
8030
8031/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8032/// `auto_assigns` but reading from `_returning_row` and writing to
8033/// `_row_mut` instead of `self`.
8034fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8035    let root = rustango_root();
8036    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
8037        let col_lit = column.as_str();
8038        quote! {
8039            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8040                _returning_row,
8041                #col_lit,
8042            )?;
8043        }
8044    });
8045    quote! { #( #lines )* }
8046}
8047
8048/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8049fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8050    let lines = entries.iter().map(|e| {
8051        let ident = &e.ident;
8052        let col_ty = column_type_ident(ident);
8053        quote! {
8054            #[allow(non_upper_case_globals)]
8055            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8056        }
8057    });
8058    quote! { #(#lines)* }
8059}
8060
8061/// Emit a hidden per-model module carrying one zero-sized type per field,
8062/// each with a `Column` impl pointing back at the model.
8063fn column_module_tokens(
8064    module_ident: &syn::Ident,
8065    struct_name: &syn::Ident,
8066    entries: &[ColumnEntry],
8067) -> TokenStream2 {
8068    let root = rustango_root();
8069    let items = entries.iter().map(|e| {
8070        let col_ty = column_type_ident(&e.ident);
8071        let value_ty = &e.value_ty;
8072        let name = &e.name;
8073        let column = &e.column;
8074        let field_type_tokens = &e.field_type_tokens;
8075        quote! {
8076            #[derive(::core::clone::Clone, ::core::marker::Copy)]
8077            pub struct #col_ty;
8078
8079            impl #root::core::Column for #col_ty {
8080                type Model = super::#struct_name;
8081                type Value = #value_ty;
8082                const NAME: &'static str = #name;
8083                const COLUMN: &'static str = #column;
8084                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8085            }
8086        }
8087    });
8088    quote! {
8089        #[doc(hidden)]
8090        #[allow(non_camel_case_types, non_snake_case)]
8091        pub mod #module_ident {
8092            // Re-import the parent scope so field types referencing
8093            // sibling models (e.g. `ForeignKey<Author>`) resolve
8094            // inside this submodule. Without this we'd hit
8095            // `proc_macro_derive_resolution_fallback` warnings.
8096            #[allow(unused_imports)]
8097            use super::*;
8098            #(#items)*
8099        }
8100    }
8101}
8102
8103fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8104    syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
8105}
8106
8107fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8108    syn::Ident::new(
8109        &format!("__rustango_cols_{struct_name}"),
8110        struct_name.span(),
8111    )
8112}
8113
8114fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
8115    let root = rustango_root();
8116    // The Postgres impl is always emitted — every rustango build pulls in
8117    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8118    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8119    // macro_rules whose body collapses to nothing when rustango is built
8120    // without the `mysql` feature. No user-facing feature shim required.
8121    //
8122    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8123    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8124    // back into a comma-separated list inside square brackets. Since each
8125    // entry is already in `field: expr` shape, the existing tokens slot in.
8126    quote! {
8127        #[cfg(feature = "postgres")]
8128        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8129            for #struct_name
8130        {
8131            fn from_row(
8132                row: &'r #root::sql::sqlx::postgres::PgRow,
8133            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8134                ::core::result::Result::Ok(Self {
8135                    #( #from_row_inits ),*
8136                })
8137            }
8138        }
8139
8140        #root::__impl_my_from_row!(#struct_name, |row| {
8141            #( #from_row_inits ),*
8142        });
8143
8144        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8145            #( #from_row_inits ),*
8146        });
8147    }
8148}
8149
8150struct ContainerAttrs {
8151    table: Option<String>,
8152    display: Option<(String, proc_macro2::Span)>,
8153    /// Explicit app label from `#[rustango(app = "blog")]`.
8154    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8155    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8156    /// at runtime — this attribute is the override for cases where
8157    /// the inference is wrong (e.g. a model that conceptually belongs
8158    /// to one app but is physically in another module).
8159    app: Option<String>,
8160    /// Per-model admin knobs from
8161    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8162    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8163    /// admin code falls back to `AdminConfig::DEFAULT`.
8164    admin: Option<AdminAttrs>,
8165    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8166    /// `None` when the model isn't audited — write paths emit no
8167    /// audit entries. When present, single-row writes capture
8168    /// before/after for the listed fields and bulk writes batch
8169    /// snapshots into one INSERT into `rustango_audit_log`.
8170    audit: Option<AuditAttrs>,
8171    /// `true` when `#[rustango(permissions)]` is present. Signals that
8172    /// `auto_create_permissions` should seed the four CRUD codenames for
8173    /// this model.
8174    permissions: bool,
8175    /// Many-to-many relations declared via
8176    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8177    ///                 src = "post_id", dst = "tag_id"))]`.
8178    m2m: Vec<M2MAttr>,
8179    /// Polymorphic M2M relations declared via
8180    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8181    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8182    ///   related_column = "tag_id"))]` (issue #818).
8183    generic_m2m: Vec<GenericM2MAttr>,
8184    /// Composite indexes declared via
8185    /// `#[rustango(index("col1, col2"))]` or
8186    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8187    /// Single-column indexes from `#[rustango(index)]` on fields are
8188    /// accumulated here during field collection.
8189    indexes: Vec<IndexAttr>,
8190    /// Table-level CHECK constraints declared via
8191    /// `#[rustango(check(name = "…", expr = "…"))]`.
8192    checks: Vec<CheckAttr>,
8193    /// Table-level PG `EXCLUDE` constraints declared via
8194    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8195    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8196    /// migration writer renders them on Postgres and skips with a
8197    /// warning on MySQL/SQLite. Issue #319.
8198    excludes: Vec<ExcludeAttr>,
8199    /// Composite (multi-column) FKs declared via
8200    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8201    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8202    composite_fks: Vec<CompositeFkAttr>,
8203    /// Generic ("any model") FKs declared via
8204    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8205    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8206    generic_fks: Vec<GenericFkAttr>,
8207    /// Where this model lives in a tenancy deployment, declared via
8208    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8209    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8210    /// to partition diff output between registry-scoped and
8211    /// tenant-scoped migration files.
8212    scope: Option<String>,
8213    /// Custom-Manager extension-trait name from
8214    /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8215    /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8216    /// adjacent to the model so users can write
8217    /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8218    /// and discover the convention from the model definition.
8219    manager_ext: Option<syn::Ident>,
8220    /// Extra QuerySet accessor names from
8221    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8222    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8223    /// the default `Self::objects()`. Multiple attributes allowed.
8224    manager_fns: Vec<syn::Ident>,
8225    /// Default ordering declared via `#[rustango(default_order =
8226    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8227    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8228    /// `-` prefix means descending; the `+` prefix or no prefix means
8229    /// ascending.
8230    default_order: Vec<(String, bool, proc_macro2::Span)>,
8231    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8232    /// Routes the emitted schema's `is_view = true` so the migration
8233    /// snapshot skips this model (its underlying SQL view is operator-
8234    /// managed, not rustango-managed).
8235    is_view: bool,
8236    /// `#[rustango(managed = false)]` — who owns the table.
8237    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8238    /// the migration snapshot skips this model so `makemigrations` /
8239    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8240    /// TABLE` against it (operator-managed schema).
8241    managed: bool,
8242    /// Name of the model's base manager, from
8243    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8244    /// `ModelSchema::base_manager_name`. Declarative-only today.
8245    base_manager_name: Option<String>,
8246    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8247    /// field this model's instances are ordered relative to.
8248    /// Declarative-only today; threaded onto
8249    /// `ModelSchema::order_with_respect_to`.
8250    order_with_respect_to: Option<String>,
8251    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8252    /// Marks the model as a proxy that
8253    /// shares its DB table with another struct. Threaded into
8254    /// `ModelSchema::proxy` so future codegen can skip table-owning
8255    /// behavior for proxies.
8256    proxy: bool,
8257    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8258    /// Each comma-separated capability token surfaces on
8259    /// `ModelSchema::required_db_features` so `manage check --deploy`
8260    /// can warn when the active dialect lacks one.
8261    required_db_features: Vec<String>,
8262    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8263    /// Normalized to the dialect name `manage check --deploy`
8264    /// compares against `Settings.database.backend`. Aliases
8265    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8266    /// stored under the canonical name.
8267    required_db_vendor: Option<String>,
8268    /// `#[rustango(default_related_name = "...")]`. Threaded into
8269    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8270    /// name to use when an FK / M2M field doesn't override it.
8271    /// Today rustango doesn't auto-emit reverse managers; the
8272    /// metadata is the foundation for that work.
8273    default_related_name: Option<String>,
8274    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8275    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8276    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8277    /// inline `COMMENT='...'`, SQLite: no-op).
8278    db_table_comment: Option<String>,
8279    /// `#[rustango(get_latest_by = "created_at")]` /
8280    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8281    /// `(column, descending)` where `descending = true` when the
8282    /// attribute value starts with `-`. Threaded into
8283    /// `ModelSchema::get_latest_by`.
8284    get_latest_by: Option<(String, bool)>,
8285    /// Extra `(codename, label)` permissions
8286    /// from `#[rustango(extra_permissions = "approve:Can approve,
8287    /// archive:Can archive")]`. Comma-separated `codename:label`
8288    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8289    extra_permissions: Vec<(String, String)>,
8290    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8291    /// `"view"`) the framework
8292    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8293    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8294    /// Validated at parse time; unknown actions fail with a span-pointing
8295    /// error.
8296    default_permissions: Vec<String>,
8297    /// `#[rustango(verbose_name = "blog post")]` — the
8298    /// human-readable singular label for the model. Threaded into
8299    /// `ModelSchema::verbose_name` so admin section headers /
8300    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8301    /// over the Rust struct identifier.
8302    verbose_name: Option<String>,
8303    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8304    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8305    /// When unset, `display_label_plural()` auto-suffixes `s`.
8306    verbose_name_plural: Option<String>,
8307    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8308    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8309    /// a name (used by `QuerySet::without_global_scope`) with a
8310    /// `fn() -> WhereExpr` path that the macro emits into
8311    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8312    global_scopes: Vec<GlobalScopeAttr>,
8313    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8314    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8315    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8316    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8317    /// entry emits an inherent `<name>_through(&self)` accessor that
8318    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8319    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8320    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8321    through_relations: Vec<ThroughAttr>,
8322    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8323    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8324    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8325    /// Each entry emits two associated functions on the parent —
8326    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8327    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8328    /// over a correlated subquery against the child table. Users
8329    /// drop the result into `QuerySet::where_raw(...)`.
8330    reverse_has_relations: Vec<ReverseHasAttr>,
8331    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8332    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8333    /// entry so the relation-existence family resolves polymorphic
8334    /// children by name.
8335    generic_has_relations: Vec<GenericHasAttr>,
8336}
8337
8338/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8339/// declaration. Each entry becomes one `core::GlobalScope` in the
8340/// emitted schema literal; `apply` resolves at macro-expand time
8341/// against the consumer's scope so the function must be in scope at
8342/// the use site. Issue #820.
8343struct GlobalScopeAttr {
8344    name: String,
8345    apply: syn::Path,
8346}
8347
8348/// Parsed `#[rustango(through(...))]` declaration. Issue
8349/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8350/// `hasManyThrough` / `hasOneThrough` parity.
8351///
8352/// `Country hasManyThrough Post via User` declares as:
8353///
8354/// ```ignore
8355/// #[rustango(through(
8356///     name                   = "posts",
8357///     far                    = "Post",
8358///     far_fk_column          = "author_id",
8359///     intermediate           = "User",
8360///     intermediate_fk_column = "country_id",
8361/// ))]
8362/// struct Country { ... }
8363/// ```
8364///
8365/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8366/// which returns a queryset filtered via a correlated subquery —
8367/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8368/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8369/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8370/// because the subquery lives inside a `WhereExpr::InSubquery` node
8371/// and the outer queryset's pending list stays empty.
8372///
8373/// All four required arguments use **SQL column / table names**
8374/// (not Rust field names) to sidestep the multi-hop-filter substrate
8375/// gap. Once that substrate lands, a higher-level Rust-field-name
8376/// shorthand can be added without breaking this surface.
8377struct ThroughAttr {
8378    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8379    name: String,
8380    /// Far model type identifier. `far = "Post"` → returns
8381    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8382    /// the derive expands.
8383    far: syn::Ident,
8384    /// SQL column on the far model's table that references the
8385    /// intermediate model's primary key. For `Post`'s
8386    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8387    /// default `<field>_id` convention) or whatever the user
8388    /// declared via `#[rustango(db_column = "...")]`.
8389    far_fk_column: String,
8390    /// Intermediate model type identifier. Needed to look up its
8391    /// `SCHEMA` so the subquery's `FROM` clause points at the
8392    /// intermediate table. `intermediate = "User"`.
8393    intermediate: syn::Ident,
8394    /// SQL column on the intermediate's table that references the
8395    /// source (this) model's primary key. For `User`'s
8396    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8397    intermediate_fk_column: String,
8398    /// SQL primary-key column on the intermediate's table — the
8399    /// column the subquery projects. Optional; defaults to `"id"`
8400    /// (rustango's default PK column name). Override when the
8401    /// intermediate declares a custom PK column.
8402    intermediate_pk_column: String,
8403}
8404
8405/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8406/// child_fk_column = "..."))]` declaration. Issue
8407/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8408/// `whereHas` / `whereDoesntHave` parity.
8409///
8410/// `Post hasMany Comment` declares as:
8411///
8412/// ```ignore
8413/// #[rustango(reverse_has(
8414///     name             = "comments",
8415///     child            = "Comment",
8416///     child_fk_column  = "post_id",
8417/// ))]
8418/// struct Post { ... }
8419/// ```
8420///
8421/// The macro emits two associated functions on `Post`:
8422///
8423/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8424///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8425///   <outer>.<self_pk_column>)`.
8426/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8427///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8428///
8429/// User code:
8430///
8431/// ```ignore
8432/// // Posts with at least one comment:
8433/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8434/// // Posts with no comments:
8435/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8436/// ```
8437///
8438/// As with #817, all column / table identifiers are **SQL names**
8439/// (not Rust field names) so the substrate is independent of the
8440/// outstanding multi-hop filter resolver gap. The emitted
8441/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8442/// SQL-emit time via the writer's scope stack.
8443struct ReverseHasAttr {
8444    /// Accessor name. `name = "comments"` → emits
8445    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8446    name: String,
8447    /// Child model type identifier. `child = "Comment"` — needed to
8448    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8449    /// points at the child table.
8450    child: syn::Ident,
8451    /// SQL column on the child's table that references this model's
8452    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8453    /// column is `"post_id"`.
8454    child_fk_column: String,
8455    /// SQL primary-key column on **this** model's table — the column
8456    /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8457    /// (rustango's default PK column name).
8458    self_pk_column: String,
8459}
8460
8461/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8462/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8463/// relation-existence family (issue #830). The child is a polymorphic,
8464/// content-type-discriminated model; emits a `Model::
8465/// generic_reverse_relations()` entry the queryset resolves by name.
8466struct GenericHasAttr {
8467    /// Accessor name, e.g. `name = "tags"`.
8468    name: String,
8469    /// Child model type identifier (`child = "Tag"`) — looked up for its
8470    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8471    child: syn::Ident,
8472    /// Column on the child table holding the parent's content-type id.
8473    /// Optional; defaults to `"content_type_id"`.
8474    ct_column: String,
8475    /// Column on the child table holding the parent's PK value.
8476    /// Optional; defaults to `"object_pk"`.
8477    pk_column: String,
8478    /// SQL primary-key column on **this** (parent) model's table.
8479    /// Optional; defaults to `"id"`.
8480    self_pk_column: String,
8481}
8482
8483/// Parsed form of one index declaration (field-level or container-level).
8484struct IndexAttr {
8485    /// Index name; auto-derived when `None` at parse time.
8486    name: Option<String>,
8487    /// Column names in the index.
8488    columns: Vec<String>,
8489    /// `true` for `CREATE UNIQUE INDEX`.
8490    unique: bool,
8491    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8492    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8493    /// `"btree"` when the attribute is absent — the DDL writer omits
8494    /// the `USING` clause and the backend uses its own default
8495    /// (btree on every supported dialect).
8496    method: String,
8497    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8498    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8499    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8500    where_clause: Option<String>,
8501    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8502    /// Empty `Vec` (the default) means "no covering columns".
8503    include: Vec<String>,
8504}
8505
8506/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8507struct CheckAttr {
8508    name: String,
8509    expr: String,
8510}
8511
8512/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8513/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8514/// PG-only — surfaced on every backend in the macro emit; the migration
8515/// writer skips the op on MySQL/SQLite. Issue #319.
8516struct ExcludeAttr {
8517    /// Constraint name (free-form Rust identifier).
8518    name: String,
8519    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8520    using: String,
8521    /// Comma-separated `(column, operator)` pairs, in declaration
8522    /// order. Parsed from `"col WITH op, col WITH op"`.
8523    elements: Vec<(String, String)>,
8524    /// Optional `WHERE` predicate (raw SQL).
8525    where_clause: Option<String>,
8526}
8527
8528/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8529/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8530/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8531/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8532/// the model, not the field.
8533struct CompositeFkAttr {
8534    /// Logical relation name (free-form Rust identifier).
8535    name: String,
8536    /// SQL table name of the target.
8537    to: String,
8538    /// Source-side column names, in declaration order.
8539    from: Vec<String>,
8540    /// Target-side column names, same length / order as `from`.
8541    on: Vec<String>,
8542}
8543
8544/// Parsed form of one `#[rustango(generic_fk(name = "target",
8545/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8546/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8547/// generic ("any model") FKs live on the model, not the field.
8548struct GenericFkAttr {
8549    /// Logical relation name (free-form Rust identifier).
8550    name: String,
8551    /// Source-side column carrying the `content_type_id` value.
8552    ct_column: String,
8553    /// Source-side column carrying the target row's primary key.
8554    pk_column: String,
8555}
8556
8557/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8558struct M2MAttr {
8559    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8560    name: String,
8561    /// Target table (e.g. `"app_tags"`).
8562    to: String,
8563    /// Junction table (e.g. `"post_tags"`).
8564    through: String,
8565    /// Source FK column in the junction table (e.g. `"post_id"`).
8566    src: String,
8567    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8568    dst: String,
8569    /// Whether the migration writer should auto-create the junction
8570    /// table. Default `true`. Set `auto_create = false` (#324) when
8571    /// the operator declares the through table as its own
8572    /// `#[derive(Model)]` struct with extra columns.
8573    auto_create: bool,
8574}
8575
8576/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8577/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8578/// junction carries a ContentType discriminator so unrelated models
8579/// share one pivot + related set.
8580struct GenericM2MAttr {
8581    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8582    name: String,
8583    /// Polymorphic junction table (e.g. `"taggables"`).
8584    through: String,
8585    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8586    pk_column: String,
8587    /// Junction column holding the owning model's `content_type_id`
8588    /// discriminator (e.g. `"taggable_type"`).
8589    ct_column: String,
8590    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8591    related_column: String,
8592}
8593
8594/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8595/// "user"))]`. `track` is a comma-separated list of field names whose
8596/// before/after values land in the JSONB `changes` column. `source`
8597/// is informational only — it pins a default source when the model
8598/// is written outside any `audit::with_source(...)` scope (rare).
8599#[derive(Default)]
8600struct AuditAttrs {
8601    /// Field names to capture in the `changes` JSONB. Validated
8602    /// against declared scalar fields at compile time. Empty means
8603    /// "track every scalar field".
8604    track: Option<(Vec<String>, proc_macro2::Span)>,
8605}
8606
8607/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8608/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8609/// comma-separated strings; we validate each ident against the model's
8610/// declared fields at compile time.
8611#[derive(Default)]
8612struct AdminAttrs {
8613    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8614    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8615    list_per_page: Option<usize>,
8616    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8617    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8618    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8619    /// Bulk action names. No field-validation against model fields —
8620    /// these are action handlers, not column references.
8621    actions: Option<(Vec<String>, proc_macro2::Span)>,
8622    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8623    /// sections, comma-separated fields per section, optional
8624    /// `Title:` prefix. Empty title omits the `<legend>`.
8625    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8626    /// `admin(list_display_links = "title")`. Names
8627    /// from `list_display` whose cells should link to detail/edit.
8628    /// Issue #350.
8629    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8630    /// `admin(search_help_text = "...")`. Short
8631    /// caption rendered beside the admin list view's search box.
8632    /// Issue #353.
8633    search_help_text: Option<String>,
8634    /// `admin(actions_on_top = false)`. Hides the
8635    /// action-bar above the table. Default `true`. Issue #354.
8636    actions_on_top: Option<bool>,
8637    /// `admin(actions_on_bottom = true)`. Renders an
8638    /// additional action-bar below the table. Default `false`.
8639    /// Issue #354.
8640    actions_on_bottom: Option<bool>,
8641    /// `admin(date_hierarchy = "created_at")`. Name of
8642    /// a date / datetime field whose values render as a clickable
8643    /// year / month / day drill-down strip above the list table.
8644    /// Empty / unset disables the strip. Issue #355.
8645    date_hierarchy: Option<String>,
8646    /// `admin(prepopulated_fields = "slug:title")`.
8647    /// Each entry is `target:source[+source2]`; multiple entries are
8648    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8649    /// The admin change-form emits JS that slugifies the source values
8650    /// into the target field on every keystroke. Issue #356.
8651    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8652    /// `admin(raw_id_fields = "parent, owner")`. Names
8653    /// of FK fields whose change-form widget renders a lookup link
8654    /// next to the input. Issue #357.
8655    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8656    /// `admin(autocomplete_fields = "author_id")`.
8657    /// Names of FK fields whose change-form widget renders an
8658    /// Ajax-driven typeahead populated from a `__autocomplete`
8659    /// endpoint on the target model. Issue #358.
8660    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8661    /// `admin(list_select_related = "all" | "none" | "author, …")`
8662    /// — tunes the admin list view's FK auto-JOIN
8663    /// policy. Default `"all"` matches rustango's join-everything
8664    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8665    list_select_related: Option<String>,
8666    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8667    /// Each entry is `field_name:widget_name`; multiple
8668    /// entries comma-separated. Empty / unset → no overrides. The
8669    /// list of widget names supported is documented on
8670    /// `AdminConfig::formfield_overrides`. Issue #359.
8671    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8672}
8673
8674fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8675    let mut out = ContainerAttrs {
8676        table: None,
8677        display: None,
8678        app: None,
8679        admin: None,
8680        audit: None,
8681        // Default `permissions = true` so every `#[derive(Model)]`
8682        // gets the four CRUD codenames seeded by `auto_create_permissions`
8683        // and is visible to non-superusers in the tenant admin without
8684        // manual per-model annotation. Models that intentionally don't
8685        // want permission rows (registry-internal types, framework
8686        // tables operators shouldn't manage directly) opt out via
8687        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8688        // out-of-the-box admin invisibility regression (#62).
8689        permissions: true,
8690        m2m: Vec::new(),
8691        generic_m2m: Vec::new(),
8692        indexes: Vec::new(),
8693        checks: Vec::new(),
8694        excludes: Vec::new(),
8695        composite_fks: Vec::new(),
8696        generic_fks: Vec::new(),
8697        scope: None,
8698        manager_ext: None,
8699        manager_fns: Vec::new(),
8700        default_order: Vec::new(),
8701        is_view: false,
8702        managed: true,
8703        verbose_name: None,
8704        verbose_name_plural: None,
8705        base_manager_name: None,
8706        order_with_respect_to: None,
8707        proxy: false,
8708        required_db_features: Vec::new(),
8709        required_db_vendor: None,
8710        default_related_name: None,
8711        db_table_comment: None,
8712        get_latest_by: None,
8713        extra_permissions: Vec::new(),
8714        default_permissions: Vec::new(),
8715        global_scopes: Vec::new(),
8716        through_relations: Vec::new(),
8717        reverse_has_relations: Vec::new(),
8718        generic_has_relations: Vec::new(),
8719    };
8720    for attr in &input.attrs {
8721        if !attr.path().is_ident("rustango") {
8722            continue;
8723        }
8724        attr.parse_nested_meta(|meta| {
8725            if meta.path.is_ident("table") {
8726                let s: LitStr = meta.value()?.parse()?;
8727                let name = s.value();
8728                // v0.27.3 (#65) — macro-time guard against table names
8729                // that compile but break SQL downstream. Hyphens are
8730                // the common footgun: PostgreSQL accepts a quoted
8731                // `"intermediate-region"` in CREATE TABLE, but the
8732                // FK / index name derivation in `migrate::ddl`
8733                // emits `intermediate-region_field_fkey` unquoted,
8734                // which then fails the SQL parser. Same shape rule
8735                // as Postgres regular identifiers so the safe path
8736                // is the only path.
8737                validate_table_name(&name, s.span())?;
8738                out.table = Some(name);
8739                return Ok(());
8740            }
8741            if meta.path.is_ident("display") {
8742                let s: LitStr = meta.value()?.parse()?;
8743                out.display = Some((s.value(), s.span()));
8744                return Ok(());
8745            }
8746            if meta.path.is_ident("app") {
8747                let s: LitStr = meta.value()?.parse()?;
8748                out.app = Some(s.value());
8749                return Ok(());
8750            }
8751            if meta.path.is_ident("scope") {
8752                let s: LitStr = meta.value()?.parse()?;
8753                let val = s.value();
8754                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8755                    return Err(meta.error(format!(
8756                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8757                    )));
8758                }
8759                out.scope = Some(val);
8760                return Ok(());
8761            }
8762            if meta.path.is_ident("admin") {
8763                let mut admin = AdminAttrs::default();
8764                meta.parse_nested_meta(|inner| {
8765                    if inner.path.is_ident("list_display") {
8766                        let s: LitStr = inner.value()?.parse()?;
8767                        admin.list_display =
8768                            Some((split_field_list(&s.value()), s.span()));
8769                        return Ok(());
8770                    }
8771                    if inner.path.is_ident("search_fields") {
8772                        let s: LitStr = inner.value()?.parse()?;
8773                        admin.search_fields =
8774                            Some((split_field_list(&s.value()), s.span()));
8775                        return Ok(());
8776                    }
8777                    if inner.path.is_ident("readonly_fields") {
8778                        let s: LitStr = inner.value()?.parse()?;
8779                        admin.readonly_fields =
8780                            Some((split_field_list(&s.value()), s.span()));
8781                        return Ok(());
8782                    }
8783                    if inner.path.is_ident("list_per_page") {
8784                        let lit: syn::LitInt = inner.value()?.parse()?;
8785                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8786                        return Ok(());
8787                    }
8788                    if inner.path.is_ident("ordering") {
8789                        let s: LitStr = inner.value()?.parse()?;
8790                        admin.ordering = Some((
8791                            parse_ordering_list(&s.value()),
8792                            s.span(),
8793                        ));
8794                        return Ok(());
8795                    }
8796                    if inner.path.is_ident("list_filter") {
8797                        let s: LitStr = inner.value()?.parse()?;
8798                        admin.list_filter =
8799                            Some((split_field_list(&s.value()), s.span()));
8800                        return Ok(());
8801                    }
8802                    if inner.path.is_ident("actions") {
8803                        let s: LitStr = inner.value()?.parse()?;
8804                        admin.actions =
8805                            Some((split_field_list(&s.value()), s.span()));
8806                        return Ok(());
8807                    }
8808                    if inner.path.is_ident("fieldsets") {
8809                        let s: LitStr = inner.value()?.parse()?;
8810                        admin.fieldsets =
8811                            Some((parse_fieldset_list(&s.value()), s.span()));
8812                        return Ok(());
8813                    }
8814                    if inner.path.is_ident("list_display_links") {
8815                        let s: LitStr = inner.value()?.parse()?;
8816                        admin.list_display_links =
8817                            Some((split_field_list(&s.value()), s.span()));
8818                        return Ok(());
8819                    }
8820                    if inner.path.is_ident("search_help_text") {
8821                        let s: LitStr = inner.value()?.parse()?;
8822                        admin.search_help_text = Some(s.value());
8823                        return Ok(());
8824                    }
8825                    if inner.path.is_ident("actions_on_top") {
8826                        let lit: syn::LitBool = inner.value()?.parse()?;
8827                        admin.actions_on_top = Some(lit.value);
8828                        return Ok(());
8829                    }
8830                    if inner.path.is_ident("actions_on_bottom") {
8831                        let lit: syn::LitBool = inner.value()?.parse()?;
8832                        admin.actions_on_bottom = Some(lit.value);
8833                        return Ok(());
8834                    }
8835                    if inner.path.is_ident("date_hierarchy") {
8836                        let s: LitStr = inner.value()?.parse()?;
8837                        admin.date_hierarchy = Some(s.value());
8838                        return Ok(());
8839                    }
8840                    if inner.path.is_ident("prepopulated_fields") {
8841                        let s: LitStr = inner.value()?.parse()?;
8842                        admin.prepopulated_fields =
8843                            Some((parse_prepopulated_list(&s.value()), s.span()));
8844                        return Ok(());
8845                    }
8846                    if inner.path.is_ident("raw_id_fields") {
8847                        let s: LitStr = inner.value()?.parse()?;
8848                        admin.raw_id_fields =
8849                            Some((split_field_list(&s.value()), s.span()));
8850                        return Ok(());
8851                    }
8852                    if inner.path.is_ident("autocomplete_fields") {
8853                        let s: LitStr = inner.value()?.parse()?;
8854                        admin.autocomplete_fields =
8855                            Some((split_field_list(&s.value()), s.span()));
8856                        return Ok(());
8857                    }
8858                    if inner.path.is_ident("list_select_related") {
8859                        let s: LitStr = inner.value()?.parse()?;
8860                        admin.list_select_related = Some(s.value());
8861                        return Ok(());
8862                    }
8863                    if inner.path.is_ident("formfield_overrides") {
8864                        let s: LitStr = inner.value()?.parse()?;
8865                        admin.formfield_overrides =
8866                            Some((parse_formfield_overrides(&s.value()), s.span()));
8867                        return Ok(());
8868                    }
8869                    Err(inner.error(
8870                        "unknown admin attribute (supported: \
8871                         `list_display`, `list_display_links`, \
8872                         `search_fields`, `search_help_text`, \
8873                         `readonly_fields`, \
8874                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8875                         `actions_on_top`, `actions_on_bottom`, \
8876                         `date_hierarchy`, \
8877                         `prepopulated_fields`, \
8878                         `raw_id_fields`, \
8879                         `autocomplete_fields`, \
8880                         `list_select_related`, \
8881                         `formfield_overrides`, \
8882                         `fieldsets`)",
8883                    ))
8884                })?;
8885                out.admin = Some(admin);
8886                return Ok(());
8887            }
8888            if meta.path.is_ident("manager") {
8889                // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8890                // Stretch `from_queryset = "..."` is left as a
8891                // follow-up — the issue's primary acceptance is the
8892                // `ext = ...` trait emission.
8893                meta.parse_nested_meta(|inner| {
8894                    if inner.path.is_ident("ext") {
8895                        let s: LitStr = inner.value()?.parse()?;
8896                        let name = s.value();
8897                        if name.is_empty() {
8898                            return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8899                        }
8900                        out.manager_ext =
8901                            Some(syn::Ident::new(&name, s.span()));
8902                        return Ok(());
8903                    }
8904                    Err(inner.error(
8905                        "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8906                    ))
8907                })?;
8908                return Ok(());
8909            }
8910            if meta.path.is_ident("manager_fn") {
8911                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8912                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8913                // next to the default `Self::objects()`. Multiple
8914                // attributes accumulate.
8915                let s: LitStr = meta.value()?.parse()?;
8916                let name = s.value();
8917                if name.is_empty() {
8918                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8919                }
8920                if name == "objects" {
8921                    return Err(meta.error(
8922                        "`manager_fn = \"objects\"` collides with the default \
8923                         accessor — pick a different name",
8924                    ));
8925                }
8926                let ident = syn::Ident::new(&name, s.span());
8927                if out.manager_fns.iter().any(|prev| *prev == ident) {
8928                    return Err(meta.error(format!(
8929                        "duplicate `manager_fn = \"{name}\"`"
8930                    )));
8931                }
8932                out.manager_fns.push(ident);
8933                return Ok(());
8934            }
8935            if meta.path.is_ident("default_order") {
8936                // `#[rustango(default_order = "-created_at, status")]`
8937                // — issue #291 / T2.5. Comma-separated list; `-prefix`
8938                // means descending, `+prefix` or bare name means ascending.
8939                // Per-query opt-in via `QuerySet::with_default_order()`.
8940                let s: LitStr = meta.value()?.parse()?;
8941                let raw = s.value();
8942                let span = s.span();
8943                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8944                    Vec::new();
8945                for entry in raw.split(',') {
8946                    let trimmed = entry.trim();
8947                    if trimmed.is_empty() {
8948                        return Err(syn::Error::new(
8949                            span,
8950                            "`default_order = \"...\"` has an empty entry — \
8951                             check for a stray comma",
8952                        ));
8953                    }
8954                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8955                        (true, rest.trim().to_owned())
8956                    } else if let Some(rest) = trimmed.strip_prefix('+') {
8957                        (false, rest.trim().to_owned())
8958                    } else {
8959                        (false, trimmed.to_owned())
8960                    };
8961                    if name.is_empty() {
8962                        return Err(syn::Error::new(
8963                            span,
8964                            "`default_order` entry has no column name after the prefix",
8965                        ));
8966                    }
8967                    if parsed.iter().any(|(n, _, _)| *n == name) {
8968                        return Err(syn::Error::new(
8969                            span,
8970                            format!("duplicate column `{name}` in `default_order`"),
8971                        ));
8972                    }
8973                    parsed.push((name, desc, span));
8974                }
8975                if parsed.is_empty() {
8976                    return Err(syn::Error::new(
8977                        span,
8978                        "`default_order = \"...\"` cannot be empty",
8979                    ));
8980                }
8981                out.default_order = parsed;
8982                return Ok(());
8983            }
8984            if meta.path.is_ident("global_scope") {
8985                // `#[rustango(global_scope(name = "active", apply =
8986                //  path::to::fn))]` — issue #820. The apply function
8987                // path resolves at macro-expand time in the consumer's
8988                // scope; the macro re-emits it verbatim into the
8989                // `ModelSchema::global_scopes` slice literal.
8990                let span = meta.path.span();
8991                let mut scope_name: Option<String> = None;
8992                let mut apply_path: Option<syn::Path> = None;
8993                meta.parse_nested_meta(|inner| {
8994                    if inner.path.is_ident("name") {
8995                        let s: LitStr = inner.value()?.parse()?;
8996                        let raw = s.value();
8997                        if raw.trim().is_empty() {
8998                            return Err(syn::Error::new(
8999                                s.span(),
9000                                "`global_scope(name = \"...\")` must not be empty",
9001                            ));
9002                        }
9003                        scope_name = Some(raw);
9004                        return Ok(());
9005                    }
9006                    if inner.path.is_ident("apply") {
9007                        let p: syn::Path = inner.value()?.parse()?;
9008                        apply_path = Some(p);
9009                        return Ok(());
9010                    }
9011                    Err(inner.error(
9012                        "unknown `global_scope` attribute (supported: \
9013                         `name`, `apply`)",
9014                    ))
9015                })?;
9016                let Some(name) = scope_name else {
9017                    return Err(syn::Error::new(
9018                        span,
9019                        "`global_scope` requires `name = \"...\"`",
9020                    ));
9021                };
9022                let Some(apply) = apply_path else {
9023                    return Err(syn::Error::new(
9024                        span,
9025                        "`global_scope` requires `apply = fn_path`",
9026                    ));
9027                };
9028                if out.global_scopes.iter().any(|s| s.name == name) {
9029                    return Err(syn::Error::new(
9030                        span,
9031                        format!(
9032                            "duplicate global scope name `{name}` — \
9033                             pick a unique identifier so \
9034                             `QuerySet::without_global_scope(\"{name}\")` \
9035                             is unambiguous"
9036                        ),
9037                    ));
9038                }
9039                out.global_scopes.push(GlobalScopeAttr { name, apply });
9040                return Ok(());
9041            }
9042            if meta.path.is_ident("through") {
9043                // `#[rustango(through(name = "posts", far = "Post",
9044                //  far_fk_column = "author_id", intermediate = "User",
9045                //  intermediate_fk_column = "country_id"
9046                //  [, intermediate_pk_column = "id"]))]` — issue #817.
9047                let span = meta.path.span();
9048                let mut accessor_name: Option<String> = None;
9049                let mut far_ident: Option<syn::Ident> = None;
9050                let mut far_fk_column: Option<String> = None;
9051                let mut intermediate_ident: Option<syn::Ident> = None;
9052                let mut intermediate_fk_column: Option<String> = None;
9053                let mut intermediate_pk_column: Option<String> = None;
9054                fn parse_nonempty_string(
9055                    inner: &syn::meta::ParseNestedMeta<'_>,
9056                    field: &str,
9057                ) -> syn::Result<String> {
9058                    let s: LitStr = inner.value()?.parse()?;
9059                    let raw = s.value();
9060                    let trimmed = raw.trim();
9061                    if trimmed.is_empty() {
9062                        return Err(syn::Error::new(
9063                            s.span(),
9064                            format!("`through({field} = \"...\")` must not be empty"),
9065                        ));
9066                    }
9067                    Ok(trimmed.to_owned())
9068                }
9069                meta.parse_nested_meta(|inner| {
9070                    if inner.path.is_ident("name") {
9071                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9072                        return Ok(());
9073                    }
9074                    if inner.path.is_ident("far") {
9075                        // Far model name accepted as a string literal
9076                        // so the attribute fits inside the existing
9077                        // `parse_nested_meta` shape; parsed back into a
9078                        // `syn::Ident` so the emitted accessor can
9079                        // reference the type directly.
9080                        let s: LitStr = inner.value()?.parse()?;
9081                        let raw = s.value();
9082                        let trimmed = raw.trim();
9083                        if trimmed.is_empty() {
9084                            return Err(syn::Error::new(
9085                                s.span(),
9086                                "`through(far = \"...\")` must not be empty",
9087                            ));
9088                        }
9089                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9090                        return Ok(());
9091                    }
9092                    if inner.path.is_ident("far_fk_column") {
9093                        far_fk_column =
9094                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9095                        return Ok(());
9096                    }
9097                    if inner.path.is_ident("intermediate") {
9098                        let s: LitStr = inner.value()?.parse()?;
9099                        let raw = s.value();
9100                        let trimmed = raw.trim();
9101                        if trimmed.is_empty() {
9102                            return Err(syn::Error::new(
9103                                s.span(),
9104                                "`through(intermediate = \"...\")` must not be empty",
9105                            ));
9106                        }
9107                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9108                        return Ok(());
9109                    }
9110                    if inner.path.is_ident("intermediate_fk_column") {
9111                        intermediate_fk_column =
9112                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9113                        return Ok(());
9114                    }
9115                    if inner.path.is_ident("intermediate_pk_column") {
9116                        intermediate_pk_column =
9117                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9118                        return Ok(());
9119                    }
9120                    Err(inner.error(
9121                        "unknown `through` attribute (supported: \
9122                         `name`, `far`, `far_fk_column`, \
9123                         `intermediate`, `intermediate_fk_column`, \
9124                         `intermediate_pk_column`)",
9125                    ))
9126                })?;
9127                let Some(name) = accessor_name else {
9128                    return Err(syn::Error::new(
9129                        span,
9130                        "`through` requires `name = \"...\"`",
9131                    ));
9132                };
9133                let Some(far) = far_ident else {
9134                    return Err(syn::Error::new(
9135                        span,
9136                        "`through` requires `far = \"FarModelType\"`",
9137                    ));
9138                };
9139                let Some(far_fk_column) = far_fk_column else {
9140                    return Err(syn::Error::new(
9141                        span,
9142                        "`through` requires `far_fk_column = \"<column>\"`",
9143                    ));
9144                };
9145                let Some(intermediate) = intermediate_ident else {
9146                    return Err(syn::Error::new(
9147                        span,
9148                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9149                    ));
9150                };
9151                let Some(intermediate_fk_column) = intermediate_fk_column else {
9152                    return Err(syn::Error::new(
9153                        span,
9154                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9155                    ));
9156                };
9157                let intermediate_pk_column =
9158                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9159                if out.through_relations.iter().any(|t| t.name == name) {
9160                    return Err(syn::Error::new(
9161                        span,
9162                        format!(
9163                            "duplicate `through(name = \"{name}\")` — \
9164                             pick a unique accessor name"
9165                        ),
9166                    ));
9167                }
9168                out.through_relations.push(ThroughAttr {
9169                    name,
9170                    far,
9171                    far_fk_column,
9172                    intermediate,
9173                    intermediate_fk_column,
9174                    intermediate_pk_column,
9175                });
9176                return Ok(());
9177            }
9178            if meta.path.is_ident("reverse_has") {
9179                // `#[rustango(reverse_has(name = "comments",
9180                //  child = "Comment", child_fk_column = "post_id"
9181                //  [, self_pk_column = "id"]))]` — issue #830.
9182                let span = meta.path.span();
9183                let mut accessor_name: Option<String> = None;
9184                let mut child_ident: Option<syn::Ident> = None;
9185                let mut child_fk_column: Option<String> = None;
9186                let mut self_pk_column: Option<String> = None;
9187                meta.parse_nested_meta(|inner| {
9188                    if inner.path.is_ident("name") {
9189                        let s: LitStr = inner.value()?.parse()?;
9190                        let raw = s.value();
9191                        if raw.trim().is_empty() {
9192                            return Err(syn::Error::new(
9193                                s.span(),
9194                                "`reverse_has(name = \"...\")` must not be empty",
9195                            ));
9196                        }
9197                        accessor_name = Some(raw);
9198                        return Ok(());
9199                    }
9200                    if inner.path.is_ident("child") {
9201                        let s: LitStr = inner.value()?.parse()?;
9202                        let raw = s.value();
9203                        let trimmed = raw.trim();
9204                        if trimmed.is_empty() {
9205                            return Err(syn::Error::new(
9206                                s.span(),
9207                                "`reverse_has(child = \"...\")` must not be empty",
9208                            ));
9209                        }
9210                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9211                        return Ok(());
9212                    }
9213                    if inner.path.is_ident("child_fk_column") {
9214                        let s: LitStr = inner.value()?.parse()?;
9215                        let raw = s.value();
9216                        let trimmed = raw.trim();
9217                        if trimmed.is_empty() {
9218                            return Err(syn::Error::new(
9219                                s.span(),
9220                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9221                            ));
9222                        }
9223                        child_fk_column = Some(trimmed.to_owned());
9224                        return Ok(());
9225                    }
9226                    if inner.path.is_ident("self_pk_column") {
9227                        let s: LitStr = inner.value()?.parse()?;
9228                        let raw = s.value();
9229                        let trimmed = raw.trim();
9230                        if trimmed.is_empty() {
9231                            return Err(syn::Error::new(
9232                                s.span(),
9233                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9234                            ));
9235                        }
9236                        self_pk_column = Some(trimmed.to_owned());
9237                        return Ok(());
9238                    }
9239                    Err(inner.error(
9240                        "unknown `reverse_has` attribute (supported: \
9241                         `name`, `child`, `child_fk_column`, \
9242                         `self_pk_column`)",
9243                    ))
9244                })?;
9245                let Some(name) = accessor_name else {
9246                    return Err(syn::Error::new(
9247                        span,
9248                        "`reverse_has` requires `name = \"...\"`",
9249                    ));
9250                };
9251                let Some(child) = child_ident else {
9252                    return Err(syn::Error::new(
9253                        span,
9254                        "`reverse_has` requires `child = \"ChildModelType\"`",
9255                    ));
9256                };
9257                let Some(child_fk_column) = child_fk_column else {
9258                    return Err(syn::Error::new(
9259                        span,
9260                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9261                    ));
9262                };
9263                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9264                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9265                    return Err(syn::Error::new(
9266                        span,
9267                        format!(
9268                            "duplicate `reverse_has(name = \"{name}\")` — \
9269                             pick a unique accessor name"
9270                        ),
9271                    ));
9272                }
9273                out.reverse_has_relations.push(ReverseHasAttr {
9274                    name,
9275                    child,
9276                    child_fk_column,
9277                    self_pk_column,
9278                });
9279                return Ok(());
9280            }
9281            if meta.path.is_ident("generic_has") {
9282                // `#[rustango(generic_has(name = "tags",
9283                //  child = "Tag", ct_column = "content_type_id",
9284                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9285                //  — issue #830, the reverse generic-FK (GFK) arm of the
9286                //  relation-existence family.
9287                let span = meta.path.span();
9288                let mut accessor_name: Option<String> = None;
9289                let mut child_ident: Option<syn::Ident> = None;
9290                let mut ct_column: Option<String> = None;
9291                let mut pk_column: Option<String> = None;
9292                let mut self_pk_column: Option<String> = None;
9293                meta.parse_nested_meta(|inner| {
9294                    if inner.path.is_ident("name") {
9295                        let s: LitStr = inner.value()?.parse()?;
9296                        let raw = s.value();
9297                        if raw.trim().is_empty() {
9298                            return Err(syn::Error::new(
9299                                s.span(),
9300                                "`generic_has(name = \"...\")` must not be empty",
9301                            ));
9302                        }
9303                        accessor_name = Some(raw);
9304                        return Ok(());
9305                    }
9306                    if inner.path.is_ident("child") {
9307                        let s: LitStr = inner.value()?.parse()?;
9308                        let trimmed = s.value().trim().to_owned();
9309                        if trimmed.is_empty() {
9310                            return Err(syn::Error::new(
9311                                s.span(),
9312                                "`generic_has(child = \"...\")` must not be empty",
9313                            ));
9314                        }
9315                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9316                        return Ok(());
9317                    }
9318                    if inner.path.is_ident("ct_column") {
9319                        let s: LitStr = inner.value()?.parse()?;
9320                        let trimmed = s.value().trim().to_owned();
9321                        if trimmed.is_empty() {
9322                            return Err(syn::Error::new(
9323                                s.span(),
9324                                "`generic_has(ct_column = \"...\")` must not be empty",
9325                            ));
9326                        }
9327                        ct_column = Some(trimmed);
9328                        return Ok(());
9329                    }
9330                    if inner.path.is_ident("pk_column") {
9331                        let s: LitStr = inner.value()?.parse()?;
9332                        let trimmed = s.value().trim().to_owned();
9333                        if trimmed.is_empty() {
9334                            return Err(syn::Error::new(
9335                                s.span(),
9336                                "`generic_has(pk_column = \"...\")` must not be empty",
9337                            ));
9338                        }
9339                        pk_column = Some(trimmed);
9340                        return Ok(());
9341                    }
9342                    if inner.path.is_ident("self_pk_column") {
9343                        let s: LitStr = inner.value()?.parse()?;
9344                        let trimmed = s.value().trim().to_owned();
9345                        if trimmed.is_empty() {
9346                            return Err(syn::Error::new(
9347                                s.span(),
9348                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9349                            ));
9350                        }
9351                        self_pk_column = Some(trimmed);
9352                        return Ok(());
9353                    }
9354                    Err(inner.error(
9355                        "unknown `generic_has` attribute (supported: \
9356                         `name`, `child`, `ct_column`, `pk_column`, \
9357                         `self_pk_column`)",
9358                    ))
9359                })?;
9360                let Some(name) = accessor_name else {
9361                    return Err(syn::Error::new(
9362                        span,
9363                        "`generic_has` requires `name = \"...\"`",
9364                    ));
9365                };
9366                let Some(child) = child_ident else {
9367                    return Err(syn::Error::new(
9368                        span,
9369                        "`generic_has` requires `child = \"ChildModelType\"`",
9370                    ));
9371                };
9372                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9373                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9374                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9375                if out.generic_has_relations.iter().any(|r| r.name == name) {
9376                    return Err(syn::Error::new(
9377                        span,
9378                        format!(
9379                            "duplicate `generic_has(name = \"{name}\")` — \
9380                             pick a unique accessor name"
9381                        ),
9382                    ));
9383                }
9384                out.generic_has_relations.push(GenericHasAttr {
9385                    name,
9386                    child,
9387                    ct_column,
9388                    pk_column,
9389                    self_pk_column,
9390                });
9391                return Ok(());
9392            }
9393            if meta.path.is_ident("audit") {
9394                let mut audit = AuditAttrs::default();
9395                meta.parse_nested_meta(|inner| {
9396                    if inner.path.is_ident("track") {
9397                        let s: LitStr = inner.value()?.parse()?;
9398                        audit.track =
9399                            Some((split_field_list(&s.value()), s.span()));
9400                        return Ok(());
9401                    }
9402                    Err(inner.error(
9403                        "unknown audit attribute (supported: `track`)",
9404                    ))
9405                })?;
9406                out.audit = Some(audit);
9407                return Ok(());
9408            }
9409            if meta.path.is_ident("permissions") {
9410                // Two forms accepted:
9411                //   #[rustango(permissions)]          — flag form, true
9412                //   #[rustango(permissions = false)]  — explicit opt-out
9413                //   #[rustango(permissions = true)]   — explicit opt-in
9414                if let Ok(v) = meta.value() {
9415                    let lit: syn::LitBool = v.parse()?;
9416                    out.permissions = lit.value;
9417                } else {
9418                    out.permissions = true;
9419                }
9420                return Ok(());
9421            }
9422            if meta.path.is_ident("view") {
9423                // Issue #293 / T2.10. Two forms accepted, matching
9424                // the `permissions` flag pattern:
9425                //   #[rustango(view)]          — flag form, true
9426                //   #[rustango(view = false)]  — explicit opt-out
9427                //   #[rustango(view = true)]   — explicit opt-in
9428                if let Ok(v) = meta.value() {
9429                    let lit: syn::LitBool = v.parse()?;
9430                    out.is_view = lit.value;
9431                } else {
9432                    out.is_view = true;
9433                }
9434                return Ok(());
9435            }
9436            if meta.path.is_ident("managed") {
9437                // Who owns the table. Issue #321.
9438                //   #[rustango(managed = false)]  — operator-managed table
9439                //   #[rustango(managed = true)]   — rustango-managed (the default)
9440                // Bare-flag form is intentionally not accepted: writing
9441                // `#[rustango(managed)]` reads as "yes please manage it"
9442                // which is already the default. The opt-out is the only
9443                // useful state, so it must be explicit.
9444                let v = meta.value()?;
9445                let lit: syn::LitBool = v.parse()?;
9446                out.managed = lit.value;
9447                return Ok(());
9448            }
9449            if meta.path.is_ident("verbose_name") {
9450                let s: LitStr = meta.value()?.parse()?;
9451                out.verbose_name = Some(s.value());
9452                return Ok(());
9453            }
9454            if meta.path.is_ident("verbose_name_plural") {
9455                let s: LitStr = meta.value()?.parse()?;
9456                out.verbose_name_plural = Some(s.value());
9457                return Ok(());
9458            }
9459            if meta.path.is_ident("db_table_comment") {
9460                // Free-form table-level comment attached to the DB
9461                // catalog.
9462                let s: LitStr = meta.value()?.parse()?;
9463                out.db_table_comment = Some(s.value());
9464                return Ok(());
9465            }
9466            if meta.path.is_ident("proxy") {
9467                // Declarative flag
9468                // marking the struct as a proxy of another model that
9469                // shares its DB table. Stored on `ModelSchema::proxy`
9470                // so future codegen can skip `CreateTable` emission
9471                // for proxies (parent owns the table) and route
9472                // per-instance method resolution to the proxy class.
9473                //
9474                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9475                let value = if meta.input.peek(syn::Token![=]) {
9476                    meta.value()?.parse::<syn::LitBool>()?.value
9477                } else {
9478                    true
9479                };
9480                out.proxy = value;
9481                return Ok(());
9482            }
9483            if meta.path.is_ident("order_with_respect_to") {
9484                // `order_with_respect_to = "parent_fk"` — the model's
9485                // instances are intrinsically ordered relative to
9486                // their parent FK.
9487                //
9488                // rustango stores the FK field name on
9489                // `ModelSchema::order_with_respect_to`. Declarative-only
9490                // today: the migration writer + admin surfaces still
9491                // treat every model identically. Future codegen will
9492                // key off the metadata to auto-emit the `_order`
9493                // column and reorder helpers.
9494                //
9495                // Validated as a Rust-shape identifier so the macro
9496                // can reject typos at derive time.
9497                let s: LitStr = meta.value()?.parse()?;
9498                let raw = s.value();
9499                if raw.is_empty() {
9500                    return Err(syn::Error::new(
9501                        s.span(),
9502                        "`order_with_respect_to` must be a non-empty FK field name",
9503                    ));
9504                }
9505                let valid = raw
9506                    .chars()
9507                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9508                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9509                if !valid {
9510                    return Err(syn::Error::new(
9511                        s.span(),
9512                        format!(
9513                            "`order_with_respect_to` must be a valid Rust \
9514                             identifier (letters / digits / underscores, \
9515                             not starting with a digit); got `{raw}`"
9516                        ),
9517                    ));
9518                }
9519                out.order_with_respect_to = Some(raw);
9520                return Ok(());
9521            }
9522            if meta.path.is_ident("required_db_features") {
9523                // Capability tokens the model needs (e.g.
9524                // `"json_extract"`, `"window_functions"`,
9525                // `"row_security"`). Comma-separated.
9526                // `manage check --deploy` walks every model and warns
9527                // when the active backend doesn't advertise the
9528                // capability.
9529                //
9530                // rustango ships a small registry of capability tokens
9531                // each dialect supports — see `Dialect::supports`.
9532                // Unknown tokens still parse (they end up on the
9533                // schema and show up in the warning) so projects can
9534                // declare aspirational capabilities and the check
9535                // verb will keep nagging until the dialect implements
9536                // them.
9537                let s: LitStr = meta.value()?.parse()?;
9538                let raw = s.value();
9539                let features: Vec<String> = raw
9540                    .split(',')
9541                    .map(str::trim)
9542                    .filter(|s| !s.is_empty())
9543                    .map(str::to_owned)
9544                    .collect();
9545                if features.is_empty() {
9546                    return Err(syn::Error::new(
9547                        s.span(),
9548                        "`required_db_features` must list at least one \
9549                         comma-separated capability token",
9550                    ));
9551                }
9552                out.required_db_features = features;
9553                return Ok(());
9554            }
9555            if meta.path.is_ident("required_db_vendor") {
9556                // The model is only meant to run against the named DB
9557                // backend. `manage check --deploy` flags a mismatch so
9558                // ops catches "I forgot to switch DATABASE_URL" at
9559                // deploy time rather than runtime.
9560                //
9561                // Restricted to the three backends rustango ships
9562                // dialects for so the check verb can compare reliably.
9563                let s: LitStr = meta.value()?.parse()?;
9564                let raw = s.value().to_ascii_lowercase();
9565                match raw.as_str() {
9566                    "postgresql" | "postgres" | "pg" => {
9567                        out.required_db_vendor = Some("postgres".to_owned());
9568                    }
9569                    "mysql" | "mariadb" => {
9570                        out.required_db_vendor = Some("mysql".to_owned());
9571                    }
9572                    "sqlite" | "sqlite3" => {
9573                        out.required_db_vendor = Some("sqlite".to_owned());
9574                    }
9575                    _ => {
9576                        return Err(syn::Error::new(
9577                            s.span(),
9578                            format!(
9579                                "unknown required_db_vendor `{raw}` — \
9580                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9581                                 `mysql` (alias: `mariadb`), or `sqlite` \
9582                                 (alias: `sqlite3`)"
9583                            ),
9584                        ));
9585                    }
9586                }
9587                return Ok(());
9588            }
9589            if meta.path.is_ident("base_manager_name") {
9590                // Name of the manager that `<instance>.<relation>_set`
9591                // uses when resolving reverse-relation managers.
9592                // Distinct from `default_manager_name` (what
9593                // `Self::objects()` returns). Stored on
9594                // `ModelSchema::base_manager_name`.
9595                //
9596                // Validated as a Rust identifier so it stays safe to
9597                // re-emit as code in future reverse-manager codegen.
9598                let s: LitStr = meta.value()?.parse()?;
9599                let raw = s.value();
9600                if raw.is_empty() {
9601                    return Err(syn::Error::new(
9602                        s.span(),
9603                        "`base_manager_name` must be a non-empty string",
9604                    ));
9605                }
9606                let valid = raw
9607                    .chars()
9608                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9609                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9610                if !valid {
9611                    return Err(syn::Error::new(
9612                        s.span(),
9613                        format!(
9614                            "`base_manager_name` must be a valid Rust \
9615                             identifier (letters / digits / underscores, \
9616                             not starting with a digit); got `{raw}`"
9617                        ),
9618                    ));
9619                }
9620                out.base_manager_name = Some(raw);
9621                return Ok(());
9622            }
9623            if meta.path.is_ident("default_related_name") {
9624                // The name reverse-relation accessors use when callers
9625                // don't override `related_name = "..."` on the FK / M2M
9626                // field. Stored on `ModelSchema::default_related_name`
9627                // so future reverse-manager codegen / schema emit /
9628                // admin templates can pick the right accessor name
9629                // (today rustango doesn't auto-emit reverse managers;
9630                // the metadata is the foundation for that work).
9631                //
9632                // Enforced non-empty + ASCII identifier-shape so the
9633                // string is safe to use as a Rust ident later.
9634                let s: LitStr = meta.value()?.parse()?;
9635                let raw = s.value();
9636                if raw.is_empty() {
9637                    return Err(syn::Error::new(
9638                        s.span(),
9639                        "`default_related_name` must be a non-empty string",
9640                    ));
9641                }
9642                let valid = raw
9643                    .chars()
9644                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9645                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9646                if !valid {
9647                    return Err(syn::Error::new(
9648                        s.span(),
9649                        format!(
9650                            "`default_related_name` must be snake_case ASCII \
9651                             (lowercase letters / digits / underscores, not \
9652                             starting with a digit); got `{raw}`"
9653                        ),
9654                    ));
9655                }
9656                out.default_related_name = Some(raw);
9657                return Ok(());
9658            }
9659            if meta.path.is_ident("extra_permissions") {
9660                // Comma-separated `codename:label` pairs.
9661                let s: LitStr = meta.value()?.parse()?;
9662                let raw = s.value();
9663                let mut pairs = Vec::new();
9664                for entry in raw.split(',') {
9665                    let entry = entry.trim();
9666                    if entry.is_empty() {
9667                        continue;
9668                    }
9669                    let (codename, label) = match entry.split_once(':') {
9670                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9671                        None => (entry.to_owned(), entry.to_owned()),
9672                    };
9673                    if codename.is_empty() {
9674                        return Err(meta.error(
9675                            "`extra_permissions` entries must be `codename:label` pairs",
9676                        ));
9677                    }
9678                    pairs.push((codename, label));
9679                }
9680                if pairs.is_empty() {
9681                    return Err(meta
9682                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9683                }
9684                out.extra_permissions = pairs;
9685                return Ok(());
9686            }
9687            if meta.path.is_ident("default_permissions") {
9688                // Comma-separated subset of the CRUD action set.
9689                // Empty means all four (the framework default).
9690                let s: LitStr = meta.value()?.parse()?;
9691                let raw = s.value();
9692                let mut actions: Vec<String> = Vec::new();
9693                for entry in raw.split(',') {
9694                    let action = entry.trim().to_ascii_lowercase();
9695                    if action.is_empty() {
9696                        continue;
9697                    }
9698                    match action.as_str() {
9699                        "add" | "change" | "delete" | "view" => {}
9700                        other => {
9701                            return Err(syn::Error::new(
9702                                s.span(),
9703                                format!(
9704                                    "unknown default_permissions action `{other}` — \
9705                                     expected one of `add`, `change`, `delete`, `view`"
9706                                ),
9707                            ));
9708                        }
9709                    }
9710                    if !actions.contains(&action) {
9711                        actions.push(action);
9712                    }
9713                }
9714                if actions.is_empty() {
9715                    return Err(syn::Error::new(
9716                        s.span(),
9717                        "`default_permissions = \"…\"` must list at least one action; \
9718                         use `permissions = false` on the container if you want NO \
9719                         permissions seeded for this model.",
9720                    ));
9721                }
9722                out.default_permissions = actions;
9723                return Ok(());
9724            }
9725            if meta.path.is_ident("get_latest_by") {
9726                // The `-` prefix selects descending order.
9727                let s: LitStr = meta.value()?.parse()?;
9728                let raw = s.value();
9729                let trimmed = raw.trim();
9730                if trimmed.is_empty() {
9731                    return Err(meta.error("`get_latest_by` must name a column"));
9732                }
9733                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9734                    (stripped.to_owned(), true)
9735                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9736                    (stripped.to_owned(), false)
9737                } else {
9738                    (trimmed.to_owned(), false)
9739                };
9740                if col.is_empty() {
9741                    return Err(meta.error("`get_latest_by` must name a column"));
9742                }
9743                out.get_latest_by = Some((col, desc));
9744                return Ok(());
9745            }
9746            if meta.path.is_ident("unique_together") {
9747                // Composite UNIQUE index. Two syntaxes:
9748                //
9749                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9750                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9751                //
9752                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9753                // (col1, col2)`, where <name> defaults to
9754                // `<table>_<col1>_<col2>_uq` when not supplied.
9755                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9756                out.indexes.push(IndexAttr {
9757                    name,
9758                    columns,
9759                    unique: true,
9760                    method: "btree".to_owned(),
9761                    where_clause: None,
9762                include: Vec::new(),
9763                });
9764                return Ok(());
9765            }
9766            if meta.path.is_ident("index_together") {
9767                // Composite (non-unique) index. Two syntaxes
9768                // mirroring `unique_together`.
9769                //
9770                //   #[rustango(index_together = "created_at, status")]
9771                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9772                let (columns, name) = parse_together_attr(&meta, "index_together")?;
9773                out.indexes.push(IndexAttr {
9774                    name,
9775                    columns,
9776                    unique: false,
9777                    method: "btree".to_owned(),
9778                    where_clause: None,
9779                include: Vec::new(),
9780                });
9781                return Ok(());
9782            }
9783            if meta.path.is_ident("unique_when") {
9784                // Partial unique index. Issue #265 / T1.3.
9785                //
9786                //   #[rustango(unique_when(
9787                //       columns   = "email",
9788                //       condition = "deleted_at IS NULL",
9789                //       name      = "unique_active_email"
9790                //   ))]
9791                //
9792                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9793                // on PG / SQLite (both ship partial indexes natively).
9794                // MySQL falls back to a plain UNIQUE index — the
9795                // condition is lost; document the limitation in the
9796                // generated migration.
9797                let mut columns: Option<Vec<String>> = None;
9798                let mut condition: Option<String> = None;
9799                let mut name: Option<String> = None;
9800                let mut include: Vec<String> = Vec::new();
9801                meta.parse_nested_meta(|inner| {
9802                    if inner.path.is_ident("columns") {
9803                        let s: LitStr = inner.value()?.parse()?;
9804                        columns = Some(split_field_list(&s.value()));
9805                        return Ok(());
9806                    }
9807                    if inner.path.is_ident("condition") {
9808                        let s: LitStr = inner.value()?.parse()?;
9809                        condition = Some(s.value());
9810                        return Ok(());
9811                    }
9812                    if inner.path.is_ident("name") {
9813                        let s: LitStr = inner.value()?.parse()?;
9814                        name = Some(s.value());
9815                        return Ok(());
9816                    }
9817                    if inner.path.is_ident("include") {
9818                        // PG 11+ covering-index columns. Non-key columns
9819                        // travel with the index leaf for index-only
9820                        // scans. Dropped on MySQL/SQLite by the writer.
9821                        let s: LitStr = inner.value()?.parse()?;
9822                        include = split_field_list(&s.value());
9823                        return Ok(());
9824                    }
9825                    Err(inner.error(
9826                        "unknown unique_when attribute (supported: \
9827                         `columns = \"...\"`, `condition = \"...\"`, \
9828                         `name = \"...\"`, `include = \"...\"`)",
9829                    ))
9830                })?;
9831                let columns = columns.ok_or_else(|| {
9832                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9833                })?;
9834                let condition = condition.ok_or_else(|| {
9835                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9836                })?;
9837                if columns.is_empty() {
9838                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9839                }
9840                out.indexes.push(IndexAttr {
9841                    name,
9842                    columns,
9843                    unique: true,
9844                    method: "btree".to_owned(),
9845                    where_clause: Some(condition),
9846                    include,
9847                });
9848                return Ok(());
9849            }
9850            if meta.path.is_ident("index_when") {
9851                // Non-unique partial index. Sibling of `unique_when`
9852                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9853                //
9854                //   #[rustango(index_when(
9855                //       columns   = "status, created_at",
9856                //       condition = "deleted_at IS NULL",
9857                //       name      = "active_status_created_idx"
9858                //   ))]
9859                //
9860                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9861                // on PG / SQLite (both ship partial indexes natively).
9862                // MySQL has no native partial-index support — the writer
9863                // emits a plain CREATE INDEX and the condition is lost;
9864                // operators wanting that selectivity on MySQL should
9865                // declare a covering index plus an application-level
9866                // filter.
9867                let mut columns: Option<Vec<String>> = None;
9868                let mut condition: Option<String> = None;
9869                let mut name: Option<String> = None;
9870                let mut method: String = "btree".to_owned();
9871                let mut include: Vec<String> = Vec::new();
9872                meta.parse_nested_meta(|inner| {
9873                    if inner.path.is_ident("columns") {
9874                        let s: LitStr = inner.value()?.parse()?;
9875                        columns = Some(split_field_list(&s.value()));
9876                        return Ok(());
9877                    }
9878                    if inner.path.is_ident("condition") {
9879                        let s: LitStr = inner.value()?.parse()?;
9880                        condition = Some(s.value());
9881                        return Ok(());
9882                    }
9883                    if inner.path.is_ident("name") {
9884                        let s: LitStr = inner.value()?.parse()?;
9885                        name = Some(s.value());
9886                        return Ok(());
9887                    }
9888                    if inner.path.is_ident("method") {
9889                        let s: LitStr = inner.value()?.parse()?;
9890                        method = s.value();
9891                        return Ok(());
9892                    }
9893                    if inner.path.is_ident("include") {
9894                        // PG 11+ covering-index columns; non-key
9895                        // columns travel with the index leaf. Dropped
9896                        // on MySQL/SQLite.
9897                        let s: LitStr = inner.value()?.parse()?;
9898                        include = split_field_list(&s.value());
9899                        return Ok(());
9900                    }
9901                    Err(inner.error(
9902                        "unknown index_when attribute (supported: \
9903                         `columns = \"...\"`, `condition = \"...\"`, \
9904                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9905                         `include = \"...\"`)",
9906                    ))
9907                })?;
9908                let columns = columns
9909                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9910                let condition = condition.ok_or_else(|| {
9911                    meta.error("`index_when(...)` requires `condition = \"...\"`")
9912                })?;
9913                if columns.is_empty() {
9914                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
9915                }
9916                out.indexes.push(IndexAttr {
9917                    name,
9918                    columns,
9919                    unique: false,
9920                    method,
9921                    where_clause: Some(condition),
9922                    include,
9923                });
9924                return Ok(());
9925            }
9926            if meta.path.is_ident("index") {
9927                // Container-level composite index. Two syntaxes:
9928                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
9929                //   #[rustango(index("col1, col2"))]    — call form (same result)
9930                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9931                // The call form takes the column list as a leading string
9932                // literal, then optional `unique` / `name = "..."` /
9933                // `method = "..."` flags (a leading literal can't compose
9934                // under `parse_nested_meta`, so the paren body is parsed by
9935                // hand). `unique_together` / `index_together` remain
9936                // aliases for the same feature.
9937                let cols_lit: LitStr;
9938                let mut unique = false;
9939                let mut name: Option<String> = None;
9940                let mut method = "btree".to_owned();
9941                if meta.input.peek(syn::token::Paren) {
9942                    let content;
9943                    syn::parenthesized!(content in meta.input);
9944                    cols_lit = content.parse()?;
9945                    while content.peek(syn::Token![,]) {
9946                        content.parse::<syn::Token![,]>()?;
9947                        if content.is_empty() {
9948                            break;
9949                        }
9950                        let flag: syn::Ident = content.parse()?;
9951                        if flag == "unique" {
9952                            unique = true;
9953                        } else if flag == "name" {
9954                            content.parse::<syn::Token![=]>()?;
9955                            let s: LitStr = content.parse()?;
9956                            name = Some(s.value());
9957                        } else if flag == "method" {
9958                            content.parse::<syn::Token![=]>()?;
9959                            let s: LitStr = content.parse()?;
9960                            let v = s.value();
9961                            match v.as_str() {
9962                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9963                                | "bloom" => method = v,
9964                                other => {
9965                                    return Err(syn::Error::new(
9966                                        s.span(),
9967                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9968                                    ));
9969                                }
9970                            }
9971                        } else {
9972                            return Err(syn::Error::new(
9973                                flag.span(),
9974                                "unknown index flag (supported: `unique`, `name`, `method`)",
9975                            ));
9976                        }
9977                    }
9978                } else {
9979                    cols_lit = meta.value()?.parse()?;
9980                }
9981                let columns = split_field_list(&cols_lit.value());
9982                out.indexes.push(IndexAttr {
9983                    name,
9984                    columns,
9985                    unique,
9986                    method,
9987                    where_clause: None,
9988                    include: Vec::new(),
9989                });
9990                return Ok(());
9991            }
9992            if meta.path.is_ident("check") {
9993                // #[rustango(check(name = "…", expr = "…"))]
9994                let mut name: Option<String> = None;
9995                let mut expr: Option<String> = None;
9996                meta.parse_nested_meta(|inner| {
9997                    if inner.path.is_ident("name") {
9998                        let s: LitStr = inner.value()?.parse()?;
9999                        name = Some(s.value());
10000                        return Ok(());
10001                    }
10002                    if inner.path.is_ident("expr") {
10003                        let s: LitStr = inner.value()?.parse()?;
10004                        expr = Some(s.value());
10005                        return Ok(());
10006                    }
10007                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10008                })?;
10009                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10010                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10011                out.checks.push(CheckAttr { name, expr });
10012                return Ok(());
10013            }
10014            if meta.path.is_ident("exclude") {
10015                // #[rustango(exclude(name = "…", using = "gist",
10016                //                    elements = "col WITH op, col WITH op",
10017                //                    where = "…"))]
10018                let mut name: Option<String> = None;
10019                let mut using: Option<String> = None;
10020                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10021                let mut where_clause: Option<String> = None;
10022                meta.parse_nested_meta(|inner| {
10023                    if inner.path.is_ident("name") {
10024                        let s: LitStr = inner.value()?.parse()?;
10025                        name = Some(s.value());
10026                        return Ok(());
10027                    }
10028                    if inner.path.is_ident("using") {
10029                        let s: LitStr = inner.value()?.parse()?;
10030                        using = Some(s.value());
10031                        return Ok(());
10032                    }
10033                    if inner.path.is_ident("elements") {
10034                        let s: LitStr = inner.value()?.parse()?;
10035                        elements_raw = Some((s.value(), s.span()));
10036                        return Ok(());
10037                    }
10038                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10039                        let s: LitStr = inner.value()?.parse()?;
10040                        where_clause = Some(s.value());
10041                        return Ok(());
10042                    }
10043                    Err(inner.error(
10044                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10045                    ))
10046                })?;
10047                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10048                let using = using.unwrap_or_else(|| "gist".to_owned());
10049                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10050                    meta.error(
10051                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
10052                    )
10053                })?;
10054                // Parse `col WITH op` pairs separated by commas.
10055                let mut elements: Vec<(String, String)> = Vec::new();
10056                for pair in elements_str.split(',') {
10057                    let pair = pair.trim();
10058                    if pair.is_empty() {
10059                        continue;
10060                    }
10061                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10062                    let col = split.next().unwrap_or("").trim();
10063                    let rest = split.next().unwrap_or("").trim();
10064                    // `WITH op` — case-insensitive on `WITH`, then op.
10065                    let rest_lc = rest.to_ascii_lowercase();
10066                    let op = rest_lc
10067                        .strip_prefix("with")
10068                        .map(|r| r.trim_start())
10069                        .filter(|r| !r.is_empty())
10070                        .map(|_| {
10071                            // Pull the original-case op from `rest` after the
10072                            // `WITH ` token (5 chars).
10073                            rest[4..].trim_start().to_owned()
10074                        });
10075                    let Some(op) = op else {
10076                        return Err(syn::Error::new(
10077                            elements_span,
10078                            format!(
10079                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10080                                 (e.g. `room_id WITH =` or `during WITH &&`)"
10081                            ),
10082                        ));
10083                    };
10084                    if col.is_empty() || op.is_empty() {
10085                        return Err(syn::Error::new(
10086                            elements_span,
10087                            format!(
10088                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10089                                 (both sides non-empty)"
10090                            ),
10091                        ));
10092                    }
10093                    elements.push((col.to_owned(), op));
10094                }
10095                if elements.is_empty() {
10096                    return Err(syn::Error::new(
10097                        elements_span,
10098                        "exclude requires at least one `col WITH op` element",
10099                    ));
10100                }
10101                out.excludes.push(ExcludeAttr {
10102                    name,
10103                    using,
10104                    elements,
10105                    where_clause,
10106                });
10107                return Ok(());
10108            }
10109            if meta.path.is_ident("generic_fk") {
10110                let mut gfk = GenericFkAttr {
10111                    name: String::new(),
10112                    ct_column: String::new(),
10113                    pk_column: String::new(),
10114                };
10115                meta.parse_nested_meta(|inner| {
10116                    if inner.path.is_ident("name") {
10117                        let s: LitStr = inner.value()?.parse()?;
10118                        gfk.name = s.value();
10119                        return Ok(());
10120                    }
10121                    if inner.path.is_ident("ct_column") {
10122                        let s: LitStr = inner.value()?.parse()?;
10123                        gfk.ct_column = s.value();
10124                        return Ok(());
10125                    }
10126                    if inner.path.is_ident("pk_column") {
10127                        let s: LitStr = inner.value()?.parse()?;
10128                        gfk.pk_column = s.value();
10129                        return Ok(());
10130                    }
10131                    Err(inner.error(
10132                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10133                    ))
10134                })?;
10135                if gfk.name.is_empty() {
10136                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10137                }
10138                if gfk.ct_column.is_empty() {
10139                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10140                }
10141                if gfk.pk_column.is_empty() {
10142                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10143                }
10144                out.generic_fks.push(gfk);
10145                return Ok(());
10146            }
10147            if meta.path.is_ident("fk_composite") {
10148                let mut fk = CompositeFkAttr {
10149                    name: String::new(),
10150                    to: String::new(),
10151                    from: Vec::new(),
10152                    on: Vec::new(),
10153                };
10154                meta.parse_nested_meta(|inner| {
10155                    if inner.path.is_ident("name") {
10156                        let s: LitStr = inner.value()?.parse()?;
10157                        fk.name = s.value();
10158                        return Ok(());
10159                    }
10160                    if inner.path.is_ident("to") {
10161                        let s: LitStr = inner.value()?.parse()?;
10162                        fk.to = s.value();
10163                        return Ok(());
10164                    }
10165                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10166                    // comma-list of string literals.
10167                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10168                        let value = inner.value()?;
10169                        let content;
10170                        syn::parenthesized!(content in value);
10171                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10172                            content.parse_terminated(
10173                                |p| p.parse::<syn::LitStr>(),
10174                                syn::Token![,],
10175                            )?;
10176                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10177                        if inner.path.is_ident("on") {
10178                            fk.on = cols;
10179                        } else {
10180                            fk.from = cols;
10181                        }
10182                        return Ok(());
10183                    }
10184                    Err(inner.error(
10185                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10186                    ))
10187                })?;
10188                if fk.name.is_empty() {
10189                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10190                }
10191                if fk.to.is_empty() {
10192                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10193                }
10194                if fk.from.is_empty() || fk.on.is_empty() {
10195                    return Err(meta.error(
10196                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10197                    ));
10198                }
10199                if fk.from.len() != fk.on.len() {
10200                    return Err(meta.error(format!(
10201                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10202                        fk.from.len(),
10203                        fk.on.len(),
10204                    )));
10205                }
10206                out.composite_fks.push(fk);
10207                return Ok(());
10208            }
10209            if meta.path.is_ident("m2m") {
10210                let mut m2m = M2MAttr {
10211                    name: String::new(),
10212                    to: String::new(),
10213                    through: String::new(),
10214                    src: String::new(),
10215                    dst: String::new(),
10216                    auto_create: true,
10217                };
10218                meta.parse_nested_meta(|inner| {
10219                    if inner.path.is_ident("name") {
10220                        let s: LitStr = inner.value()?.parse()?;
10221                        m2m.name = s.value();
10222                        return Ok(());
10223                    }
10224                    if inner.path.is_ident("to") {
10225                        let s: LitStr = inner.value()?.parse()?;
10226                        m2m.to = s.value();
10227                        return Ok(());
10228                    }
10229                    if inner.path.is_ident("through") {
10230                        let s: LitStr = inner.value()?.parse()?;
10231                        m2m.through = s.value();
10232                        return Ok(());
10233                    }
10234                    if inner.path.is_ident("src") {
10235                        let s: LitStr = inner.value()?.parse()?;
10236                        m2m.src = s.value();
10237                        return Ok(());
10238                    }
10239                    if inner.path.is_ident("dst") {
10240                        let s: LitStr = inner.value()?.parse()?;
10241                        m2m.dst = s.value();
10242                        return Ok(());
10243                    }
10244                    if inner.path.is_ident("auto_create") {
10245                        let lit: syn::LitBool = inner.value()?.parse()?;
10246                        m2m.auto_create = lit.value;
10247                        return Ok(());
10248                    }
10249                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10250                })?;
10251                if m2m.name.is_empty() {
10252                    return Err(meta.error("m2m requires `name = \"...\"`"));
10253                }
10254                if m2m.to.is_empty() {
10255                    return Err(meta.error("m2m requires `to = \"...\"`"));
10256                }
10257                if m2m.through.is_empty() {
10258                    return Err(meta.error("m2m requires `through = \"...\"`"));
10259                }
10260                if m2m.src.is_empty() {
10261                    return Err(meta.error("m2m requires `src = \"...\"`"));
10262                }
10263                if m2m.dst.is_empty() {
10264                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10265                }
10266                out.m2m.push(m2m);
10267                return Ok(());
10268            }
10269            if meta.path.is_ident("generic_m2m") {
10270                let mut gm = GenericM2MAttr {
10271                    name: String::new(),
10272                    through: String::new(),
10273                    pk_column: String::new(),
10274                    ct_column: String::new(),
10275                    related_column: String::new(),
10276                };
10277                meta.parse_nested_meta(|inner| {
10278                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10279                        let s: LitStr = inner.value()?.parse()?;
10280                        Ok(s.value())
10281                    };
10282                    if inner.path.is_ident("name") {
10283                        gm.name = field(&inner)?;
10284                        return Ok(());
10285                    }
10286                    if inner.path.is_ident("through") {
10287                        gm.through = field(&inner)?;
10288                        return Ok(());
10289                    }
10290                    if inner.path.is_ident("pk_column") {
10291                        gm.pk_column = field(&inner)?;
10292                        return Ok(());
10293                    }
10294                    if inner.path.is_ident("ct_column") {
10295                        gm.ct_column = field(&inner)?;
10296                        return Ok(());
10297                    }
10298                    if inner.path.is_ident("related_column") {
10299                        gm.related_column = field(&inner)?;
10300                        return Ok(());
10301                    }
10302                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10303                })?;
10304                for (val, label) in [
10305                    (&gm.name, "name"),
10306                    (&gm.through, "through"),
10307                    (&gm.pk_column, "pk_column"),
10308                    (&gm.ct_column, "ct_column"),
10309                    (&gm.related_column, "related_column"),
10310                ] {
10311                    if val.is_empty() {
10312                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10313                    }
10314                }
10315                out.generic_m2m.push(gm);
10316                return Ok(());
10317            }
10318            Err(meta.error("unknown rustango container attribute"))
10319        })?;
10320    }
10321    Ok(out)
10322}
10323
10324/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10325/// owned field names, trimming whitespace and skipping empty entries.
10326/// Field-name validation against the model is done by the caller.
10327fn split_field_list(raw: &str) -> Vec<String> {
10328    raw.split(',')
10329        .map(str::trim)
10330        .filter(|s| !s.is_empty())
10331        .map(str::to_owned)
10332        .collect()
10333}
10334
10335/// Shared parser for `unique_together` and `index_together` container
10336/// attrs. Accepts both shapes:
10337///
10338///   * `attr = "col1, col2"`              — auto-derived index name.
10339///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10340///
10341/// Returns `(columns, name)`.
10342fn parse_together_attr(
10343    meta: &syn::meta::ParseNestedMeta<'_>,
10344    attr: &str,
10345) -> syn::Result<(Vec<String>, Option<String>)> {
10346    // Disambiguate by whether the next token is `=` (key-value) or
10347    // `(` (parenthesized).
10348    if meta.input.peek(syn::Token![=]) {
10349        let cols_lit: LitStr = meta.value()?.parse()?;
10350        let columns = split_field_list(&cols_lit.value());
10351        check_together_columns(meta, attr, &columns)?;
10352        return Ok((columns, None));
10353    }
10354    let mut columns: Option<Vec<String>> = None;
10355    let mut name: Option<String> = None;
10356    meta.parse_nested_meta(|inner| {
10357        if inner.path.is_ident("columns") {
10358            let s: LitStr = inner.value()?.parse()?;
10359            columns = Some(split_field_list(&s.value()));
10360            return Ok(());
10361        }
10362        if inner.path.is_ident("name") {
10363            let s: LitStr = inner.value()?.parse()?;
10364            name = Some(s.value());
10365            return Ok(());
10366        }
10367        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10368    })?;
10369    let columns = columns.ok_or_else(|| {
10370        meta.error(format!(
10371            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10372        ))
10373    })?;
10374    check_together_columns(meta, attr, &columns)?;
10375    Ok((columns, name))
10376}
10377
10378fn check_together_columns(
10379    meta: &syn::meta::ParseNestedMeta<'_>,
10380    attr: &str,
10381    columns: &[String],
10382) -> syn::Result<()> {
10383    if columns.len() < 2 {
10384        let single = if attr == "unique_together" {
10385            "#[rustango(unique)] on the field"
10386        } else {
10387            "#[rustango(index)] on the field"
10388        };
10389        return Err(meta.error(format!(
10390            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10391        )));
10392    }
10393    Ok(())
10394}
10395
10396/// Parse the fieldsets DSL: pipe-separated sections, optional
10397/// `"Title:"` prefix on each, comma-separated field names after.
10398/// Examples:
10399/// * `"name, office"` → one untitled section with two fields
10400/// * `"Identity: name, office | Metadata: created_at"` → two titled
10401///   sections
10402///
10403/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10404fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10405    raw.split('|')
10406        .map(str::trim)
10407        .filter(|s| !s.is_empty())
10408        .map(|section| {
10409            // Split off an optional `Title:` prefix (first colon).
10410            let (title, rest) = match section.split_once(':') {
10411                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10412                _ => (String::new(), section),
10413            };
10414            let fields = split_field_list(rest);
10415            (title, fields)
10416        })
10417        .collect()
10418}
10419
10420/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10421/// comma-separated entry maps a target field to one or more source
10422/// fields joined with `+`. Whitespace around tokens is trimmed.
10423/// Entries missing `:` or with empty target/source lists are dropped.
10424fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10425    raw.split(',')
10426        .map(str::trim)
10427        .filter(|s| !s.is_empty())
10428        .filter_map(|entry| {
10429            let (target, sources_raw) = entry.split_once(':')?;
10430            let target = target.trim().to_owned();
10431            if target.is_empty() {
10432                return None;
10433            }
10434            let sources: Vec<String> = sources_raw
10435                .split('+')
10436                .map(|s| s.trim().to_owned())
10437                .filter(|s| !s.is_empty())
10438                .collect();
10439            if sources.is_empty() {
10440                return None;
10441            }
10442            Some((target, sources))
10443        })
10444        .collect()
10445}
10446
10447/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10448/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10449/// and empty halves drop silently — the macro layer only enforces shape,
10450/// not field-name vs. widget-name validity (those checks happen at
10451/// `AdminConfig` consumption time). Issue #359.
10452fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10453    raw.split(',')
10454        .map(str::trim)
10455        .filter(|s| !s.is_empty())
10456        .filter_map(|entry| {
10457            let (field, widget) = entry.split_once(':')?;
10458            let field = field.trim().to_owned();
10459            let widget = widget.trim().to_owned();
10460            if field.is_empty() || widget.is_empty() {
10461                return None;
10462            }
10463            Some((field, widget))
10464        })
10465        .collect()
10466}
10467
10468/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10469/// Returns `(field_name, desc)` pairs in the same order as the input.
10470fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10471    raw.split(',')
10472        .map(str::trim)
10473        .filter(|s| !s.is_empty())
10474        .map(|spec| {
10475            spec.strip_prefix('-')
10476                .map_or((spec.to_owned(), false), |rest| {
10477                    (rest.trim().to_owned(), true)
10478                })
10479        })
10480        .collect()
10481}
10482
10483struct FieldAttrs {
10484    column: Option<String>,
10485    primary_key: bool,
10486    fk: Option<String>,
10487    o2o: Option<String>,
10488    on: Option<String>,
10489    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10490    /// "set_default" | "no_action")]` — what happens to this row when
10491    /// the row it points at is deleted.
10492    /// Only meaningful when `fk` / `o2o` is
10493    /// also set; the macro errors at compile time if applied to a
10494    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10495    /// DDL writer renders `ON DELETE <action>` after the constraint
10496    /// clause when this is `Some`; `None` falls back to the database
10497    /// default (NO ACTION on every backend rustango supports).
10498    on_delete: Option<String>,
10499    /// `#[rustango(related_name = "...")]` — per-FK
10500    /// reverse-accessor override. When set, the derive emits
10501    /// `Parent::<related_name>[_pool]` instead of the container-level
10502    /// `default_related_name` or the `<child_snake>_set[_pool]`
10503    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10504    /// silently ignored on non-FK fields. Follow-up to #816.
10505    related_name: Option<String>,
10506    max_length: Option<u32>,
10507    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10508    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10509    /// unconstrained `vector` column.
10510    vector_dims: Option<u32>,
10511    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10512    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10513    /// unconstrained `geometry(Point)` column (SRID 0).
10514    geometry_srid: Option<u32>,
10515    min: Option<i64>,
10516    max: Option<i64>,
10517    default: Option<String>,
10518    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10519    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10520    /// "gen_random_uuid()"`. The Rust field type must be
10521    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10522    /// INSERTs so the DB DEFAULT fires.
10523    auto_uuid: bool,
10524    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10525    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10526    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10527    /// the field is `Auto::Unset`, then bound as a normal parameter
10528    /// rather than relying on a per-dialect DB function. Issue #823
10529    /// (Eloquent `HasUuids`).
10530    ///
10531    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10532    /// Composes with every backend (PG / MySQL / SQLite) — no
10533    /// `gen_random_uuid()` requirement on the database.
10534    default_uuid_v7: bool,
10535    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10536    /// Server-set on insert, immutable from app code afterwards.
10537    /// Implies `auto + default = "now()"`. Field type must be
10538    /// `DateTime<Utc>`.
10539    auto_now_add: bool,
10540    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10541    /// every insert AND every update. Implies `auto + default =
10542    /// "now()"`; the macro additionally rewrites `update_on` /
10543    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10544    /// field value.
10545    auto_now: bool,
10546    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10547    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10548    /// `soft_delete_on(executor)` and `restore_on(executor)`
10549    /// methods on the model.
10550    soft_delete: bool,
10551    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10552    /// the column in the generated DDL.
10553    unique: bool,
10554    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10555    /// generates a `CREATE INDEX` for this column. `unique` here means
10556    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10557    index: bool,
10558    index_unique: bool,
10559    index_name: Option<String>,
10560    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10561    /// `"btree"`. Issue #34.
10562    index_method: String,
10563    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10564    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10565    /// the macro skips this column from every INSERT and UPDATE
10566    /// path, so the database always recomputes the value from
10567    /// `EXPR`. Backlog item #35.
10568    generated_as: Option<String>,
10569    /// `#[rustango(help_text = "…")]` — help text
10570    /// rendered below the admin form's input. Threaded into
10571    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10572    /// layers can read it.
10573    help_text: Option<String>,
10574    /// `#[rustango(choices = "value:Label, value:Label")]` — the
10575    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10576    /// as a `&'static [(&'static str, &'static str)]` slice. When
10577    /// present, the admin form renders a `<select>` instead of `<input>`
10578    /// and the validator rejects values not in the list. Only meaningful
10579    /// for `FieldType::String`; the macro errors at compile time if
10580    /// applied to a non-string field.
10581    choices: Option<Vec<(String, String)>>,
10582    /// `#[rustango(db_comment = "…")]` — DB-side column
10583    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10584    /// the comment in CREATE TABLE; Postgres emits a separate
10585    /// `COMMENT ON COLUMN` statement after the table is created;
10586    /// SQLite silently drops the value (no native column comments).
10587    db_comment: Option<String>,
10588    /// `#[rustango(verbose_name = "…")]` — human-readable
10589    /// label for the field. Threaded into `FieldSchema::verbose_name`
10590    /// so admin column headers, form labels, and other display
10591    /// surfaces can prefer the friendly caption over the Rust
10592    /// identifier. `None` means renderers fall back to the field name.
10593    verbose_name: Option<String>,
10594    /// `#[rustango(editable = false)]` — opt out of
10595    /// auto-generated form rendering. Defaults to `true` so existing
10596    /// fields keep their current admin / form behavior; setting
10597    /// `false` removes the field from the admin change-form entirely
10598    /// (the value is still visible on detail / list views, just not
10599    /// editable).
10600    editable: bool,
10601    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10602    /// "form may submit empty even when DB is NOT NULL". Threaded into
10603    /// `FieldSchema::blank`. Defaults to `false`.
10604    blank: bool,
10605    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10606    /// case-insensitive text column. Threaded into
10607    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10608    /// fields; the macro errors at derive time if applied elsewhere.
10609    case_insensitive: bool,
10610    /// `#[rustango(validators = "email,url")]` — the
10611    /// model-level validator chain. Comma-separated names that
10612    /// dispatch to the `validators::*` family in `validate_value`.
10613    /// Empty by default; fires on every typed INSERT/UPDATE.
10614    validators: Vec<String>,
10615}
10616
10617fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10618    let mut out = FieldAttrs {
10619        column: None,
10620        primary_key: false,
10621        fk: None,
10622        o2o: None,
10623        on: None,
10624        on_delete: None,
10625        related_name: None,
10626        max_length: None,
10627        vector_dims: None,
10628        geometry_srid: None,
10629        min: None,
10630        max: None,
10631        default: None,
10632        auto_uuid: false,
10633        default_uuid_v7: false,
10634        auto_now_add: false,
10635        auto_now: false,
10636        soft_delete: false,
10637        unique: false,
10638        index: false,
10639        index_unique: false,
10640        index_name: None,
10641        index_method: "btree".to_owned(),
10642        generated_as: None,
10643        help_text: None,
10644        choices: None,
10645        db_comment: None,
10646        verbose_name: None,
10647        editable: true,
10648        blank: false,
10649        case_insensitive: false,
10650        validators: Vec::new(),
10651    };
10652    for attr in &field.attrs {
10653        if !attr.path().is_ident("rustango") {
10654            continue;
10655        }
10656        attr.parse_nested_meta(|meta| {
10657            if meta.path.is_ident("column") {
10658                let s: LitStr = meta.value()?.parse()?;
10659                let name = s.value();
10660                validate_sql_identifier(&name, "column", s.span())?;
10661                out.column = Some(name);
10662                return Ok(());
10663            }
10664            if meta.path.is_ident("primary_key") {
10665                out.primary_key = true;
10666                return Ok(());
10667            }
10668            if meta.path.is_ident("fk") {
10669                let s: LitStr = meta.value()?.parse()?;
10670                out.fk = Some(s.value());
10671                return Ok(());
10672            }
10673            if meta.path.is_ident("o2o") {
10674                let s: LitStr = meta.value()?.parse()?;
10675                out.o2o = Some(s.value());
10676                return Ok(());
10677            }
10678            if meta.path.is_ident("on") {
10679                let s: LitStr = meta.value()?.parse()?;
10680                out.on = Some(s.value());
10681                return Ok(());
10682            }
10683            if meta.path.is_ident("on_delete") {
10684                let s: LitStr = meta.value()?.parse()?;
10685                let raw = s.value();
10686                let normalized = raw.trim().to_ascii_lowercase();
10687                // Validate at parse time so the user gets a clear span
10688                // rather than a downstream compile error in the emit.
10689                match normalized.as_str() {
10690                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10691                    _ => {
10692                        return Err(syn::Error::new(
10693                            s.span(),
10694                            format!(
10695                                "unknown on_delete action `{raw}`; expected one of \
10696                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10697                            ),
10698                        ));
10699                    }
10700                }
10701                out.on_delete = Some(normalized);
10702                return Ok(());
10703            }
10704            if meta.path.is_ident("related_name") {
10705                let s: LitStr = meta.value()?.parse()?;
10706                let raw = s.value();
10707                if raw.trim().is_empty() {
10708                    return Err(syn::Error::new(
10709                        s.span(),
10710                        "`related_name` must be a non-empty identifier",
10711                    ));
10712                }
10713                // Validate as a Rust-ident shape — the value becomes
10714                // a method name on the parent type. Same rule as
10715                // `default_related_name` so the two surfaces match.
10716                if !raw
10717                    .chars()
10718                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10719                    || raw.starts_with(char::is_numeric)
10720                {
10721                    return Err(syn::Error::new(
10722                        s.span(),
10723                        "`related_name` must be snake_case ASCII (lowercase letters, \
10724                         digits, underscores; no leading digit)",
10725                    ));
10726                }
10727                out.related_name = Some(raw);
10728                return Ok(());
10729            }
10730            if meta.path.is_ident("max_length") {
10731                let lit: syn::LitInt = meta.value()?.parse()?;
10732                out.max_length = Some(lit.base10_parse::<u32>()?);
10733                return Ok(());
10734            }
10735            // `#[rustango(vector(dims = N))]` — pgvector column
10736            // dimension (#824). Nested-meta form so it reads like the
10737            // other typed-column attrs.
10738            if meta.path.is_ident("vector") {
10739                meta.parse_nested_meta(|inner| {
10740                    if inner.path.is_ident("dims") {
10741                        let lit: syn::LitInt = inner.value()?.parse()?;
10742                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10743                        return Ok(());
10744                    }
10745                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10746                })?;
10747                return Ok(());
10748            }
10749            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10750            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10751            if meta.path.is_ident("geometry") {
10752                meta.parse_nested_meta(|inner| {
10753                    if inner.path.is_ident("srid") {
10754                        let lit: syn::LitInt = inner.value()?.parse()?;
10755                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10756                        return Ok(());
10757                    }
10758                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10759                })?;
10760                return Ok(());
10761            }
10762            if meta.path.is_ident("min") {
10763                out.min = Some(parse_signed_i64(&meta)?);
10764                return Ok(());
10765            }
10766            if meta.path.is_ident("max") {
10767                out.max = Some(parse_signed_i64(&meta)?);
10768                return Ok(());
10769            }
10770            if meta.path.is_ident("default") {
10771                let s: LitStr = meta.value()?.parse()?;
10772                out.default = Some(s.value());
10773                return Ok(());
10774            }
10775            if meta.path.is_ident("generated_as") {
10776                let s: LitStr = meta.value()?.parse()?;
10777                out.generated_as = Some(s.value());
10778                return Ok(());
10779            }
10780            if meta.path.is_ident("help_text") {
10781                let s: LitStr = meta.value()?.parse()?;
10782                out.help_text = Some(s.value());
10783                return Ok(());
10784            }
10785            if meta.path.is_ident("choices") {
10786                let s: LitStr = meta.value()?.parse()?;
10787                let raw = s.value();
10788                let mut pairs: Vec<(String, String)> = Vec::new();
10789                for chunk in raw.split(',') {
10790                    let chunk = chunk.trim();
10791                    if chunk.is_empty() {
10792                        continue;
10793                    }
10794                    let (value, label) = match chunk.split_once(':') {
10795                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10796                        None => (chunk.to_owned(), chunk.to_owned()),
10797                    };
10798                    if value.is_empty() {
10799                        return Err(syn::Error::new(
10800                            s.span(),
10801                            "`choices` entry has empty value before `:`",
10802                        ));
10803                    }
10804                    pairs.push((value, label));
10805                }
10806                if pairs.is_empty() {
10807                    return Err(syn::Error::new(
10808                        s.span(),
10809                        "`choices = \"…\"` must contain at least one value",
10810                    ));
10811                }
10812                out.choices = Some(pairs);
10813                return Ok(());
10814            }
10815            if meta.path.is_ident("db_comment") {
10816                let s: LitStr = meta.value()?.parse()?;
10817                out.db_comment = Some(s.value());
10818                return Ok(());
10819            }
10820            if meta.path.is_ident("verbose_name") {
10821                let s: LitStr = meta.value()?.parse()?;
10822                out.verbose_name = Some(s.value());
10823                return Ok(());
10824            }
10825            if meta.path.is_ident("editable") {
10826                // Two forms accepted:
10827                //   #[rustango(editable = false)] / true — explicit
10828                //   #[rustango(editable)] — flag form (= true, the
10829                //   default, so harmless; included for symmetry)
10830                if let Ok(v) = meta.value() {
10831                    let lit: syn::LitBool = v.parse()?;
10832                    out.editable = lit.value;
10833                } else {
10834                    out.editable = true;
10835                }
10836                return Ok(());
10837            }
10838            if meta.path.is_ident("blank") {
10839                // Two forms accepted:
10840                //   #[rustango(blank)] — flag form, true
10841                //   #[rustango(blank = false)] / true — explicit
10842                if let Ok(v) = meta.value() {
10843                    let lit: syn::LitBool = v.parse()?;
10844                    out.blank = lit.value;
10845                } else {
10846                    out.blank = true;
10847                }
10848                return Ok(());
10849            }
10850            if meta.path.is_ident("citext") {
10851                // Case-insensitive text column (#344). Two forms:
10852                //   #[rustango(citext)]          — flag form, true
10853                //   #[rustango(citext = true)]   — explicit
10854                //   #[rustango(citext = false)]  — explicit opt-out
10855                // String-only validation lives in the field-type
10856                // emitter (the FieldType discriminant is computed in
10857                // `detect_type`); the macro records the flag and the
10858                // DDL writer emits dialect-specific COLLATE / CITEXT.
10859                if let Ok(v) = meta.value() {
10860                    let lit: syn::LitBool = v.parse()?;
10861                    out.case_insensitive = lit.value;
10862                } else {
10863                    out.case_insensitive = true;
10864                }
10865                return Ok(());
10866            }
10867            if meta.path.is_ident("validators") {
10868                let s: LitStr = meta.value()?.parse()?;
10869                let raw = s.value();
10870                out.validators = raw
10871                    .split(',')
10872                    .map(str::trim)
10873                    .filter(|s| !s.is_empty())
10874                    .map(str::to_owned)
10875                    .collect();
10876                if out.validators.is_empty() {
10877                    return Err(syn::Error::new(
10878                        s.span(),
10879                        "`validators = \"…\"` must list at least one name",
10880                    ));
10881                }
10882                return Ok(());
10883            }
10884            if meta.path.is_ident("auto_uuid") {
10885                out.auto_uuid = true;
10886                // Implied: PK + auto + DEFAULT gen_random_uuid().
10887                // Each is also explicitly settable; the explicit
10888                // value wins if conflicting.
10889                out.primary_key = true;
10890                if out.default.is_none() {
10891                    out.default = Some("gen_random_uuid()".into());
10892                }
10893                return Ok(());
10894            }
10895            if meta.path.is_ident("default_uuid_v7") {
10896                // Backend-neutral counterpart of `auto_uuid` — issue #823.
10897                // No SQL DEFAULT (the macro fills the value Rust-side
10898                // before binding); just mark the field as PK + Auto
10899                // so the insert path is the `Auto::Unset → generate`
10900                // branch.
10901                out.default_uuid_v7 = true;
10902                out.primary_key = true;
10903                return Ok(());
10904            }
10905            if meta.path.is_ident("auto_now_add") {
10906                out.auto_now_add = true;
10907                if out.default.is_none() {
10908                    out.default = Some("now()".into());
10909                }
10910                return Ok(());
10911            }
10912            if meta.path.is_ident("auto_now") {
10913                out.auto_now = true;
10914                if out.default.is_none() {
10915                    out.default = Some("now()".into());
10916                }
10917                return Ok(());
10918            }
10919            if meta.path.is_ident("soft_delete") {
10920                out.soft_delete = true;
10921                return Ok(());
10922            }
10923            if meta.path.is_ident("unique") {
10924                out.unique = true;
10925                return Ok(());
10926            }
10927            if meta.path.is_ident("index") {
10928                out.index = true;
10929                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10930                if meta.input.peek(syn::token::Paren) {
10931                    meta.parse_nested_meta(|inner| {
10932                        if inner.path.is_ident("unique") {
10933                            out.index_unique = true;
10934                            return Ok(());
10935                        }
10936                        if inner.path.is_ident("name") {
10937                            let s: LitStr = inner.value()?.parse()?;
10938                            out.index_name = Some(s.value());
10939                            return Ok(());
10940                        }
10941                        if inner.path.is_ident("method") {
10942                            let s: LitStr = inner.value()?.parse()?;
10943                            let v = s.value();
10944                            match v.as_str() {
10945                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10946                                    out.index_method = v;
10947                                }
10948                                other => {
10949                                    return Err(inner.error(format!(
10950                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10951                                    )));
10952                                }
10953                            }
10954                            return Ok(());
10955                        }
10956                        Err(inner.error(
10957                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10958                        ))
10959                    })?;
10960                }
10961                return Ok(());
10962            }
10963            Err(meta.error("unknown rustango field attribute"))
10964        })?;
10965    }
10966    Ok(out)
10967}
10968
10969/// Parse a signed integer literal, accepting optional leading `-`.
10970fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10971    let expr: syn::Expr = meta.value()?.parse()?;
10972    match expr {
10973        syn::Expr::Lit(syn::ExprLit {
10974            lit: syn::Lit::Int(lit),
10975            ..
10976        }) => lit.base10_parse::<i64>(),
10977        syn::Expr::Unary(syn::ExprUnary {
10978            op: syn::UnOp::Neg(_),
10979            expr,
10980            ..
10981        }) => {
10982            if let syn::Expr::Lit(syn::ExprLit {
10983                lit: syn::Lit::Int(lit),
10984                ..
10985            }) = *expr
10986            {
10987                let v: i64 = lit.base10_parse()?;
10988                Ok(-v)
10989            } else {
10990                Err(syn::Error::new_spanned(expr, "expected integer literal"))
10991            }
10992        }
10993        other => Err(syn::Error::new_spanned(
10994            other,
10995            "expected integer literal (signed)",
10996        )),
10997    }
10998}
10999
11000struct FieldInfo<'a> {
11001    ident: &'a syn::Ident,
11002    column: String,
11003    primary_key: bool,
11004    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11005    /// skip this column when `Auto::Unset` and emit it under
11006    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11007    auto: bool,
11008    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11009    /// the `Column::Value` associated type for typed-column tokens.
11010    value_ty: &'a Type,
11011    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11012    field_type_tokens: TokenStream2,
11013    schema: TokenStream2,
11014    from_row_init: TokenStream2,
11015    /// Variant of [`Self::from_row_init`] that reads the column via
11016    /// `format!("{prefix}__{col}")` so a model can be decoded out of
11017    /// the aliased columns of a JOINed row. Drives slice 9.0d's
11018    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11019    /// helper that `select_related` calls when stitching loaded FKs.
11020    from_aliased_row_init: TokenStream2,
11021    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11022    /// relation helper emit (`Author::<child>_set`) needs to know `T`
11023    /// to point the generated method at the right child model.
11024    fk_inner: Option<Type>,
11025    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11026    /// `kind` (since ForeignKey detection sets `kind` to K's
11027    /// underlying type) but stored separately for clarity at the
11028    /// `FkRelation` construction site, which only sees the FK's
11029    /// surface fields.
11030    fk_pk_kind: DetectedKind,
11031    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11032    /// the bare `ForeignKey<T, K>`. Routes the load_related and
11033    /// fk_pk_access emitters to wrap assignments / accessors in
11034    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11035    /// FK column compiles end-to-end. The DDL writer reads this off
11036    /// the field schema (`nullable` flag); the macro just needs to
11037    /// keep the Rust-side codegen consistent.
11038    nullable: bool,
11039    /// `true` when this column was marked `#[rustango(auto_now)]` —
11040    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11041    /// column instead of the user-supplied value, so `updated_at`
11042    /// always reflects the latest write without the caller having
11043    /// to remember to set it.
11044    auto_now: bool,
11045    /// `true` when this column was marked `#[rustango(auto_now_add)]`
11046    /// — the column is server-set on INSERT (DB DEFAULT) and
11047    /// **immutable** afterwards. `update_on` / `save_on` skip the
11048    /// column entirely so a stale `created_at` value in memory never
11049    /// rewrites the persisted timestamp.
11050    auto_now_add: bool,
11051    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11052    /// Triggers emission of `soft_delete_on(executor)` and
11053    /// `restore_on(executor)` on the model's inherent impl. There is
11054    /// at most one such column per model — emission asserts this.
11055    soft_delete: bool,
11056    /// `Some` when this column was marked
11057    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11058    /// every INSERT and UPDATE path; the database recomputes the
11059    /// value from `EXPR`. Backlog item #35.
11060    generated_as: Option<String>,
11061    /// `true` when this column was marked
11062    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11063    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11064    /// with `Uuid::now_v7()` before binding, so the PK is generated
11065    /// Rust-side and the column is always present in the INSERT
11066    /// statement (no DB DEFAULT requirement). Issue #823.
11067    default_uuid_v7: bool,
11068    /// `Some` when this FK field carried `#[rustango(related_name =
11069    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11070    /// consumed by [`reverse_helper_tokens`] to override the default
11071    /// `<child_snake>_set` accessor name. Follow-up to #816.
11072    related_name: Option<String>,
11073}
11074
11075/// Reject table names that won't survive SQL identifier
11076/// derivation downstream. Postgres' regular-identifier rule
11077/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11078/// through the framework's unquoted FK / index / constraint name
11079/// emission without surprises. We also disallow leading-digit and
11080/// the empty string for clarity.
11081///
11082/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11083/// here — those produce a runtime error from the SQL parser,
11084/// which is loud enough; statically enumerating reserved words
11085/// across the three supported dialects is more friction than help.
11086///
11087/// Backlog item #65.
11088fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11089    validate_sql_identifier(name, "table", span)
11090}
11091
11092/// Reject SQL identifiers that compile but break downstream SQL
11093/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11094/// `kind` is "table" / "column" — used for the error message so users
11095/// see which attribute caused the failure.
11096fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11097    if name.is_empty() {
11098        return Err(syn::Error::new(
11099            span,
11100            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11101        ));
11102    }
11103    let mut chars = name.chars();
11104    let first = chars.next().unwrap();
11105    if !(first.is_ascii_alphabetic() || first == '_') {
11106        return Err(syn::Error::new(
11107            span,
11108            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11109        ));
11110    }
11111    for c in chars {
11112        if !(c.is_ascii_alphanumeric() || c == '_') {
11113            return Err(syn::Error::new(
11114                span,
11115                format!(
11116                    "{kind} name `{name}` contains invalid character {c:?} — \
11117                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11118                     Hyphens in particular break FK / index name derivation \
11119                     downstream; use underscores instead (e.g. `{}`)",
11120                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11121                ),
11122            ));
11123        }
11124    }
11125    Ok(())
11126}
11127
11128fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11129    let root = rustango_root();
11130    let attrs = parse_field_attrs(field)?;
11131    let ident = field
11132        .ident
11133        .as_ref()
11134        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11135    let name = ident.to_string();
11136    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11137    let primary_key = attrs.primary_key;
11138    let DetectedType {
11139        kind,
11140        nullable,
11141        auto: detected_auto,
11142        fk_inner,
11143    } = detect_type(&field.ty)?;
11144    check_bound_compatibility(field, &attrs, kind)?;
11145    let auto = detected_auto;
11146    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11147    // INSERT path: the user must wrap the field in `Auto<T>`, which
11148    // marks the column as DB-default-supplied. The mixin attrs then
11149    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11150    // for `auto_now`, force the value on UPDATE too.
11151    if attrs.auto_uuid {
11152        if kind != DetectedKind::Uuid {
11153            return Err(syn::Error::new_spanned(
11154                field,
11155                "`#[rustango(auto_uuid)]` requires the field type to be \
11156                 `Auto<uuid::Uuid>`",
11157            ));
11158        }
11159        if !detected_auto {
11160            return Err(syn::Error::new_spanned(
11161                field,
11162                "`#[rustango(auto_uuid)]` requires the field type to be \
11163                 wrapped in `Auto<...>` so the macro skips the column on \
11164                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11165            ));
11166        }
11167    }
11168    if attrs.default_uuid_v7 {
11169        if kind != DetectedKind::Uuid {
11170            return Err(syn::Error::new_spanned(
11171                field,
11172                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11173                 `Auto<uuid::Uuid>`",
11174            ));
11175        }
11176        if !detected_auto {
11177            return Err(syn::Error::new_spanned(
11178                field,
11179                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11180                 wrapped in `Auto<...>` so the macro can detect the \
11181                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11182            ));
11183        }
11184        if attrs.auto_uuid {
11185            return Err(syn::Error::new_spanned(
11186                field,
11187                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11188                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11189                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11190                 Pick one.",
11191            ));
11192        }
11193    }
11194    if attrs.auto_now_add || attrs.auto_now {
11195        if kind != DetectedKind::DateTime {
11196            return Err(syn::Error::new_spanned(
11197                field,
11198                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11199                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11200            ));
11201        }
11202        if !detected_auto {
11203            return Err(syn::Error::new_spanned(
11204                field,
11205                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11206                 the field type to be wrapped in `Auto<...>` so the macro skips \
11207                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11208            ));
11209        }
11210    }
11211    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11212        return Err(syn::Error::new_spanned(
11213            field,
11214            "`#[rustango(soft_delete)]` requires the field type to be \
11215             `Option<chrono::DateTime<chrono::Utc>>`",
11216        ));
11217    }
11218    let is_mixin_auto =
11219        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11220    if detected_auto && !primary_key && !is_mixin_auto {
11221        return Err(syn::Error::new_spanned(
11222            field,
11223            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11224             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11225             `auto_now`",
11226        ));
11227    }
11228    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11229        return Err(syn::Error::new_spanned(
11230            field,
11231            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11232             SERIAL / BIGSERIAL already supplies a default sequence.",
11233        ));
11234    }
11235    if fk_inner.is_some() && primary_key {
11236        return Err(syn::Error::new_spanned(
11237            field,
11238            "`ForeignKey<T>` is not allowed on a primary-key field — \
11239             a row's PK is its own identity, not a reference to a parent.",
11240        ));
11241    }
11242    if attrs.generated_as.is_some() {
11243        if primary_key {
11244            return Err(syn::Error::new_spanned(
11245                field,
11246                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11247                 primary-key field — a PK must be writable so the row \
11248                 has an identity at INSERT time.",
11249            ));
11250        }
11251        if attrs.default.is_some() {
11252            return Err(syn::Error::new_spanned(
11253                field,
11254                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11255                 `default = \"…\"` — Postgres rejects DEFAULT on \
11256                 generated columns. The expression IS the default.",
11257            ));
11258        }
11259        if detected_auto {
11260            return Err(syn::Error::new_spanned(
11261                field,
11262                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11263                 an `Auto<T>` field — generated columns are computed \
11264                 by the DB, not server-assigned via a sequence. Use a \
11265                 plain Rust type (e.g. `f64`).",
11266            ));
11267        }
11268        if fk_inner.is_some() {
11269            return Err(syn::Error::new_spanned(
11270                field,
11271                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11272                 ForeignKey field.",
11273            ));
11274        }
11275    }
11276    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11277    let column_lit = column.as_str();
11278    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11279    // dimension that the bare `Vector` Rust type can't carry, so emit
11280    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11281    // fallback of `Vector(0)`.
11282    let field_type_tokens = if kind == DetectedKind::Vector {
11283        let root = rustango_root();
11284        let dims = attrs.vector_dims.unwrap_or(0);
11285        quote!(#root::core::FieldType::Vector(#dims))
11286    } else if kind == DetectedKind::Geometry {
11287        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11288        // SRID that the bare `Point` Rust type can't carry, so emit
11289        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11290        let root = rustango_root();
11291        let srid = attrs.geometry_srid.unwrap_or(0);
11292        quote!(#root::core::FieldType::Geometry(#srid))
11293    } else {
11294        kind.variant_tokens()
11295    };
11296    let max_length = optional_u32(attrs.max_length);
11297    let min = optional_i64(attrs.min);
11298    let max = optional_i64(attrs.max);
11299    let default = optional_str(attrs.default.as_deref());
11300
11301    let unique = attrs.unique;
11302    let generated_as = optional_str(attrs.generated_as.as_deref());
11303    let help_text = optional_str(attrs.help_text.as_deref());
11304    let choices = optional_choices(attrs.choices.as_deref());
11305    let db_comment = optional_str(attrs.db_comment.as_deref());
11306    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11307    let editable = attrs.editable;
11308    let blank = attrs.blank;
11309    let case_insensitive = attrs.case_insensitive;
11310    let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11311    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11312        return Err(syn::Error::new_spanned(
11313            field,
11314            "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11315             or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11316             non-FK column.",
11317        ));
11318    }
11319    let fk_on_delete = match attrs.on_delete.as_deref() {
11320        None => quote!(::core::option::Option::None),
11321        Some(action) => {
11322            let variant = match action {
11323                "cascade" => quote!(Cascade),
11324                "restrict" => quote!(Restrict),
11325                "set_null" => quote!(SetNull),
11326                "set_default" => quote!(SetDefault),
11327                "no_action" => quote!(NoAction),
11328                // parse_field_attrs already validated this — guard against future drift.
11329                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11330            };
11331            quote!(::core::option::Option::Some(
11332                #root::core::OnDeleteAction::#variant
11333            ))
11334        }
11335    };
11336    let schema = quote! {
11337        #root::core::FieldSchema {
11338            name: #name,
11339            column: #column_lit,
11340            ty: #field_type_tokens,
11341            nullable: #nullable,
11342            primary_key: #primary_key,
11343            relation: #relation,
11344            max_length: #max_length,
11345            min: #min,
11346            max: #max,
11347            default: #default,
11348            auto: #auto,
11349            unique: #unique,
11350            generated_as: #generated_as,
11351            help_text: #help_text,
11352            choices: #choices,
11353            db_comment: #db_comment,
11354            verbose_name: #verbose_name,
11355            editable: #editable,
11356            blank: #blank,
11357            case_insensitive: #case_insensitive,
11358            fk_on_delete: #fk_on_delete,
11359            validators: &[ #(#validators_lits),* ],
11360        }
11361    };
11362
11363    let from_row_init = quote! {
11364        #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11365    };
11366    let from_aliased_row_init = quote! {
11367        #ident: #root::sql::sqlx::Row::try_get(
11368            row,
11369            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11370        )?
11371    };
11372
11373    Ok(FieldInfo {
11374        ident,
11375        column,
11376        primary_key,
11377        auto,
11378        value_ty: &field.ty,
11379        field_type_tokens,
11380        schema,
11381        from_row_init,
11382        from_aliased_row_init,
11383        fk_inner: fk_inner.cloned(),
11384        fk_pk_kind: kind,
11385        nullable,
11386        auto_now: attrs.auto_now,
11387        auto_now_add: attrs.auto_now_add,
11388        soft_delete: attrs.soft_delete,
11389        generated_as: attrs.generated_as.clone(),
11390        default_uuid_v7: attrs.default_uuid_v7,
11391        related_name: attrs.related_name.clone(),
11392    })
11393}
11394
11395fn check_bound_compatibility(
11396    field: &syn::Field,
11397    attrs: &FieldAttrs,
11398    kind: DetectedKind,
11399) -> syn::Result<()> {
11400    if attrs.max_length.is_some() && kind != DetectedKind::String {
11401        return Err(syn::Error::new_spanned(
11402            field,
11403            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11404        ));
11405    }
11406    if attrs.choices.is_some() && kind != DetectedKind::String {
11407        return Err(syn::Error::new_spanned(
11408            field,
11409            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11410             integer-valued enumerations should be modeled with a Rust enum and \
11411             custom (de)serializer for now",
11412        ));
11413    }
11414    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11415        return Err(syn::Error::new_spanned(
11416            field,
11417            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11418        ));
11419    }
11420    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11421        if min > max {
11422            return Err(syn::Error::new_spanned(
11423                field,
11424                format!("`min` ({min}) is greater than `max` ({max})"),
11425            ));
11426        }
11427    }
11428    Ok(())
11429}
11430
11431fn optional_u32(value: Option<u32>) -> TokenStream2 {
11432    if let Some(v) = value {
11433        quote!(::core::option::Option::Some(#v))
11434    } else {
11435        quote!(::core::option::Option::None)
11436    }
11437}
11438
11439fn optional_i64(value: Option<i64>) -> TokenStream2 {
11440    if let Some(v) = value {
11441        quote!(::core::option::Option::Some(#v))
11442    } else {
11443        quote!(::core::option::Option::None)
11444    }
11445}
11446
11447fn optional_str(value: Option<&str>) -> TokenStream2 {
11448    if let Some(v) = value {
11449        quote!(::core::option::Option::Some(#v))
11450    } else {
11451        quote!(::core::option::Option::None)
11452    }
11453}
11454
11455fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11456    let Some(pairs) = pairs else {
11457        return quote!(::core::option::Option::None);
11458    };
11459    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11460    quote!(::core::option::Option::Some(&[#(#entries),*]))
11461}
11462
11463fn relation_tokens(
11464    field: &syn::Field,
11465    attrs: &FieldAttrs,
11466    fk_inner: Option<&syn::Type>,
11467    table: &str,
11468) -> syn::Result<TokenStream2> {
11469    let root = rustango_root();
11470    if let Some(inner) = fk_inner {
11471        if attrs.fk.is_some() || attrs.o2o.is_some() {
11472            return Err(syn::Error::new_spanned(
11473                field,
11474                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11475                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11476            ));
11477        }
11478        let on = attrs.on.as_deref().unwrap_or("id");
11479        return Ok(quote! {
11480            ::core::option::Option::Some(#root::core::Relation::Fk {
11481                to: <#inner as #root::core::Model>::SCHEMA.table,
11482                on: #on,
11483            })
11484        });
11485    }
11486    match (&attrs.fk, &attrs.o2o) {
11487        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11488            field,
11489            "`fk` and `o2o` are mutually exclusive",
11490        )),
11491        (Some(to), None) => {
11492            let on = attrs.on.as_deref().unwrap_or("id");
11493            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11494            // the model's own table. Threaded as a literal string at
11495            // macro-expansion time to sidestep the const-eval cycle
11496            // that `Self::SCHEMA.table` would create when referenced
11497            // inside Self::SCHEMA's own initializer.
11498            let resolved = if to == "self" { table } else { to };
11499            Ok(quote! {
11500                ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11501            })
11502        }
11503        (None, Some(to)) => {
11504            let on = attrs.on.as_deref().unwrap_or("id");
11505            let resolved = if to == "self" { table } else { to };
11506            Ok(quote! {
11507                ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11508            })
11509        }
11510        (None, None) => {
11511            if attrs.on.is_some() {
11512                return Err(syn::Error::new_spanned(
11513                    field,
11514                    "`on` requires `fk` or `o2o`",
11515                ));
11516            }
11517            Ok(quote!(::core::option::Option::None))
11518        }
11519    }
11520}
11521
11522/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11523/// about kinds without depending on `rustango-core` (which would require a
11524/// proc-macro/normal split it doesn't have today).
11525#[derive(Clone, Copy, PartialEq, Eq)]
11526enum DetectedKind {
11527    I16,
11528    I32,
11529    I64,
11530    F32,
11531    F64,
11532    Bool,
11533    String,
11534    DateTime,
11535    Date,
11536    Time,
11537    Uuid,
11538    Json,
11539    Decimal,
11540    Binary,
11541    /// `Array<String>` → PG `text[]` (#341).
11542    ArrayText,
11543    /// `Array<i32>` → PG `integer[]` (#341).
11544    ArrayInt,
11545    /// `Array<i64>` → PG `bigint[]` (#341).
11546    ArrayBigInt,
11547    /// `Range<i32>` → PG `int4range` (#343).
11548    RangeInt,
11549    /// `Range<i64>` → PG `int8range` (#343).
11550    RangeBigInt,
11551    /// `Range<Decimal>` → PG `numrange` (#343).
11552    RangeNumeric,
11553    /// `Range<NaiveDate>` → PG `daterange` (#343).
11554    RangeDate,
11555    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11556    RangeDateTime,
11557    /// `HStore` → PG `hstore` (#342).
11558    HStore,
11559    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11560    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11561    /// in at the `FieldType` emission site (not carried on this enum).
11562    Vector,
11563    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11564    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11565    /// threaded in at the `FieldType` emission site.
11566    Geometry,
11567}
11568
11569impl DetectedKind {
11570    fn variant_tokens(self) -> TokenStream2 {
11571        let root = rustango_root();
11572        match self {
11573            Self::I16 => quote!(#root::core::FieldType::I16),
11574            Self::I32 => quote!(#root::core::FieldType::I32),
11575            Self::I64 => quote!(#root::core::FieldType::I64),
11576            Self::F32 => quote!(#root::core::FieldType::F32),
11577            Self::F64 => quote!(#root::core::FieldType::F64),
11578            Self::Bool => quote!(#root::core::FieldType::Bool),
11579            Self::String => quote!(#root::core::FieldType::String),
11580            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11581            Self::Date => quote!(#root::core::FieldType::Date),
11582            Self::Time => quote!(#root::core::FieldType::Time),
11583            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11584            Self::Json => quote!(#root::core::FieldType::Json),
11585            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11586            Self::Binary => quote!(#root::core::FieldType::Binary),
11587            Self::ArrayText => {
11588                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11589            }
11590            Self::ArrayInt => {
11591                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11592            }
11593            Self::ArrayBigInt => {
11594                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11595            }
11596            Self::RangeInt => {
11597                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11598            }
11599            Self::RangeBigInt => {
11600                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11601            }
11602            Self::RangeNumeric => {
11603                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11604            }
11605            Self::RangeDate => {
11606                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11607            }
11608            Self::RangeDateTime => {
11609                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11610            }
11611            Self::HStore => quote!(#root::core::FieldType::HStore),
11612            // Dimension comes from the `vector(dims = N)` attribute,
11613            // applied at the emission site; `0` here is just a fallback.
11614            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11615            // SRID comes from the `geometry(srid = N)` attribute, applied
11616            // at the emission site; `0` here is just a fallback.
11617            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11618        }
11619    }
11620
11621    fn is_integer(self) -> bool {
11622        matches!(self, Self::I16 | Self::I32 | Self::I64)
11623    }
11624
11625    /// `(SqlValue::<Variant>, default expr)` for emitting the
11626    /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11627    /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11628    /// fires only when the parent's `__rustango_pk_value` returns a
11629    /// different variant than expected, which is a compile-time bug —
11630    /// but we still need a value-typed fallback to keep the match
11631    /// total.
11632    fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11633        let root = rustango_root();
11634        match self {
11635            Self::I16 => (quote!(I16), quote!(0i16)),
11636            Self::I32 => (quote!(I32), quote!(0i32)),
11637            Self::I64 => (quote!(I64), quote!(0i64)),
11638            Self::F32 => (quote!(F32), quote!(0f32)),
11639            Self::F64 => (quote!(F64), quote!(0f64)),
11640            Self::Bool => (quote!(Bool), quote!(false)),
11641            Self::String => (quote!(String), quote!(::std::string::String::new())),
11642            Self::DateTime => (
11643                quote!(DateTime),
11644                quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11645            ),
11646            Self::Date => (
11647                quote!(Date),
11648                quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11649            ),
11650            Self::Time => (
11651                quote!(Time),
11652                quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11653            ),
11654            Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11655            Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11656            Self::Decimal => (
11657                quote!(Decimal),
11658                quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11659            ),
11660            Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11661            // Arrays (#341) can never be a foreign-key primary key, so
11662            // this arm is never reached at runtime — but the match must
11663            // stay total. A bare empty `SqlValue::Array` is the dummy.
11664            Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11665                (quote!(Array), quote!(::std::vec::Vec::new()))
11666            }
11667            // Ranges (#343) likewise can't be a FK PK — never reached.
11668            Self::RangeInt
11669            | Self::RangeBigInt
11670            | Self::RangeNumeric
11671            | Self::RangeDate
11672            | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11673            // HStore (#342) can't be a FK PK — never reached.
11674            Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11675            // Vector (#824) can't be a FK PK — never reached.
11676            Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11677            // Geometry (#443) can't be a FK PK — never reached (arm is
11678            // exhaustiveness-only; never interpolated into emitted code).
11679            Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11680        }
11681    }
11682}
11683
11684/// Result of walking a field's Rust type. `kind` is the underlying
11685/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11686/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11687/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11688/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11689#[derive(Clone, Copy)]
11690struct DetectedType<'a> {
11691    kind: DetectedKind,
11692    nullable: bool,
11693    auto: bool,
11694    fk_inner: Option<&'a syn::Type>,
11695}
11696
11697/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11698/// non-`Auto` types — the caller should already have routed Auto-only
11699/// codegen through this helper, so a `None` indicates a macro-internal
11700/// invariant break.
11701fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11702    let Type::Path(TypePath { path, qself: None }) = ty else {
11703        return None;
11704    };
11705    let last = path.segments.last()?;
11706    if last.ident != "Auto" {
11707        return None;
11708    }
11709    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11710        return None;
11711    };
11712    args.args.iter().find_map(|a| match a {
11713        syn::GenericArgument::Type(t) => Some(t),
11714        _ => None,
11715    })
11716}
11717
11718fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11719    let Type::Path(TypePath { path, qself: None }) = ty else {
11720        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11721    };
11722    let last = path
11723        .segments
11724        .last()
11725        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11726
11727    if last.ident == "Option" {
11728        let inner = generic_inner(ty, &last.arguments, "Option")?;
11729        let inner_det = detect_type(inner)?;
11730        if inner_det.nullable {
11731            return Err(syn::Error::new_spanned(
11732                ty,
11733                "nested Option is not supported",
11734            ));
11735        }
11736        if inner_det.auto {
11737            return Err(syn::Error::new_spanned(
11738                ty,
11739                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11740            ));
11741        }
11742        return Ok(DetectedType {
11743            nullable: true,
11744            ..inner_det
11745        });
11746    }
11747
11748    if last.ident == "Auto" {
11749        let inner = generic_inner(ty, &last.arguments, "Auto")?;
11750        let inner_det = detect_type(inner)?;
11751        if inner_det.auto {
11752            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11753        }
11754        if inner_det.nullable {
11755            return Err(syn::Error::new_spanned(
11756                ty,
11757                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11758            ));
11759        }
11760        if inner_det.fk_inner.is_some() {
11761            return Err(syn::Error::new_spanned(
11762                ty,
11763                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11764            ));
11765        }
11766        if !matches!(
11767            inner_det.kind,
11768            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11769        ) {
11770            return Err(syn::Error::new_spanned(
11771                ty,
11772                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11773                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11774                 (DEFAULT now())",
11775            ));
11776        }
11777        return Ok(DetectedType {
11778            auto: true,
11779            ..inner_det
11780        });
11781    }
11782
11783    if last.ident == "ForeignKey" {
11784        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11785        // Resolve the FK column's underlying SQL type from `K`. When the
11786        // user wrote `ForeignKey<T>` without a key parameter, the type
11787        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11788        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11789        // recurse into K so the column DDL emits the right SQL type
11790        // (VARCHAR for String, UUID for Uuid, …) and the load_related
11791        // emitter knows which `SqlValue` variant to match.
11792        let kind = match key_ty {
11793            Some(k) => detect_type(k)?.kind,
11794            None => DetectedKind::I64,
11795        };
11796        return Ok(DetectedType {
11797            kind,
11798            nullable: false,
11799            auto: false,
11800            fk_inner: Some(inner),
11801        });
11802    }
11803
11804    let kind = match last.ident.to_string().as_str() {
11805        "i16" => DetectedKind::I16,
11806        "i32" => DetectedKind::I32,
11807        "i64" => DetectedKind::I64,
11808        "f32" => DetectedKind::F32,
11809        "f64" => DetectedKind::F64,
11810        "bool" => DetectedKind::Bool,
11811        "String" => DetectedKind::String,
11812        "DateTime" => DetectedKind::DateTime,
11813        "NaiveDate" => DetectedKind::Date,
11814        "NaiveTime" => DetectedKind::Time,
11815        "Uuid" => DetectedKind::Uuid,
11816        "Value" => DetectedKind::Json,
11817        "Decimal" => DetectedKind::Decimal,
11818        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11819        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11820        // — that would emit Binary DDL and decode-fail at runtime.
11821        "Vec" => {
11822            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11823            if let Type::Path(TypePath { path, qself: None }) = inner {
11824                if let Some(seg) = path.segments.last() {
11825                    if seg.ident == "u8" && seg.arguments.is_empty() {
11826                        return Ok(DetectedType {
11827                            kind: DetectedKind::Binary,
11828                            nullable: false,
11829                            auto: false,
11830                            fk_inner: None,
11831                        });
11832                    }
11833                }
11834            }
11835            return Err(syn::Error::new_spanned(
11836                ty,
11837                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11838                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11839            ));
11840        }
11841        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11842        // `integer[]` / `bigint[]` (#341).
11843        "Array" => {
11844            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11845            let elem = match inner {
11846                Type::Path(TypePath { path, qself: None }) => {
11847                    path.segments.last().map(|s| s.ident.to_string())
11848                }
11849                _ => None,
11850            };
11851            let kind = match elem.as_deref() {
11852                Some("String") => DetectedKind::ArrayText,
11853                Some("i32") => DetectedKind::ArrayInt,
11854                Some("i64") => DetectedKind::ArrayBigInt,
11855                _ => {
11856                    return Err(syn::Error::new_spanned(
11857                        ty,
11858                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11859                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11860                    ));
11861                }
11862            };
11863            return Ok(DetectedType {
11864                kind,
11865                nullable: false,
11866                auto: false,
11867                fk_inner: None,
11868            });
11869        }
11870        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11871        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11872        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
11873        "Range" => {
11874            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11875            let elem = match inner {
11876                Type::Path(TypePath { path, qself: None }) => {
11877                    path.segments.last().map(|s| s.ident.to_string())
11878                }
11879                _ => None,
11880            };
11881            let kind = match elem.as_deref() {
11882                Some("i32") => DetectedKind::RangeInt,
11883                Some("i64") => DetectedKind::RangeBigInt,
11884                Some("Decimal") => DetectedKind::RangeNumeric,
11885                Some("NaiveDate") => DetectedKind::RangeDate,
11886                Some("DateTime") => DetectedKind::RangeDateTime,
11887                _ => {
11888                    return Err(syn::Error::new_spanned(
11889                        ty,
11890                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11891                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11892                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11893                         (→ tstzrange) are supported (#343)",
11894                    ));
11895                }
11896            };
11897            return Ok(DetectedType {
11898                kind,
11899                nullable: false,
11900                auto: false,
11901                fk_inner: None,
11902            });
11903        }
11904        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11905        // (the `CastValue` impl bridges logical↔stored); the field's own
11906        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11907        // schema just needs `FieldType::String`.
11908        "Cast" => {
11909            return Ok(DetectedType {
11910                kind: DetectedKind::String,
11911                nullable: false,
11912                auto: false,
11913                fk_inner: None,
11914            });
11915        }
11916        // `HStore` → PG `hstore` (#342). No generic
11917        // parameter — always a string→string map.
11918        "HStore" => {
11919            return Ok(DetectedType {
11920                kind: DetectedKind::HStore,
11921                nullable: false,
11922                auto: false,
11923                fk_inner: None,
11924            });
11925        }
11926        // `Vector` → pgvector `vector(N)` (#824). The dimension is
11927        // supplied by `#[rustango(vector(dims = N))]`, not the type.
11928        "Vector" => {
11929            return Ok(DetectedType {
11930                kind: DetectedKind::Vector,
11931                nullable: false,
11932                auto: false,
11933                fk_inner: None,
11934            });
11935        }
11936        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11937        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11938        "Point" => {
11939            return Ok(DetectedType {
11940                kind: DetectedKind::Geometry,
11941                nullable: false,
11942                auto: false,
11943                fk_inner: None,
11944            });
11945        }
11946        other => {
11947            return Err(syn::Error::new_spanned(
11948                ty,
11949                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)"),
11950            ));
11951        }
11952    };
11953    Ok(DetectedType {
11954        kind,
11955        nullable: false,
11956        auto: false,
11957        fk_inner: None,
11958    })
11959}
11960
11961fn generic_inner<'a>(
11962    ty: &'a Type,
11963    arguments: &'a PathArguments,
11964    wrapper: &str,
11965) -> syn::Result<&'a Type> {
11966    let PathArguments::AngleBracketed(args) = arguments else {
11967        return Err(syn::Error::new_spanned(
11968            ty,
11969            format!("{wrapper} requires a generic argument"),
11970        ));
11971    };
11972    args.args
11973        .iter()
11974        .find_map(|a| match a {
11975            GenericArgument::Type(t) => Some(t),
11976            _ => None,
11977        })
11978        .ok_or_else(|| {
11979            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11980        })
11981}
11982
11983/// Like [`generic_inner`] but pulls *two* type args — the first is
11984/// required, the second is optional. Used by the `ForeignKey<T, K>`
11985/// detection where K defaults to `i64` when omitted.
11986fn generic_pair<'a>(
11987    ty: &'a Type,
11988    arguments: &'a PathArguments,
11989    wrapper: &str,
11990) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11991    let PathArguments::AngleBracketed(args) = arguments else {
11992        return Err(syn::Error::new_spanned(
11993            ty,
11994            format!("{wrapper} requires a generic argument"),
11995        ));
11996    };
11997    let mut types = args.args.iter().filter_map(|a| match a {
11998        GenericArgument::Type(t) => Some(t),
11999        _ => None,
12000    });
12001    let first = types.next().ok_or_else(|| {
12002        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12003    })?;
12004    let second = types.next();
12005    Ok((first, second))
12006}
12007
12008fn to_snake_case(s: &str) -> String {
12009    let mut out = String::with_capacity(s.len() + 4);
12010    for (i, ch) in s.chars().enumerate() {
12011        if ch.is_ascii_uppercase() {
12012            if i > 0 {
12013                out.push('_');
12014            }
12015            out.push(ch.to_ascii_lowercase());
12016        } else {
12017            out.push(ch);
12018        }
12019    }
12020    out
12021}
12022
12023// ============================================================
12024//  #[derive(Form)]  —  slice 8.4B
12025// ============================================================
12026
12027/// Per-field `#[form(...)]` attributes recognised by the derive.
12028#[derive(Default)]
12029struct FormFieldAttrs {
12030    min: Option<i64>,
12031    max: Option<i64>,
12032    min_length: Option<u32>,
12033    max_length: Option<u32>,
12034    /// `#[form(clean = "fn_name")]` — per-field clean hook.
12035    /// The named static method on the form struct is called after the
12036    /// field's typed parse + length/range checks; it gets the parsed
12037    /// value by reference and returns `Result<<FieldType>, String>`.
12038    /// On Ok, the returned value replaces the parsed one; on Err, the
12039    /// message is attached to the field error list. Issue #372.
12040    clean: Option<syn::Ident>,
12041}
12042
12043/// Container-level `#[form(...)]` attributes. Currently only the
12044/// cross-field `validate` hook (issue #373).
12045#[derive(Default)]
12046struct FormContainerAttrs {
12047    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12048    /// After every per-field parse succeeds, the named method on the
12049    /// form struct is called with `&self` and may return
12050    /// `Result<(), FormErrors>`. Errors merge into the field error
12051    /// list. Issue #373.
12052    validate: Option<syn::Ident>,
12053}
12054
12055/// Detected shape of a form field's Rust type.
12056#[derive(Clone, Copy)]
12057enum FormFieldKind {
12058    String,
12059    I16,
12060    I32,
12061    I64,
12062    F32,
12063    F64,
12064    Bool,
12065}
12066
12067impl FormFieldKind {
12068    fn parse_method(self) -> &'static str {
12069        match self {
12070            Self::I16 => "i16",
12071            Self::I32 => "i32",
12072            Self::I64 => "i64",
12073            Self::F32 => "f32",
12074            Self::F64 => "f64",
12075            // String + Bool don't go through `str::parse`; the codegen
12076            // handles them inline.
12077            Self::String | Self::Bool => "",
12078        }
12079    }
12080}
12081
12082fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12083    let root = rustango_root();
12084    let struct_name = &input.ident;
12085
12086    let Data::Struct(data) = &input.data else {
12087        return Err(syn::Error::new_spanned(
12088            struct_name,
12089            "Form can only be derived on structs",
12090        ));
12091    };
12092    let Fields::Named(named) = &data.fields else {
12093        return Err(syn::Error::new_spanned(
12094            struct_name,
12095            "Form requires a struct with named fields",
12096        ));
12097    };
12098
12099    // #373 — container-level `#[form(validate = "fn")]` hook.
12100    let container = parse_form_container_attrs(input)?;
12101    let post_field_clean: Vec<TokenStream2> = Vec::new();
12102    let _ = post_field_clean;
12103
12104    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12105    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12106
12107    for field in &named.named {
12108        let ident = field
12109            .ident
12110            .as_ref()
12111            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12112        let attrs = parse_form_field_attrs(field)?;
12113        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12114
12115        let name_lit = ident.to_string();
12116        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12117        // #372 — append the per-field `clean_<field>` call right after
12118        // the parse block when the attribute is set. The clean fn
12119        // takes &T and returns Result<T, String>; on Err we attach
12120        // the message to the field error list without aborting, so
12121        // the caller sees every field error at once.
12122        let clean_block = if let Some(clean_fn) = &attrs.clean {
12123            quote! {
12124                if __errors.fields().get(#name_lit).is_none() {
12125                    match Self::#clean_fn(&#ident) {
12126                        ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12127                        ::core::result::Result::Err(__msg) => {
12128                            __errors.add(#name_lit, __msg);
12129                        }
12130                    }
12131                }
12132            }
12133        } else {
12134            quote! {}
12135        };
12136        field_blocks.push(quote! {
12137            #parse_block
12138            #clean_block
12139        });
12140        field_idents.push(ident);
12141    }
12142
12143    // #373 — after every per-field parse + clean succeeds, call the
12144    // cross-field validator if declared. Errors merge into the
12145    // outgoing FormErrors via the existing `FormErrors::merge` helper
12146    // (same primitive the serializer cross-field hook uses).
12147    let cross_field_call = if let Some(validate_fn) = &container.validate {
12148        quote! {
12149            if __errors.is_empty() {
12150                let __candidate = Self { #( #field_idents ),* };
12151                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12152                    __errors.merge(__other);
12153                }
12154                if !__errors.is_empty() {
12155                    return ::core::result::Result::Err(__errors);
12156                }
12157                return ::core::result::Result::Ok(__candidate);
12158            }
12159        }
12160    } else {
12161        quote! {}
12162    };
12163
12164    Ok(quote! {
12165        impl #root::forms::Form for #struct_name {
12166            fn parse(
12167                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12168            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12169                let mut __errors = #root::forms::FormErrors::default();
12170                #( #field_blocks )*
12171                #cross_field_call
12172                if !__errors.is_empty() {
12173                    return ::core::result::Result::Err(__errors);
12174                }
12175                ::core::result::Result::Ok(Self {
12176                    #( #field_idents ),*
12177                })
12178            }
12179        }
12180    })
12181}
12182
12183fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12184    let mut out = FormContainerAttrs::default();
12185    for attr in &input.attrs {
12186        if !attr.path().is_ident("form") {
12187            continue;
12188        }
12189        attr.parse_nested_meta(|meta| {
12190            if meta.path.is_ident("validate") {
12191                let s: LitStr = meta.value()?.parse()?;
12192                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12193                return Ok(());
12194            }
12195            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12196        })?;
12197    }
12198    Ok(out)
12199}
12200
12201fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12202    let mut out = FormFieldAttrs::default();
12203    for attr in &field.attrs {
12204        if !attr.path().is_ident("form") {
12205            continue;
12206        }
12207        attr.parse_nested_meta(|meta| {
12208            if meta.path.is_ident("min") {
12209                let lit: syn::LitInt = meta.value()?.parse()?;
12210                out.min = Some(lit.base10_parse::<i64>()?);
12211                return Ok(());
12212            }
12213            if meta.path.is_ident("max") {
12214                let lit: syn::LitInt = meta.value()?.parse()?;
12215                out.max = Some(lit.base10_parse::<i64>()?);
12216                return Ok(());
12217            }
12218            if meta.path.is_ident("min_length") {
12219                let lit: syn::LitInt = meta.value()?.parse()?;
12220                out.min_length = Some(lit.base10_parse::<u32>()?);
12221                return Ok(());
12222            }
12223            if meta.path.is_ident("max_length") {
12224                let lit: syn::LitInt = meta.value()?.parse()?;
12225                out.max_length = Some(lit.base10_parse::<u32>()?);
12226                return Ok(());
12227            }
12228            if meta.path.is_ident("clean") {
12229                let s: LitStr = meta.value()?.parse()?;
12230                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12231                return Ok(());
12232            }
12233            Err(meta.error(
12234                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12235            ))
12236        })?;
12237    }
12238    Ok(out)
12239}
12240
12241fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12242    let Type::Path(TypePath { path, qself: None }) = ty else {
12243        return Err(syn::Error::new(
12244            span,
12245            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12246        ));
12247    };
12248    let last = path
12249        .segments
12250        .last()
12251        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12252
12253    if last.ident == "Option" {
12254        let inner = generic_inner(ty, &last.arguments, "Option")?;
12255        let (kind, nested) = detect_form_field(inner, span)?;
12256        if nested {
12257            return Err(syn::Error::new(
12258                span,
12259                "nested Option in Form fields is not supported",
12260            ));
12261        }
12262        return Ok((kind, true));
12263    }
12264
12265    let kind = match last.ident.to_string().as_str() {
12266        "String" => FormFieldKind::String,
12267        "i16" => FormFieldKind::I16,
12268        "i32" => FormFieldKind::I32,
12269        "i64" => FormFieldKind::I64,
12270        "f32" => FormFieldKind::F32,
12271        "f64" => FormFieldKind::F64,
12272        "bool" => FormFieldKind::Bool,
12273        other => {
12274            return Err(syn::Error::new(
12275                span,
12276                format!(
12277                    "Form field type `{other}` is not supported in v0.8 — use String / \
12278                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12279                ),
12280            ));
12281        }
12282    };
12283    Ok((kind, false))
12284}
12285
12286#[allow(clippy::too_many_lines)]
12287fn render_form_field_parse(
12288    ident: &syn::Ident,
12289    name_lit: &str,
12290    kind: FormFieldKind,
12291    nullable: bool,
12292    attrs: &FormFieldAttrs,
12293) -> TokenStream2 {
12294    // Pull the raw &str from the payload. Uses variable name `data` to
12295    // match the new `Form::parse(data: &HashMap<…>)` signature.
12296    let lookup = quote! {
12297        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12298    };
12299
12300    let parsed_value = match kind {
12301        FormFieldKind::Bool => quote! {
12302            let __v: bool = match __raw {
12303                ::core::option::Option::None => false,
12304                ::core::option::Option::Some(__s) => !matches!(
12305                    __s.to_ascii_lowercase().as_str(),
12306                    "" | "false" | "0" | "off" | "no"
12307                ),
12308            };
12309        },
12310        FormFieldKind::String => {
12311            if nullable {
12312                quote! {
12313                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12314                        ::core::option::Option::None => ::core::option::Option::None,
12315                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12316                            ::core::option::Option::None
12317                        }
12318                        ::core::option::Option::Some(__s) => {
12319                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12320                        }
12321                    };
12322                }
12323            } else {
12324                quote! {
12325                    let __v: ::std::string::String = match __raw {
12326                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12327                            ::core::clone::Clone::clone(__s)
12328                        }
12329                        _ => {
12330                            __errors.add(#name_lit, "This field is required.");
12331                            ::std::string::String::new()
12332                        }
12333                    };
12334                }
12335            }
12336        }
12337        FormFieldKind::I16
12338        | FormFieldKind::I32
12339        | FormFieldKind::I64
12340        | FormFieldKind::F32
12341        | FormFieldKind::F64 => {
12342            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12343            let ty_lit = kind.parse_method();
12344            let default_val = match kind {
12345                FormFieldKind::I16 => quote! { 0i16 },
12346                FormFieldKind::I32 => quote! { 0i32 },
12347                FormFieldKind::I64 => quote! { 0i64 },
12348                FormFieldKind::F32 => quote! { 0f32 },
12349                FormFieldKind::F64 => quote! { 0f64 },
12350                _ => quote! { Default::default() },
12351            };
12352            if nullable {
12353                quote! {
12354                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12355                        ::core::option::Option::None => ::core::option::Option::None,
12356                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12357                            ::core::option::Option::None
12358                        }
12359                        ::core::option::Option::Some(__s) => {
12360                            match __s.parse::<#parse_ty>() {
12361                                ::core::result::Result::Ok(__n) => {
12362                                    ::core::option::Option::Some(__n)
12363                                }
12364                                ::core::result::Result::Err(__e) => {
12365                                    __errors.add(
12366                                        #name_lit,
12367                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12368                                    );
12369                                    ::core::option::Option::None
12370                                }
12371                            }
12372                        }
12373                    };
12374                }
12375            } else {
12376                quote! {
12377                    let __v: #parse_ty = match __raw {
12378                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12379                            match __s.parse::<#parse_ty>() {
12380                                ::core::result::Result::Ok(__n) => __n,
12381                                ::core::result::Result::Err(__e) => {
12382                                    __errors.add(
12383                                        #name_lit,
12384                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12385                                    );
12386                                    #default_val
12387                                }
12388                            }
12389                        }
12390                        _ => {
12391                            __errors.add(#name_lit, "This field is required.");
12392                            #default_val
12393                        }
12394                    };
12395                }
12396            }
12397        }
12398    };
12399
12400    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12401
12402    quote! {
12403        // `mut` so the per-field `clean` hook (#372) can rewrite the
12404        // parsed value in-place when it returns Ok with a normalized
12405        // form (e.g. trim / lowercase).
12406        let mut #ident = {
12407            #lookup
12408            #parsed_value
12409            #validators
12410            __v
12411        };
12412    }
12413}
12414
12415fn render_form_validators(
12416    name_lit: &str,
12417    kind: FormFieldKind,
12418    nullable: bool,
12419    attrs: &FormFieldAttrs,
12420) -> TokenStream2 {
12421    let mut checks: Vec<TokenStream2> = Vec::new();
12422
12423    let val_ref = if nullable {
12424        quote! { __v.as_ref() }
12425    } else {
12426        quote! { ::core::option::Option::Some(&__v) }
12427    };
12428
12429    let is_string = matches!(kind, FormFieldKind::String);
12430    let is_numeric = matches!(
12431        kind,
12432        FormFieldKind::I16
12433            | FormFieldKind::I32
12434            | FormFieldKind::I64
12435            | FormFieldKind::F32
12436            | FormFieldKind::F64
12437    );
12438
12439    if is_string {
12440        if let Some(min_len) = attrs.min_length {
12441            let min_len_usize = min_len as usize;
12442            checks.push(quote! {
12443                if let ::core::option::Option::Some(__s) = #val_ref {
12444                    if __s.len() < #min_len_usize {
12445                        __errors.add(
12446                            #name_lit,
12447                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12448                        );
12449                    }
12450                }
12451            });
12452        }
12453        if let Some(max_len) = attrs.max_length {
12454            let max_len_usize = max_len as usize;
12455            checks.push(quote! {
12456                if let ::core::option::Option::Some(__s) = #val_ref {
12457                    if __s.len() > #max_len_usize {
12458                        __errors.add(
12459                            #name_lit,
12460                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12461                        );
12462                    }
12463                }
12464            });
12465        }
12466    }
12467
12468    if is_numeric {
12469        if let Some(min) = attrs.min {
12470            checks.push(quote! {
12471                if let ::core::option::Option::Some(__n) = #val_ref {
12472                    if (*__n as f64) < (#min as f64) {
12473                        __errors.add(
12474                            #name_lit,
12475                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12476                        );
12477                    }
12478                }
12479            });
12480        }
12481        if let Some(max) = attrs.max {
12482            checks.push(quote! {
12483                if let ::core::option::Option::Some(__n) = #val_ref {
12484                    if (*__n as f64) > (#max as f64) {
12485                        __errors.add(
12486                            #name_lit,
12487                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12488                        );
12489                    }
12490                }
12491            });
12492        }
12493    }
12494
12495    quote! { #( #checks )* }
12496}
12497
12498// ============================================================
12499//  #[derive(ViewSet)]
12500// ============================================================
12501
12502struct ViewSetAttrs {
12503    model: syn::Path,
12504    fields: Option<Vec<String>>,
12505    filter_fields: Vec<String>,
12506    search_fields: Vec<String>,
12507    /// (field_name, desc)
12508    ordering: Vec<(String, bool)>,
12509    page_size: Option<usize>,
12510    read_only: bool,
12511    perms: ViewSetPermsAttrs,
12512    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12513    /// retrieve / create responses through this `#[derive(Serializer)]`
12514    /// type instead of the default field-level projection (requires the
12515    /// `serializer` feature). Tri-dialect.
12516    serializer: Option<syn::Path>,
12517}
12518
12519#[derive(Default)]
12520struct ViewSetPermsAttrs {
12521    list: Vec<String>,
12522    retrieve: Vec<String>,
12523    create: Vec<String>,
12524    update: Vec<String>,
12525    destroy: Vec<String>,
12526}
12527
12528fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12529    let root = rustango_root();
12530    let struct_name = &input.ident;
12531
12532    // Must be a unit struct or an empty named struct.
12533    match &input.data {
12534        Data::Struct(s) => match &s.fields {
12535            Fields::Unit | Fields::Named(_) => {}
12536            Fields::Unnamed(_) => {
12537                return Err(syn::Error::new_spanned(
12538                    struct_name,
12539                    "ViewSet can only be derived on a unit struct or an empty named struct",
12540                ));
12541            }
12542        },
12543        _ => {
12544            return Err(syn::Error::new_spanned(
12545                struct_name,
12546                "ViewSet can only be derived on a struct",
12547            ));
12548        }
12549    }
12550
12551    let attrs = parse_viewset_attrs(input)?;
12552    let model_path = &attrs.model;
12553
12554    // `.fields(&[...])` call — None means skip (use all scalar fields).
12555    let fields_call = if let Some(ref fields) = attrs.fields {
12556        let lits = fields.iter().map(|f| f.as_str());
12557        quote!(.fields(&[ #(#lits),* ]))
12558    } else {
12559        quote!()
12560    };
12561
12562    let filter_fields_call = if attrs.filter_fields.is_empty() {
12563        quote!()
12564    } else {
12565        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12566        quote!(.filter_fields(&[ #(#lits),* ]))
12567    };
12568
12569    let search_fields_call = if attrs.search_fields.is_empty() {
12570        quote!()
12571    } else {
12572        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12573        quote!(.search_fields(&[ #(#lits),* ]))
12574    };
12575
12576    let ordering_call = if attrs.ordering.is_empty() {
12577        quote!()
12578    } else {
12579        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12580            let f = f.as_str();
12581            quote!((#f, #desc))
12582        });
12583        quote!(.ordering(&[ #(#pairs),* ]))
12584    };
12585
12586    let page_size_call = if let Some(n) = attrs.page_size {
12587        quote!(.page_size(#n))
12588    } else {
12589        quote!()
12590    };
12591
12592    let read_only_call = if attrs.read_only {
12593        quote!(.read_only())
12594    } else {
12595        quote!()
12596    };
12597
12598    // `.serializer::<S>()` — reshape responses through a derived
12599    // serializer. Requires the downstream crate to enable the
12600    // `serializer` feature (the method is gated on it).
12601    let serializer_call = if let Some(ref ser) = attrs.serializer {
12602        quote!(.serializer::<#ser>())
12603    } else {
12604        quote!()
12605    };
12606
12607    let perms = &attrs.perms;
12608    let perms_call = if perms.list.is_empty()
12609        && perms.retrieve.is_empty()
12610        && perms.create.is_empty()
12611        && perms.update.is_empty()
12612        && perms.destroy.is_empty()
12613    {
12614        quote!()
12615    } else {
12616        let list_lits = perms.list.iter().map(|s| s.as_str());
12617        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12618        let create_lits = perms.create.iter().map(|s| s.as_str());
12619        let update_lits = perms.update.iter().map(|s| s.as_str());
12620        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12621        quote! {
12622            .permissions(#root::viewset::ViewSetPerms {
12623                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
12624                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12625                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
12626                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
12627                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
12628            })
12629        }
12630    };
12631
12632    Ok(quote! {
12633        impl #struct_name {
12634            /// Build an `axum::Router` with the six standard REST endpoints
12635            /// for this ViewSet, mounted at `prefix`.
12636            ///
12637            /// Accepts any backend's pool — a `sqlx::PgPool`,
12638            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12639            /// enum — because they all `Into<Pool>` (#1273). This is why
12640            /// the derive no longer names `PgPool`, which would not
12641            /// exist on a project built without the `postgres` feature.
12642            pub fn router(
12643                prefix: &str,
12644                pool: impl ::core::convert::Into<#root::sql::Pool>,
12645            ) -> #root::__axum::Router {
12646                #root::viewset::ViewSet::for_model(
12647                    <#model_path as #root::core::Model>::SCHEMA
12648                )
12649                    #fields_call
12650                    #filter_fields_call
12651                    #search_fields_call
12652                    #ordering_call
12653                    #page_size_call
12654                    #perms_call
12655                    #read_only_call
12656                    #serializer_call
12657                    .router_pool(prefix, pool.into())
12658            }
12659        }
12660    })
12661}
12662
12663fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12664    let mut model: Option<syn::Path> = None;
12665    let mut fields: Option<Vec<String>> = None;
12666    let mut filter_fields: Vec<String> = Vec::new();
12667    let mut search_fields: Vec<String> = Vec::new();
12668    let mut ordering: Vec<(String, bool)> = Vec::new();
12669    let mut page_size: Option<usize> = None;
12670    let mut read_only = false;
12671    let mut perms = ViewSetPermsAttrs::default();
12672    let mut serializer: Option<syn::Path> = None;
12673
12674    for attr in &input.attrs {
12675        if !attr.path().is_ident("viewset") {
12676            continue;
12677        }
12678        attr.parse_nested_meta(|meta| {
12679            if meta.path.is_ident("model") {
12680                let path: syn::Path = meta.value()?.parse()?;
12681                model = Some(path);
12682                return Ok(());
12683            }
12684            if meta.path.is_ident("serializer") {
12685                let path: syn::Path = meta.value()?.parse()?;
12686                serializer = Some(path);
12687                return Ok(());
12688            }
12689            if meta.path.is_ident("fields") {
12690                let s: LitStr = meta.value()?.parse()?;
12691                fields = Some(split_field_list(&s.value()));
12692                return Ok(());
12693            }
12694            if meta.path.is_ident("filter_fields") {
12695                let s: LitStr = meta.value()?.parse()?;
12696                filter_fields = split_field_list(&s.value());
12697                return Ok(());
12698            }
12699            if meta.path.is_ident("search_fields") {
12700                let s: LitStr = meta.value()?.parse()?;
12701                search_fields = split_field_list(&s.value());
12702                return Ok(());
12703            }
12704            if meta.path.is_ident("ordering") {
12705                let s: LitStr = meta.value()?.parse()?;
12706                ordering = parse_ordering_list(&s.value());
12707                return Ok(());
12708            }
12709            if meta.path.is_ident("page_size") {
12710                let lit: syn::LitInt = meta.value()?.parse()?;
12711                page_size = Some(lit.base10_parse::<usize>()?);
12712                return Ok(());
12713            }
12714            if meta.path.is_ident("read_only") {
12715                read_only = true;
12716                return Ok(());
12717            }
12718            if meta.path.is_ident("permissions") {
12719                meta.parse_nested_meta(|inner| {
12720                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12721                        let s: LitStr = inner.value()?.parse()?;
12722                        Ok(split_field_list(&s.value()))
12723                    };
12724                    if inner.path.is_ident("list") {
12725                        perms.list = parse_codenames(&inner)?;
12726                    } else if inner.path.is_ident("retrieve") {
12727                        perms.retrieve = parse_codenames(&inner)?;
12728                    } else if inner.path.is_ident("create") {
12729                        perms.create = parse_codenames(&inner)?;
12730                    } else if inner.path.is_ident("update") {
12731                        perms.update = parse_codenames(&inner)?;
12732                    } else if inner.path.is_ident("destroy") {
12733                        perms.destroy = parse_codenames(&inner)?;
12734                    } else {
12735                        return Err(inner.error(
12736                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12737                        ));
12738                    }
12739                    Ok(())
12740                })?;
12741                return Ok(());
12742            }
12743            Err(meta.error(
12744                "unknown viewset attribute (supported: model, fields, filter_fields, \
12745                 search_fields, ordering, page_size, read_only, serializer, permissions(...))",
12746            ))
12747        })?;
12748    }
12749
12750    let model = model.ok_or_else(|| {
12751        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12752    })?;
12753
12754    Ok(ViewSetAttrs {
12755        model,
12756        fields,
12757        filter_fields,
12758        search_fields,
12759        ordering,
12760        page_size,
12761        read_only,
12762        perms,
12763        serializer,
12764    })
12765}
12766
12767// ============================================================ #[derive(Serializer)]
12768
12769struct SerializerContainerAttrs {
12770    model: syn::Path,
12771    /// `#[serializer(validate = "fn_name")]` on the struct — the
12772    /// cross-field validation hook (#436). The named inherent method
12773    /// must take `&self` and return
12774    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12775    /// `validate()` runs every per-field validator first; then calls
12776    /// the cross-field method if declared; aggregates all errors into
12777    /// one `FormErrors`.
12778    cross_validate: Option<syn::Ident>,
12779}
12780
12781#[derive(Default)]
12782struct SerializerFieldAttrs {
12783    read_only: bool,
12784    write_only: bool,
12785    source: Option<String>,
12786    skip: bool,
12787    /// `#[serializer(method = "fn_name")]` — computed field. The macro
12788    /// emits a `from_model` initializer that calls
12789    /// `Self::fn_name(&model)` and stores the return value.
12790    method: Option<String>,
12791    /// `#[serializer(validate = "fn_name")]` — per-field validator
12792    /// callable run by `Self::validate(&self)`. Must return
12793    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12794    /// the field name.
12795    validate: Option<String>,
12796    /// `#[serializer(nested)]` on a field whose type is another
12797    /// `Serializer` — the macro emits `from_model` initializer that
12798    /// reads the parent via `model.<source>.value()` then calls the
12799    /// child serializer's `from_model(parent)`. When the FK is
12800    /// unloaded the field falls back to `Default::default()` (does
12801    /// NOT panic) so a missing prefetch in prod degrades gracefully.
12802    /// Source field on the model defaults to the field name; override
12803    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12804    /// panic-on-unloaded behavior for tests.
12805    nested: bool,
12806    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12807    /// strict behavior: panic when the FK isn't loaded. Useful in
12808    /// test code where forgetting select_related must trip a hard
12809    /// failure rather than render a blank nested object.
12810    nested_strict: bool,
12811    /// `#[serializer(many = TagSerializer)]` — declare the field as
12812    /// a list of nested serializers. Field type must be `Vec<S>`
12813    /// where `S` is the inner serializer. The macro initializes the
12814    /// field to `Vec::new()` in `from_model` and emits a typed
12815    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12816    /// maps each model row through `S::from_model`. Auto-load isn't
12817    /// possible (the M2M / one-to-many accessor is async); callers
12818    /// fetch the children + call the setter post-from_model.
12819    many: Option<syn::Type>,
12820    /// `#[serializer(slug = "name")]` — render a relation as one of
12821    /// its fields.
12822    /// Source field on the model must be a `ForeignKey<T>`; the
12823    /// macro emits `from_model` glue that walks
12824    /// `model.<source>.value()?.<slug>` and clones it. Field type on
12825    /// the serializer is typically `String` (whatever type the slug
12826    /// column has). When the FK is unloaded the field falls back to
12827    /// `Default::default()`, same graceful-degrade contract as
12828    /// `nested`. Source defaults to the field name; override with
12829    /// `source = "..."`. v0.44.
12830    slug: Option<String>,
12831    /// `#[serializer(max_length = N)]` — caps
12832    /// the character count of a string field on write. Overrides the
12833    /// model's `max_length`; when absent the model value is inherited.
12834    max_length: Option<u64>,
12835    /// `#[serializer(min_length = N)]` — minimum character count.
12836    /// Serializer-only (the model has no `min_length` column).
12837    min_length: Option<u64>,
12838    /// `#[serializer(min = N)]` — inclusive
12839    /// integer lower bound; overrides the model's `min` when given.
12840    min: Option<i64>,
12841    /// `#[serializer(max = N)]` — inclusive
12842    /// integer upper bound; overrides the model's `max` when given.
12843    max: Option<i64>,
12844}
12845
12846fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12847    let mut model: Option<syn::Path> = None;
12848    let mut cross_validate: Option<syn::Ident> = None;
12849    for attr in &input.attrs {
12850        if !attr.path().is_ident("serializer") {
12851            continue;
12852        }
12853        attr.parse_nested_meta(|meta| {
12854            if meta.path.is_ident("model") {
12855                let _eq: syn::Token![=] = meta.input.parse()?;
12856                model = Some(meta.input.parse()?);
12857                return Ok(());
12858            }
12859            if meta.path.is_ident("validate") {
12860                // #436 — container-level `validate = "fn_name"` for
12861                // cross-field validation. Field-level
12862                // `#[serializer(validate = "...")]` on a field is
12863                // parsed separately in `parse_serializer_field_attrs`.
12864                let s: LitStr = meta.value()?.parse()?;
12865                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12866                return Ok(());
12867            }
12868            Err(meta.error(
12869                "unknown serializer container attribute \
12870                 (supported: `model`, `validate`)",
12871            ))
12872        })?;
12873    }
12874    let model = model.ok_or_else(|| {
12875        syn::Error::new_spanned(
12876            &input.ident,
12877            "`#[serializer(model = SomeModel)]` is required",
12878        )
12879    })?;
12880    Ok(SerializerContainerAttrs {
12881        model,
12882        cross_validate,
12883    })
12884}
12885
12886fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12887    let mut out = SerializerFieldAttrs::default();
12888    for attr in &field.attrs {
12889        if !attr.path().is_ident("serializer") {
12890            continue;
12891        }
12892        attr.parse_nested_meta(|meta| {
12893            if meta.path.is_ident("read_only") {
12894                out.read_only = true;
12895                return Ok(());
12896            }
12897            if meta.path.is_ident("write_only") {
12898                out.write_only = true;
12899                return Ok(());
12900            }
12901            if meta.path.is_ident("skip") {
12902                out.skip = true;
12903                return Ok(());
12904            }
12905            if meta.path.is_ident("source") {
12906                let s: LitStr = meta.value()?.parse()?;
12907                out.source = Some(s.value());
12908                return Ok(());
12909            }
12910            if meta.path.is_ident("method") {
12911                let s: LitStr = meta.value()?.parse()?;
12912                out.method = Some(s.value());
12913                return Ok(());
12914            }
12915            if meta.path.is_ident("validate") {
12916                let s: LitStr = meta.value()?.parse()?;
12917                out.validate = Some(s.value());
12918                return Ok(());
12919            }
12920            if meta.path.is_ident("many") {
12921                let _eq: syn::Token![=] = meta.input.parse()?;
12922                out.many = Some(meta.input.parse()?);
12923                return Ok(());
12924            }
12925            if meta.path.is_ident("nested") {
12926                out.nested = true;
12927                // Optional strict flag inside parentheses:
12928                //   #[serializer(nested(strict))]
12929                if meta.input.peek(syn::token::Paren) {
12930                    meta.parse_nested_meta(|inner| {
12931                        if inner.path.is_ident("strict") {
12932                            out.nested_strict = true;
12933                            return Ok(());
12934                        }
12935                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12936                    })?;
12937                }
12938                return Ok(());
12939            }
12940            if meta.path.is_ident("slug") {
12941                let s: LitStr = meta.value()?.parse()?;
12942                out.slug = Some(s.value());
12943                return Ok(());
12944            }
12945            if meta.path.is_ident("max_length") {
12946                let lit: syn::LitInt = meta.value()?.parse()?;
12947                out.max_length = Some(lit.base10_parse::<u64>()?);
12948                return Ok(());
12949            }
12950            if meta.path.is_ident("min_length") {
12951                let lit: syn::LitInt = meta.value()?.parse()?;
12952                out.min_length = Some(lit.base10_parse::<u64>()?);
12953                return Ok(());
12954            }
12955            if meta.path.is_ident("min") {
12956                let lit: syn::LitInt = meta.value()?.parse()?;
12957                out.min = Some(lit.base10_parse::<i64>()?);
12958                return Ok(());
12959            }
12960            if meta.path.is_ident("max") {
12961                let lit: syn::LitInt = meta.value()?.parse()?;
12962                out.max = Some(lit.base10_parse::<i64>()?);
12963                return Ok(());
12964            }
12965            Err(meta.error(
12966                "unknown serializer field attribute (supported: \
12967                 `read_only`, `write_only`, `source`, `skip`, `method`, \
12968                 `validate`, `nested`, `many`, `slug`, `max_length`, \
12969                 `min_length`, `min`, `max`)",
12970            ))
12971        })?;
12972    }
12973    // Validate: read_only + write_only is nonsensical
12974    if out.read_only && out.write_only {
12975        return Err(syn::Error::new_spanned(
12976            field,
12977            "a field cannot be both `read_only` and `write_only`",
12978        ));
12979    }
12980    if out.method.is_some() && out.source.is_some() {
12981        return Err(syn::Error::new_spanned(
12982            field,
12983            "`method` and `source` are mutually exclusive — `method` computes \
12984             the value from a method, `source` reads it from a different model field",
12985        ));
12986    }
12987    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12988        return Err(syn::Error::new_spanned(
12989            field,
12990            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12991             — pick one strategy for populating the field",
12992        ));
12993    }
12994    Ok(out)
12995}
12996
12997fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12998    let root = rustango_root();
12999    let struct_name = &input.ident;
13000    let struct_name_lit = struct_name.to_string();
13001
13002    let Data::Struct(data) = &input.data else {
13003        return Err(syn::Error::new_spanned(
13004            struct_name,
13005            "Serializer can only be derived on structs",
13006        ));
13007    };
13008    let Fields::Named(named) = &data.fields else {
13009        return Err(syn::Error::new_spanned(
13010            struct_name,
13011            "Serializer requires a struct with named fields",
13012        ));
13013    };
13014
13015    let container = parse_serializer_container_attrs(input)?;
13016    let model_path = &container.model;
13017
13018    // Classify each field. `ty` is only consumed by the
13019    // `#[cfg(feature = "openapi")]` block below, but we always
13020    // capture it to keep the field-info build a single pass.
13021    #[allow(dead_code)]
13022    struct FieldInfo {
13023        ident: syn::Ident,
13024        ty: syn::Type,
13025        attrs: SerializerFieldAttrs,
13026    }
13027    let mut fields_info: Vec<FieldInfo> = Vec::new();
13028    for field in &named.named {
13029        let ident = field.ident.clone().expect("named field has ident");
13030        let attrs = parse_serializer_field_attrs(field)?;
13031        fields_info.push(FieldInfo {
13032            ident,
13033            ty: field.ty.clone(),
13034            attrs,
13035        });
13036    }
13037
13038    // Generate from_model body: struct literal with each field assigned.
13039    let from_model_fields = fields_info.iter().map(|fi| {
13040        let ident = &fi.ident;
13041        let ty = &fi.ty;
13042        if let Some(_inner) = &fi.attrs.many {
13043            // Many — collection field. Initialize empty; caller
13044            // populates via the macro-emitted set_<field> helper
13045            // after fetching the M2M children.
13046            quote! { #ident: ::std::vec::Vec::new() }
13047        } else if let Some(method) = &fi.attrs.method {
13048            // SerializerMethodField: call Self::<method>(&model) to
13049            // compute the value. Method signature must be
13050            // `fn <method>(model: &T) -> <field type>`.
13051            let method_ident = syn::Ident::new(method, ident.span());
13052            quote! { #ident: Self::#method_ident(model) }
13053        } else if let Some(slug_field) = &fi.attrs.slug {
13054            // v0.44 — SlugRelatedField. Source defaults to the field
13055            // name on this struct; override via `source = "..."`. The
13056            // source field on the model is expected to be a
13057            // `ForeignKey<T>`; the slug field on the parent is named
13058            // by the attribute value. When the FK is unloaded the
13059            // field falls back to `Default::default()` — same
13060            // graceful-degrade contract as `nested`.
13061            let src_name = fi
13062                .attrs
13063                .source
13064                .as_deref()
13065                .unwrap_or(&fi.ident.to_string())
13066                .to_owned();
13067            let src_ident = syn::Ident::new(&src_name, ident.span());
13068            let slug_ident = syn::Ident::new(slug_field, ident.span());
13069            quote! {
13070                #ident: match model.#src_ident.value() {
13071                    ::core::option::Option::Some(__loaded) =>
13072                        ::core::clone::Clone::clone(&__loaded.#slug_ident),
13073                    ::core::option::Option::None =>
13074                        ::core::default::Default::default(),
13075                }
13076            }
13077        } else if fi.attrs.nested {
13078            // Nested serializer. Source defaults to the field name on
13079            // this struct; override via `source = "..."`. The source
13080            // field on the model is expected to be a `ForeignKey<T>`
13081            // whose `.value()` returns `Option<&T>` after lazy-load.
13082            //
13083            // Behavior matrix (tweakable per-field):
13084            //   * FK loaded   → nested object materializes via
13085            //                   ChildSerializer::from_model(parent).
13086            //   * FK unloaded → fall back to ChildSerializer::default()
13087            //                   (so prod doesn't crash on a missing
13088            //                   prefetch — just renders a blank nested
13089            //                   object). Add `#[serializer(nested,
13090            //                   strict)]` to keep the v0.18.1
13091            //                   panic-on-unloaded behavior for tests
13092            //                   that want hard guardrails.
13093            let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
13094            let src_ident = syn::Ident::new(&src_name, ident.span());
13095            if fi.attrs.nested_strict {
13096                let panic_msg = format!(
13097                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13098                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13099                );
13100                quote! {
13101                    #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13102                        model.#src_ident.value().expect(#panic_msg),
13103                    )
13104                }
13105            } else {
13106                quote! {
13107                    #ident: match model.#src_ident.value() {
13108                        ::core::option::Option::Some(__loaded) =>
13109                            <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13110                        ::core::option::Option::None =>
13111                            ::core::default::Default::default(),
13112                    }
13113                }
13114            }
13115        } else if fi.attrs.write_only || fi.attrs.skip {
13116            // Not read from model — use default
13117            quote! { #ident: ::core::default::Default::default() }
13118        } else if let Some(src) = &fi.attrs.source {
13119            let src_ident = syn::Ident::new(src, ident.span());
13120            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13121        } else {
13122            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13123        }
13124    });
13125
13126    // is_writable predicate (also used by writable_lits /
13127    // writable_source_lits below).
13128    let is_writable = |fi: &&FieldInfo| {
13129        !fi.attrs.read_only
13130            && !fi.attrs.skip
13131            && fi.attrs.method.is_none()
13132            && !fi.attrs.nested
13133            && fi.attrs.many.is_none()
13134            && fi.attrs.slug.is_none()
13135    };
13136
13137    // Declarative field constraints: one block
13138    // per writable field, run inside `validate()`. Each resolves its
13139    // bounds as the serializer attr when given (`#[serializer(max_length
13140    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13141    // `max` / `choices`), then dispatches on the field's JSON value via
13142    // `forms::validators::check_value` (string → length/choices, integer
13143    // → min/max).
13144    let opt_usize = |v: Option<u64>| match v {
13145        Some(n) => {
13146            let n = n as usize;
13147            quote!(::core::option::Option::Some(#n))
13148        }
13149        None => quote!(::core::option::Option::None),
13150    };
13151    let opt_i64 = |v: Option<i64>| match v {
13152        Some(n) => quote!(::core::option::Option::Some(#n)),
13153        None => quote!(::core::option::Option::None),
13154    };
13155    let constraint_blocks: Vec<_> = fields_info
13156        .iter()
13157        .filter(is_writable)
13158        .map(|fi| {
13159            let ident = &fi.ident;
13160            let fname = ident.to_string();
13161            let mname = fi
13162                .attrs
13163                .source
13164                .clone()
13165                .unwrap_or_else(|| fi.ident.to_string());
13166            let attr_max_len = opt_usize(fi.attrs.max_length);
13167            let attr_min_len = opt_usize(fi.attrs.min_length);
13168            let attr_min = opt_i64(fi.attrs.min);
13169            let attr_max = opt_i64(fi.attrs.max);
13170            quote! {
13171                {
13172                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13173                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13174                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13175                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13176                    let __min: ::core::option::Option<i64> =
13177                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13178                    let __max: ::core::option::Option<i64> =
13179                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13180                    let __choices = __sf.and_then(|__f| __f.choices);
13181                    let __v = #root::__serde_json::to_value(&self.#ident)
13182                        .unwrap_or(#root::__serde_json::Value::Null);
13183                    #root::forms::validators::check_value(
13184                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13185                        &mut __errors,
13186                    );
13187                }
13188            }
13189        })
13190        .collect();
13191    let has_constraints = !constraint_blocks.is_empty();
13192
13193    // Per-field validators. Emit a
13194    // `validate(&self)` method that runs each user-defined validator
13195    // and aggregates errors into `FormErrors`.
13196    let validator_calls: Vec<_> = fields_info
13197        .iter()
13198        .filter_map(|fi| {
13199            let ident = &fi.ident;
13200            let name_lit = ident.to_string();
13201            let method = fi.attrs.validate.as_ref()?;
13202            let method_ident = syn::Ident::new(method, ident.span());
13203            Some(quote! {
13204                if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13205                    __errors.add(#name_lit.to_owned(), __e);
13206                }
13207            })
13208        })
13209        .collect();
13210    // #436 — cross-field `validate(&self)`. If the
13211    // container declared `#[serializer(validate = "fn_name")]`,
13212    // the macro-generated `validate(&self)` runs every per-field
13213    // validator first, then calls the user's cross-field method,
13214    // merging its `FormErrors` into the per-field errors. Either
13215    // alone is enough to emit the wrapper.
13216    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13217        quote! {
13218            // Merge cross-field errors into the per-field bucket so
13219            // a single .validate() call surfaces both layers.
13220            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13221                __errors.merge(__cross);
13222            }
13223        }
13224    });
13225    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13226    // Anything to run? Declarative constraints (for any writable field) +
13227    // per-field validators + cross-field hook.
13228    let has_run_validations = has_validators || has_constraints;
13229    // Shared body: declarative field constraints first (length / range /
13230    // choices, serializer-attr-or-model), then per-field validators, then
13231    // the cross-field hook.
13232    let validate_body = quote! {
13233        let mut __errors = #root::forms::FormErrors::default();
13234        #( #constraint_blocks )*
13235        #( #validator_calls )*
13236        #cross_validate_call
13237        if __errors.is_empty() {
13238            ::core::result::Result::Ok(())
13239        } else {
13240            ::core::result::Result::Err(__errors)
13241        }
13242    };
13243    // Inherent `validate(&self)` — kept for back-compat with direct
13244    // `serializer.validate()` calls that don't import `ModelSerializer`.
13245    // Emitted only when the serializer declares per-field/cross-field
13246    // validators (unchanged rule) so it never collides with a
13247    // hand-written inherent `validate`.
13248    let validate_method = if has_validators {
13249        quote! {
13250            impl #struct_name {
13251                /// Run the declarative field constraints, every
13252                /// `#[serializer(validate = "...")]` per-field validator,
13253                /// and (when declared) the container-level cross-field
13254                /// validator. Aggregates errors into `FormErrors` keyed by
13255                /// the field name. Returns `Ok(())` when all pass.
13256                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13257                    #validate_body
13258                }
13259            }
13260        }
13261    } else {
13262        quote! {}
13263    };
13264    // Trait-method override so the type-erased ViewSet write path can
13265    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13266    // there's anything to run (declarative constraints inherited from the
13267    // model count); otherwise the trait's default no-op is used.
13268    let trait_validate_override = if has_run_validations {
13269        quote! {
13270            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13271                #validate_body
13272            }
13273        }
13274    } else {
13275        quote! {}
13276    };
13277
13278    // For every `#[serializer(many = S)]` field, emit a
13279    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13280    // helper that maps the parents through `S::from_model`.
13281    let many_setters: Vec<_> = fields_info
13282        .iter()
13283        .filter_map(|fi| {
13284            let many_ty = fi.attrs.many.as_ref()?;
13285            let ident = &fi.ident;
13286            let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
13287            Some(quote! {
13288                /// Populate this `many` field by mapping each parent model
13289                /// through the inner serializer's `from_model`. Call after
13290                /// fetching the M2M / one-to-many children since
13291                /// `from_model` itself can't await an SQL query.
13292                pub fn #setter(
13293                    &mut self,
13294                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13295                ) -> &mut Self {
13296                    self.#ident = models.iter()
13297                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13298                        .collect();
13299                    self
13300                }
13301            })
13302        })
13303        .collect();
13304    let many_setters_impl = if many_setters.is_empty() {
13305        quote! {}
13306    } else {
13307        quote! {
13308            impl #struct_name {
13309                #( #many_setters )*
13310            }
13311        }
13312    };
13313
13314    // Generate custom Serialize: skip write_only fields
13315    let output_fields: Vec<_> = fields_info
13316        .iter()
13317        .filter(|fi| !fi.attrs.write_only)
13318        .collect();
13319    let output_field_count = output_fields.len();
13320    let serialize_fields = output_fields.iter().map(|fi| {
13321        let ident = &fi.ident;
13322        let name_lit = ident.to_string();
13323        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13324    });
13325
13326    // writable_fields: normal + write_only.
13327    // Exclude:
13328    //   - `read_only` — server-computed.
13329    //   - `skip` — caller sets manually post-from_model.
13330    //   - `method` — computed from a Self::fn(&model) call; accepting
13331    //     it on write is meaningless.
13332    //   - `nested` / `many` — populated from related-model data, not
13333    //     from a field on the wire body.
13334    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13335    // `many` in `writable_fields()`, which made the ViewSet write
13336    // path accept those fields from the JSON body and try to bind
13337    // them to the SQL UPDATE — a silent no-op at best, a type
13338    // mismatch at worst.
13339    // (`is_writable` is defined above, near the constraint codegen.)
13340    let writable_lits: Vec<_> = fields_info
13341        .iter()
13342        .filter(is_writable)
13343        .map(|fi| fi.ident.to_string())
13344        .collect();
13345
13346    // `writable_source_fields`: the MODEL field names of writable
13347    // serializer fields (source-resolved). The ViewSet write path skips
13348    // every model column NOT in this set, so `read_only` / `method` /
13349    // computed fields a client posts are ignored instead of written.
13350    let writable_source_lits: Vec<String> = fields_info
13351        .iter()
13352        .filter(is_writable)
13353        .map(|fi| {
13354            fi.attrs
13355                .source
13356                .clone()
13357                .unwrap_or_else(|| fi.ident.to_string())
13358        })
13359        .collect();
13360
13361    // `from_writable_json`: build a partial instance for input
13362    // validation. Writable fields are parsed from the JSON body (keyed
13363    // by serializer field name); every other field defaults. Per-field
13364    // type errors collect into `FormErrors` keyed by the field name.
13365    // Construction is field-by-field (not `Self::default()`) so it works
13366    // for serializers regardless of a struct-level `Default`.
13367    let from_writable_json_inits: Vec<_> = fields_info
13368        .iter()
13369        .map(|fi| {
13370            let ident = &fi.ident;
13371            let fname = ident.to_string();
13372            let ty = &fi.ty;
13373            if is_writable(&fi) {
13374                quote! {
13375                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13376                        ::core::option::Option::Some(__v) => {
13377                            match #root::__serde_json::from_value::<#ty>(
13378                                ::core::clone::Clone::clone(__v),
13379                            ) {
13380                                ::core::result::Result::Ok(__x) => __x,
13381                                ::core::result::Result::Err(__e) => {
13382                                    __errors.add(#fname.to_owned(), __e.to_string());
13383                                    ::core::default::Default::default()
13384                                }
13385                            }
13386                        }
13387                        ::core::option::Option::None => ::core::default::Default::default(),
13388                    }
13389                }
13390            } else {
13391                quote! { #ident: ::core::default::Default::default() }
13392            }
13393        })
13394        .collect();
13395
13396    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13397    // Only includes fields shown in JSON output (skips write_only). For each
13398    // `Option<T>` field, omit from `required` and add `.nullable()`.
13399    let openapi_impl = {
13400        #[cfg(feature = "openapi")]
13401        {
13402            let property_calls = output_fields.iter().map(|fi| {
13403                let ident = &fi.ident;
13404                let name_lit = ident.to_string();
13405                let ty = &fi.ty;
13406                let nullable_call = if is_option(ty) {
13407                    quote! { .nullable() }
13408                } else {
13409                    quote! {}
13410                };
13411                quote! {
13412                    .property(
13413                        #name_lit,
13414                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13415                            #nullable_call,
13416                    )
13417                }
13418            });
13419            let required_lits: Vec<_> = output_fields
13420                .iter()
13421                .filter(|fi| !is_option(&fi.ty))
13422                .map(|fi| fi.ident.to_string())
13423                .collect();
13424            quote! {
13425                impl #root::openapi::OpenApiSchema for #struct_name {
13426                    fn openapi_schema() -> #root::openapi::Schema {
13427                        #root::openapi::Schema::object()
13428                            #( #property_calls )*
13429                            .required([ #( #required_lits ),* ])
13430                    }
13431                }
13432            }
13433        }
13434        #[cfg(not(feature = "openapi"))]
13435        {
13436            quote! {}
13437        }
13438    };
13439
13440    Ok(quote! {
13441        impl #root::serializer::ModelSerializer for #struct_name {
13442            type Model = #model_path;
13443
13444            fn from_model(model: &Self::Model) -> Self {
13445                Self {
13446                    #( #from_model_fields ),*
13447                }
13448            }
13449
13450            fn writable_fields() -> &'static [&'static str] {
13451                &[ #( #writable_lits ),* ]
13452            }
13453
13454            fn writable_source_fields() -> &'static [&'static str] {
13455                &[ #( #writable_source_lits ),* ]
13456            }
13457
13458            fn from_writable_json(
13459                __body: &#root::__serde_json::Value,
13460            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13461                let mut __errors = #root::forms::FormErrors::default();
13462                let __obj = __body.as_object();
13463                let __out = Self {
13464                    #( #from_writable_json_inits ),*
13465                };
13466                if __errors.is_empty() {
13467                    ::core::result::Result::Ok(__out)
13468                } else {
13469                    ::core::result::Result::Err(__errors)
13470                }
13471            }
13472
13473            #trait_validate_override
13474        }
13475
13476        impl #root::__serde::Serialize for #struct_name {
13477            fn serialize<S>(&self, serializer: S)
13478                -> ::core::result::Result<S::Ok, S::Error>
13479            where
13480                S: #root::__serde::Serializer,
13481            {
13482                use #root::__serde::ser::SerializeStruct;
13483                let mut __state = serializer.serialize_struct(
13484                    #struct_name_lit,
13485                    #output_field_count,
13486                )?;
13487                #( #serialize_fields )*
13488                __state.end()
13489            }
13490        }
13491
13492        #openapi_impl
13493
13494        #validate_method
13495
13496        #many_setters_impl
13497    })
13498}
13499
13500/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13501/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13502/// when the feature is off.
13503#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13504fn is_option(ty: &syn::Type) -> bool {
13505    if let syn::Type::Path(p) = ty {
13506        if let Some(last) = p.path.segments.last() {
13507            return last.ident == "Option";
13508        }
13509    }
13510    false
13511}
13512
13513#[cfg(test)]
13514mod main_attr_tests {
13515    use super::*;
13516
13517    fn expand(args: &str) -> String {
13518        let body = quote! { async fn main() { run().await } };
13519        expand_main(args.parse().unwrap(), body)
13520            .expect("expansion failed")
13521            .to_string()
13522    }
13523
13524    /// Asserts on the expansion rather than on `parse_logging`,
13525    /// because the defect was a subscriber reaching the output, not
13526    /// an argument being misread (#1465).
13527    #[test]
13528    fn logging_false_emits_no_subscriber() {
13529        let out = expand("logging = false");
13530        assert!(
13531            !out.contains("tracing_subscriber"),
13532            "`logging = false` must not install a subscriber:\n{out}",
13533        );
13534        assert!(
13535            out.contains("block_on"),
13536            "the runtime must still be built:\n{out}",
13537        );
13538    }
13539
13540    #[test]
13541    fn default_still_installs_a_subscriber() {
13542        // The control. Without it, an `expand_main` that dropped the
13543        // prologue unconditionally would pass the test above while
13544        // leaving every default project with no logging at all.
13545        let out = expand("");
13546        assert!(
13547            out.contains("tracing_subscriber"),
13548            "the default entrypoint must still install one:\n{out}",
13549        );
13550    }
13551
13552    #[test]
13553    fn logging_false_composes_with_flavor() {
13554        let out = expand("flavor = \"current_thread\", logging = false");
13555        assert!(!out.contains("tracing_subscriber"), "{out}");
13556        assert!(out.contains("new_current_thread"), "{out}");
13557    }
13558
13559    #[test]
13560    fn logging_true_is_the_default_shape() {
13561        // `logging = true` is accepted and means the default, so the
13562        // match must not fire on the substring `logging`.
13563        assert!(parse_logging(&"logging = true".parse().unwrap()));
13564        assert!(!parse_logging(&"logging=false".parse().unwrap()));
13565    }
13566}