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, rgo))]
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"` — *required*. The only scalar fields list/retrieve
82///   JSON renders and create/update accept; other body keys are ignored.
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; requests are capped
87///   at `ViewSet::max_page_size`, default 100).
88/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
89/// * `allow_anonymous` — flag; write actions without codenames are
90///   intended, so the mount warning is skipped.
91/// * `serializer = SomeSerializer` — render list / retrieve / create
92///   responses through a `#[derive(Serializer)]` type instead of the
93///   default field-level projection (`read_only` / `source` / `method`
94///   / `write_only` overrides apply). Tri-dialect. Requires the
95///   `serializer` feature.
96/// * `permissions(list = "...", retrieve = "...", create = "...",
97///   update = "...", destroy = "...")` — codenames required per action.
98#[proc_macro_derive(ViewSet, attributes(viewset))]
99pub fn derive_viewset(input: TokenStream) -> TokenStream {
100    let input = parse_macro_input!(input as DeriveInput);
101    expand_viewset(&input)
102        .unwrap_or_else(syn::Error::into_compile_error)
103        .into()
104}
105
106/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
107/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
108/// that walks every named field and:
109///
110/// * Parses the string value into the field's Rust type (`String`,
111///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
112///   nullable case).
113/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
114///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
115///   validators in declaration order, returning `FormError::Parse`
116///   on the first failure.
117///
118/// Example:
119///
120/// ```ignore
121/// #[derive(Form)]
122/// pub struct CreateItemForm {
123///     #[form(min_length = 1, max_length = 64)]
124///     pub name: String,
125///     #[form(min = 0, max = 150)]
126///     pub age: i32,
127///     pub active: bool,
128///     pub email: Option<String>,
129/// }
130///
131/// let parsed = CreateItemForm::parse(&form_map)?;
132/// ```
133#[proc_macro_derive(Form, attributes(form))]
134pub fn derive_form(input: TokenStream) -> TokenStream {
135    let input = parse_macro_input!(input as DeriveInput);
136    expand_form(&input)
137        .unwrap_or_else(syn::Error::into_compile_error)
138        .into()
139}
140
141/// Derive `rustango::serializer::ModelSerializer` for a struct.
142/// (intra-doc link disabled — the macro crate doesn't depend on
143/// `rustango` itself, so rustdoc can't resolve the path.)
144///
145/// # Container attribute (required)
146/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
147///
148/// # Field attributes
149/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
150/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
151/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
152/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
153/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
154/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
155/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
156/// - `#[serializer(slug = "name")]` — render a relation (`ForeignKey<T>` or `Option<ForeignKey<T>>`) as one of its fields: clones the loaded parent's `name`; excluded from `writable_fields()` (v0.44)
157/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
158/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
159///   write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
160///   no attr is given, override it when given (`min_length` is serializer-only)
161///
162/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
163/// It honours `#[serde(rename, rename_all, skip, skip_serializing, skip_serializing_if,
164/// skip_deserializing)]`; other `serde` keys are ignored.
165#[proc_macro_derive(Serializer, attributes(serializer, serde))]
166pub fn derive_serializer(input: TokenStream) -> TokenStream {
167    let input = parse_macro_input!(input as DeriveInput);
168    expand_serializer(&input)
169        .unwrap_or_else(syn::Error::into_compile_error)
170        .into()
171}
172
173/// Bake every `*.json` migration file in a directory into the binary
174/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
175/// of `(name, json_content)` pairs, lex-sorted by file stem.
176///
177/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
178/// behaviour as `migrate(pool, dir)` but with no filesystem access.
179/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
180/// (i.e. the crate that invokes the macro). Default is
181/// `"./migrations"` if no argument is supplied.
182///
183/// ```ignore
184/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
185/// // or:
186/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
187///
188/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
189/// ```
190///
191/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
192/// file's `name` field must equal its file stem, every `prev`
193/// reference must point to another migration in the same directory,
194/// and the JSON must parse. A broken chain — orphan `prev`, missing
195/// predecessor, malformed file — fails at macro-expansion time with
196/// a clear `compile_error!`. *No other Rust web framework
197/// validates migration chains at compile time*: Cot's migrations are
198/// imperative Rust code (no static chain), Loco's are SeaORM
199/// up/down (same), Rwf's are raw SQL (no chain at all).
200///
201/// Each migration is included via `include_str!` so cargo's rebuild
202/// detection picks up file *content* changes. Cargo does not watch
203/// directory listings, so a new migration file is missed until the
204/// crate rebuilds. Add a `build.rs` that tracks the directory:
205///
206/// ```ignore
207/// fn main() { println!("cargo:rerun-if-changed=migrations"); }
208/// ```
209///
210/// A missing directory embeds nothing and emits a deprecation warning.
211#[proc_macro]
212pub fn embed_migrations(input: TokenStream) -> TokenStream {
213    expand_embed_migrations(input.into())
214        .unwrap_or_else(syn::Error::into_compile_error)
215        .into()
216}
217
218/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
219/// compile time against typed columns. Issue #269 / T1.7.
220///
221/// Each invocation lowers to the equivalent typed-column method call:
222///
223/// ```ignore
224/// // These expand identically:
225/// Q!(User.email__icontains = "alice")
226/// User::email.ilike("%alice%")
227/// ```
228///
229/// Field-name typos fail the build (the macro emits `User::no_such_field`
230/// which doesn't exist) — the headline ergonomic win of this slice over
231/// filters keyed by a plain string.
232///
233/// # Supported lookup suffixes
234///
235/// * bare `=` / `__exact` → `.eq(value)`
236/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
237/// * `__ne` → `.ne(value)`
238/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
239/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
240/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
241/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
242/// * `__in` → `.is_in(iterable)`
243/// * `__not_in` → `.not_in(iterable)`
244/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
245/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
246/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
247///
248/// Unknown suffixes fail the build with a `compile_error!` pointing at
249/// the lookup token.
250///
251/// # Combine
252///
253/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
254/// `.and()` / `.or()` / `.not()` methods:
255///
256/// ```ignore
257/// User::objects()
258///     .where_(
259///         Q!(User.active = true)
260///             .and(Q!(User.email__icontains = "alice"))
261///     )
262///     .fetch(&pool).await?;
263/// ```
264///
265/// All emitted code routes through existing per-dialect writers — no new
266/// SQL emission machinery. Tri-dialect support is inherent.
267#[allow(non_snake_case)]
268#[proc_macro]
269pub fn Q(input: TokenStream) -> TokenStream {
270    expand_q(input.into())
271        .unwrap_or_else(syn::Error::into_compile_error)
272        .into()
273}
274
275/// `#[rustango::main]` — the runserver entrypoint. Wraps
276/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
277/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
278/// functions are zero-boilerplate. `RUST_LOG` and
279/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
280/// environment, else from `./.env` (only those keys are read):
281///
282/// ```ignore
283/// #[rustango::main]
284/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
285///     rustango::server::Builder::from_env().await?
286///         .migrate("migrations").await?
287///         .api(my_app::urls::api())
288///         .seed_with(my_app::seed::run).await?
289///         .serve("0.0.0.0:8080").await
290/// }
291/// ```
292///
293/// Optional `flavor = "current_thread"` passes through to
294/// `#[tokio::main]`; default is the multi-threaded runtime.
295///
296/// `logging = false` suppresses the default subscriber, for apps
297/// that install their own. Needed whenever the body reaches
298/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
299/// macro installs gets there first, and `tracing` keeps the first
300/// one, so the `[logging]` section would otherwise be read and
301/// discarded (#1465).
302///
303/// ```ignore
304/// #[rustango::main(logging = false)]
305/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
306///     rustango::manage::Cli::new()
307///         .with_settings_from_env()
308///         .with_logging()          // now the first installer
309///         .run().await
310/// }
311/// ```
312///
313/// Pulls `tracing-subscriber` into the rustango crate behind the
314/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
315/// out get plain `#[tokio::main]` ergonomics without the dependency.
316#[proc_macro_attribute]
317pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
318    expand_main(args.into(), item.into())
319        .unwrap_or_else(syn::Error::into_compile_error)
320        .into()
321}
322
323fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
324    let mut input: syn::ItemFn = syn::parse2(item)?;
325    if input.sig.asyncness.is_none() {
326        return Err(syn::Error::new(
327            input.sig.ident.span(),
328            "`#[rustango::main]` must wrap an `async fn`",
329        ));
330    }
331
332    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
333    // to `#[tokio::main]`. Tokio's proc-macro internally emits
334    // `::tokio::*` paths that resolve against the user crate's deps,
335    // so calling it through the rustango re-export still requires the
336    // user to add tokio to their own Cargo.toml. Building the
337    // runtime ourselves keeps the dep transitive through the
338    // `runtime` feature on rustango.
339    //
340    // `flavor` and `logging`; anything else is a compile error.
341    let root = rustango_root();
342    let MainArgs { flavor, logging } = parse_main_args(&args)?;
343    let builder_call = match flavor {
344        Flavor::CurrentThread => quote! {
345            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
346        },
347        Flavor::MultiThread => quote! {
348            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
349        },
350    };
351
352    // Detach the user body and rewrite `main` as a sync fn that
353    // builds the runtime and blocks on the async body.
354    let user_body = input.block.clone();
355    input.sig.asyncness = None;
356
357    // `logging = false` drops the block entirely, so a later
358    // `Cli::with_logging()` is the first installer and its
359    // `[logging]` settings actually take effect (#1465).
360    let logging_prologue = if logging {
361        quote! {
362            // `Setup`'s defaults, not a copy of them: filter, format and
363            // the `Color::Auto` rule live in one place (#1493).
364            // `logging` and this macro share the `runtime` gate.
365            // The real `RUST_LOG` still wins over `./.env`'s (#2204).
366            let _ = #root::logging::Setup::new()
367                .with_default_env_filter(#root::__private_runtime::main_default_filter())
368                .with_format(#root::__private_runtime::main_format())
369                .install();
370        }
371    } else {
372        quote! {}
373    };
374
375    input.block = syn::parse2(quote! {{
376        { #logging_prologue }
377        let __rt = #builder_call
378            .enable_all()
379            .build()
380            .expect("failed to build tokio runtime");
381        __rt.block_on(async move #user_body)
382    }})?;
383
384    Ok(quote! {
385        #input
386    })
387}
388
389enum Flavor {
390    MultiThread,
391    CurrentThread,
392}
393
394/// What `#[rustango::main]` was asked for.
395struct MainArgs {
396    flavor: Flavor,
397    logging: bool,
398}
399
400const MAIN_ARGS_HELP: &str =
401    "supported: `flavor = \"current_thread\" | \"multi_thread\"`, `logging = true | false`";
402
403/// Parse `#[rustango::main(...)]`; an unknown or misspelt argument is an error.
404fn parse_main_args(args: &TokenStream2) -> syn::Result<MainArgs> {
405    use syn::parse::Parser as _;
406    let metas = syn::punctuated::Punctuated::<syn::MetaNameValue, syn::Token![,]>::parse_terminated
407        .parse2(args.clone())
408        .map_err(|e| {
409            syn::Error::new(
410                e.span(),
411                format!("#[rustango::main]: {e}; {MAIN_ARGS_HELP}"),
412            )
413        })?;
414    let mut out = MainArgs {
415        flavor: Flavor::MultiThread,
416        logging: true,
417    };
418    for meta in &metas {
419        let lit = match &meta.value {
420            syn::Expr::Lit(l) => Some(&l.lit),
421            _ => None,
422        };
423        let name = meta.path.get_ident().map(ToString::to_string);
424        match (name.as_deref(), lit) {
425            (Some("flavor"), Some(syn::Lit::Str(s))) if s.value() == "current_thread" => {
426                out.flavor = Flavor::CurrentThread;
427            }
428            (Some("flavor"), Some(syn::Lit::Str(s))) if s.value() == "multi_thread" => {
429                out.flavor = Flavor::MultiThread;
430            }
431            (Some("logging"), Some(syn::Lit::Bool(b))) => out.logging = b.value,
432            _ => {
433                return Err(syn::Error::new(
434                    meta.span(),
435                    format!("#[rustango::main]: unsupported argument; {MAIN_ARGS_HELP}"),
436                ))
437            }
438        }
439    }
440    Ok(out)
441}
442
443/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
444struct QInput {
445    base_path: syn::Path,
446    field: syn::Ident,
447    value: syn::Expr,
448}
449
450impl syn::parse::Parse for QInput {
451    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
452        let base_path: syn::Path = input.parse()?;
453        input.parse::<syn::Token![.]>()?;
454        let field: syn::Ident = input.parse()?;
455        input.parse::<syn::Token![=]>()?;
456        let value: syn::Expr = input.parse()?;
457        Ok(QInput {
458            base_path,
459            field,
460            value,
461        })
462    }
463}
464
465fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
466    let q: QInput = syn::parse2(input)?;
467    let root = rustango_root();
468    let field_str = ident_name(&q.field);
469    let field_span = q.field.span();
470    let (base, suffix) = match field_str.find("__") {
471        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
472        None => (field_str.as_str(), ""),
473    };
474    if base.is_empty() {
475        return Err(syn::Error::new(
476            field_span,
477            "Q!(): field name is empty before `__` suffix",
478        ));
479    }
480    let base_ident = ident_from_name(base, field_span);
481    let value = &q.value;
482    let path = &q.base_path;
483
484    // Most suffixes map directly to a Column method with the value
485    // forwarded unchanged. Some need value-shape massaging (wildcards
486    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
487    // ISNULL). Unknown suffixes fail the build.
488    let expanded = match suffix {
489        "" | "exact" => quote! {
490            #root::core::Column::eq(#path::#base_ident, #value)
491        },
492        "ne" => quote! {
493            #root::core::Column::ne(#path::#base_ident, #value)
494        },
495        "gt" => quote! {
496            #root::core::Column::gt(#path::#base_ident, #value)
497        },
498        "gte" => quote! {
499            #root::core::Column::gte(#path::#base_ident, #value)
500        },
501        "lt" => quote! {
502            #root::core::Column::lt(#path::#base_ident, #value)
503        },
504        "lte" => quote! {
505            #root::core::Column::lte(#path::#base_ident, #value)
506        },
507        // The escaped-LIKE lookups delegate to the typed Column
508        // methods, which escape the value and pair it with the
509        // *Escaped ops so `%` / `_` match literally on every dialect
510        // (#1257) — the macro no longer owns any pattern wrapping.
511        "iexact" => quote! {
512            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
513        },
514        "contains" => quote! {
515            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
516        },
517        "icontains" => quote! {
518            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
519        },
520        "startswith" => quote! {
521            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
522        },
523        "istartswith" => quote! {
524            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
525        },
526        "endswith" => quote! {
527            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
528        },
529        "iendswith" => quote! {
530            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
531        },
532        "in" => quote! {
533            #root::core::Column::is_in(#path::#base_ident, #value)
534        },
535        "not_in" => quote! {
536            #root::core::Column::not_in(#path::#base_ident, #value)
537        },
538        "isnull" => {
539            // Must be a bool literal at macro time so we can route to
540            // is_null vs is_not_null without a runtime branch.
541            let b = match value {
542                syn::Expr::Lit(syn::ExprLit {
543                    lit: syn::Lit::Bool(b),
544                    ..
545                }) => b.value(),
546                _ => {
547                    return Err(syn::Error::new_spanned(
548                        value,
549                        "Q!(): `__isnull` requires a `true` or `false` literal",
550                    ));
551                }
552            };
553            if b {
554                quote! { #root::core::Column::is_null(#path::#base_ident) }
555            } else {
556                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
557            }
558        }
559        "between" => {
560            // Accept a tuple literal `(lo, hi)`.
561            let tuple = match value {
562                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
563                _ => {
564                    return Err(syn::Error::new_spanned(
565                        value,
566                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
567                    ));
568                }
569            };
570            let lo = &tuple.elems[0];
571            let hi = &tuple.elems[1];
572            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
573        }
574        "regex" => quote! {
575            #root::core::Column::regex(#path::#base_ident, #value)
576        },
577        "iregex" => quote! {
578            #root::core::Column::iregex(#path::#base_ident, #value)
579        },
580        _ => {
581            return Err(syn::Error::new(
582                field_span,
583                format!(
584                    "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",
585                    suffix
586                ),
587            ));
588        }
589    };
590    Ok(expanded)
591}
592
593fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
594    // Default to "./migrations" if invoked without args.
595    let path_str = if input.is_empty() {
596        "./migrations".to_string()
597    } else {
598        let lit: LitStr = syn::parse2(input)?;
599        lit.value()
600    };
601
602    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
603        syn::Error::new(
604            proc_macro2::Span::call_site(),
605            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
606        )
607    })?;
608    let abs = std::path::Path::new(&manifest).join(&path_str);
609
610    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
611    if abs.is_dir() {
612        let read = std::fs::read_dir(&abs).map_err(|e| {
613            syn::Error::new(
614                proc_macro2::Span::call_site(),
615                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
616            )
617        })?;
618        for entry in read.flatten() {
619            let path = entry.path();
620            if !path.is_file() {
621                continue;
622            }
623            if path.extension().and_then(|s| s.to_str()) != Some("json") {
624                continue;
625            }
626            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
627                continue;
628            };
629            entries.push((stem.to_owned(), path));
630        }
631    }
632    entries.sort_by(|a, b| a.0.cmp(&b.0));
633
634    // Compile-time chain validation: read each migration's JSON,
635    // pull `name` and `prev` (file-stem-keyed for the chain check),
636    // and verify every `prev` points to another migration in the
637    // slice. Mismatches between the file stem and the embedded
638    // `name` field — and broken `prev` chains — fail at MACRO
639    // EXPANSION time so a misshapen migration set never compiles.
640    //
641    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
642    // migrations + a Rust proc-macro that reads them is a combo no
643    // other Rust web framework matches (Cot's are imperative Rust
644    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
645    // a static chain to validate).
646    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
647    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
648    for (stem, path) in &entries {
649        let raw = std::fs::read_to_string(path).map_err(|e| {
650            syn::Error::new(
651                proc_macro2::Span::call_site(),
652                format!(
653                    "embed_migrations!: cannot read {} for chain validation: {e}",
654                    path.display()
655                ),
656            )
657        })?;
658        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
659            syn::Error::new(
660                proc_macro2::Span::call_site(),
661                format!(
662                    "embed_migrations!: {} is not valid JSON: {e}",
663                    path.display()
664                ),
665            )
666        })?;
667        let name = json
668            .get("name")
669            .and_then(|v| v.as_str())
670            .ok_or_else(|| {
671                syn::Error::new(
672                    proc_macro2::Span::call_site(),
673                    format!(
674                        "embed_migrations!: {} is missing the `name` field",
675                        path.display()
676                    ),
677                )
678            })?
679            .to_owned();
680        if name != *stem {
681            return Err(syn::Error::new(
682                proc_macro2::Span::call_site(),
683                format!(
684                    "embed_migrations!: file stem `{stem}` does not match the migration's \
685                     `name` field `{name}` — rename the file or fix the JSON",
686                ),
687            ));
688        }
689        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
690        chain_names.push(name.clone());
691        prev_refs.push((name, prev));
692    }
693
694    let name_set: std::collections::HashSet<&str> =
695        chain_names.iter().map(String::as_str).collect();
696    for (name, prev) in &prev_refs {
697        if let Some(p) = prev {
698            if !name_set.contains(p.as_str()) {
699                return Err(syn::Error::new(
700                    proc_macro2::Span::call_site(),
701                    format!(
702                        "embed_migrations!: broken migration chain — `{name}` declares \
703                         prev=`{p}` but no migration with that name exists in {}",
704                        abs.display()
705                    ),
706                ));
707            }
708        }
709    }
710
711    let pairs: Vec<TokenStream2> = entries
712        .iter()
713        .map(|(name, path)| {
714            let path_lit = path.display().to_string();
715            quote! { (#name, ::core::include_str!(#path_lit)) }
716        })
717        .collect();
718
719    // A wrong path used to embed nothing silently; a deprecation use is
720    // the only stable way for a proc macro to warn without breaking builds.
721    let missing_warning = if abs.is_dir() {
722        quote! {}
723    } else {
724        let note = format!(
725            "embed_migrations!: directory {} does not exist; no migrations are embedded",
726            abs.display()
727        );
728        quote! {
729            #[deprecated(note = #note)]
730            const __RUSTANGO_MIGRATIONS_DIR_MISSING: () = ();
731            #[allow(clippy::let_unit_value)]
732            let _ = __RUSTANGO_MIGRATIONS_DIR_MISSING;
733        }
734    };
735
736    Ok(quote! {
737        {
738            #missing_warning
739            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
740            __RUSTANGO_EMBEDDED
741        }
742    })
743}
744
745fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
746    let root = rustango_root();
747    let struct_name = &input.ident;
748
749    let Data::Struct(data) = &input.data else {
750        return Err(syn::Error::new_spanned(
751            struct_name,
752            "Model can only be derived on structs",
753        ));
754    };
755    let Fields::Named(named) = &data.fields else {
756        return Err(syn::Error::new_spanned(
757            struct_name,
758            "Model requires a struct with named fields",
759        ));
760    };
761
762    let mut container = parse_container_attrs(input)?;
763    let table = container
764        .table
765        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
766    let model_name = struct_name.to_string();
767
768    let collected = collect_fields(named, &table)?;
769    // An explicit `self_pk_column` must name a column of this model.
770    let explicit_pks = container
771        .reverse_has_relations
772        .iter()
773        .map(|r| (&r.self_pk_column, r.self_pk_span))
774        .chain(
775            container
776                .generic_has_relations
777                .iter()
778                .map(|r| (&r.self_pk_column, r.self_pk_span)),
779        );
780    for (column, span) in explicit_pks {
781        if let (Some(column), Some(span)) = (column, span) {
782            if !collected.column_entries.iter().any(|c| &c.column == column) {
783                return Err(syn::Error::new(
784                    span,
785                    format!("`self_pk_column = \"{column}\"` names no column of `{model_name}`"),
786                ));
787            }
788        }
789    }
790    // `reverse_has` / `generic_has` join on the PK column unless they name another.
791    if let Some((_, pk_column)) = &collected.primary_key {
792        for rel in &mut container.reverse_has_relations {
793            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
794        }
795        for rel in &mut container.generic_has_relations {
796            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
797        }
798    }
799
800    // Validate that #[rustango(display = "…")] names a real field.
801    if let Some((ref display, span)) = container.display {
802        if !collected.field_names.iter().any(|n| n == display) {
803            return Err(syn::Error::new(
804                span,
805                format!("`display = \"{display}\"` does not match any field on this struct"),
806            ));
807        }
808    }
809    let display = container.display.map(|(name, _)| name);
810    let app_label = container.app.clone();
811
812    // Validate admin field-name lists against declared field names.
813    // Note: `list_display` is intentionally NOT validated here. As of
814    // v0.32 it may also reference inventory-registered computed
815    // fields (via `register_admin_computed!`) whose existence the
816    // macro can't see at compile time — they're submitted from any
817    // crate that depends on rustango. The runtime list-view resolves
818    // unknown names against the inventory + silently drops the
819    // truly-bogus ones, which is the cheaper trade-off versus
820    // forcing a per-Model attr to opt out.
821    if let Some(admin) = &container.admin {
822        for (label, list) in [
823            ("search_fields", &admin.search_fields),
824            ("readonly_fields", &admin.readonly_fields),
825            ("list_filter", &admin.list_filter),
826        ] {
827            if let Some((names, span)) = list {
828                for name in names {
829                    if !collected.field_names.iter().any(|n| n == name) {
830                        return Err(syn::Error::new(
831                            *span,
832                            format!(
833                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
834                            ),
835                        ));
836                    }
837                }
838            }
839        }
840        if let Some((pairs, span)) = &admin.ordering {
841            for (name, _) in pairs {
842                if !collected.field_names.iter().any(|n| n == name) {
843                    return Err(syn::Error::new(
844                        *span,
845                        format!(
846                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
847                        ),
848                    ));
849                }
850            }
851        }
852        if let Some((groups, span)) = &admin.fieldsets {
853            for (_, fields) in groups {
854                for name in fields {
855                    if !collected.field_names.iter().any(|n| n == name) {
856                        return Err(syn::Error::new(
857                            *span,
858                            format!(
859                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
860                            ),
861                        ));
862                    }
863                }
864            }
865        }
866    }
867    if let Some(audit) = &container.audit {
868        if let Some((names, span)) = &audit.track {
869            for name in names {
870                if !collected.field_names.iter().any(|n| n == name) {
871                    return Err(syn::Error::new(
872                        *span,
873                        format!(
874                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
875                        ),
876                    ));
877                }
878            }
879        }
880    }
881
882    // Issue #291 / T2.5 — validate each `default_order` column name
883    // against the model's collected fields. Typos fail at macro-expand
884    // time, not at the database.
885    for (col, _desc, span) in &container.default_order {
886        if !collected.field_names.iter().any(|n| n == col) {
887            return Err(syn::Error::new(
888                *span,
889                format!(
890                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
891                ),
892            ));
893        }
894    }
895
896    check_known_columns(&container.indexes, &container.get_latest_by, &collected)?;
897
898    // Build the audit_track list for ModelSchema: None when no audit attr,
899    // Some(empty) when audit present without track, Some(names) when explicit.
900    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
901        audit
902            .track
903            .as_ref()
904            .map(|(names, _)| names.clone())
905            .unwrap_or_default()
906    });
907
908    // The upsert conflict target: a container-level composite unique,
909    // never a field `index(unique)` or a partial index PG can't target.
910    let upsert_unique: Option<Vec<String>> = container
911        .indexes
912        .iter()
913        .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
914        .map(|i| i.columns.clone());
915
916    // Merge field-level indexes into the container's index list.
917    let mut all_indexes: Vec<IndexAttr> = container.indexes;
918    for field in &named.named {
919        let ident = field.ident.as_ref().expect("named");
920        // The same default column as `process_field` (#1937).
921        let col = ident_name(ident);
922        // Re-parse field attrs to check for index flag
923        if let Ok(fa) = parse_field_attrs(field) {
924            if fa.index {
925                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
926                let auto_name = capped_index_name(if fa.index_unique {
927                    format!("{table}_{col_name}_uq_idx")
928                } else {
929                    format!("{table}_{col_name}_idx")
930                });
931                all_indexes.push(IndexAttr {
932                    span: field.span(),
933                    name: fa.index_name.or(Some(auto_name)),
934                    columns: vec![col_name],
935                    unique: fa.index_unique,
936                    method: fa.index_method,
937                    where_clause: None,
938                    include: Vec::new(),
939                });
940            }
941        }
942    }
943    // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
944    for field in &named.named {
945        let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
946            continue;
947        };
948        if det.kind != DetectedKind::String || fa.max_length.is_some() {
949            continue;
950        }
951        let col = fa
952            .column
953            .clone()
954            .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
955        // MySQL indexes every foreign-key column too.
956        let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
957        if fa.primary_key
958            || fa.unique
959            || relation
960            || all_indexes.iter().any(|i| i.columns.contains(&col))
961        {
962            return Err(syn::Error::new_spanned(
963                field,
964                "a primary-key, unique, indexed or foreign-key `String` needs \
965                 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
966            ));
967        }
968    }
969
970    let model_impl = model_impl_tokens(
971        struct_name,
972        &model_name,
973        &table,
974        display.as_deref(),
975        app_label.as_deref(),
976        container.admin.as_ref(),
977        &container.default_order,
978        &collected.field_schemas,
979        collected.soft_delete_column.as_deref(),
980        container.permissions,
981        audit_track_names.as_deref(),
982        &container.m2m,
983        &all_indexes,
984        &container.checks,
985        &container.excludes,
986        &container.composite_fks,
987        &container.generic_fks,
988        container.scope.as_deref(),
989        container.is_view,
990        container.verbose_name.as_deref(),
991        container.verbose_name_plural.as_deref(),
992        container.managed,
993        container.base_manager_name.as_deref(),
994        container.order_with_respect_to.as_deref(),
995        container.proxy,
996        &container.required_db_features,
997        container.required_db_vendor.as_deref(),
998        container.default_related_name.as_deref(),
999        container.db_table_comment.as_deref(),
1000        container
1001            .get_latest_by
1002            .as_ref()
1003            .map(|(c, d, _)| (c.as_str(), *d)),
1004        &container.extra_permissions,
1005        &container.default_permissions,
1006        &container.global_scopes,
1007        &container.reverse_has_relations,
1008        &container.generic_has_relations,
1009    );
1010    let module_ident = column_module_ident(struct_name);
1011    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
1012    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
1013        let track_set: Option<std::collections::HashSet<&str>> = audit
1014            .track
1015            .as_ref()
1016            .map(|(names, _)| names.iter().map(String::as_str).collect());
1017        collected
1018            .column_entries
1019            .iter()
1020            .filter(|c| {
1021                track_set
1022                    .as_ref()
1023                    .map_or(true, |s| s.contains(c.name.as_str()))
1024            })
1025            .collect()
1026    });
1027    let inherent_impl = inherent_impl_tokens(
1028        struct_name,
1029        &collected,
1030        collected.primary_key.as_ref(),
1031        &column_consts,
1032        audited_fields.as_deref(),
1033        upsert_unique.as_deref(),
1034        &container.manager_fns,
1035    );
1036    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
1037    let from_row_impl = from_row_impl_tokens(
1038        struct_name,
1039        &collected.from_row_inits,
1040        &collected.from_row_inits_my,
1041    );
1042    let reverse_helpers = reverse_helper_tokens(
1043        struct_name,
1044        &collected.fk_relations,
1045        container.default_related_name.as_deref(),
1046    )?;
1047    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
1048    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
1049    // Issue #817 — `#[rustango(through(...))]` accessors.
1050    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
1051    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
1052    let reverse_has_accessors = reverse_has_accessor_tokens(
1053        struct_name,
1054        &container.reverse_has_relations,
1055        &collected.column_entries,
1056    );
1057    let generic_fk_accessors = generic_fk_accessor_tokens(
1058        struct_name,
1059        &container.generic_fks,
1060        &collected.column_entries,
1061    );
1062
1063    Ok(quote! {
1064        #model_impl
1065        #inherent_impl
1066        #from_row_impl
1067        #column_module
1068        #reverse_helpers
1069        #m2m_accessors
1070        #generic_m2m_accessors
1071        #through_accessors
1072        #reverse_has_accessors
1073        #generic_fk_accessors
1074
1075        #root::core::inventory::submit! {
1076            // `module_path!()` evaluates at the registration site,
1077            // so a Model declared in `crate::blog::models` records
1078            // `"<crate>::blog::models"` and `resolved_app_label()`
1079            // can infer "blog" without an explicit attribute.
1080            #root::core::ModelEntry::new(
1081                <#struct_name as #root::core::Model>::SCHEMA,
1082                ::core::module_path!(),
1083            )
1084            .with_audited(
1085                <#struct_name as #root::core::Model>::__rustango_audited_update,
1086                <#struct_name as #root::core::Model>::__rustango_audited_delete,
1087            )
1088            .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1089            .with_audited_update_record(
1090                <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1091            )
1092            .with_audited_cascade(<#struct_name as #root::core::Model>::__rustango_audited_cascade)
1093        }
1094    })
1095}
1096
1097/// An index or `get_latest_by` name that is no column of the model is a
1098/// compile error; an expression index such as `lower(email)` is not checked.
1099fn check_known_columns(
1100    indexes: &[IndexAttr],
1101    get_latest_by: &Option<(String, bool, proc_macro2::Span)>,
1102    collected: &CollectedFields,
1103) -> syn::Result<()> {
1104    let is_column = |c: &str| collected.column_entries.iter().any(|e| e.column == c);
1105    // Only plain names: an expression index such as `lower(email)` is legal.
1106    let plain = |c: &str| c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
1107    let mut found: Vec<(proc_macro2::Span, String)> = Vec::new();
1108    for idx in indexes {
1109        for col in idx.columns.iter().filter(|c| plain(c) && !is_column(c)) {
1110            found.push((
1111                idx.span,
1112                format!("index column `{col}` is not a column of this model; its DDL will fail"),
1113            ));
1114        }
1115    }
1116    if let Some((col, _, span)) = get_latest_by {
1117        if !is_column(col) && !collected.field_names.iter().any(|n| n == col) {
1118            found.push((
1119                *span,
1120                format!("`get_latest_by`: `{col}` is not a field of this model"),
1121            ));
1122        }
1123    }
1124    let mut errors = found
1125        .into_iter()
1126        .map(|(span, note)| syn::Error::new(span, note));
1127    let Some(mut first) = errors.next() else {
1128        return Ok(());
1129    };
1130    first.extend(errors);
1131    Err(first)
1132}
1133
1134/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1135/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1136fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1137    let root = rustango_root();
1138    let parent_ty = &rel.parent_type;
1139    let field_ident = &rel.field_ident;
1140    let null_fk = if rel.nullable {
1141        quote! {
1142            if #receiver.#field_ident.is_none() {
1143                return ::core::result::Result::Ok(false);
1144            }
1145        }
1146    } else {
1147        quote! {}
1148    };
1149    quote! {
1150        #null_fk
1151        #root::sql::__rustango_require_join(
1152            row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1153        )?;
1154    }
1155}
1156
1157/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1158/// reused, and the deeper chain stitched into it; re-decoding it would
1159/// drop what that chain loaded.
1160fn loaded_reuse_tokens(
1161    rel: &FkRelation,
1162    receiver: &TokenStream2,
1163    load: &TokenStream2,
1164) -> TokenStream2 {
1165    let root = rustango_root();
1166    let field_ident = &rel.field_ident;
1167    let pattern = if rel.nullable {
1168        quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1169    } else {
1170        quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1171    };
1172    quote! {
1173        if let #pattern = &mut #receiver.#field_ident {
1174            if let ::core::option::Option::Some(__r) = __rest {
1175                let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1176            }
1177            return ::core::result::Result::Ok(true);
1178        }
1179    }
1180}
1181
1182/// Store the decoded `_parent` on the FK field, keeping the key the row
1183/// holds: with `on = <unique column>` it is not the parent's PK.
1184fn loaded_assign_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1185    let root = rustango_root();
1186    let field_ident = &rel.field_ident;
1187    if rel.nullable {
1188        quote! {
1189            if let ::core::option::Option::Some(__fk) = &mut #receiver.#field_ident {
1190                let _pk = ::core::clone::Clone::clone(__fk.pk_ref());
1191                *__fk = #root::sql::ForeignKey::loaded(_pk, _parent);
1192            }
1193        }
1194    } else {
1195        quote! {
1196            let _pk = ::core::clone::Clone::clone(#receiver.#field_ident.pk_ref());
1197            #receiver.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1198        }
1199    }
1200}
1201
1202/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1203/// matches `field_name` against the model's FK fields and, for a
1204/// match, decodes the FK target via the parent's macro-generated
1205/// `__rustango_from_aliased_row`, and stores `ForeignKey::Loaded` on
1206/// `self` under the key the row already holds.
1207///
1208/// Always emitted (with empty arms for FK-less models, which
1209/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1210/// trait bound on `fetch_on` is universally satisfied — users
1211/// never have to think about implementing it.
1212fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1213    let root = rustango_root();
1214    let arms = fk_relations.iter().map(|rel| {
1215        let parent_ty = &rel.parent_type;
1216        let field_ident = &rel.field_ident;
1217        let fk_name = ident_name(field_ident);
1218        let fk_col = fk_name.as_str();
1219        let guard = join_guard_tokens(rel, &quote!(self));
1220        let reuse = loaded_reuse_tokens(
1221            rel,
1222            &quote!(self),
1223            &quote!(#root::sql::LoadRelated::__rustango_load_related),
1224        );
1225        let assign = loaded_assign_tokens(rel, &quote!(self));
1226        quote! {
1227            #fk_col => {
1228                #guard
1229                #reuse
1230                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1231                // Audit #451 — multi-hop `select_related("a__b__c")`:
1232                // stitch the deeper relation onto this parent first,
1233                // decoding it at the accumulated `__next_alias`. The
1234                // parent type also impls `LoadRelated`, so this recurses
1235                // the FK chain to arbitrary depth.
1236                if let ::core::option::Option::Some(__r) = __rest {
1237                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1238                        &mut _parent, row, __r, &__next_alias,
1239                    )?;
1240                }
1241                #assign
1242                ::core::result::Result::Ok(true)
1243            }
1244        }
1245    });
1246    quote! {
1247        #[cfg(feature = "postgres")]
1248        impl #root::sql::LoadRelated for #struct_name {
1249            #[allow(unused_variables)]
1250            fn __rustango_load_related(
1251                &mut self,
1252                row: &#root::sql::sqlx::postgres::PgRow,
1253                field_name: &str,
1254                alias: &str,
1255            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1256                // Audit #451 — split the multi-hop path: `base` is the FK
1257                // on THIS model, `__rest` (if any) is the remaining chain
1258                // to stitch onto the loaded parent. `__next_alias` is the
1259                // accumulated join alias the parent's columns live under
1260                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1261                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1262                    match field_name.split_once("__") {
1263                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1264                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1265                    };
1266                let __next_alias: ::std::string::String = match __rest {
1267                    ::core::option::Option::Some(__r) => {
1268                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1269                        ::std::format!("{}__{}", alias, __rb)
1270                    }
1271                    ::core::option::Option::None => ::std::string::String::new(),
1272                };
1273                match __base {
1274                    #( #arms )*
1275                    _ => ::core::result::Result::Ok(false),
1276                }
1277            }
1278        }
1279    }
1280}
1281
1282/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1283/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1284/// which expands to a `LoadRelatedMy` impl when rustango is built with
1285/// the `mysql` feature, and to nothing otherwise. The decoded parent
1286/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1287/// decoder, also batch8) so the dual emission is symmetric across
1288/// backends.
1289fn load_related_impl_my_tokens(
1290    struct_name: &syn::Ident,
1291    fk_relations: &[FkRelation],
1292) -> TokenStream2 {
1293    let root = rustango_root();
1294    let arms = fk_relations.iter().map(|rel| {
1295        let parent_ty = &rel.parent_type;
1296        let field_ident = &rel.field_ident;
1297        let fk_name = ident_name(field_ident);
1298        let fk_col = fk_name.as_str();
1299        let guard = join_guard_tokens(rel, &quote!(__self));
1300        let reuse = loaded_reuse_tokens(
1301            rel,
1302            &quote!(__self),
1303            &quote!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1304        );
1305        let assign = loaded_assign_tokens(rel, &quote!(__self));
1306        // `self` IS hygiene-tracked through macro_rules — emitted from
1307        // a different context than the `&mut self` parameter inside
1308        // the macro_rules-expanded fn. Pass it through as `__self`
1309        // and let the macro_rules rebind it to the receiver.
1310        quote! {
1311            #fk_col => {
1312                #guard
1313                #reuse
1314                let mut _parent: #parent_ty =
1315                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1316                // Audit #451 — multi-hop: stitch the deeper relation onto
1317                // the parent at the accumulated alias (see PG twin).
1318                if let ::core::option::Option::Some(__r) = __rest {
1319                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1320                        &mut _parent, row, __r, &__next_alias,
1321                    )?;
1322                }
1323                #assign
1324                ::core::result::Result::Ok(true)
1325            }
1326        }
1327    });
1328    quote! {
1329        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1330            #( #arms )*
1331        });
1332    }
1333}
1334
1335/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1336/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1337/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1338fn load_related_impl_sqlite_tokens(
1339    struct_name: &syn::Ident,
1340    fk_relations: &[FkRelation],
1341) -> TokenStream2 {
1342    let root = rustango_root();
1343    let arms = fk_relations.iter().map(|rel| {
1344        let parent_ty = &rel.parent_type;
1345        let field_ident = &rel.field_ident;
1346        let fk_name = ident_name(field_ident);
1347        let fk_col = fk_name.as_str();
1348        let guard = join_guard_tokens(rel, &quote!(__self));
1349        let reuse = loaded_reuse_tokens(
1350            rel,
1351            &quote!(__self),
1352            &quote!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1353        );
1354        let assign = loaded_assign_tokens(rel, &quote!(__self));
1355        quote! {
1356            #fk_col => {
1357                #guard
1358                #reuse
1359                let mut _parent: #parent_ty =
1360                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1361                // Audit #451 — multi-hop: stitch the deeper relation onto
1362                // the parent at the accumulated alias (see PG twin).
1363                if let ::core::option::Option::Some(__r) = __rest {
1364                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1365                        &mut _parent, row, __r, &__next_alias,
1366                    )?;
1367                }
1368                #assign
1369                ::core::result::Result::Ok(true)
1370            }
1371        }
1372    });
1373    quote! {
1374        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1375            #( #arms )*
1376        });
1377    }
1378}
1379
1380/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1381/// matches `field_name` against the model's FK fields and returns
1382/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1383/// group children by parent PK.
1384///
1385/// Always emitted (with `_ => None` for FK-less models) so the
1386/// trait bound on `fetch_with_prefetch` is universally satisfied.
1387fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1388    let root = rustango_root();
1389    let arms = fk_relations.iter().map(|rel| {
1390        let field_ident = &rel.field_ident;
1391        let fk_name = ident_name(field_ident);
1392        let fk_col = fk_name.as_str();
1393        if rel.pk_kind == DetectedKind::I64 {
1394            // i64 FK — return the stored PK so prefetch_related can
1395            // group children by it. Nullable variant unwraps via
1396            // `as_ref().map(...)`: an unset (NULL) FK column yields
1397            // `None` and that child sits out of the grouping (correct
1398            // semantics — it has no parent to attach to).
1399            if rel.nullable {
1400                quote! {
1401                    #fk_col => self.#field_ident
1402                        .as_ref()
1403                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1404                }
1405            } else {
1406                quote! {
1407                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1408                }
1409            }
1410        } else {
1411            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1412            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1413            // i64-keyed grouping path — the trait signature is
1414            // `Option<i64>` and a non-i64 PK can't lower into it.
1415            // The FK still works for everything else (CRUD, lazy
1416            // load via `.get()`, select_related JOINs); only the
1417            // bulk prefetch grouper needs the integer key.
1418            quote! {
1419                #fk_col => ::core::option::Option::None,
1420            }
1421        }
1422    });
1423    // PK-type-agnostic version: every FK arm emits an
1424    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1425    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1426    // opt OUT of the legacy i64 method (it returns None) but opt IN
1427    // here.
1428    let value_arms = fk_relations.iter().map(|rel| {
1429        let field_ident = &rel.field_ident;
1430        let fk_name = ident_name(field_ident);
1431        let fk_col = fk_name.as_str();
1432        if rel.nullable {
1433            quote! {
1434                #fk_col => self.#field_ident
1435                    .as_ref()
1436                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1437                        #root::sql::ForeignKey::pk(fk)
1438                    )),
1439            }
1440        } else {
1441            quote! {
1442                #fk_col => ::core::option::Option::Some(
1443                    ::core::convert::Into::<#root::core::SqlValue>::into(
1444                        self.#field_ident.pk()
1445                    )
1446                ),
1447            }
1448        }
1449    });
1450    quote! {
1451        impl #root::sql::FkPkAccess for #struct_name {
1452            #[allow(unused_variables)]
1453            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1454                match field_name {
1455                    #( #arms )*
1456                    _ => ::core::option::Option::None,
1457                }
1458            }
1459            #[allow(unused_variables)]
1460            fn __rustango_fk_pk_value(
1461                &self,
1462                field_name: &str,
1463            ) -> ::core::option::Option<#root::core::SqlValue> {
1464                match field_name {
1465                    #( #value_arms )*
1466                    _ => ::core::option::Option::None,
1467                }
1468            }
1469        }
1470    }
1471}
1472
1473/// `related_name = "+"`: the FK gets no reverse accessor on its parent.
1474const NO_REVERSE_ACCESSOR: &str = "+";
1475
1476/// For every `ForeignKey<Parent>` field on `Child`, emit
1477/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1478/// Reads the column the FK points at (`on`, else the PK) and runs one
1479/// `SELECT … WHERE <fk_column> = $1`: one round trip, no N+1.
1480///
1481/// The accessor takes any `SqlExecutor`; `_set_pool` is its deprecated
1482/// old name.
1483fn reverse_helper_tokens(
1484    child_ident: &syn::Ident,
1485    fk_relations: &[FkRelation],
1486    default_related_name: Option<&str>,
1487) -> syn::Result<TokenStream2> {
1488    let root = rustango_root();
1489    // `related_name = "+"`: no accessor, so a parent from another crate works.
1490    let fk_relations: Vec<&FkRelation> = fk_relations
1491        .iter()
1492        .filter(|rel| rel.related_name.as_deref() != Some(NO_REVERSE_ACCESSOR))
1493        .collect();
1494    if fk_relations.is_empty() {
1495        return Ok(TokenStream2::new());
1496    }
1497    // Method-name resolution per FK (issue #816 + follow-up):
1498    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1499    //      itself — wins over everything else.
1500    //   2. Container-level `default_related_name = "..."` on the
1501    //      child. Applies to every FK on this model that didn't
1502    //      override.
1503    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1504    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1505    //      edge cases.
1506    //
1507    // The PG-on-executor variant keeps the resolved name; the
1508    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1509    // framework's convention for the `&Pool` flavor of every helper).
1510    let default_pg_suffix = default_related_name
1511        .map(str::to_owned)
1512        .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1513    let name_of = |rel: &FkRelation| {
1514        rel.related_name
1515            .clone()
1516            .unwrap_or_else(|| default_pg_suffix.clone())
1517    };
1518    // Two FKs to one parent would define the same method twice (E0592).
1519    for (i, rel) in fk_relations.iter().enumerate() {
1520        let parent_ty = &rel.parent_type;
1521        let parent = quote!(#parent_ty).to_string();
1522        let name = name_of(rel);
1523        if let Some(first) = fk_relations[..i].iter().find(|r| {
1524            let other = &r.parent_type;
1525            quote!(#other).to_string() == parent && name_of(r) == name
1526        }) {
1527            return Err(syn::Error::new_spanned(
1528                &rel.field_ident,
1529                format!(
1530                    "`{}` and `{}` both point at `{parent}` and would both add `{parent}::{name}`; \
1531                     give one `#[rustango(related_name = \"...\")]`, or `related_name = \"+\"` for none",
1532                    first.field_ident, rel.field_ident
1533                ),
1534            ));
1535        }
1536    }
1537    let impls = fk_relations.iter().map(|rel| {
1538        let pg_suffix = name_of(rel);
1539        let pool_suffix = format!("{}_pool", pg_suffix);
1540        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1541        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1542        let parent_ty = &rel.parent_type;
1543        let fk_name = ident_name(&rel.field_ident);
1544        let fk_col = fk_name.as_str();
1545        let doc = format!(
1546            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1547             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1548             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1549             further `{child_ident}::objects()` filters via direct queryset use."
1550        );
1551        quote! {
1552            impl #parent_ty {
1553                #[doc = #doc]
1554                ///
1555                /// Runs on any [`#root::sql::SqlExecutor`]: a `&Pool`, a held
1556                /// `PoolConn`, a `PoolTx` or a Postgres sqlx executor.
1557                ///
1558                /// # Errors
1559                /// Returns [`#root::sql::ExecError`] for SQL-writing
1560                /// or driver failures.
1561                pub async fn #pg_method_ident<'_c, _E: #root::sql::SqlExecutor<'_c>>(
1562                    &self,
1563                    _executor: _E,
1564                ) -> ::core::result::Result<
1565                    ::std::vec::Vec<#child_ident>,
1566                    #root::sql::ExecError,
1567                > {
1568                    let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1569                    #root::query::QuerySet::<#child_ident>::new()
1570                        .filter_op(#fk_col, #root::core::Op::Eq, _key)
1571                        .fetch_on(_executor)
1572                        .await
1573                }
1574
1575                /// Old name of the reverse accessor without `_pool`.
1576                ///
1577                /// # Errors
1578                /// As the accessor.
1579                #[deprecated(since = "0.62.0", note = "drop the `_pool` suffix")]
1580                pub async fn #pool_method_ident(
1581                    &self,
1582                    pool: &#root::sql::Pool,
1583                ) -> ::core::result::Result<
1584                    ::std::vec::Vec<#child_ident>,
1585                    #root::sql::ExecError,
1586                > {
1587                    self.#pg_method_ident(pool).await
1588                }
1589            }
1590        }
1591    });
1592    Ok(quote! { #( #impls )* })
1593}
1594
1595/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1596/// relation declared on the model.
1597/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1598/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1599///
1600/// Closes #239 + #240 — `comment.content_object` /
1601/// `comment.content_object = post` ergonomics on top of the existing
1602/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1603///
1604/// `column_entries` is passed so we can resolve each `ct_column` /
1605/// `pk_column` SQL name back to its Rust field ident — the macro
1606/// only sees the column-side strings in the attribute, but the
1607/// emitted accessor needs to read the actual struct field.
1608fn generic_fk_accessor_tokens(
1609    struct_name: &syn::Ident,
1610    generic_fks: &[GenericFkAttr],
1611    column_entries: &[ColumnEntry],
1612) -> TokenStream2 {
1613    let root = rustango_root();
1614    if generic_fks.is_empty() {
1615        return TokenStream2::new();
1616    }
1617    let methods = generic_fks.iter().filter_map(|gfk| {
1618        // Resolve `ct_column` + `pk_column` to the struct's Rust
1619        // field idents. A typo (column name doesn't match any field)
1620        // emits no method for that registration — the user will see
1621        // the compiler reject the SCHEMA literal anyway, so there's
1622        // a clear error path without us double-reporting.
1623        let ct_ident = column_entries
1624            .iter()
1625            .find(|c| c.column == gfk.ct_column)
1626            .map(|c| c.ident.clone())?;
1627        let pk_ident = column_entries
1628            .iter()
1629            .find(|c| c.column == gfk.pk_column)
1630            .map(|c| c.ident.clone())?;
1631
1632        let accessor_ident =
1633            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1634        let setter_ident =
1635            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1636        let name_literal = gfk.name.as_str();
1637
1638        Some(quote! {
1639            #[doc = concat!(
1640                "Resolve the polymorphic `",
1641                #name_literal,
1642                "` relation. Reads `self.",
1643                stringify!(#ct_ident),
1644                "` + `self.",
1645                stringify!(#pk_ident),
1646                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1647                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1648                #name_literal,
1649                "\", ...))]`."
1650            )]
1651            pub async fn #accessor_ident(
1652                &self,
1653                pool: &#root::sql::Pool,
1654            ) -> ::core::result::Result<
1655                ::core::option::Option<#root::__serde_json::Value>,
1656                #root::sql::ExecError,
1657            > {
1658                let gfk = #root::contenttypes::GenericForeignKey::new(
1659                    self.#ct_ident as i64,
1660                    self.#pk_ident as i64,
1661                );
1662                gfk.get_object(pool).await
1663            }
1664
1665            #[doc = concat!(
1666                "Set the polymorphic `",
1667                #name_literal,
1668                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1669                stringify!(#ct_ident),
1670                "` and `self.",
1671                stringify!(#pk_ident),
1672                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1673                #name_literal,
1674                "\", ...))]`."
1675            )]
1676            pub async fn #setter_ident<T: #root::core::Model>(
1677                &mut self,
1678                pool: &#root::sql::Pool,
1679                target_pk: i64,
1680            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1681                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1682                    pool,
1683                    target_pk,
1684                ).await?;
1685                self.#ct_ident = gfk.content_type_id as _;
1686                self.#pk_ident = gfk.object_pk as _;
1687                ::core::result::Result::Ok(())
1688            }
1689        })
1690    });
1691    quote! {
1692        impl #struct_name {
1693            #( #methods )*
1694        }
1695    }
1696}
1697
1698fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1699    let root = rustango_root();
1700    if m2m_relations.is_empty() {
1701        return TokenStream2::new();
1702    }
1703    let methods = m2m_relations.iter().map(|rel| {
1704        let method_name = format!("{}_m2m", rel.name);
1705        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1706        let through = rel.through.as_str();
1707        let src_col = rel.src.as_str();
1708        let dst_col = rel.dst.as_str();
1709        quote! {
1710            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1711                #root::sql::M2MManager {
1712                    src_pk: self.__rustango_pk_value(),
1713                    through: #through,
1714                    src_col: #src_col,
1715                    dst_col: #dst_col,
1716                }
1717            }
1718        }
1719    });
1720    quote! {
1721        impl #struct_name {
1722            #( #methods )*
1723        }
1724    }
1725}
1726
1727/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1728/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1729fn generic_m2m_accessor_tokens(
1730    struct_name: &syn::Ident,
1731    relations: &[GenericM2MAttr],
1732) -> TokenStream2 {
1733    let root = rustango_root();
1734    if relations.is_empty() {
1735        return TokenStream2::new();
1736    }
1737    let methods = relations.iter().map(|rel| {
1738        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1739        let through = rel.through.as_str();
1740        let pk_col = rel.pk_column.as_str();
1741        let ct_col = rel.ct_column.as_str();
1742        let related_col = rel.related_column.as_str();
1743        quote! {
1744            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1745                #root::sql::GenericM2MManager {
1746                    src_pk: self.__rustango_pk_value(),
1747                    src_schema: <Self as #root::core::Model>::SCHEMA,
1748                    through: #through,
1749                    pk_col: #pk_col,
1750                    ct_col: #ct_col,
1751                    dst_col: #related_col,
1752                }
1753            }
1754        }
1755    });
1756    // Lets an owner delete find and clear its junction rows.
1757    let entries = relations.iter().map(|rel| {
1758        let through = rel.through.as_str();
1759        let pk_col = rel.pk_column.as_str();
1760        let ct_col = rel.ct_column.as_str();
1761        quote! {
1762            #root::core::inventory::submit! {
1763                #root::core::GenericM2MEntry::new(
1764                    <#struct_name as #root::core::Model>::SCHEMA,
1765                    #through,
1766                    #pk_col,
1767                    #ct_col,
1768                )
1769            }
1770        }
1771    });
1772    quote! {
1773        impl #struct_name {
1774            #( #methods )*
1775        }
1776        #( #entries )*
1777    }
1778}
1779
1780/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1781/// associated functions for each `#[rustango(reverse_has(...))]`
1782/// attribute. Issue #830.
1783///
1784/// The two emitted functions return ready-to-use `WhereExpr` nodes
1785/// that downstream callers drop into
1786/// `QuerySet::<Self>::where_raw(...)`:
1787///
1788/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1789///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1790///   `whereHas` parity (without the closure-style sub-predicate
1791///   refinement — that's a follow-up; users can layer additional
1792///   predicates by constructing the SelectQuery themselves and
1793///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1794/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1795///   `whereDoesntHave` parity.
1796///
1797/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1798/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1799/// machinery threads the outer-table reference through automatically
1800/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1801fn reverse_has_accessor_tokens(
1802    struct_name: &syn::Ident,
1803    reverse_has_relations: &[ReverseHasAttr],
1804    columns: &[ColumnEntry],
1805) -> TokenStream2 {
1806    let root = rustango_root();
1807    if reverse_has_relations.is_empty() {
1808        return TokenStream2::new();
1809    }
1810    let methods = reverse_has_relations.iter().map(|rel| {
1811        let exists_name = format!("{}_exists_expr", rel.name);
1812        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1813        let count_name = format!("{}_count", rel.name);
1814        let fetch_name = format!("{}_fetch", rel.name);
1815        let first_name = format!("{}_first", rel.name);
1816        let pluck_name = format!("{}_pluck", rel.name);
1817        let accessor_name = rel.name.as_str();
1818        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1819        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1820        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1821        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1822        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1823        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1824        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1825        let child = &rel.child;
1826        let child_fk_column = rel.child_fk_column.as_str();
1827        let self_pk_column = rel.self_pk_column();
1828        // The instance helpers filter on the value the EXISTS form joins on.
1829        let self_value = columns
1830            .iter()
1831            .find(|c| c.column == self_pk_column)
1832            .map_or_else(
1833                || quote! { self.__rustango_pk_value() },
1834                |c| {
1835                    let ident = &c.ident;
1836                    quote! {
1837                        ::core::convert::Into::<#root::core::SqlValue>::into(
1838                            ::core::clone::Clone::clone(&self.#ident)
1839                        )
1840                    }
1841                },
1842            );
1843        let exists_doc = format!(
1844            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1845             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1846             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1847             filter to {struct_name}s with at least one matching child.",
1848        );
1849        let not_exists_doc = format!(
1850            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1851             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1852             <outer>.{self_pk_column})`. Drop into \
1853             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1854             {struct_name}s with **no** matching child.",
1855        );
1856        let count_doc = format!(
1857            "Eloquent `$model->{name}->count()` analog — returns \
1858             the number of `{child}` rows whose `{child_fk_column}` \
1859             matches this `{struct_name}` instance's `{self_pk_column}`. \
1860             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1861             {child_fk_column} = <self.{self_pk_column}>`.",
1862            name = rel.name,
1863        );
1864        let accessor_doc = format!(
1865            "Eloquent `$model->{name}` accessor — returns a \
1866             `QuerySet<{child}>` filtered to rows whose \
1867             `{child_fk_column}` matches this `{struct_name}` \
1868             instance's `{self_pk_column}`. **Chainable**: compose `.filter()` \
1869             / `.order_by()` / `.limit()` etc. on top, then call \
1870             `.fetch(&pool)` (the QuerySet trait method) when \
1871             done. For the simple \"fetch all\" hot path with no \
1872             further composition, prefer the bare-name \
1873             `{name}_fetch(&pool)` companion.",
1874            name = rel.name,
1875        );
1876        let fetch_doc = format!(
1877            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1878             over `{name}(&self).fetch(&pool)`. Use this when \
1879             you don't need further `.filter()` / `.order_by()` \
1880             composition; falls back to the chainable accessor when \
1881             you do. Avoids the `_pool` suffix on the most common \
1882             call-site shape.",
1883            name = rel.name,
1884        );
1885        quote! {
1886            #[doc = #accessor_doc]
1887            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1888                #root::query::QuerySet::<#child>::new()
1889                    .filter(#child_fk_column, #self_value)
1890            }
1891
1892            #[doc = #fetch_doc]
1893            pub async fn #fetch_ident(
1894                &self,
1895                pool: &#root::sql::Pool,
1896            ) -> ::core::result::Result<
1897                ::std::vec::Vec<#child>,
1898                #root::sql::ExecError,
1899            > {
1900                use #root::sql::FetcherPool as _;
1901                self.#accessor_ident().fetch(pool).await
1902            }
1903
1904            /// Eloquent `$model->relation->first()` / `hasOne`
1905            /// semantics — bare-name shortcut over
1906            /// `self.<name>().first(&pool)`. Returns `None` when no
1907            /// child rows match. Useful when the relation is
1908            /// nominally many-to-one in shape but at most one row is
1909            /// expected (latest comment, primary tag, etc.).
1910            pub async fn #first_ident(
1911                &self,
1912                pool: &#root::sql::Pool,
1913            ) -> ::core::result::Result<
1914                ::core::option::Option<#child>,
1915                #root::sql::ExecError,
1916            > {
1917                self.#accessor_ident().first(pool).await
1918            }
1919
1920            /// Eloquent `$model->relation->pluck($col)` — project a
1921            /// single column from the child rows into a `Vec<U>`.
1922            /// Skips the typed `Child` decode, which is cheaper when
1923            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1924            pub async fn #pluck_ident<U>(
1925                &self,
1926                col: &'static str,
1927                pool: &#root::sql::Pool,
1928            ) -> ::core::result::Result<
1929                ::std::vec::Vec<U>,
1930                #root::sql::ExecError,
1931            >
1932            where
1933                U: #root::sql::FlatScalar,
1934            {
1935                self.#accessor_ident()
1936                    .values_list_flat(col)
1937                    .fetch::<U>(pool)
1938                    .await
1939            }
1940
1941            #[doc = #exists_doc]
1942            pub fn #exists_ident() -> #root::core::WhereExpr {
1943                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1944                let child_schema =
1945                    <#child as #root::core::Model>::SCHEMA;
1946                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1947                    lhs: Expr::Column(#child_fk_column),
1948                    op: Op::Eq,
1949                    rhs: Expr::OuterRef(#self_pk_column),
1950                });
1951                WhereExpr::Exists(::std::boxed::Box::new(inner))
1952            }
1953
1954            #[doc = #not_exists_doc]
1955            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1956                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1957                let child_schema =
1958                    <#child as #root::core::Model>::SCHEMA;
1959                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1960                    lhs: Expr::Column(#child_fk_column),
1961                    op: Op::Eq,
1962                    rhs: Expr::OuterRef(#self_pk_column),
1963                });
1964                WhereExpr::NotExists(::std::boxed::Box::new(inner))
1965            }
1966
1967            #[doc = #count_doc]
1968            pub async fn #count_ident(
1969                &self,
1970                pool: &#root::sql::Pool,
1971            ) -> ::core::result::Result<
1972                i64,
1973                #root::sql::ExecError,
1974            > {
1975                use #root::sql::CounterPool as _;
1976                self.#accessor_ident().count(pool).await
1977            }
1978        }
1979    });
1980    quote! {
1981        impl #struct_name {
1982            #( #methods )*
1983        }
1984    }
1985}
1986
1987/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1988/// `#[rustango(through(...))]` attribute. Issue #817.
1989///
1990/// Each accessor builds a correlated subquery via
1991/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1992/// intermediate table, filters on the FK column pointing at this
1993/// model, and projects the intermediate PK. The outer queryset
1994/// filters the far model by its FK-to-intermediate column being in
1995/// that set.
1996///
1997/// The returned `QuerySet<Far>` is **chainable** — the subquery
1998/// lives inside a `where_raw` clause so the user's later
1999/// `.filter()` / `.order_by()` / `.limit()` compositions don't
2000/// disturb it.
2001///
2002/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
2003/// SQLite — no LATERAL or backend-specific syntax involved.
2004fn through_accessor_tokens(
2005    struct_name: &syn::Ident,
2006    through_relations: &[ThroughAttr],
2007) -> TokenStream2 {
2008    let root = rustango_root();
2009    if through_relations.is_empty() {
2010        return TokenStream2::new();
2011    }
2012    let methods = through_relations.iter().map(|rel| {
2013        let method_name = format!("{}_through", rel.name);
2014        let count_name = format!("{}_through_count", rel.name);
2015        let fetch_name = format!("{}_through_fetch", rel.name);
2016        let first_name = format!("{}_through_first", rel.name);
2017        let pluck_name = format!("{}_through_pluck", rel.name);
2018        let method_ident = syn::Ident::new(&method_name, struct_name.span());
2019        let count_ident = syn::Ident::new(&count_name, struct_name.span());
2020        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
2021        let first_ident = syn::Ident::new(&first_name, struct_name.span());
2022        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
2023        let far = &rel.far;
2024        let intermediate = &rel.intermediate;
2025        let far_fk_column = rel.far_fk_column.as_str();
2026        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
2027        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
2028        let doc = format!(
2029            "Eloquent `hasManyThrough` accessor — returns a \
2030             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
2031             instance through the intermediate `{intermediate}` table. \
2032             Generated SQL shape: \
2033             `… WHERE {far_fk_column} IN (SELECT \
2034             {intermediate_pk_column} FROM <{intermediate}> WHERE \
2035             {intermediate_fk_column} = self.pk)`. Chainable like any \
2036             other QuerySet — compose `.filter()` / `.order_by()` / \
2037             `.limit()` etc. on top.",
2038        );
2039        let count_doc = format!(
2040            "Eloquent `$model->{name}->count()` analog for the \
2041             through-relation — returns the number of `{far}` rows \
2042             reachable through `{intermediate}`. Equivalent to \
2043             `self.{name}_through().count(pool)` but spelled \
2044             as a bare instance method for parity with the \
2045             `reverse_has` `<name>_count` shape.",
2046            name = rel.name,
2047        );
2048        quote! {
2049            #[doc = #doc]
2050            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
2051                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
2052                let intermediate_schema =
2053                    <#intermediate as #root::core::Model>::SCHEMA;
2054                let sub = SelectQuery::new(intermediate_schema)
2055                    .where_clause(WhereExpr::Predicate(Filter::new(
2056                        #intermediate_fk_column,
2057                        Op::Eq,
2058                        self.__rustango_pk_value(),
2059                    )))
2060                    .projection(::std::vec![#intermediate_pk_column]);
2061                #root::query::QuerySet::<#far>::new().where_raw(
2062                    WhereExpr::InSubquery {
2063                        column: #far_fk_column,
2064                        negated: false,
2065                        subquery: ::std::boxed::Box::new(sub),
2066                    },
2067                )
2068            }
2069
2070            #[doc = #count_doc]
2071            pub async fn #count_ident(
2072                &self,
2073                pool: &#root::sql::Pool,
2074            ) -> ::core::result::Result<
2075                i64,
2076                #root::sql::ExecError,
2077            > {
2078                use #root::sql::CounterPool as _;
2079                self.#method_ident().count(pool).await
2080            }
2081
2082            /// Eloquent `$model->relation->get()` for the
2083            /// through-relation — bare-name hot-path over
2084            /// `self.<name>_through().fetch(&pool)`. Use this
2085            /// when you don't need further `.filter()` /
2086            /// `.order_by()` composition; falls back to the
2087            /// chainable accessor when you do. Avoids the `_pool`
2088            /// suffix on the most common call-site shape.
2089            pub async fn #fetch_ident(
2090                &self,
2091                pool: &#root::sql::Pool,
2092            ) -> ::core::result::Result<
2093                ::std::vec::Vec<#far>,
2094                #root::sql::ExecError,
2095            > {
2096                use #root::sql::FetcherPool as _;
2097                self.#method_ident().fetch(pool).await
2098            }
2099
2100            /// Eloquent `hasOneThrough` analog — bare-name shortcut
2101            /// over `self.<name>_through().first(&pool)`. Returns
2102            /// `None` when no far rows are reachable through the
2103            /// intermediate. Useful when at most one row is
2104            /// expected (latest comment by country, primary tag,
2105            /// etc.).
2106            pub async fn #first_ident(
2107                &self,
2108                pool: &#root::sql::Pool,
2109            ) -> ::core::result::Result<
2110                ::core::option::Option<#far>,
2111                #root::sql::ExecError,
2112            > {
2113                self.#method_ident().first(pool).await
2114            }
2115
2116            /// Pluck a single column from the far rows into `Vec<U>`
2117            /// — cheaper than the typed `<Far>` decode when only one
2118            /// scalar column is needed.
2119            pub async fn #pluck_ident<U>(
2120                &self,
2121                col: &'static str,
2122                pool: &#root::sql::Pool,
2123            ) -> ::core::result::Result<
2124                ::std::vec::Vec<U>,
2125                #root::sql::ExecError,
2126            >
2127            where
2128                U: #root::sql::FlatScalar,
2129            {
2130                self.#method_ident()
2131                    .values_list_flat(col)
2132                    .fetch::<U>(pool)
2133                    .await
2134            }
2135        }
2136    });
2137    quote! {
2138        impl #struct_name {
2139            #( #methods )*
2140        }
2141    }
2142}
2143
2144struct ColumnEntry {
2145    /// The struct field ident, used both for the inherent const name on
2146    /// the model and for the inner column type's name.
2147    ident: syn::Ident,
2148    /// The struct's field type, used as `Column::Value`.
2149    value_ty: Type,
2150    /// Rust-side field name (e.g. `"id"`).
2151    name: String,
2152    /// SQL-side column name (e.g. `"user_id"`).
2153    column: String,
2154    /// `#root::core::FieldType::I64` etc.
2155    field_type_tokens: TokenStream2,
2156    /// A bare or optional `Uuid` field; MySQL decodes it from text.
2157    uuid_column: bool,
2158}
2159
2160struct CollectedFields {
2161    field_schemas: Vec<TokenStream2>,
2162    from_row_inits: Vec<TokenStream2>,
2163    /// Aliased counterparts of `from_row_inits` — read columns via
2164    /// `format!("{prefix}__{col}")` aliases so a Model can be
2165    /// decoded from a JOINed row's projected target columns.
2166    from_aliased_row_inits: Vec<TokenStream2>,
2167    /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2168    from_row_inits_my: Vec<TokenStream2>,
2169    from_aliased_row_inits_my: Vec<TokenStream2>,
2170    /// Static column-name list — used by the simple insert path
2171    /// (no `Auto<T>` fields). Aligned with `insert_values`.
2172    insert_columns: Vec<TokenStream2>,
2173    /// Static `Into<SqlValue>` expressions, one per field. Aligned
2174    /// with `insert_columns`. Used by the simple insert path only.
2175    insert_values: Vec<TokenStream2>,
2176    /// Per-field push expressions for the dynamic (Auto-aware)
2177    /// insert path. Each statement either unconditionally pushes
2178    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2179    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2180    insert_pushes: Vec<TokenStream2>,
2181    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2182    /// when `has_auto == false`.
2183    returning_cols: Vec<TokenStream2>,
2184    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2185    /// field. Run after `insert_returning` to populate the model.
2186    auto_assigns: Vec<TokenStream2>,
2187    /// `(ident, column_literal, is_uuid)` for every Auto field. Used by
2188    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2189    /// instead of `self`.
2190    auto_field_idents: Vec<(syn::Ident, String, bool)>,
2191    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2192    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2193    /// back into the struct after insert (MySQL has no RETURNING → the
2194    /// field stays at its placeholder until the next read).
2195    generated_field_idents: Vec<(syn::Ident, String)>,
2196    /// First `Auto<T>` field the database fills, with its inner `T`: the
2197    /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2198    first_db_auto: Option<(syn::Ident, Type)>,
2199    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2200    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2201    /// `columns`).
2202    bulk_pushes_no_auto: Vec<TokenStream2>,
2203    /// Bulk-insert per-row pushes for **all fields including Auto**.
2204    /// Used by the all-Auto-Set bulk path (Auto col included with the
2205    /// caller-supplied value).
2206    bulk_pushes_all: Vec<TokenStream2>,
2207    /// Column-name literals for non-Auto fields only (paired with
2208    /// `bulk_pushes_no_auto`).
2209    bulk_columns_no_auto: Vec<TokenStream2>,
2210    /// Column-name literals for every field including Auto (paired
2211    /// with `bulk_pushes_all`).
2212    bulk_columns_all: Vec<TokenStream2>,
2213    /// Fills each `default_uuid_v7` field of `_row` in place, so a
2214    /// mutable bulk insert hands the PKs back (#1934).
2215    bulk_fill_in_place: Vec<TokenStream2>,
2216    /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2217    rust_filled_idents: Vec<syn::Ident>,
2218    /// `auto_now` fields: `upsert` stamps them before binding.
2219    auto_now_idents: Vec<syn::Ident>,
2220    /// `default_uuid_v7` fields: an `Unset` one gets a fresh id, so the
2221    /// upsert marks it `generated` for the MySQL key guard.
2222    uuid_v7_fields: Vec<(syn::Ident, String)>,
2223    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2224    /// + the loop that asserts every row matches. One pair per Auto
2225    /// field. Empty when `has_auto == false`.
2226    bulk_auto_uniformity: Vec<TokenStream2>,
2227    /// Identifier of the first Auto field, used as the witness for
2228    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2229    first_auto_ident: Option<syn::Ident>,
2230    /// `true` if any field on the struct is `Auto<T>`.
2231    has_auto: bool,
2232    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2233    /// Gates `save()` codegen — only Auto PKs let us infer
2234    /// insert-vs-update from the in-memory value.
2235    pk_is_auto: bool,
2236    /// `Assignment` constructors for every non-PK column. Drives the
2237    /// UPDATE branch of `save()`.
2238    update_assignments: Vec<TokenStream2>,
2239    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2240    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2241    upsert_update_columns: Vec<TokenStream2>,
2242    primary_key: Option<(syn::Ident, String)>,
2243    column_entries: Vec<ColumnEntry>,
2244    /// Rust-side field names, in declaration order. Used to validate
2245    /// container attributes like `display = "…"`.
2246    field_names: Vec<String>,
2247    /// FK fields on this child model. Drives the reverse-relation
2248    /// helper emit — for each FK, the macro adds an inherent
2249    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2250    /// method on the parent type.
2251    fk_relations: Vec<FkRelation>,
2252    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2253    /// the model has one. Drives emission of the `soft_delete_on` /
2254    /// `restore_on` inherent methods. At most one such column per
2255    /// model is allowed; collect_fields rejects duplicates.
2256    soft_delete_column: Option<String>,
2257    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2258    /// companion to `soft_delete_column` for emitting predicates
2259    /// that need to read the field off `&self` (e.g. `trashed()`).
2260    soft_delete_field_ident: Option<syn::Ident>,
2261}
2262
2263#[derive(Clone)]
2264struct FkRelation {
2265    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2266    /// helper is emitted as `impl <ParentType> { … }`.
2267    parent_type: Type,
2268    /// Rust field ident of the FK. Its name is the key for select_related,
2269    /// prefetch and filters; the SQL column may differ (#1936).
2270    field_ident: syn::Ident,
2271    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2272    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2273    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2274    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2275    pk_kind: DetectedKind,
2276    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2277    /// FK column). Drives the `Some(...)` wrapping in load_related
2278    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2279    /// the codegen matches the field's declared shape.
2280    nullable: bool,
2281    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2282    /// override. When set, the reverse helper picks this name instead
2283    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2284    /// #816 (issue's "Related" note re: per-FK override).
2285    related_name: Option<String>,
2286}
2287
2288fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2289    let root = rustango_root();
2290    let cap = named.named.len();
2291    let mut out = CollectedFields {
2292        field_schemas: Vec::with_capacity(cap),
2293        from_row_inits: Vec::with_capacity(cap),
2294        from_aliased_row_inits: Vec::with_capacity(cap),
2295        from_row_inits_my: Vec::with_capacity(cap),
2296        from_aliased_row_inits_my: Vec::with_capacity(cap),
2297        insert_columns: Vec::with_capacity(cap),
2298        insert_values: Vec::with_capacity(cap),
2299        insert_pushes: Vec::with_capacity(cap),
2300        returning_cols: Vec::new(),
2301        auto_assigns: Vec::new(),
2302        auto_field_idents: Vec::new(),
2303        generated_field_idents: Vec::new(),
2304        first_db_auto: None,
2305        bulk_pushes_no_auto: Vec::with_capacity(cap),
2306        bulk_pushes_all: Vec::with_capacity(cap),
2307        bulk_columns_no_auto: Vec::with_capacity(cap),
2308        bulk_columns_all: Vec::with_capacity(cap),
2309        bulk_fill_in_place: Vec::new(),
2310        rust_filled_idents: Vec::new(),
2311        auto_now_idents: Vec::new(),
2312        uuid_v7_fields: Vec::new(),
2313        bulk_auto_uniformity: Vec::new(),
2314        first_auto_ident: None,
2315        has_auto: false,
2316        pk_is_auto: false,
2317        update_assignments: Vec::with_capacity(cap),
2318        upsert_update_columns: Vec::with_capacity(cap),
2319        primary_key: None,
2320        column_entries: Vec::with_capacity(cap),
2321        field_names: Vec::with_capacity(cap),
2322        fk_relations: Vec::new(),
2323        soft_delete_column: None,
2324        soft_delete_field_ident: None,
2325    };
2326
2327    for field in &named.named {
2328        let info = process_field(field, table)?;
2329        out.field_names.push(ident_name(info.ident));
2330        out.field_schemas.push(info.schema);
2331        out.from_row_inits.push(info.from_row_init);
2332        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2333        out.from_row_inits_my.push(info.from_row_init_my);
2334        out.from_aliased_row_inits_my
2335            .push(info.from_aliased_row_init_my);
2336        if let Some(parent_ty) = info.fk_inner.clone() {
2337            out.fk_relations.push(FkRelation {
2338                parent_type: parent_ty,
2339                field_ident: info.ident.clone(),
2340                pk_kind: info.fk_pk_kind,
2341                nullable: info.nullable,
2342                related_name: info.related_name.clone(),
2343            });
2344        }
2345        if info.soft_delete {
2346            if out.soft_delete_column.is_some() {
2347                return Err(syn::Error::new_spanned(
2348                    field,
2349                    "only one field may be marked `#[rustango(soft_delete)]`",
2350                ));
2351            }
2352            out.soft_delete_column = Some(info.column.clone());
2353            out.soft_delete_field_ident = Some(info.ident.clone());
2354        }
2355        let column = info.column.as_str();
2356        let ident = info.ident;
2357        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2358        // skip every write path — Postgres recomputes the value
2359        // from EXPR. Push only the column-entry record (so typed
2360        // column constants still exist for filtering / projection)
2361        // and the schema literal (already pushed above) and move
2362        // on. No insert_columns/values, no insert_pushes, no
2363        // bulk_*, no update_assignments, no upsert_update_columns,
2364        // no returning_cols.
2365        if info.generated_as.is_some() {
2366            out.column_entries.push(ColumnEntry {
2367                ident: ident.clone(),
2368                value_ty: info.value_ty.clone(),
2369                name: ident_name(ident),
2370                column: info.column.clone(),
2371                field_type_tokens: info.field_type_tokens,
2372                uuid_column: info.uuid_column,
2373            });
2374            // #1028 — refresh the DB-computed value after insert on
2375            // RETURNING-capable backends. The column joins `returning_cols`
2376            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2377            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2378            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2379            // it keeps the placeholder until the next read.
2380            out.returning_cols.push(quote!(#column));
2381            out.generated_field_idents
2382                .push((ident.clone(), info.column.clone()));
2383            continue;
2384        }
2385        out.insert_columns.push(quote!(#column));
2386        out.insert_values.push(quote! {
2387            ::core::convert::Into::<#root::core::SqlValue>::into(
2388                ::core::clone::Clone::clone(&self.#ident)
2389            )
2390        });
2391        if info.auto {
2392            out.has_auto = true;
2393            if out.first_auto_ident.is_none() {
2394                out.first_auto_ident = Some(ident.clone());
2395            }
2396            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2397            // before binding, so the value is already in
2398            // `self.#ident` after the insert_push — RETURNING is
2399            // redundant. Skip adding this column to returning_cols /
2400            // auto_assigns / auto_field_idents to avoid (a) an
2401            // unnecessary RETURNING column on every dialect, and (b)
2402            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2403            // integer PK.
2404            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2405            // for the same reason: they are filled Rust-side below and
2406            // bound, so the value is already in `self` and RETURNING
2407            // would be a redundant column on every dialect.
2408            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2409                if out.first_db_auto.is_none() {
2410                    out.first_db_auto = auto_inner_type(info.value_ty)
2411                        .cloned()
2412                        .map(|ty| (ident.clone(), ty));
2413                }
2414                out.returning_cols.push(quote!(#column));
2415                out.auto_field_idents.push((
2416                    ident.clone(),
2417                    info.column.clone(),
2418                    info.fk_pk_kind == DetectedKind::Uuid,
2419                ));
2420                out.auto_assigns.push(quote! {
2421                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2422                });
2423            }
2424            if info.default_uuid_v7 {
2425                // Rust-side UUIDv7 generation (issue #823, Eloquent
2426                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2427                // and bind; Auto::Set → bind the user's value. The
2428                // column is ALWAYS present in the INSERT statement —
2429                // no RETURNING / no DB DEFAULT needed.
2430                out.insert_pushes.push(quote! {
2431                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2432                        self.#ident = #root::sql::Auto::Set(
2433                            #root::__uuid::Uuid::now_v7(),
2434                        );
2435                    }
2436                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2437                        _columns.push(#column);
2438                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2439                            ::core::clone::Clone::clone(_v)
2440                        ));
2441                    }
2442                });
2443            } else if info.auto_now_add || info.auto_now {
2444                // Rust-side write timestamp (#1464), mirroring
2445                // `default_uuid_v7` above. `auto_now` has always bound
2446                // `Utc::now()` on UPDATE but not on INSERT, so the
2447                // first write of an `updated_at` took the DB default
2448                // while every later one took the clock — the same
2449                // column, filled two different ways.
2450                //
2451                // The default is the problem. `CREATE TABLE IF NOT
2452                // EXISTS` leaves an upgraded table's
2453                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2454                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2455                // statement that changes it — so every insert relying
2456                // on that default wrote the legacy shape forever, while
2457                // the migrate sweep converted the rows around it. The
2458                // column went permanently mixed and `ORDER BY` inverted
2459                // the admin audit log (#1616 rework review,
2460                // correctness-001 / dialects-005 / security-002).
2461                //
2462                // Binding it here makes the stale default unreachable
2463                // rather than trying to migrate it: the timestamp comes
2464                // from the application, not the database.
2465                out.insert_pushes.push(quote! {
2466                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2467                        self.#ident = #root::sql::Auto::Set(
2468                            #root::__chrono::Utc::now(),
2469                        );
2470                    }
2471                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2472                        _columns.push(#column);
2473                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2474                            ::core::clone::Clone::clone(_v)
2475                        ));
2476                    }
2477                });
2478            } else {
2479                out.insert_pushes.push(quote! {
2480                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2481                        _columns.push(#column);
2482                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2483                            ::core::clone::Clone::clone(_v)
2484                        ));
2485                    }
2486                });
2487            }
2488            // …except the timestamp columns, which must appear in BOTH
2489            // paths (#1464). The path is chosen by the *first* Auto
2490            // field — in practice the PK — so a bulk insert of rows
2491            // with an unset PK took the no-auto branch and dropped the
2492            // timestamp column, falling back to the DB default that
2493            // this change exists to stop depending on.
2494            //
2495            // `rows` is borrowed immutably here, so this fills per row
2496            // at push time rather than writing back into the struct. An
2497            // explicitly-Set value is still honoured; only `Unset`
2498            // takes the clock.
2499            // `default_uuid_v7` likewise: its column has no DB default (#1934).
2500            if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2501                out.rust_filled_idents.push(ident.clone());
2502            }
2503            if info.auto_now {
2504                out.auto_now_idents.push(ident.clone());
2505            }
2506            if info.default_uuid_v7 {
2507                out.uuid_v7_fields
2508                    .push((ident.clone(), info.column.clone()));
2509            }
2510            let rust_fill = if info.default_uuid_v7 {
2511                Some(quote!(#root::__uuid::Uuid::now_v7()))
2512            } else if info.auto_now_add || info.auto_now {
2513                Some(quote!(#root::__chrono::Utc::now()))
2514            } else {
2515                None
2516            };
2517            // Bulk: Auto fields appear in the all-Set path. An unset filled
2518            // field takes its fill there too, not NULL (#1950).
2519            out.bulk_columns_all.push(quote!(#column));
2520            if let Some(fill) = &rust_fill {
2521                let push = quote! {
2522                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2523                        match &_row.#ident {
2524                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2525                            #root::sql::Auto::Unset => #fill,
2526                        }
2527                    ));
2528                };
2529                out.bulk_columns_no_auto.push(quote!(#column));
2530                out.bulk_pushes_no_auto.push(push.clone());
2531                out.bulk_pushes_all.push(push);
2532            } else {
2533                out.bulk_pushes_all.push(quote! {
2534                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2535                        ::core::clone::Clone::clone(&_row.#ident)
2536                    ));
2537                });
2538            }
2539            if info.default_uuid_v7 {
2540                out.bulk_fill_in_place.push(quote! {
2541                    if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2542                        _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2543                    }
2544                });
2545            }
2546            // Uniformity check: every row's Auto state must match the
2547            // first row's. Mixed Set/Unset within one bulk_insert is
2548            // rejected here so the column list stays consistent.
2549            // A filled field may be unset on any row, so it is exempt.
2550            let ident_clone = ident.clone();
2551            if rust_fill.is_none() {
2552                out.bulk_auto_uniformity.push(quote! {
2553                    for _r in rows.iter().skip(1) {
2554                        if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2555                            return ::core::result::Result::Err(
2556                                #root::sql::ExecError::Sql(
2557                                    #root::sql::SqlError::BulkAutoMixed
2558                                )
2559                            );
2560                        }
2561                    }
2562                });
2563            }
2564        } else {
2565            out.insert_pushes.push(quote! {
2566                _columns.push(#column);
2567                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2568                    ::core::clone::Clone::clone(&self.#ident)
2569                ));
2570            });
2571            // Bulk: non-Auto fields appear in BOTH paths.
2572            out.bulk_columns_no_auto.push(quote!(#column));
2573            out.bulk_columns_all.push(quote!(#column));
2574            let push_expr = quote! {
2575                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2576                    ::core::clone::Clone::clone(&_row.#ident)
2577                ));
2578            };
2579            out.bulk_pushes_no_auto.push(push_expr.clone());
2580            out.bulk_pushes_all.push(push_expr);
2581        }
2582        if info.primary_key {
2583            if out.primary_key.is_some() {
2584                return Err(syn::Error::new_spanned(
2585                    field,
2586                    "only one field may be marked `#[rustango(primary_key)]`",
2587                ));
2588            }
2589            out.primary_key = Some((ident.clone(), info.column.clone()));
2590            if info.auto {
2591                out.pk_is_auto = true;
2592            }
2593        } else if info.auto_now_add {
2594            // Immutable post-insert: skip from UPDATE entirely.
2595        } else if info.auto_now {
2596            // `auto_now` columns: bind `chrono::Utc::now()` on every
2597            // UPDATE, and write it back so `self` matches the row.
2598            out.update_assignments.push(quote! {
2599                #root::core::Assignment::new(
2600                    #column,
2601                    {
2602                        let _now = #root::__chrono::Utc::now();
2603                        self.#ident = #root::sql::Auto::Set(_now);
2604                        ::core::convert::Into::<#root::core::SqlValue>::into(_now)
2605                    },
2606                )
2607            });
2608            out.upsert_update_columns.push(quote!(#column));
2609        } else {
2610            out.update_assignments.push(quote! {
2611                #root::core::Assignment::new(
2612                    #column,
2613                    ::core::convert::Into::<#root::core::SqlValue>::into(
2614                        ::core::clone::Clone::clone(&self.#ident)
2615                    ),
2616                )
2617            });
2618            out.upsert_update_columns.push(quote!(#column));
2619        }
2620        out.column_entries.push(ColumnEntry {
2621            ident: ident.clone(),
2622            value_ty: info.value_ty.clone(),
2623            name: ident_name(ident),
2624            column: info.column.clone(),
2625            field_type_tokens: info.field_type_tokens,
2626            uuid_column: info.uuid_column,
2627        });
2628    }
2629    Ok(out)
2630}
2631
2632/// A derived index name within PG's 63 bytes (MySQL takes 64); a longer one
2633/// keeps a prefix plus a hash of the whole, so two long names stay distinct.
2634fn capped_index_name(name: String) -> String {
2635    const MAX: usize = 63;
2636    if name.len() <= MAX {
2637        return name;
2638    }
2639    // FNV-1a: stable across builds, unlike `DefaultHasher`.
2640    let hash = name.bytes().fold(0x811c_9dc5_u32, |h, b| {
2641        (h ^ u32::from(b)).wrapping_mul(0x0100_0193)
2642    });
2643    let mut cut = MAX - 9;
2644    while !name.is_char_boundary(cut) {
2645        cut -= 1;
2646    }
2647    format!("{}_{hash:08x}", &name[..cut])
2648}
2649
2650fn model_impl_tokens(
2651    struct_name: &syn::Ident,
2652    model_name: &str,
2653    table: &str,
2654    display: Option<&str>,
2655    app_label: Option<&str>,
2656    admin: Option<&AdminAttrs>,
2657    default_order: &[(String, bool, proc_macro2::Span)],
2658    field_schemas: &[TokenStream2],
2659    soft_delete_column: Option<&str>,
2660    permissions: bool,
2661    audit_track: Option<&[String]>,
2662    m2m_relations: &[M2MAttr],
2663    indexes: &[IndexAttr],
2664    checks: &[CheckAttr],
2665    excludes: &[ExcludeAttr],
2666    composite_fks: &[CompositeFkAttr],
2667    generic_fks: &[GenericFkAttr],
2668    scope: Option<&str>,
2669    is_view: bool,
2670    verbose_name: Option<&str>,
2671    verbose_name_plural: Option<&str>,
2672    managed: bool,
2673    base_manager_name: Option<&str>,
2674    order_with_respect_to: Option<&str>,
2675    proxy: bool,
2676    required_db_features: &[String],
2677    required_db_vendor: Option<&str>,
2678    default_related_name: Option<&str>,
2679    db_table_comment: Option<&str>,
2680    get_latest_by: Option<(&str, bool)>,
2681    extra_permissions: &[(String, String)],
2682    default_permissions: &[String],
2683    global_scopes: &[GlobalScopeAttr],
2684    reverse_has_relations: &[ReverseHasAttr],
2685    generic_has_relations: &[GenericHasAttr],
2686) -> TokenStream2 {
2687    let root = rustango_root();
2688    let display_tokens = if let Some(name) = display {
2689        quote!(::core::option::Option::Some(#name))
2690    } else {
2691        quote!(::core::option::Option::None)
2692    };
2693    let app_label_tokens = if let Some(name) = app_label {
2694        quote!(::core::option::Option::Some(#name))
2695    } else {
2696        quote!(::core::option::Option::None)
2697    };
2698    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2699        quote!(::core::option::Option::Some(#col))
2700    } else {
2701        quote!(::core::option::Option::None)
2702    };
2703    let audit_track_tokens = match audit_track {
2704        None => quote!(::core::option::Option::None),
2705        Some(names) => {
2706            let lits = names.iter().map(|n| n.as_str());
2707            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2708        }
2709    };
2710    let admin_tokens = admin_config_tokens(admin);
2711    // Default `tenant` so single-tenant projects (no `scope` attr
2712    // anywhere) keep the v0.24.x behavior. Container-attr parser
2713    // already validated the value is "registry" or "tenant".
2714    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2715        Some("registry") => quote!(#root::core::ModelScope::Registry),
2716        _ => quote!(#root::core::ModelScope::Tenant),
2717    };
2718    let verbose_name_tokens = optional_str(verbose_name);
2719    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2720    let base_manager_name_tokens = optional_str(base_manager_name);
2721    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2722    let required_db_features_lits: Vec<&str> =
2723        required_db_features.iter().map(String::as_str).collect();
2724    let required_db_vendor_tokens = optional_str(required_db_vendor);
2725    let default_related_name_tokens = optional_str(default_related_name);
2726    let db_table_comment_tokens = optional_str(db_table_comment);
2727    let get_latest_by_tokens = match get_latest_by {
2728        Some((col, desc)) => {
2729            quote!(::core::option::Option::Some((#col, #desc)))
2730        }
2731        None => quote!(::core::option::Option::None),
2732    };
2733    let extra_permission_tokens: Vec<_> = extra_permissions
2734        .iter()
2735        .map(|(c, l)| quote!((#c, #l)))
2736        .collect();
2737    let default_permission_tokens: Vec<_> = default_permissions
2738        .iter()
2739        .map(|action| quote!(#action))
2740        .collect();
2741    let indexes_tokens = indexes.iter().map(|idx| {
2742        // When no explicit `name = "..."` was given, derive a stable,
2743        // collision-free name from the table + columns
2744        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2745        // would clash the moment a model declares two unnamed indexes.
2746        let derived_name = idx
2747            .name
2748            .clone()
2749            .unwrap_or_else(|| capped_index_name(format!("{table}_{}_idx", idx.columns.join("_"))));
2750        let name = derived_name.as_str();
2751        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2752        let unique = idx.unique;
2753        // Map the parsed method string onto the IndexMethod enum
2754        // variant — kept at the codegen layer so the IR doesn't
2755        // carry the string form.
2756        let method_variant = match idx.method.as_str() {
2757            "gin" => quote!(#root::core::IndexMethod::Gin),
2758            "gist" => quote!(#root::core::IndexMethod::Gist),
2759            "brin" => quote!(#root::core::IndexMethod::Brin),
2760            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2761            "hash" => quote!(#root::core::IndexMethod::Hash),
2762            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2763            _ => quote!(#root::core::IndexMethod::BTree),
2764        };
2765        let where_clause = match &idx.where_clause {
2766            Some(s) => quote!(::core::option::Option::Some(#s)),
2767            None => quote!(::core::option::Option::None),
2768        };
2769        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2770        quote! {{
2771            let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2772            i.unique = #unique;
2773            i.method = #method_variant;
2774            i.where_clause = #where_clause;
2775            i.include = &[ #(#include_lits),* ];
2776            i
2777        }}
2778    });
2779    let checks_tokens = checks.iter().map(|c| {
2780        let name = c.name.as_str();
2781        let expr = c.expr.as_str();
2782        quote!(#root::core::CheckConstraint::new(#name, #expr))
2783    });
2784    let excludes_tokens = excludes.iter().map(|e| {
2785        let name = e.name.as_str();
2786        let using = e.using.as_str();
2787        let element_tokens = e.elements.iter().map(|(col, op)| {
2788            let col_s = col.as_str();
2789            let op_s = op.as_str();
2790            quote!((#col_s, #op_s))
2791        });
2792        let where_tokens = match e.where_clause.as_deref() {
2793            Some(w) => quote!(::core::option::Option::Some(#w)),
2794            None => quote!(::core::option::Option::None),
2795        };
2796        quote! {{
2797            let mut e = #root::core::ExclusionConstraint::new(
2798                #name,
2799                #using,
2800                &[ #(#element_tokens),* ],
2801            );
2802            e.where_clause = #where_tokens;
2803            e
2804        }}
2805    });
2806    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2807        let name = rel.name.as_str();
2808        let to = rel.to.as_str();
2809        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2810        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2811        quote! {
2812            #root::core::CompositeFkRelation::new(
2813                #name,
2814                #to,
2815                &[ #(#from_cols),* ],
2816                &[ #(#on_cols),* ],
2817            )
2818        }
2819    });
2820    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2821        let name = rel.name.as_str();
2822        let ct_col = rel.ct_column.as_str();
2823        let pk_col = rel.pk_column.as_str();
2824        quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2825    });
2826    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2827    // no `#[rustango(default_order = "...")]` attribute was supplied.
2828    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2829        let col_lit = col.as_str();
2830        quote! { (#col_lit, #desc) }
2831    });
2832
2833    // Issue #820 — `global_scopes` slice literal. Empty when no
2834    // `#[rustango(global_scope(...))]` attribute was supplied. The
2835    // `apply` path is re-emitted verbatim so it resolves in the
2836    // consumer's scope; the name is stored as a string literal.
2837    let global_scope_tokens = global_scopes.iter().map(|s| {
2838        let name = s.name.as_str();
2839        // The slice sits in a nested `const`, where `Self` does not resolve.
2840        let mut apply = s.apply.clone();
2841        if let Some(first) = apply.segments.first_mut() {
2842            if first.ident == "Self" {
2843                first.ident = struct_name.clone();
2844            }
2845        }
2846        quote!(#root::core::GlobalScope::new(#name, #apply))
2847    });
2848
2849    let m2m_tokens = m2m_relations.iter().map(|rel| {
2850        let name = rel.name.as_str();
2851        let to = rel.to.as_str();
2852        let through = rel.through.as_str();
2853        let src = rel.src.as_str();
2854        let dst = rel.dst.as_str();
2855        let auto_create = rel.auto_create;
2856        quote! {{
2857            let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2858            m.auto_create = #auto_create;
2859            m
2860        }}
2861    });
2862    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2863    // when the model declares `#[rustango(reverse_has(...))]`. Each
2864    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2865    // a const expression. Models without reverse_has fall through to
2866    // the trait's empty default — no override emitted.
2867    let reverse_relations_override = if reverse_has_relations.is_empty() {
2868        quote!()
2869    } else {
2870        let entries = reverse_has_relations.iter().map(|rel| {
2871            let name = rel.name.as_str();
2872            let child = &rel.child;
2873            let child_fk_column = rel.child_fk_column.as_str();
2874            let self_pk_column = rel.self_pk_column();
2875            quote! {
2876                #root::core::ReverseRelation::new(
2877                    #name,
2878                    <#child as #root::core::Model>::SCHEMA,
2879                    #child_fk_column,
2880                    #self_pk_column,
2881                )
2882            }
2883        });
2884        quote! {
2885            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2886                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2887                RELS
2888            }
2889        }
2890    };
2891    // Issue #830 — `Model::generic_reverse_relations()` override for
2892    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2893    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2894        quote!()
2895    } else {
2896        let entries = generic_has_relations.iter().map(|rel| {
2897            let name = rel.name.as_str();
2898            let child = &rel.child;
2899            let ct_column = rel.ct_column.as_str();
2900            let pk_column = rel.pk_column.as_str();
2901            let self_pk_column = rel.self_pk_column();
2902            quote! {
2903                #root::core::GenericReverseRelation::new(
2904                    #name,
2905                    <#child as #root::core::Model>::SCHEMA,
2906                    #ct_column,
2907                    #pk_column,
2908                    #self_pk_column,
2909                )
2910            }
2911        });
2912        quote! {
2913            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2914                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2915                RELS
2916            }
2917        }
2918    };
2919    let fields_slice = const_slice(
2920        &quote!(#root::core::FieldSchema),
2921        field_schemas.iter().cloned(),
2922    );
2923    let m2m_slice = const_slice(&quote!(#root::core::M2MRelation), m2m_tokens);
2924    let indexes_slice = const_slice(&quote!(#root::core::IndexSchema), indexes_tokens);
2925    let checks_slice = const_slice(&quote!(#root::core::CheckConstraint), checks_tokens);
2926    let excludes_slice = const_slice(&quote!(#root::core::ExclusionConstraint), excludes_tokens);
2927    let composite_fk_slice = const_slice(
2928        &quote!(#root::core::CompositeFkRelation),
2929        composite_fk_tokens,
2930    );
2931    let generic_fk_slice = const_slice(&quote!(#root::core::GenericRelation), generic_fk_tokens);
2932    let audited_update_override = if audit_track.is_some() {
2933        quote! {
2934            fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2935                ::core::option::Option::Some(Self::__rustango_update_audited)
2936            }
2937            fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2938                ::core::option::Option::Some(Self::__rustango_delete_audited)
2939            }
2940            fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2941                ::core::option::Option::Some(Self::__rustango_create_audited)
2942            }
2943            fn __rustango_audited_update_record(
2944            ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
2945                ::core::option::Option::Some(Self::__rustango_update_record_audited)
2946            }
2947            fn __rustango_audited_cascade() -> ::core::option::Option<#root::audit::AuditedCascade> {
2948                ::core::option::Option::Some(Self::__rustango_cascade_audited)
2949            }
2950        }
2951    } else {
2952        quote!()
2953    };
2954    let global_scope_slice = const_slice(&quote!(#root::core::GlobalScope), global_scope_tokens);
2955    quote! {
2956        impl #root::core::Model for #struct_name {
2957            const SCHEMA: &'static #root::core::ModelSchema = &{
2958                #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2959                    name: #model_name,
2960                    table: #table,
2961                    fields: #fields_slice,
2962                    display: #display_tokens,
2963                    app_label: #app_label_tokens,
2964                    admin: #admin_tokens,
2965                    soft_delete_column: #soft_delete_tokens,
2966                    permissions: #permissions,
2967                    audit_track: #audit_track_tokens,
2968                    m2m: #m2m_slice,
2969                    indexes: #indexes_slice,
2970                    check_constraints: #checks_slice,
2971                    exclusion_constraints: #excludes_slice,
2972                    composite_relations: #composite_fk_slice,
2973                    generic_relations: #generic_fk_slice,
2974                    scope: #scope_tokens,
2975                    default_order: &[ #(#default_order_tokens),* ],
2976                    is_view: #is_view,
2977                    verbose_name: #verbose_name_tokens,
2978                    verbose_name_plural: #verbose_name_plural_tokens,
2979                    managed: #managed,
2980                    base_manager_name: #base_manager_name_tokens,
2981                    order_with_respect_to: #order_with_respect_to_tokens,
2982                    proxy: #proxy,
2983                    required_db_features: &[ #(#required_db_features_lits),* ],
2984                    required_db_vendor: #required_db_vendor_tokens,
2985                    default_related_name: #default_related_name_tokens,
2986                    db_table_comment: #db_table_comment_tokens,
2987                    get_latest_by: #get_latest_by_tokens,
2988                    extra_permissions: &[ #(#extra_permission_tokens),* ],
2989                    default_permissions: &[ #(#default_permission_tokens),* ],
2990                    global_scopes: #global_scope_slice,
2991                })
2992            };
2993
2994            #reverse_relations_override
2995            #generic_reverse_relations_override
2996            #audited_update_override
2997        }
2998    }
2999}
3000
3001/// Emit the `admin: Option<&'static AdminConfig>` field for the model
3002/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
3003/// otherwise a static reference to a populated `AdminConfig`.
3004fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
3005    let root = rustango_root();
3006    let Some(admin) = admin else {
3007        return quote!(::core::option::Option::None);
3008    };
3009
3010    let list_display = admin
3011        .list_display
3012        .as_ref()
3013        .map(|(v, _)| v.as_slice())
3014        .unwrap_or(&[]);
3015    let list_display_lits = list_display.iter().map(|s| s.as_str());
3016
3017    let search_fields = admin
3018        .search_fields
3019        .as_ref()
3020        .map(|(v, _)| v.as_slice())
3021        .unwrap_or(&[]);
3022    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
3023
3024    let readonly_fields = admin
3025        .readonly_fields
3026        .as_ref()
3027        .map(|(v, _)| v.as_slice())
3028        .unwrap_or(&[]);
3029    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
3030
3031    let list_filter = admin
3032        .list_filter
3033        .as_ref()
3034        .map(|(v, _)| v.as_slice())
3035        .unwrap_or(&[]);
3036    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
3037
3038    let actions = admin
3039        .actions
3040        .as_ref()
3041        .map(|(v, _)| v.as_slice())
3042        .unwrap_or(&[]);
3043    let actions_lits = actions.iter().map(|s| s.as_str());
3044
3045    let fieldsets = admin
3046        .fieldsets
3047        .as_ref()
3048        .map(|(v, _)| v.as_slice())
3049        .unwrap_or(&[]);
3050    let fieldset_tokens = const_slice(
3051        &quote!(#root::core::Fieldset),
3052        fieldsets.iter().map(|(title, fields)| {
3053            let title = title.as_str();
3054            let field_lits = fields.iter().map(String::as_str);
3055            quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
3056        }),
3057    );
3058
3059    let list_display_links = admin
3060        .list_display_links
3061        .as_ref()
3062        .map(|(v, _)| v.as_slice())
3063        .unwrap_or(&[]);
3064    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
3065
3066    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
3067    let actions_on_top = admin.actions_on_top.unwrap_or(true);
3068    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
3069    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
3070
3071    let prepopulated = admin
3072        .prepopulated_fields
3073        .as_ref()
3074        .map(|(v, _)| v.as_slice())
3075        .unwrap_or(&[]);
3076    let prepopulated_tokens = const_slice(
3077        &quote!(#root::core::PrepopulatedField),
3078        prepopulated.iter().map(|(target, sources)| {
3079            let target = target.as_str();
3080            let source_lits = sources.iter().map(String::as_str);
3081            quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
3082        }),
3083    );
3084
3085    let raw_id_fields = admin
3086        .raw_id_fields
3087        .as_ref()
3088        .map(|(v, _)| v.as_slice())
3089        .unwrap_or(&[]);
3090    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
3091
3092    let autocomplete_fields = admin
3093        .autocomplete_fields
3094        .as_ref()
3095        .map(|(v, _)| v.as_slice())
3096        .unwrap_or(&[]);
3097    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
3098
3099    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
3100    let list_select_related_tokens = match admin.list_select_related.as_deref() {
3101        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
3102        Some("none") => quote!(#root::core::ListSelectRelated::None),
3103        Some(raw) => {
3104            let names: Vec<&str> = raw
3105                .split(',')
3106                .map(str::trim)
3107                .filter(|s| !s.is_empty())
3108                .collect();
3109            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
3110        }
3111    };
3112
3113    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
3114    // a Vec<(String, String)>. Empty / unset → no overrides.
3115    let formfield_pairs: Vec<(&str, &str)> = admin
3116        .formfield_overrides
3117        .as_ref()
3118        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
3119        .unwrap_or_default();
3120    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
3121        let field = *field;
3122        let widget = *widget;
3123        quote!((#field, #widget))
3124    });
3125
3126    let list_per_page = admin.list_per_page.unwrap_or(0);
3127
3128    let ordering_pairs = admin
3129        .ordering
3130        .as_ref()
3131        .map(|(v, _)| v.as_slice())
3132        .unwrap_or(&[]);
3133    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3134        let name = name.as_str();
3135        let desc = *desc;
3136        quote!((#name, #desc))
3137    });
3138
3139    quote! {
3140        ::core::option::Option::Some({
3141            const ADMIN: &#root::core::AdminConfig = &{
3142                #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3143                    list_display: &[ #( #list_display_lits ),* ],
3144                    search_fields: &[ #( #search_fields_lits ),* ],
3145                    list_per_page: #list_per_page,
3146                    ordering: &[ #( #ordering_tokens ),* ],
3147                    readonly_fields: &[ #( #readonly_fields_lits ),* ],
3148                    list_filter: &[ #( #list_filter_lits ),* ],
3149                    actions: &[ #( #actions_lits ),* ],
3150                    fieldsets: #fieldset_tokens,
3151                    list_display_links: &[ #( #list_display_links_lits ),* ],
3152                    search_help_text: #search_help_text,
3153                    actions_on_top: #actions_on_top,
3154                    actions_on_bottom: #actions_on_bottom,
3155                    date_hierarchy: #date_hierarchy,
3156                    prepopulated_fields: #prepopulated_tokens,
3157                    raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3158                    autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3159                    list_select_related: #list_select_related_tokens,
3160                    formfield_overrides: &[ #( #formfield_tokens ),* ],
3161                })
3162            };
3163            ADMIN
3164        })
3165    }
3166}
3167
3168fn inherent_impl_tokens(
3169    struct_name: &syn::Ident,
3170    fields: &CollectedFields,
3171    primary_key: Option<&(syn::Ident, String)>,
3172    column_consts: &TokenStream2,
3173    audited_fields: Option<&[&ColumnEntry]>,
3174    upsert_unique: Option<&[String]>,
3175    manager_fns: &[syn::Ident],
3176) -> TokenStream2 {
3177    let root = rustango_root();
3178
3179    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3180    // (audit-on-connection over &Pool needs a bi-dialect transaction
3181    // helper, deferred). Two body shapes:
3182    // - has_auto: build InsertQuery skipping Auto::Unset columns,
3183    //   request Auto cols in `returning`, dispatch via
3184    //   `insert_returning_pool`, then on the returned `PgRow` /
3185    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
3186    //   row OR drop the single i64 into the first Auto field on MySQL
3187    //   (multi-Auto models on MySQL error at runtime since
3188    //   `LAST_INSERT_ID()` only reports one)
3189    // - non-Auto: build InsertQuery with explicit columns/values and
3190    //   call `insert_pool` (no returning needed)
3191    // pool_insert_method body for the audited Auto-PK case is moved
3192    // to after audit_pair_tokens / audit_pk_to_string (they live
3193    // ~150 lines below). This block keeps the non-audited and
3194    // non-Auto branches in place — the audited Auto-PK arm is
3195    // computed below and merged via the dispatch helper variable.
3196    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3197        // Audited models with explicit (non-Auto) PKs go through
3198        // the non-Auto insert path below — the audit emit is one
3199        // round-trip after the INSERT inside the same tx via
3200        // audit::save_one_with_audit? No, INSERT semantics
3201        // differ. For non-Auto PK + audited, route through a
3202        // dedicated insert + audit emit on the same tx, but defer
3203        // the macro emission to the audit-bundle-aware block below
3204        // — this `quote!()` placeholder gets overwritten there.
3205        quote!()
3206    } else if audited_fields.is_some() && fields.has_auto {
3207        // Audited Auto-PK insert_pool — assembled after the audit
3208        // bundles. Placeholder; real emission below.
3209        quote!()
3210    } else if fields.has_auto {
3211        let pushes = &fields.insert_pushes;
3212        // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3213        let filled = &fields.rust_filled_idents;
3214        let was_unset: Vec<syn::Ident> = filled
3215            .iter()
3216            .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3217            .collect();
3218        let insert_or_ignore_call = quote! {
3219            #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3220            let mut _columns: ::std::vec::Vec<&'static str> =
3221                ::std::vec::Vec::new();
3222            let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3223                ::std::vec::Vec::new();
3224            #( #pushes )*
3225            let _query = #root::core::InsertQuery::new(
3226                <Self as #root::core::Model>::SCHEMA,
3227                _columns,
3228                _values,
3229            );
3230            let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3231            if !matches!(_result, ::core::result::Result::Ok(true)) {
3232                #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3233            }
3234            _result
3235        };
3236        // When every `Auto<T>` field is filled Rust-side
3237        // (`default_uuid_v7`, issue #823), there is no column to read
3238        // back from the database — `returning_cols` is empty. Route
3239        // through plain `insert_pool` instead of
3240        // `insert_returning_pool` to skip the redundant RETURNING /
3241        // LAST_INSERT_ID round-trip.
3242        if fields.returning_cols.is_empty() {
3243            quote! {
3244
3245
3246                /// Eloquent `Model::insertOrIgnore()` — same shape
3247                /// as the auto-PK branch above. Returns `Ok(true)`
3248                /// when inserted, `Ok(false)` when a conflict caused
3249                /// the INSERT to silently skip.
3250                ///
3251                /// # Errors
3252                /// As [`Self::insert`].
3253                pub async fn insert_or_ignore(
3254                    &mut self,
3255                    pool: &#root::sql::Pool,
3256                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3257                    #insert_or_ignore_call
3258                }
3259            }
3260        } else {
3261            quote! {
3262
3263
3264                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3265                /// row or silently skip on unique-constraint
3266                /// violation. Returns `Ok(true)` when a row was
3267                /// inserted, `Ok(false)` when a conflict caused the
3268                /// INSERT to silently skip.
3269                ///
3270                /// **Caveat on auto-PK models**: when the row is
3271                /// skipped (conflict), this instance's `Auto<T>`
3272                /// fields stay `Unset` — no PK is back-populated
3273                /// because the server didn't auto-assign one. For
3274                /// "insert then read back the PK or the existing
3275                /// row's PK", use the `upsert` family or
3276                /// `get_or_create`.
3277                ///
3278                /// # Errors
3279                /// As [`Self::insert`].
3280                pub async fn insert_or_ignore(
3281                    &mut self,
3282                    pool: &#root::sql::Pool,
3283                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3284                    #insert_or_ignore_call
3285                }
3286            }
3287        }
3288    } else {
3289        let insert_columns = &fields.insert_columns;
3290        let insert_values = &fields.insert_values;
3291        quote! {
3292
3293
3294            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3295            /// or silently skip on unique-constraint violation. Maps
3296            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3297            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3298            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3299            /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3300            ///
3301            /// Returns `Ok(true)` when a row was inserted,
3302            /// `Ok(false)` when a conflict caused the INSERT to
3303            /// silently skip.
3304            ///
3305            /// Use for "create-if-absent" patterns where you don't
3306            /// need the row back. For "find-or-create with the row
3307            /// returned", use the queryset-level
3308            /// `crate::sql::get_or_create` free function.
3309            ///
3310            /// # Errors
3311            /// As [`Self::insert`], plus any dialect-specific
3312            /// translation error from the ConflictClause writer.
3313            pub async fn insert_or_ignore(
3314                &self,
3315                pool: &#root::sql::Pool,
3316            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3317                let _query = #root::core::InsertQuery::new(
3318                    <Self as #root::core::Model>::SCHEMA,
3319                    ::std::vec![ #( #insert_columns ),* ],
3320                    ::std::vec![ #( #insert_values ),* ],
3321                );
3322                #root::sql::insert_or_ignore(pool, &_query).await
3323            }
3324        }
3325    };
3326
3327    // The tracked (column, JSON value) pairs, spliced only into
3328    // `__rustango_audit_pairs`. Empty when the model isn't audited.
3329    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3330        .map(|tracked| {
3331            tracked
3332                .iter()
3333                .map(|c| {
3334                    let column_lit = c.column.as_str();
3335                    let ident = &c.ident;
3336                    quote! {
3337                        (
3338                            #column_lit,
3339                            #root::core::__json_value(&self.#ident),
3340                        )
3341                    }
3342                })
3343                .collect()
3344        })
3345        .unwrap_or_default();
3346    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3347        if fields.pk_is_auto {
3348            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3349        } else {
3350            quote!(::std::format!("{}", &self.#pk_ident))
3351        }
3352    } else {
3353        quote!(::std::string::String::new())
3354    };
3355    // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3356    // is built, so every audited write path records the same shape.
3357    let delete_audited_body = if primary_key.is_some() {
3358        quote! {
3359            ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3360                pool,
3361                query,
3362                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3363            ))
3364        }
3365    } else {
3366        quote! {
3367            let _ = (pool, query);
3368            ::std::boxed::Box::pin(async {
3369                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3370                    table: <Self as #root::core::Model>::SCHEMA.table,
3371                })
3372            })
3373        }
3374    };
3375    let cascade_audited_body = if primary_key.is_some() {
3376        quote! {
3377            ::std::boxed::Box::pin(#root::audit::cascade_rows::<Self>(
3378                tx,
3379                <Self as #root::core::Model>::SCHEMA,
3380                rows,
3381                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3382            ))
3383        }
3384    } else {
3385        quote! {
3386            let _ = (tx, rows);
3387            ::std::boxed::Box::pin(async {
3388                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3389                    table: <Self as #root::core::Model>::SCHEMA.table,
3390                })
3391            })
3392        }
3393    };
3394    let update_audited_body = if primary_key.is_some() {
3395        quote! {
3396            ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3397                pool,
3398                query,
3399                move |_r: &Self| _r.__rustango_audit_entry(op),
3400            ))
3401        }
3402    } else {
3403        quote! {
3404            let _ = (pool, query, op);
3405            ::std::boxed::Box::pin(async {
3406                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3407                    table: <Self as #root::core::Model>::SCHEMA.table,
3408                })
3409            })
3410        }
3411    };
3412    let audit_entry_method = if audited_fields.is_some() {
3413        let pairs = audit_pair_tokens.iter();
3414        let pk_str = audit_pk_to_string.clone();
3415        quote! {
3416            /// This row's tracked `(column, value)` pairs.
3417            #[doc(hidden)]
3418            pub fn __rustango_audit_pairs(
3419                &self,
3420            ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3421                ::std::vec![ #( #pairs ),* ]
3422            }
3423
3424            /// This row's audit PK.
3425            #[doc(hidden)]
3426            pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3427                #pk_str
3428            }
3429
3430            /// Snapshot audit entry for `operation` on this row.
3431            #[doc(hidden)]
3432            pub fn __rustango_audit_entry(
3433                &self,
3434                operation: #root::audit::AuditOp,
3435            ) -> #root::audit::PendingEntry {
3436                #root::audit::PendingEntry {
3437                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3438                    entity_pk: self.__rustango_audit_pk(),
3439                    operation,
3440                    source: #root::audit::current_source(),
3441                    changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3442                }
3443            }
3444
3445            /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3446            #[doc(hidden)]
3447            pub fn __rustango_update_audited<'a>(
3448                pool: &'a #root::sql::Pool,
3449                query: &'a #root::core::UpdateQuery,
3450                op: #root::audit::AuditOp,
3451            ) -> ::std::pin::Pin<::std::boxed::Box<
3452                dyn ::core::future::Future<
3453                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3454                > + ::core::marker::Send + 'a,
3455            >> {
3456                #update_audited_body
3457            }
3458
3459            /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3460            #[doc(hidden)]
3461            pub fn __rustango_create_audited<'a, 't>(
3462                tx: &'a mut #root::sql::PoolTx<'t>,
3463                pk: #root::core::SqlValue,
3464            ) -> ::std::pin::Pin<::std::boxed::Box<
3465                dyn ::core::future::Future<
3466                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3467                > + ::core::marker::Send + 'a,
3468            >> {
3469                ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3470                    tx,
3471                    <Self as #root::core::Model>::SCHEMA,
3472                    pk,
3473                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3474                ))
3475            }
3476
3477            /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3478            #[doc(hidden)]
3479            pub fn __rustango_update_record_audited<'a, 't>(
3480                tx: &'a mut #root::sql::PoolTx<'t>,
3481                pk: #root::core::SqlValue,
3482            ) -> ::std::pin::Pin<::std::boxed::Box<
3483                dyn ::core::future::Future<
3484                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3485                > + ::core::marker::Send + 'a,
3486            >> {
3487                ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3488                    tx,
3489                    <Self as #root::core::Model>::SCHEMA,
3490                    pk,
3491                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3492                ))
3493            }
3494
3495            /// Rows an FK cascade will delete, behind `Model::__rustango_audited_cascade`.
3496            #[doc(hidden)]
3497            pub fn __rustango_cascade_audited<'a, 't>(
3498                tx: &'a mut #root::sql::PoolTx<'t>,
3499                rows: #root::core::WhereExpr,
3500            ) -> ::std::pin::Pin<::std::boxed::Box<
3501                dyn ::core::future::Future<
3502                    Output = ::core::result::Result<
3503                        ::std::vec::Vec<(#root::core::SqlValue, #root::audit::PendingEntry)>,
3504                        #root::sql::ExecError,
3505                    >,
3506                > + ::core::marker::Send + 'a,
3507            >> {
3508                #cascade_audited_body
3509            }
3510
3511            /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3512            #[doc(hidden)]
3513            pub fn __rustango_delete_audited<'a>(
3514                pool: &'a #root::sql::Pool,
3515                query: &'a #root::core::DeleteQuery,
3516            ) -> ::std::pin::Pin<::std::boxed::Box<
3517                dyn ::core::future::Future<
3518                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3519                > + ::core::marker::Send + 'a,
3520            >> {
3521                #delete_audited_body
3522            }
3523        }
3524    } else {
3525        quote!()
3526    };
3527    // Builds `_audit_entry`; `written` is the soft-delete column's new
3528    // value, recorded in place of the stale `self` field.
3529    let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3530        if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3531            quote! {
3532                let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3533                _audit_entry.set_tracked(#col, #v);
3534            }
3535        } else {
3536            quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3537        }
3538    };
3539    let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3540        .unwrap_or(#root::__serde_json::Value::Null));
3541    let restored_json = quote!(#root::__serde_json::Value::Null);
3542    // `&Pool` update runner for soft_delete / restore: one tx with the
3543    // audit row when audited, a plain UPDATE otherwise.
3544    let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3545        if audited_fields.is_some() {
3546            let entry = make_entry(op_path, Some(written));
3547            quote! {
3548                #entry
3549                #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3550            }
3551        } else {
3552            quote!(#root::sql::update_pool(pool, &_query).await)
3553        }
3554    };
3555    let pool_softdelete_run =
3556        make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3557    let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3558
3559    // `save_pool(&Pool)` — emitted for every model with a PK. The
3560    // audited `save_pool` is built below on the diff path; both share
3561    // `save_partial_methods`, which differ only in how they run `_query`.
3562    let save_ret = quote!(::core::result::Result<u64, #root::sql::ExecError>);
3563    let save_keep = quote!(matches!(_saved, ::core::result::Result::Ok(n) if n > 0));
3564    let save_partial_methods = |pk_ident: &syn::Ident,
3565                                pk_column_lit: &str,
3566                                run: TokenStream2|
3567     -> TokenStream2 {
3568        let assignments = &fields.update_assignments;
3569        // Pushed in step with `update_assignments`: one column each.
3570        let assignment_cols = &fields.upsert_update_columns;
3571        let write = restore_auto_now_unless(
3572            &fields.auto_now_idents,
3573            &save_ret,
3574            &save_keep,
3575            &quote! {
3576                // Build only the wanted assignments, so an unnamed
3577                // `auto_now` field is not stamped.
3578                let mut _filtered: ::std::vec::Vec<#root::core::Assignment> =
3579                    ::std::vec::Vec::with_capacity(_wanted_cols.len());
3580                #(
3581                    if _wanted_cols.contains(#assignment_cols) {
3582                        _filtered.push(#assignments);
3583                    }
3584                )*
3585                if _filtered.is_empty() {
3586                    // All field names valid, but they all map to
3587                    // non-assignable slots (PK column, computed/
3588                    // virtual fields, relations without an
3589                    // assignment). Same no-op as an empty list.
3590                    #root::__tracing::warn!(
3591                        target: "rustango::save_partial",
3592                        model = _schema.name,
3593                        "save_partial: every named field maps to a non-assignable column — no-op"
3594                    );
3595                    return ::core::result::Result::Ok(0);
3596                }
3597                let _query = #root::core::UpdateQuery::new(
3598                    _schema,
3599                    _filtered,
3600                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3601                        #pk_column_lit,
3602                        #root::core::Op::Eq,
3603                        ::core::convert::Into::<#root::core::SqlValue>::into(
3604                            ::core::clone::Clone::clone(&self.#pk_ident)
3605                        ),
3606                    )),
3607                );
3608                #run
3609            },
3610        );
3611        quote! {
3612            /// Save (UPDATE) only the listed Rust-side fields,
3613            /// leaving every other column untouched. Issue #66.
3614            ///
3615            /// `fields` are Rust-side struct field names; the macro
3616            /// resolves each to its SQL column. Unknown field
3617            /// names return [`#root::core::QueryError::UnknownField`]
3618            /// wrapped in `ExecError::Query`. An empty list is a
3619            /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3620            /// On an audited model the audit diff covers only these fields.
3621            ///
3622            /// Use this when:
3623            /// * you only mutated a couple of fields on a wide row
3624            ///   (avoid re-writing every column on every save), or
3625            /// * two writers diverged after their initial read and
3626            ///   you want to preserve the other writer's changes to
3627            ///   columns you didn't touch.
3628            ///
3629            /// Auto-PK models with an unset PK return
3630            /// [`#root::core::QueryError::UnknownField`] with
3631            /// field name `<pk>` — `save_partial` is an
3632            /// UPDATE-only path. Call [`Self::insert`]
3633            /// (or [`Self::save`] which dispatches based on
3634            /// PK state) for the INSERT case.
3635            ///
3636            /// # Errors
3637            /// As [`Self::save`], plus `UnknownField` for
3638            /// unknown / empty / Auto-Unset cases.
3639            pub async fn save_partial(
3640                &mut self,
3641                fields: &[&str],
3642                pool: &#root::sql::Pool,
3643            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3644                if fields.is_empty() {
3645                    #root::__tracing::warn!(
3646                        target: "rustango::save_partial",
3647                        model = <Self as #root::core::Model>::SCHEMA.name,
3648                        "save_partial called with empty field list — no-op"
3649                    );
3650                    return ::core::result::Result::Ok(0);
3651                }
3652                let _schema = <Self as #root::core::Model>::SCHEMA;
3653                // Validate field names against the schema.
3654                let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3655                    ::std::collections::HashSet::with_capacity(fields.len());
3656                for f in fields {
3657                    match _schema.field(f) {
3658                        ::core::option::Option::Some(fs) => {
3659                            _wanted_cols.insert(fs.column);
3660                        }
3661                        ::core::option::Option::None => {
3662                            return ::core::result::Result::Err(
3663                                #root::sql::ExecError::Query(
3664                                    #root::core::QueryError::UnknownField {
3665                                        model: _schema.name,
3666                                        field: (*f).to_owned(),
3667                                    }
3668                                )
3669                            );
3670                        }
3671                    }
3672                }
3673                #write
3674            }
3675
3676            /// Typed-column counterpart of [`Self::save_partial`] —
3677            /// issue #67. `fields` is a tuple of [`Column`]
3678            /// constants whose `Model` matches `Self`; typos and
3679            /// model mismatches surface at *compile time*
3680            /// (`Author::name` inside a `Post::save_partial_typed`
3681            /// call is a type error, no runtime check).
3682            ///
3683            /// ```ignore
3684            /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3685            /// ```
3686            ///
3687            /// Lowers to [`Self::save_partial`] under the hood — the
3688            /// tuple is reduced to a `&[&str]` slice of Rust-side
3689            /// field names and forwarded.
3690            ///
3691            /// [`Column`]: #root::core::Column
3692            ///
3693            /// # Errors
3694            /// As [`Self::save_partial`].
3695            pub async fn save_partial_typed<
3696                L: #root::core::TypedFieldList<Self>,
3697            >(
3698                &mut self,
3699                fields: L,
3700                pool: &#root::sql::Pool,
3701            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3702                let _names = fields.rust_field_names();
3703                let _refs: ::std::vec::Vec<&str> =
3704                    _names.iter().copied().collect();
3705                self.save_partial(&_refs, pool).await
3706            }
3707        }
3708    };
3709    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3710        let pk_column_lit = pk_col.as_str();
3711        if audited_fields.is_some() {
3712            quote!()
3713        } else {
3714            let partial = save_partial_methods(
3715                pk_ident,
3716                pk_column_lit,
3717                quote!(#root::sql::update_pool(pool, &_query).await),
3718            );
3719            quote! {
3720                /// Save this row to its table against either backend.
3721                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3722                /// `UPDATE` keyed on the PK; global scopes do not apply.
3723                ///
3724                /// # Errors
3725                /// As [`Self::save`].
3726                pub async fn save(
3727                    &mut self,
3728                    pool: &#root::sql::Pool,
3729                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3730                    self.__rustango_save_exec(pool).await
3731                }
3732
3733                #partial
3734            }
3735        }
3736    } else {
3737        quote!()
3738    };
3739
3740    // Audited `insert_pool` (overrides the placeholder set higher up
3741    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3742    // audited models get insert_pool routing through
3743    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3744    // + auto-PK readback + audit emit). Snapshot-style audit (the
3745    // PendingEntry's `changes` carries post-write field values).
3746    let pool_insert_method = if audited_fields.is_some() {
3747        if let Some(_) = primary_key {
3748            // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
3749            let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
3750            {
3751                fields.returning_cols.clone()
3752            } else {
3753                // Non-Auto-PK: still need RETURNING something for the
3754                // audit helper's contract (it errors on empty
3755                // returning). Return the PK column so the audit row
3756                // can carry the assigned PK back. Some non-Auto PKs
3757                // are server-side-default (e.g. UUIDv4 default), so
3758                // RETURNING is genuinely useful.
3759                primary_key
3760                    .map(|(_, col)| {
3761                        let lit = col.as_str();
3762                        vec![quote!(#lit)]
3763                    })
3764                    .unwrap_or_default()
3765            };
3766            // A failed COMMIT rolls the row back: put the read-back values back too.
3767            let assigned: Vec<syn::Ident> = fields
3768                .auto_field_idents
3769                .iter()
3770                .map(|(ident, _, _)| ident.clone())
3771                .collect();
3772            let body = restore_auto_now_unless(
3773                &assigned,
3774                &quote!(::core::result::Result<(), #root::sql::ExecError>),
3775                &quote!(_saved.is_ok()),
3776                &quote! {
3777                    let mut _query = self.__rustango_insert_query();
3778                    // The audit row needs the PK even when Rust filled it.
3779                    if _query.returning.is_empty() {
3780                        _query.returning = ::std::vec![ #( #returning_cols ),* ];
3781                    }
3782                    #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self, _op| {
3783                        _m.__rustango_audit_entry(_op)
3784                    })
3785                    .await
3786                },
3787            );
3788            quote! {
3789                /// Insert this row on any backend, with its audit row in the
3790                /// same transaction.
3791                ///
3792                /// Snapshot-style audit (post-write field values).
3793                ///
3794                /// # Errors
3795                /// As [`Self::insert`].
3796                pub async fn insert(
3797                    &mut self,
3798                    pool: &#root::sql::Pool,
3799                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3800                    #body
3801                }
3802            }
3803        } else {
3804            quote!()
3805        }
3806    } else {
3807        // Keep the non-audited pool_insert_method we built earlier.
3808        pool_insert_method
3809    };
3810
3811    // Update audited save_pool: now that insert_pool is wired for
3812    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3813    // insert_pool. Non-audited save_pool already does this.
3814    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3815    // Replaces the snapshot-only emission with a per-backend transaction
3816    // body that:
3817    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3818    //     column), capturing BEFORE values
3819    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3820    //  3. builds AFTER pairs from &self
3821    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3822    //     AuditOp::Update + diff_changes(...) on the same tx connection
3823    //  5. commits
3824    //
3825    // Per-backend arms inline the SQL string + placeholder shape, then
3826    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3827    // is polymorphic over Row type — the same tokens work against
3828    // PgRow and MySqlRow as long as the field's Rust type implements
3829    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3830    // primitives + chrono/uuid/serde_json::Value all do).
3831    let pool_save_method = if let Some(tracked) = audited_fields {
3832        if let Some((pk_ident, pk_col)) = primary_key {
3833            let pk_column_lit = pk_col.as_str();
3834            let partial = save_partial_methods(
3835                pk_ident,
3836                pk_column_lit,
3837                quote! {
3838                    self.__rustango_save_with_diff(
3839                        pool,
3840                        &_query,
3841                        ::core::option::Option::Some(&_wanted_cols),
3842                    )
3843                    .await
3844                },
3845            );
3846            // Per-tracked-column BEFORE-pair token list. Each entry
3847            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3848            // The Row alias resolves to PgRow / MySqlRow per call site,
3849            // so the same template generates both the PG and MySQL bodies.
3850            let mk_before_pairs =
3851                |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
3852                    tracked
3853                        .iter()
3854                        .map(|c| {
3855                            let getter = getter(c);
3856                            let column_lit = c.column.as_str();
3857                            let value_ty = &c.value_ty;
3858                            quote! {
3859                                (
3860                                    #column_lit,
3861                                    match #getter::<#value_ty>(
3862                                        _audit_before_row, #column_lit,
3863                                    ) {
3864                                        ::core::result::Result::Ok(v) => {
3865                                            #root::core::__json_value(&v)
3866                                        }
3867                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3868                                    },
3869                                )
3870                            }
3871                        })
3872                        .collect()
3873                };
3874            let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
3875            let before_pairs_my = mk_before_pairs(&|c| {
3876                if c.uuid_column {
3877                    quote!(#root::sql::try_get_flat_my)
3878                } else {
3879                    quote!(#root::sql::try_get_returning_my)
3880                }
3881            });
3882            let before_pairs_sqlite =
3883                mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
3884            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
3885            let pk_value_for_bind = if fields.pk_is_auto {
3886                quote!(self.#pk_ident.get().copied().unwrap_or_default())
3887            } else {
3888                quote!(::core::clone::Clone::clone(&self.#pk_ident))
3889            };
3890            let assignments = &fields.update_assignments;
3891            let unset_dispatch = if fields.has_auto {
3892                quote! {
3893                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3894                        return self.insert(pool).await.map(|()| 1u64);
3895                    }
3896                }
3897            } else {
3898                quote!()
3899            };
3900            let write = restore_auto_now_unless(
3901                &fields.auto_now_idents,
3902                &save_ret,
3903                &save_keep,
3904                &quote! {
3905                    let _query = #root::core::UpdateQuery::new(
3906                        <Self as #root::core::Model>::SCHEMA,
3907                        ::std::vec![ #( #assignments ),* ],
3908                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3909                            #pk_column_lit,
3910                            #root::core::Op::Eq,
3911                            ::core::convert::Into::<#root::core::SqlValue>::into(
3912                                ::core::clone::Clone::clone(&self.#pk_ident)
3913                            ),
3914                        )),
3915                    );
3916                    self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
3917                        .await
3918                },
3919            );
3920            quote! {
3921                /// Save this row on any backend, with a diff audit row
3922                /// (before and after) in the same transaction. An `Unset`
3923                /// `Auto` PK inserts instead.
3924                ///
3925                /// The audit row's `changes` JSON contains one
3926                /// `{ "field": { "before": …, "after": … } }` entry
3927                /// per tracked column whose value actually changed
3928                /// Keyed on the PK; global scopes do not apply.
3929                ///
3930                /// # Errors
3931                /// As [`Self::save`].
3932                pub async fn save(
3933                    &mut self,
3934                    pool: &#root::sql::Pool,
3935                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3936                    #unset_dispatch
3937                    #write
3938                }
3939
3940                /// Run `query` and write its diff entry, limited to `only` columns.
3941                #[doc(hidden)]
3942                async fn __rustango_save_with_diff(
3943                    &self,
3944                    pool: &#root::sql::Pool,
3945                    query: &#root::core::UpdateQuery,
3946                    only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
3947                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3948                    let mut _after_pairs = self.__rustango_audit_pairs();
3949                    if let ::core::option::Option::Some(only) = only {
3950                        _after_pairs.retain(|(col, _)| only.contains(col));
3951                    }
3952                    let _before_query = #root::audit::before_image_query(
3953                        <Self as #root::core::Model>::SCHEMA,
3954                        #pk_column_lit,
3955                        ::core::convert::Into::<#root::core::SqlValue>::into(
3956                            #pk_value_for_bind,
3957                        ),
3958                        &[ #( #tracked_cols ),* ],
3959                    );
3960                    #root::audit::save_one_with_diff(
3961                        pool,
3962                        query,
3963                        &_before_query,
3964                        <Self as #root::core::Model>::SCHEMA.table,
3965                        self.__rustango_audit_pk(),
3966                        _after_pairs,
3967                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3968                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3969                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3970                    ).await
3971                }
3972
3973                #partial
3974            }
3975        } else {
3976            quote!()
3977        }
3978    } else {
3979        pool_save_method
3980    };
3981
3982    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3983    // body shapes:
3984    // - non-audited: simple dispatch through `sql::delete_pool`
3985    // - audited: routes through `audit::delete_one_with_audit`,
3986    //   which opens a per-backend transaction wrapping DELETE +
3987    //   audit emit so the data write and audit row commit atomically.
3988    let pool_delete_method = {
3989        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3990        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3991        if let Some(pk_ident) = pk_ident_for_pool {
3992            if audited_fields.is_some() {
3993                quote! {
3994                    /// Delete this row on any backend, with its audit row in
3995                    /// the same transaction.
3996                    /// Keyed on the PK; global scopes do not apply.
3997                    ///
3998                    /// # Errors
3999                    /// As [`Self::delete`].
4000                    pub async fn delete(
4001                        &self,
4002                        pool: &#root::sql::Pool,
4003                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4004                        let _query = #root::core::DeleteQuery::by_pk(
4005                            <Self as #root::core::Model>::SCHEMA,
4006                            #pk_column_lit,
4007                            ::core::convert::Into::<#root::core::SqlValue>::into(
4008                                ::core::clone::Clone::clone(&self.#pk_ident)
4009                            ),
4010                        );
4011                        let _audit_entry =
4012                            self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4013                        #root::audit::delete_one_with_audit(
4014                            pool, &_query, &_audit_entry,
4015                        ).await
4016                    }
4017                }
4018            } else {
4019                quote! {
4020                    /// Delete the row identified by this instance's primary key
4021                    /// against either backend. Equivalent to [`Self::delete`] but
4022                    /// takes [`#root::sql::Pool`] and dispatches per backend.
4023                    /// Global scopes do not apply.
4024                    ///
4025                    /// # Errors
4026                    /// As [`Self::delete`].
4027                    pub async fn delete(
4028                        &self,
4029                        pool: &#root::sql::Pool,
4030                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4031                        self.__rustango_delete_exec(pool).await
4032                    }
4033                }
4034            }
4035        } else {
4036            quote!()
4037        }
4038    };
4039
4040    // Bulk writers on audited models write one audit row per affected
4041    // row, in the write's transaction (#1747).
4042    let (bulk_delete_run, destroy_run, bulk_update_run, truncate_run, truncate_cascade_run) =
4043        if audited_fields.is_some() {
4044            (
4045                quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4046                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4047            })
4048            .await),
4049                quote!(#root::audit::delete_each_with_audit::<Self>(pool, &_queries, |_r: &Self| {
4050                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4051            })
4052            .await),
4053                quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4054                _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4055            })
4056            .await),
4057                quote!(#root::audit::truncate_with_audit(
4058                pool,
4059                <Self as #root::core::Model>::SCHEMA.table,
4060                #root::sql::TruncateMode::Plain,
4061            )
4062            .await),
4063                quote!(#root::audit::truncate_with_audit(
4064                pool,
4065                <Self as #root::core::Model>::SCHEMA.table,
4066                #root::sql::TruncateMode::Cascade,
4067            )
4068            .await
4069            .map(drop)),
4070            )
4071        } else {
4072            (
4073                quote!(#root::sql::delete_pool(pool, &_query).await),
4074                quote!(#root::sql::__rustango_delete_all_or_none(pool, _queries).await),
4075                quote!(#root::sql::update_pool(pool, &_query).await),
4076                quote!(#root::sql::truncate(pool, <Self as #root::core::Model>::SCHEMA).await),
4077                quote!(#root::sql::truncate_cascade(pool, <Self as #root::core::Model>::SCHEMA).await),
4078            )
4079        };
4080
4081    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4082    // matching this instance's PK and overwrite the in-memory state
4083    // with the freshly-fetched columns.
4084    // Issue #825. Only emitted when the model declares a PK; non-PK
4085    // models can't address a specific row.
4086    //
4087    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4088    // every field from `self`; resets the PK to `Auto::Unset` when
4089    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4090    // a fresh autoincrement value. Non-Auto PKs preserve the source
4091    // PK — the caller must overwrite before insert. Pure-Rust, no
4092    // I/O, no dialect surface.
4093    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4094        let other_field_clones: Vec<TokenStream2> = fields
4095            .column_entries
4096            .iter()
4097            .filter(|c| &c.ident != pk_ident)
4098            .map(|c| {
4099                let ident = &c.ident;
4100                quote! {
4101                    #ident: ::core::clone::Clone::clone(&self.#ident)
4102                }
4103            })
4104            .collect();
4105        let pk_clone_token = if fields.pk_is_auto {
4106            quote! { #pk_ident: #root::sql::Auto::Unset }
4107        } else {
4108            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4109        };
4110        let replicate_doc = if fields.pk_is_auto {
4111            quote! {
4112                /// Eloquent-style `replicate()` — return a clone of this
4113                /// row with the primary key reset to [`Auto::Unset`] so
4114                /// the copy is ready for `insert_pool` / `save_pool` to
4115                /// allocate a fresh autoincrement value. Every other
4116                /// field is `Clone`d verbatim. Issue #825.
4117                ///
4118                /// `auto_now_add` / `auto_now` timestamp fields are
4119                /// **not** reset (Eloquent's `replicate` doesn't reset
4120                /// them either) — pass them through the normal insert
4121                /// path if you want fresh values, or assign them
4122                /// explicitly after the call.
4123            }
4124        } else {
4125            quote! {
4126                /// Eloquent-style `replicate()` — clone this row
4127                /// verbatim. Because the primary key is **not** an
4128                /// `Auto<T>`, the clone keeps the source PK; the
4129                /// caller must overwrite `copy.<pk>` before inserting
4130                /// to avoid a unique-key violation. Issue #825.
4131            }
4132        };
4133        // 2026-06-07 — field-name / shortcut collision guard.
4134        //
4135        // The macro emits both `pub const <field>: <field>_col = ...`
4136        // (per-field typed-column const, used by the typed-builder
4137        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4138        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4139        // model has a field named e.g. `count`, both items would land
4140        // in the same inherent impl with the same name, and the
4141        // compiler rejects the derive with "duplicate definitions".
4142        //
4143        // Drop the conflicting shortcut for that model. Callers can
4144        // still reach the same behavior via
4145        // `QuerySet::<Model>::default().count(&pool)`.
4146        let column_names: ::std::collections::HashSet<String> = fields
4147            .column_entries
4148            .iter()
4149            .map(|c| ident_name(&c.ident))
4150            .collect();
4151        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4152            if column_names.contains(name) {
4153                quote! {}
4154            } else {
4155                tokens
4156            }
4157        };
4158        let count_method = emit_if_no_field_collision(
4159            "count",
4160            quote! {
4161                /// Count rows of this model — `SELECT COUNT(*) FROM
4162                /// <table>`. Eloquent `Model::count()` parity.
4163                ///
4164                /// Skipped on models that already declare a field named
4165                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4166                /// in that case.
4167                ///
4168                /// # Errors
4169                /// As [`CounterPool::count`].
4170                ///
4171                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4172                pub async fn count(
4173                    pool: &#root::sql::Pool,
4174                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4175                    use #root::sql::CounterPool as _;
4176                    #root::query::QuerySet::<Self>::default()
4177                        .count(pool)
4178                        .await
4179                }
4180            },
4181        );
4182        let value_method = emit_if_no_field_collision(
4183            "value",
4184            quote! {
4185                /// Pluck a single scalar from the first row.
4186                /// Eloquent `Model::query()->value($col)` parity.
4187                ///
4188                /// Skipped on models that already declare a field named
4189                /// `value`. Drop into
4190                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4191                ///
4192                /// # Errors
4193                /// As `ValuesFlatQuerySet::first`.
4194                pub async fn value<U>(
4195                    col: &str,
4196                    pool: &#root::sql::Pool,
4197                ) -> ::core::result::Result<
4198                    ::core::option::Option<U>,
4199                    #root::sql::ExecError,
4200                >
4201                where
4202                    U: #root::sql::FlatScalar,
4203                {
4204                    let _col_static: &'static str = Self::__resolve_col(col)?;
4205                    #root::query::QuerySet::<Self>::default()
4206                        .values_list_flat(_col_static)
4207                        .first::<U>(pool)
4208                        .await
4209                }
4210            },
4211        );
4212        let sum_method = emit_if_no_field_collision(
4213            "sum",
4214            quote! {
4215                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4216                /// Skipped on models that already declare a field named
4217                /// `sum`.
4218                ///
4219                /// # Errors
4220                /// As [`#root::sql::fetch_aggregate_pool`].
4221                pub async fn sum<U>(
4222                    col: &str,
4223                    pool: &#root::sql::Pool,
4224                ) -> ::core::result::Result<
4225                    ::core::option::Option<U>,
4226                    #root::sql::ExecError,
4227                >
4228                where
4229                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4230                        + #root::sql::MaybeMyFromRow
4231                        + #root::sql::MaybeSqliteFromRow
4232                        + ::core::marker::Send
4233                        + ::core::marker::Unpin,
4234                {
4235                    #root::query::QuerySet::<Self>::default()
4236                        .sum::<U>(col, pool)
4237                        .await
4238                }
4239            },
4240        );
4241        let avg_method = emit_if_no_field_collision(
4242            "avg",
4243            quote! {
4244                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4245                /// models that already declare a field named `avg`.
4246                ///
4247                /// # Errors
4248                /// As [`#root::sql::fetch_aggregate_pool`].
4249                pub async fn avg<U>(
4250                    col: &str,
4251                    pool: &#root::sql::Pool,
4252                ) -> ::core::result::Result<
4253                    ::core::option::Option<U>,
4254                    #root::sql::ExecError,
4255                >
4256                where
4257                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4258                        + #root::sql::MaybeMyFromRow
4259                        + #root::sql::MaybeSqliteFromRow
4260                        + ::core::marker::Send
4261                        + ::core::marker::Unpin,
4262                {
4263                    #root::query::QuerySet::<Self>::default()
4264                        .avg::<U>(col, pool)
4265                        .await
4266                }
4267            },
4268        );
4269        let min_method = emit_if_no_field_collision(
4270            "min",
4271            quote! {
4272                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4273                /// models that already declare a field named `min`.
4274                ///
4275                /// # Errors
4276                /// As [`#root::sql::fetch_aggregate_pool`].
4277                pub async fn min<U>(
4278                    col: &str,
4279                    pool: &#root::sql::Pool,
4280                ) -> ::core::result::Result<
4281                    ::core::option::Option<U>,
4282                    #root::sql::ExecError,
4283                >
4284                where
4285                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4286                        + #root::sql::MaybeMyFromRow
4287                        + #root::sql::MaybeSqliteFromRow
4288                        + ::core::marker::Send
4289                        + ::core::marker::Unpin,
4290                {
4291                    #root::query::QuerySet::<Self>::default()
4292                        .min::<U>(col, pool)
4293                        .await
4294                }
4295            },
4296        );
4297        let max_method = emit_if_no_field_collision(
4298            "max",
4299            quote! {
4300                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4301                /// models that already declare a field named `max`.
4302                ///
4303                /// # Errors
4304                /// As [`#root::sql::fetch_aggregate_pool`].
4305                pub async fn max<U>(
4306                    col: &str,
4307                    pool: &#root::sql::Pool,
4308                ) -> ::core::result::Result<
4309                    ::core::option::Option<U>,
4310                    #root::sql::ExecError,
4311                >
4312                where
4313                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4314                        + #root::sql::MaybeMyFromRow
4315                        + #root::sql::MaybeSqliteFromRow
4316                        + ::core::marker::Send
4317                        + ::core::marker::Unpin,
4318                {
4319                    #root::query::QuerySet::<Self>::default()
4320                        .max::<U>(col, pool)
4321                        .await
4322                }
4323            },
4324        );
4325        let first_method = emit_if_no_field_collision(
4326            "first",
4327            quote! {
4328                /// First row of this model. Eloquent `Model::first()`.
4329                /// Skipped on models that already declare a field named
4330                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4331                ///
4332                /// # Errors
4333                /// As `QuerySet::first`.
4334                pub async fn first(
4335                    pool: &#root::sql::Pool,
4336                ) -> ::core::result::Result<
4337                    ::core::option::Option<Self>,
4338                    #root::sql::ExecError,
4339                > {
4340                    #root::query::QuerySet::<Self>::default()
4341                        .first(pool)
4342                        .await
4343                }
4344            },
4345        );
4346        let last_method = emit_if_no_field_collision(
4347            "last",
4348            quote! {
4349                /// Last row of this model by primary-key DESC.
4350                /// Eloquent `Model::query()->latest('id')->first()`
4351                /// parity — fetches the highest-PK row without
4352                /// requiring the caller to spell the PK column.
4353                /// Returns `None` on an empty table.
4354                ///
4355                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4356                /// Skipped on models that already declare a field
4357                /// named `last`.
4358                ///
4359                /// # Errors
4360                /// As `QuerySet::last`.
4361                pub async fn last(
4362                    pool: &#root::sql::Pool,
4363                ) -> ::core::result::Result<
4364                    ::core::option::Option<Self>,
4365                    #root::sql::ExecError,
4366                > {
4367                    #root::query::QuerySet::<Self>::default()
4368                        .last(pool)
4369                        .await
4370                }
4371            },
4372        );
4373        quote! {
4374            /// Re-SELECT this row by its primary key and overwrite
4375            /// every in-memory field with the freshly-fetched value.
4376            /// Issue #825.
4377            ///
4378            /// Use this when the row may have been modified by another
4379            /// process / connection / job since you read it — e.g. after
4380            /// a queued task callback, or to re-sync stale UI state
4381            /// before re-saving.
4382            ///
4383            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4384            /// when the primary key no longer matches any row (e.g.
4385            /// the row was deleted concurrently).
4386            ///
4387            /// # Errors
4388            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4389            /// the PK no longer exists.
4390            ///
4391            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4392            pub async fn refresh_from_db(
4393                &mut self,
4394                pool: &#root::sql::Pool,
4395            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4396                use #root::sql::FetcherPool as _;
4397                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4398                    ::core::clone::Clone::clone(&self.#pk_ident),
4399                );
4400                let mut _rows: ::std::vec::Vec<Self> =
4401                    #root::query::QuerySet::<Self>::default()
4402                        .filter(::core::stringify!(#pk_ident), _pk_val)
4403                        .limit(1)
4404                        .fetch(pool)
4405                        .await?;
4406                match _rows.into_iter().next() {
4407                    ::core::option::Option::Some(_fresh) => {
4408                        *self = _fresh;
4409                        ::core::result::Result::Ok(())
4410                    }
4411                    ::core::option::Option::None => ::core::result::Result::Err(
4412                        #root::sql::ExecError::Driver(
4413                            #root::sql::sqlx::Error::RowNotFound,
4414                        ),
4415                    ),
4416                }
4417            }
4418
4419            /// Atomically increment the integer column `col` by
4420            /// `by` for this row. Equivalent to
4421            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4422            ///
4423            /// **Doesn't mutate `self`** — the in-memory copy is now
4424            /// stale; call [`Self::refresh_from_db`] /
4425            /// [`Self::fresh`] to re-sync. Returns the rows-
4426            /// affected count (0 when the PK doesn't match any row,
4427            /// 1 on success).
4428            ///
4429            /// `col` is the Rust field name as a string; unknown
4430            /// fields surface as `UnknownField` at runtime. Negative
4431            /// `by` values atomically decrement (see also
4432            /// [`Self::decrement`]).
4433            ///
4434            /// # Errors
4435            /// As [`UpdaterPool::execute_pool`].
4436            ///
4437            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4438            pub async fn increment(
4439                &self,
4440                col: &str,
4441                by: i64,
4442                pool: &#root::sql::Pool,
4443            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4444                Self::__increment_one(self, col, by, pool).await
4445            }
4446
4447            /// Sibling of [`Self::increment`] — atomically
4448            /// decrement this row's `col` by `by`. Eloquent
4449            /// `$model->decrement($col, $by)` parity. Equivalent to
4450            /// `self.increment(col, -by, &pool)`; the separate name
4451            /// keeps call sites readable.
4452            ///
4453            /// # Errors
4454            /// As [`Self::increment`].
4455            pub async fn decrement(
4456                &self,
4457                col: &str,
4458                by: i64,
4459                pool: &#root::sql::Pool,
4460            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4461                Self::__increment_one(self, col, -by, pool).await
4462            }
4463
4464            /// Bulk-increment: add `by` to `col` on every row of the
4465            /// table. Eloquent `Model::query()->increment($col, $by)`
4466            /// parity. Use for counters, score adjustments, view
4467            /// rollups.
4468            ///
4469            /// # Errors
4470            /// As [`UpdaterPool::execute_pool`].
4471            ///
4472            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4473            pub async fn increment_each(
4474                col: &str,
4475                by: i64,
4476                pool: &#root::sql::Pool,
4477            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4478                Self::__increment_all(col, by, pool).await
4479            }
4480
4481            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4482            ///
4483            /// # Errors
4484            /// As [`Self::increment_each`].
4485            pub async fn decrement_each(
4486                col: &str,
4487                by: i64,
4488                pool: &#root::sql::Pool,
4489            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4490                Self::__increment_all(col, -by, pool).await
4491            }
4492
4493            /// Internal: forward to
4494            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4495            /// One-line wrapper kept as a per-Model method so the
4496            /// macro's emitted `increment` / `decrement` instance
4497            /// calls don't have to thread `Self` through manually.
4498            #[doc(hidden)]
4499            pub async fn __increment_one(
4500                this: &Self,
4501                col: &str,
4502                by: i64,
4503                pool: &#root::sql::Pool,
4504            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4505                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4506                    ::core::clone::Clone::clone(&this.#pk_ident),
4507                );
4508                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4509                    ::core::stringify!(#pk_ident),
4510                    _pk_val,
4511                    col,
4512                    by,
4513                    pool,
4514                )
4515                .await
4516            }
4517
4518            /// Internal: run
4519            /// [`#root::sql::model_shortcuts::increment_all_query`].
4520            #[doc(hidden)]
4521            pub async fn __increment_all(
4522                col: &str,
4523                by: i64,
4524                pool: &#root::sql::Pool,
4525            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4526                let _query =
4527                    #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4528                #bulk_update_run
4529            }
4530
4531            /// Internal: forward to
4532            /// [`#root::sql::model_shortcuts::resolve_col`].
4533            #[doc(hidden)]
4534            pub fn __resolve_col(
4535                col: &str,
4536            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4537                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4538            }
4539
4540            /// Internal: forward to
4541            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4542            #[doc(hidden)]
4543            #[must_use]
4544            pub fn __add_signed_expr(
4545                col_static: &'static str,
4546                signed_by: i64,
4547            ) -> #root::core::Expr {
4548                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4549            }
4550
4551            /// Re-SELECT this row by its primary key and return a
4552            /// **new** instance with the freshly-fetched fields.
4553            /// Eloquent `Model::fresh()` parity — non-mutating
4554            /// counterpart of [`Self::refresh_from_db`].
4555            ///
4556            /// Returns `Ok(None)` when the row was deleted
4557            /// concurrently — vs [`Self::refresh_from_db`]
4558            /// which surfaces that as `RowNotFound` because
4559            /// in-place mutation has nothing to write to.
4560            ///
4561            /// Useful when you want to compare the in-memory
4562            /// instance against the persisted state (audit-style
4563            /// diffs, conflict detection) without mutating the
4564            /// reference you already hold.
4565            ///
4566            /// # Errors
4567            /// As [`FetcherPool::fetch`].
4568            ///
4569            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4570            pub async fn fresh(
4571                &self,
4572                pool: &#root::sql::Pool,
4573            ) -> ::core::result::Result<
4574                ::core::option::Option<Self>,
4575                #root::sql::ExecError,
4576            > {
4577                use #root::sql::FetcherPool as _;
4578                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4579                    ::core::clone::Clone::clone(&self.#pk_ident),
4580                );
4581                let _rows: ::std::vec::Vec<Self> =
4582                    #root::query::QuerySet::<Self>::default()
4583                        .filter(::core::stringify!(#pk_ident), _pk_val)
4584                        .limit(1)
4585                        .fetch(pool)
4586                        .await?;
4587                ::core::result::Result::Ok(_rows.into_iter().next())
4588            }
4589
4590            #replicate_doc
4591            #[must_use]
4592            pub fn replicate(&self) -> Self {
4593                Self {
4594                    #pk_clone_token,
4595                    #( #other_field_clones, )*
4596                }
4597            }
4598
4599            #first_method
4600
4601            #last_method
4602
4603            /// Throwing counterpart of [`Self::first`] —
4604            /// errors with `RowNotFound` when the table is empty.
4605            /// Eloquent `Model::firstOrFail()` parity.
4606            ///
4607            /// # Errors
4608            /// As [`Self::first`]; additionally
4609            /// [`sqlx::Error::RowNotFound`] on empty tables.
4610            ///
4611            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4612            pub async fn first_or_fail(
4613                pool: &#root::sql::Pool,
4614            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4615                // Route through the queryset rather than `Self::first`,
4616                // which is suppressed on models with a field named
4617                // `first` (field/shortcut collision guard).
4618                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4619                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4620                    ::core::option::Option::None => ::core::result::Result::Err(
4621                        #root::sql::ExecError::Driver(
4622                            #root::sql::sqlx::Error::RowNotFound,
4623                        ),
4624                    ),
4625                }
4626            }
4627
4628            /// Single-column projection — `SELECT <col> FROM
4629            /// <table>`. Returns `Vec<U>` where each element is the
4630            /// decoded value of the column. Eloquent
4631            /// `Model::pluck($column)` parity.
4632            ///
4633            /// Thin wrapper over `QuerySet::<Self>::default()
4634            /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4635            /// `FlatScalar`; use `Option<_>` for a nullable column.
4636            ///
4637            /// # Errors
4638            /// As `ValuesFlatQuerySet::fetch`.
4639            pub async fn pluck<U>(
4640                col: &'static str,
4641                pool: &#root::sql::Pool,
4642            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4643            where
4644                U: #root::sql::FlatScalar,
4645            {
4646                #root::query::QuerySet::<Self>::default()
4647                    .values_list_flat(col)
4648                    .fetch::<U>(pool)
4649                    .await
4650            }
4651
4652            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4653            /// stream every row of this model in batches of `n`,
4654            /// invoking the callback once per batch. Stable PK-ASC
4655            /// ordering so the LIMIT/OFFSET pagination is
4656            /// deterministic across drivers.
4657            ///
4658            /// The callback is async — it can do further DB work
4659            /// (writes, related-row lookups, queue dispatch) per
4660            /// batch. Return `Err(...)` from the callback to abort
4661            /// the iteration early; the error bubbles up.
4662            ///
4663            /// **When to use**: bulk processing flows that can't
4664            /// fit the whole table in memory — sending newsletters,
4665            /// running data migrations, computing summary
4666            /// statistics. For small / known-bounded tables, plain
4667            /// `Self::all(&pool)` is simpler.
4668            ///
4669            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4670            /// large tables. For multi-million-row scans prefer
4671            /// keyset-by-PK (the standard "WHERE id > last_seen"
4672            /// shape) over `chunk(...)`.
4673            ///
4674            /// Skipped on models without a primary key — chunking
4675            /// needs a stable order to avoid skipping / repeating
4676            /// rows across batches.
4677            pub async fn chunk<F, Fut>(
4678                n: i64,
4679                pool: &#root::sql::Pool,
4680                mut cb: F,
4681            ) -> ::core::result::Result<(), #root::sql::ExecError>
4682            where
4683                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4684                Fut: ::core::future::Future<
4685                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4686                >,
4687            {
4688                use #root::sql::FetcherPool as _;
4689                // The field name: `filter` / `order_by` take fields, not columns.
4690                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4691                    ::core::option::Option::Some(f) => f.name,
4692                    ::core::option::Option::None => {
4693                        return ::core::result::Result::Ok(());
4694                    }
4695                };
4696                let mut offset: i64 = 0;
4697                loop {
4698                    let rows: ::std::vec::Vec<Self> =
4699                        #root::query::QuerySet::<Self>::default()
4700                            .order_by(&[(pk_col, false)])
4701                            .limit(n)
4702                            .offset(offset)
4703                            .fetch(pool)
4704                            .await?;
4705                    if rows.is_empty() {
4706                        return ::core::result::Result::Ok(());
4707                    }
4708                    let len = rows.len() as i64;
4709                    cb(rows).await?;
4710                    if len < n {
4711                        return ::core::result::Result::Ok(());
4712                    }
4713                    offset += n;
4714                }
4715            }
4716
4717            /// Eloquent `Model::chunkById($n, fn (...))` — same
4718            /// per-batch callback shape as [`Self::chunk`], but uses
4719            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4720            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4721            /// — the right choice for multi-million-row sweeps.
4722            ///
4723            /// The keyset compares the primary key in its own type,
4724            /// so any orderable PK works (`i32`, `i64`, UUID, string).
4725            ///
4726            /// Callback errors abort iteration; the error bubbles up
4727            /// unchanged. Empty table → callback invoked zero times.
4728            pub async fn chunk_by_id<F, Fut>(
4729                n: i64,
4730                pool: &#root::sql::Pool,
4731                mut cb: F,
4732            ) -> ::core::result::Result<(), #root::sql::ExecError>
4733            where
4734                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4735                Fut: ::core::future::Future<
4736                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4737                >,
4738            {
4739                use #root::sql::FetcherPool as _;
4740                // The field name: `filter` / `order_by` take fields, not columns.
4741                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4742                    ::core::option::Option::Some(f) => f.name,
4743                    ::core::option::Option::None => {
4744                        return ::core::result::Result::Ok(());
4745                    }
4746                };
4747                // The largest PK seen, in the PK's own type; none before
4748                // the first batch.
4749                let key = ::std::format!("{}__gt", pk_col);
4750                let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
4751                    ::core::option::Option::None;
4752                loop {
4753                    let mut qs = #root::query::QuerySet::<Self>::default();
4754                    if let ::core::option::Option::Some(v) = last_seen.take() {
4755                        qs = qs.filter(key.as_str(), v);
4756                    }
4757                    let rows: ::std::vec::Vec<Self> = qs
4758                        .order_by(&[(pk_col, false)])
4759                        .limit(n)
4760                        .fetch(pool)
4761                        .await?;
4762                    let len = rows.len() as i64;
4763                    // Capture the last row's PK BEFORE moving rows
4764                    // into the callback.
4765                    let ::core::option::Option::Some(max_pk) =
4766                        rows.last().map(|r| r.__rustango_pk_value())
4767                    else {
4768                        return ::core::result::Result::Ok(());
4769                    };
4770                    cb(rows).await?;
4771                    if len < n {
4772                        return ::core::result::Result::Ok(());
4773                    }
4774                    last_seen = ::core::option::Option::Some(max_pk);
4775                }
4776            }
4777
4778            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4779            /// per-row callback companion to [`Self::chunk`].
4780            /// Streams every row in keyset-paginated batches of
4781            /// `batch` size, calling `cb` once per row.
4782            ///
4783            /// Inherits the keyset-paginated scan of
4784            /// [`Self::chunk_by_id`] (O(N) total).
4785            ///
4786            /// ```ignore
4787            /// Post::each(500, &pool, |p| async move {
4788            ///     reindex(p).await?;
4789            ///     Ok(())
4790            /// }).await?;
4791            /// ```
4792            ///
4793            /// Return `Err(...)` from the callback to abort
4794            /// iteration; the error bubbles up unchanged.
4795            pub async fn each<F, Fut>(
4796                batch: i64,
4797                pool: &#root::sql::Pool,
4798                mut cb: F,
4799            ) -> ::core::result::Result<(), #root::sql::ExecError>
4800            where
4801                F: ::core::ops::FnMut(Self) -> Fut,
4802                Fut: ::core::future::Future<
4803                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4804                >,
4805            {
4806                use #root::sql::FetcherPool as _;
4807                // The field name: `filter` / `order_by` take fields, not columns.
4808                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4809                    ::core::option::Option::Some(f) => f.name,
4810                    ::core::option::Option::None => {
4811                        return ::core::result::Result::Ok(());
4812                    }
4813                };
4814                let key = ::std::format!("{}__gt", pk_col);
4815                let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
4816                    ::core::option::Option::None;
4817                loop {
4818                    let mut qs = #root::query::QuerySet::<Self>::default();
4819                    if let ::core::option::Option::Some(v) = last_seen.take() {
4820                        qs = qs.filter(key.as_str(), v);
4821                    }
4822                    let rows: ::std::vec::Vec<Self> = qs
4823                        .order_by(&[(pk_col, false)])
4824                        .limit(batch)
4825                        .fetch(pool)
4826                        .await?;
4827                    let len = rows.len() as i64;
4828                    let ::core::option::Option::Some(max_pk) =
4829                        rows.last().map(|r| r.__rustango_pk_value())
4830                    else {
4831                        return ::core::result::Result::Ok(());
4832                    };
4833                    for row in rows {
4834                        cb(row).await?;
4835                    }
4836                    if len < batch {
4837                        return ::core::result::Result::Ok(());
4838                    }
4839                    last_seen = ::core::option::Option::Some(max_pk);
4840                }
4841            }
4842
4843            /// Delete every row of this model with one `DELETE FROM
4844            /// <table>` on every backend; returns the rows deleted. Each
4845            /// FK's `ON DELETE` applies, so a `RESTRICT` / `NO ACTION`
4846            /// row still pointing here refuses it; see
4847            /// [`truncate_cascade`](Self::truncate_cascade).
4848            ///
4849            /// **Use only in tests / fixture-reset flows.** No per-row
4850            /// `pre_delete` / `post_delete` signals fire. An audited model
4851            /// gets one bulk audit entry, not one per row. Global scopes
4852            /// do not apply: every row goes, generic M2M links too.
4853            ///
4854            /// # Errors
4855            /// As [`truncate`].
4856            ///
4857            /// [`truncate`]: rustango::sql::truncate
4858            pub async fn truncate(
4859                pool: &#root::sql::Pool,
4860            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4861                #truncate_run
4862            }
4863
4864            /// [`truncate`](Self::truncate), also emptying every table
4865            /// that references this one, transitively and whatever its
4866            /// `ON DELETE`. The same on every backend.
4867            ///
4868            /// # Errors
4869            /// As [`truncate_cascade`].
4870            ///
4871            /// [`truncate_cascade`]: rustango::sql::truncate_cascade
4872            pub async fn truncate_cascade(
4873                pool: &#root::sql::Pool,
4874            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4875                #truncate_cascade_run
4876            }
4877
4878            /// Bulk-delete every row whose primary key is in
4879            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4880            /// Returns the affected row count.
4881            ///
4882            /// Eloquent `Model::destroy([1, 2, 3])` parity.
4883            /// Empty `pks` is a no-op (returns 0). All or none: past
4884            /// the bind cap the batches share one transaction.
4885            ///
4886            /// Accepts any iterable whose elements are
4887            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4888            /// `[i64; N]`, etc.
4889            ///
4890            /// # Errors
4891            /// As `delete_pool`.
4892            pub async fn destroy<V>(
4893                pks: impl ::core::iter::IntoIterator<Item = V>,
4894                pool: &#root::sql::Pool,
4895            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4896            where
4897                V: ::core::convert::Into<#root::core::SqlValue>,
4898            {
4899                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4900                    pks.into_iter().map(::core::convert::Into::into).collect();
4901                if _values.is_empty() {
4902                    return ::core::result::Result::Ok(0);
4903                }
4904                let _pk = <Self as #root::core::Model>::SCHEMA
4905                    .primary_key()
4906                    .ok_or_else(|| {
4907                        #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
4908                    })?;
4909                // One statement per `IN` list the dialect can bind, all or none.
4910                let mut _queries = ::std::vec::Vec::new();
4911                for _keys in #root::sql::__rustango_in_batches::<Self>(pool, _values)? {
4912                    _queries.push(
4913                        #root::query::QuerySet::<Self>::default()
4914                            .filter_op(
4915                                _pk.name,
4916                                #root::core::Op::In,
4917                                #root::core::SqlValue::List(_keys),
4918                            )
4919                            .compile_delete()?,
4920                    );
4921                }
4922                #destroy_run
4923            }
4924
4925            /// Fetch every row where `<col> = <val>`. Eloquent
4926            /// `Model::where($col, $val)->get()` parity.
4927            ///
4928            /// Thin wrapper over `QuerySet::<Self>::default()
4929            /// .filter(col, val).fetch(pool)`. For one row,
4930            /// use [`Self::first_where`]; for a chain that
4931            /// needs further `.filter()` / `.order_by()` /
4932            /// `.limit()`, drop down to `Self::query().filter(...)`
4933            /// directly.
4934            ///
4935            /// `val` accepts any value `Into<SqlValue>` so plain
4936            /// strings, ints, UUIDs, etc. all work.
4937            ///
4938            /// # Errors
4939            /// As [`FetcherPool::fetch`].
4940            ///
4941            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4942            pub async fn where_(
4943                col: &str,
4944                val: impl ::core::convert::Into<#root::core::SqlValue>,
4945                pool: &#root::sql::Pool,
4946            ) -> ::core::result::Result<
4947                ::std::vec::Vec<Self>,
4948                #root::sql::ExecError,
4949            > {
4950                use #root::sql::FetcherPool as _;
4951                #root::query::QuerySet::<Self>::default()
4952                    .filter(col, val)
4953                    .fetch(pool)
4954                    .await
4955            }
4956
4957            /// Fetch every row where `<col> IN (vals)`. Eloquent
4958            /// `Model::whereIn($col, $vals)->get()` parity. Empty
4959            /// `vals` returns no rows (matches SQL's empty-IN
4960            /// semantics).
4961            ///
4962            /// `vals` accepts any iterable whose items are
4963            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4964            /// etc.
4965            ///
4966            /// # Errors
4967            /// As [`FetcherPool::fetch`].
4968            ///
4969            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4970            pub async fn where_in<V>(
4971                col: &str,
4972                vals: impl ::core::iter::IntoIterator<Item = V>,
4973                pool: &#root::sql::Pool,
4974            ) -> ::core::result::Result<
4975                ::std::vec::Vec<Self>,
4976                #root::sql::ExecError,
4977            >
4978            where
4979                V: ::core::convert::Into<#root::core::SqlValue>,
4980            {
4981                use #root::sql::FetcherPool as _;
4982                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4983                    vals.into_iter().map(::core::convert::Into::into).collect();
4984                if _values.is_empty() {
4985                    return ::core::result::Result::Ok(::std::vec::Vec::new());
4986                }
4987                let _key = ::std::format!("{}__in", col);
4988                let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
4989                let _split = _batches.len() > 1;
4990                let mut _rows = ::std::vec::Vec::new();
4991                for _keys in _batches {
4992                    _rows.extend(
4993                        #root::query::QuerySet::<Self>::default()
4994                            .filter(&_key, #root::core::SqlValue::List(_keys))
4995                            .fetch(pool)
4996                            .await?,
4997                    );
4998                }
4999                if _split {
5000                    _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
5001                }
5002                ::core::result::Result::Ok(_rows)
5003            }
5004
5005            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5006            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5007            /// `vals` returns every row (matches SQL's empty-NOT-IN
5008            /// semantics — vacuously true for every row).
5009            ///
5010            /// # Errors
5011            /// As [`FetcherPool::fetch`]; `ExecError::InListUnsplittable`
5012            /// when `vals` pass the backend's bind cap.
5013            ///
5014            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5015            pub async fn where_not_in<V>(
5016                col: &str,
5017                vals: impl ::core::iter::IntoIterator<Item = V>,
5018                pool: &#root::sql::Pool,
5019            ) -> ::core::result::Result<
5020                ::std::vec::Vec<Self>,
5021                #root::sql::ExecError,
5022            >
5023            where
5024                V: ::core::convert::Into<#root::core::SqlValue>,
5025            {
5026                use #root::sql::FetcherPool as _;
5027                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5028                    vals.into_iter().map(::core::convert::Into::into).collect();
5029                if _values.is_empty() {
5030                    return #root::query::QuerySet::<Self>::default()
5031                        .fetch(pool)
5032                        .await;
5033                }
5034                let _key = ::std::format!("{}__not_in", col);
5035                // A `NOT IN` cannot be split: past the bind cap it is refused.
5036                let _values = #root::sql::__rustango_one_in_list::<Self>(pool, _values)?;
5037                #root::query::QuerySet::<Self>::default()
5038                    .filter(&_key, #root::core::SqlValue::List(_values))
5039                    .fetch(pool)
5040                    .await
5041            }
5042
5043            /// Fetch every row where `<col> IS NULL`. Eloquent
5044            /// `Model::whereNull($col)->get()` parity.
5045            ///
5046            /// # Errors
5047            /// As [`FetcherPool::fetch`].
5048            ///
5049            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5050            pub async fn where_null(
5051                col: &str,
5052                pool: &#root::sql::Pool,
5053            ) -> ::core::result::Result<
5054                ::std::vec::Vec<Self>,
5055                #root::sql::ExecError,
5056            > {
5057                use #root::sql::FetcherPool as _;
5058                let _key = ::std::format!("{}__isnull", col);
5059                #root::query::QuerySet::<Self>::default()
5060                    .filter(&_key, #root::core::SqlValue::Bool(true))
5061                    .fetch(pool)
5062                    .await
5063            }
5064
5065            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5066            /// `Model::whereNotNull($col)->get()` parity.
5067            ///
5068            /// # Errors
5069            /// As [`FetcherPool::fetch`].
5070            ///
5071            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5072            pub async fn where_not_null(
5073                col: &str,
5074                pool: &#root::sql::Pool,
5075            ) -> ::core::result::Result<
5076                ::std::vec::Vec<Self>,
5077                #root::sql::ExecError,
5078            > {
5079                use #root::sql::FetcherPool as _;
5080                let _key = ::std::format!("{}__isnull", col);
5081                #root::query::QuerySet::<Self>::default()
5082                    .filter(&_key, #root::core::SqlValue::Bool(false))
5083                    .fetch(pool)
5084                    .await
5085            }
5086
5087            /// Fetch up to `n` rows in random order. Eloquent
5088            /// `Model::inRandomOrder()->limit($n)->get()` /
5089            /// `Model::query()->inRandomOrder()->get()->take($n)`
5090            /// parity. **Performance caveat**: random ordering
5091            /// forces a full table scan + per-row random key sort;
5092            /// the optimizer cannot use an index. Prefer a
5093            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5094            ///
5095            /// # Errors
5096            /// As [`FetcherPool::fetch`].
5097            ///
5098            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5099            pub async fn random_n(
5100                n: i64,
5101                pool: &#root::sql::Pool,
5102            ) -> ::core::result::Result<
5103                ::std::vec::Vec<Self>,
5104                #root::sql::ExecError,
5105            > {
5106                use #root::sql::FetcherPool as _;
5107                #root::query::QuerySet::<Self>::default()
5108                    .order_random()
5109                    .limit(n)
5110                    .fetch(pool)
5111                    .await
5112            }
5113
5114            /// Fetch one row in random order. Eloquent
5115            /// `Model::inRandomOrder()->first()` parity. Same
5116            /// performance caveat as [`Self::random_n`].
5117            ///
5118            /// # Errors
5119            /// As [`FetcherPool::fetch`].
5120            ///
5121            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5122            pub async fn random(
5123                pool: &#root::sql::Pool,
5124            ) -> ::core::result::Result<
5125                ::core::option::Option<Self>,
5126                #root::sql::ExecError,
5127            > {
5128                ::core::result::Result::Ok(
5129                    Self::random_n(1, pool).await?.into_iter().next(),
5130                )
5131            }
5132
5133            /// Fetch every row ordered ASC by `field`. Eloquent
5134            /// `Model::oldest($field)->get()` parity — the multi-row
5135            /// counterpart of [`Self::earliest`].
5136            ///
5137            /// # Errors
5138            /// As [`FetcherPool::fetch`].
5139            ///
5140            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5141            pub async fn oldest(
5142                field: &str,
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                #root::query::QuerySet::<Self>::default()
5150                    .order_by(&[(field, false)])
5151                    .fetch(pool)
5152                    .await
5153            }
5154
5155            /// Fetch every row ordered DESC by `field`. Eloquent
5156            /// `Model::latest($field)->get()` parity — the multi-row
5157            /// counterpart of [`Self::latest`].
5158            ///
5159            /// # Errors
5160            /// As [`FetcherPool::fetch`].
5161            ///
5162            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5163            pub async fn newest(
5164                field: &str,
5165                pool: &#root::sql::Pool,
5166            ) -> ::core::result::Result<
5167                ::std::vec::Vec<Self>,
5168                #root::sql::ExecError,
5169            > {
5170                use #root::sql::FetcherPool as _;
5171                #root::query::QuerySet::<Self>::default()
5172                    .order_by(&[(field, true)])
5173                    .fetch(pool)
5174                    .await
5175            }
5176
5177            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5178            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5179            /// Routes through the existing `__year` lookup suffix
5180            /// (issue #829).
5181            ///
5182            /// # Errors
5183            /// As [`FetcherPool::fetch`].
5184            ///
5185            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5186            pub async fn where_year(
5187                col: &str,
5188                year: i64,
5189                pool: &#root::sql::Pool,
5190            ) -> ::core::result::Result<
5191                ::std::vec::Vec<Self>,
5192                #root::sql::ExecError,
5193            > {
5194                use #root::sql::FetcherPool as _;
5195                let _key = ::std::format!("{}__year", col);
5196                #root::query::QuerySet::<Self>::default()
5197                    .filter(&_key, #root::core::SqlValue::I64(year))
5198                    .fetch(pool)
5199                    .await
5200            }
5201
5202            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5203            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5204            /// `month` is 1–12.
5205            ///
5206            /// # Errors
5207            /// As [`FetcherPool::fetch`].
5208            ///
5209            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5210            pub async fn where_month(
5211                col: &str,
5212                month: i64,
5213                pool: &#root::sql::Pool,
5214            ) -> ::core::result::Result<
5215                ::std::vec::Vec<Self>,
5216                #root::sql::ExecError,
5217            > {
5218                use #root::sql::FetcherPool as _;
5219                let _key = ::std::format!("{}__month", col);
5220                #root::query::QuerySet::<Self>::default()
5221                    .filter(&_key, #root::core::SqlValue::I64(month))
5222                    .fetch(pool)
5223                    .await
5224            }
5225
5226            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5227            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5228            /// `day` is 1–31.
5229            ///
5230            /// # Errors
5231            /// As [`FetcherPool::fetch`].
5232            ///
5233            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5234            pub async fn where_day(
5235                col: &str,
5236                day: i64,
5237                pool: &#root::sql::Pool,
5238            ) -> ::core::result::Result<
5239                ::std::vec::Vec<Self>,
5240                #root::sql::ExecError,
5241            > {
5242                use #root::sql::FetcherPool as _;
5243                let _key = ::std::format!("{}__day", col);
5244                #root::query::QuerySet::<Self>::default()
5245                    .filter(&_key, #root::core::SqlValue::I64(day))
5246                    .fetch(pool)
5247                    .await
5248            }
5249
5250            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5251            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5252            /// `hour` is 0–23.
5253            ///
5254            /// # Errors
5255            /// As [`FetcherPool::fetch`].
5256            ///
5257            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5258            pub async fn where_hour(
5259                col: &str,
5260                hour: i64,
5261                pool: &#root::sql::Pool,
5262            ) -> ::core::result::Result<
5263                ::std::vec::Vec<Self>,
5264                #root::sql::ExecError,
5265            > {
5266                use #root::sql::FetcherPool as _;
5267                let _key = ::std::format!("{}__hour", col);
5268                #root::query::QuerySet::<Self>::default()
5269                    .filter(&_key, #root::core::SqlValue::I64(hour))
5270                    .fetch(pool)
5271                    .await
5272            }
5273
5274            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5275            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5276            /// `minute` is 0–59.
5277            ///
5278            /// # Errors
5279            /// As [`FetcherPool::fetch`].
5280            ///
5281            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5282            pub async fn where_minute(
5283                col: &str,
5284                minute: i64,
5285                pool: &#root::sql::Pool,
5286            ) -> ::core::result::Result<
5287                ::std::vec::Vec<Self>,
5288                #root::sql::ExecError,
5289            > {
5290                use #root::sql::FetcherPool as _;
5291                let _key = ::std::format!("{}__minute", col);
5292                #root::query::QuerySet::<Self>::default()
5293                    .filter(&_key, #root::core::SqlValue::I64(minute))
5294                    .fetch(pool)
5295                    .await
5296            }
5297
5298            /// Fetch every row where `<col> LIKE <pattern>` —
5299            /// caller-supplied pattern (must include `%` / `_`
5300            /// wildcards manually). Eloquent `Model::whereLike`
5301            /// parity.
5302            ///
5303            /// # Errors
5304            /// As [`FetcherPool::fetch`].
5305            ///
5306            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5307            pub async fn where_like(
5308                col: &str,
5309                pattern: impl ::core::convert::Into<::std::string::String>,
5310                pool: &#root::sql::Pool,
5311            ) -> ::core::result::Result<
5312                ::std::vec::Vec<Self>,
5313                #root::sql::ExecError,
5314            > {
5315                use #root::sql::FetcherPool as _;
5316                let _key = ::std::format!("{}__like", col);
5317                #root::query::QuerySet::<Self>::default()
5318                    .filter(
5319                        &_key,
5320                        #root::core::SqlValue::String(pattern.into()),
5321                    )
5322                    .fetch(pool)
5323                    .await
5324            }
5325
5326            /// Fetch every row where `<col> ILIKE <pattern>` —
5327            /// case-insensitive LIKE (PG native, MySQL/SQLite
5328            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5329            ///
5330            /// # Errors
5331            /// As [`FetcherPool::fetch`].
5332            ///
5333            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5334            pub async fn where_ilike(
5335                col: &str,
5336                pattern: impl ::core::convert::Into<::std::string::String>,
5337                pool: &#root::sql::Pool,
5338            ) -> ::core::result::Result<
5339                ::std::vec::Vec<Self>,
5340                #root::sql::ExecError,
5341            > {
5342                use #root::sql::FetcherPool as _;
5343                let _key = ::std::format!("{}__ilike", col);
5344                #root::query::QuerySet::<Self>::default()
5345                    .filter(
5346                        &_key,
5347                        #root::core::SqlValue::String(pattern.into()),
5348                    )
5349                    .fetch(pool)
5350                    .await
5351            }
5352
5353            /// Fetch every row where `<col>` starts with `prefix`
5354            /// (auto-appends `%`). Eloquent
5355            /// `whereLike("col", "$prefix%")` parity.
5356            ///
5357            /// # Errors
5358            /// As [`FetcherPool::fetch`].
5359            ///
5360            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5361            pub async fn where_starts_with(
5362                col: &str,
5363                prefix: impl ::core::convert::Into<::std::string::String>,
5364                pool: &#root::sql::Pool,
5365            ) -> ::core::result::Result<
5366                ::std::vec::Vec<Self>,
5367                #root::sql::ExecError,
5368            > {
5369                use #root::sql::FetcherPool as _;
5370                let _key = ::std::format!("{}__startswith", col);
5371                #root::query::QuerySet::<Self>::default()
5372                    .filter(
5373                        &_key,
5374                        #root::core::SqlValue::String(prefix.into()),
5375                    )
5376                    .fetch(pool)
5377                    .await
5378            }
5379
5380            /// Fetch every row where `<col>` ends with `suffix`
5381            /// (auto-prepends `%`). Eloquent
5382            /// `whereLike("col", "%$suffix")` parity.
5383            ///
5384            /// # Errors
5385            /// As [`FetcherPool::fetch`].
5386            ///
5387            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5388            pub async fn where_ends_with(
5389                col: &str,
5390                suffix: impl ::core::convert::Into<::std::string::String>,
5391                pool: &#root::sql::Pool,
5392            ) -> ::core::result::Result<
5393                ::std::vec::Vec<Self>,
5394                #root::sql::ExecError,
5395            > {
5396                use #root::sql::FetcherPool as _;
5397                let _key = ::std::format!("{}__endswith", col);
5398                #root::query::QuerySet::<Self>::default()
5399                    .filter(
5400                        &_key,
5401                        #root::core::SqlValue::String(suffix.into()),
5402                    )
5403                    .fetch(pool)
5404                    .await
5405            }
5406
5407            /// Fetch every row where `<col>` contains `substr`
5408            /// (auto-wraps with `%`).
5409            /// Eloquent `whereLike("col", "%$substr%")` parity.
5410            ///
5411            /// # Errors
5412            /// As [`FetcherPool::fetch`].
5413            ///
5414            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5415            pub async fn where_contains(
5416                col: &str,
5417                substr: impl ::core::convert::Into<::std::string::String>,
5418                pool: &#root::sql::Pool,
5419            ) -> ::core::result::Result<
5420                ::std::vec::Vec<Self>,
5421                #root::sql::ExecError,
5422            > {
5423                use #root::sql::FetcherPool as _;
5424                let _key = ::std::format!("{}__contains", col);
5425                #root::query::QuerySet::<Self>::default()
5426                    .filter(
5427                        &_key,
5428                        #root::core::SqlValue::String(substr.into()),
5429                    )
5430                    .fetch(pool)
5431                    .await
5432            }
5433
5434            /// Fetch every row where `<col> > val`. Eloquent
5435            /// `Model::where($col, ">", $val)->get()` parity.
5436            ///
5437            /// # Errors
5438            /// As [`FetcherPool::fetch`].
5439            ///
5440            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5441            pub async fn where_gt(
5442                col: &str,
5443                val: impl ::core::convert::Into<#root::core::SqlValue>,
5444                pool: &#root::sql::Pool,
5445            ) -> ::core::result::Result<
5446                ::std::vec::Vec<Self>,
5447                #root::sql::ExecError,
5448            > {
5449                use #root::sql::FetcherPool as _;
5450                let _key = ::std::format!("{}__gt", col);
5451                #root::query::QuerySet::<Self>::default()
5452                    .filter(&_key, val)
5453                    .fetch(pool)
5454                    .await
5455            }
5456
5457            /// Fetch every row where `<col> >= val`. Eloquent
5458            /// `Model::where($col, ">=", $val)->get()` parity.
5459            ///
5460            /// # Errors
5461            /// As [`FetcherPool::fetch`].
5462            ///
5463            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5464            pub async fn where_gte(
5465                col: &str,
5466                val: impl ::core::convert::Into<#root::core::SqlValue>,
5467                pool: &#root::sql::Pool,
5468            ) -> ::core::result::Result<
5469                ::std::vec::Vec<Self>,
5470                #root::sql::ExecError,
5471            > {
5472                use #root::sql::FetcherPool as _;
5473                let _key = ::std::format!("{}__gte", col);
5474                #root::query::QuerySet::<Self>::default()
5475                    .filter(&_key, val)
5476                    .fetch(pool)
5477                    .await
5478            }
5479
5480            /// Fetch every row where `<col> < val`. Eloquent
5481            /// `Model::where($col, "<", $val)->get()` parity.
5482            ///
5483            /// # Errors
5484            /// As [`FetcherPool::fetch`].
5485            ///
5486            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5487            pub async fn where_lt(
5488                col: &str,
5489                val: impl ::core::convert::Into<#root::core::SqlValue>,
5490                pool: &#root::sql::Pool,
5491            ) -> ::core::result::Result<
5492                ::std::vec::Vec<Self>,
5493                #root::sql::ExecError,
5494            > {
5495                use #root::sql::FetcherPool as _;
5496                let _key = ::std::format!("{}__lt", col);
5497                #root::query::QuerySet::<Self>::default()
5498                    .filter(&_key, val)
5499                    .fetch(pool)
5500                    .await
5501            }
5502
5503            /// Fetch every row where `<col> <= val`. Eloquent
5504            /// `Model::where($col, "<=", $val)->get()` parity.
5505            ///
5506            /// # Errors
5507            /// As [`FetcherPool::fetch`].
5508            ///
5509            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5510            pub async fn where_lte(
5511                col: &str,
5512                val: impl ::core::convert::Into<#root::core::SqlValue>,
5513                pool: &#root::sql::Pool,
5514            ) -> ::core::result::Result<
5515                ::std::vec::Vec<Self>,
5516                #root::sql::ExecError,
5517            > {
5518                use #root::sql::FetcherPool as _;
5519                let _key = ::std::format!("{}__lte", col);
5520                #root::query::QuerySet::<Self>::default()
5521                    .filter(&_key, val)
5522                    .fetch(pool)
5523                    .await
5524            }
5525
5526            /// Fetch every row where `<col> <> val`. Eloquent
5527            /// `Model::where($col, "!=", $val)->get()` parity.
5528            ///
5529            /// # Errors
5530            /// As [`FetcherPool::fetch`].
5531            ///
5532            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5533            pub async fn where_ne(
5534                col: &str,
5535                val: impl ::core::convert::Into<#root::core::SqlValue>,
5536                pool: &#root::sql::Pool,
5537            ) -> ::core::result::Result<
5538                ::std::vec::Vec<Self>,
5539                #root::sql::ExecError,
5540            > {
5541                use #root::sql::FetcherPool as _;
5542                let _key = ::std::format!("{}__ne", col);
5543                #root::query::QuerySet::<Self>::default()
5544                    .filter(&_key, val)
5545                    .fetch(pool)
5546                    .await
5547            }
5548
5549            /// Fetch every row matching `<col> = val` for ANY of the
5550            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5551            /// parity. Empty `cols` returns no rows.
5552            ///
5553            /// Resolves each `&str` column to its SCHEMA-registered
5554            /// `&'static str` once and builds a single OR-composed
5555            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5556            ///
5557            /// # Errors
5558            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5559            /// when any column is not declared on the model.
5560            ///
5561            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5562            pub async fn where_any(
5563                cols: &[&str],
5564                val: impl ::core::convert::Into<#root::core::SqlValue>,
5565                pool: &#root::sql::Pool,
5566            ) -> ::core::result::Result<
5567                ::std::vec::Vec<Self>,
5568                #root::sql::ExecError,
5569            > {
5570                Self::__where_multi(cols, val, false, pool).await
5571            }
5572
5573            /// Fetch every row matching `<col> = val` for ALL listed
5574            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5575            /// Empty `cols` returns every row (vacuous AND).
5576            ///
5577            /// # Errors
5578            /// As [`Self::where_any`].
5579            pub async fn where_all(
5580                cols: &[&str],
5581                val: impl ::core::convert::Into<#root::core::SqlValue>,
5582                pool: &#root::sql::Pool,
5583            ) -> ::core::result::Result<
5584                ::std::vec::Vec<Self>,
5585                #root::sql::ExecError,
5586            > {
5587                Self::__where_multi(cols, val, true, pool).await
5588            }
5589
5590            /// Internal: build a Q expression composing `cols` via
5591            /// AND (`all`) or OR (`!all`), then fetch. Backs
5592            /// `where_any` / `where_all`.
5593            #[doc(hidden)]
5594            pub async fn __where_multi(
5595                cols: &[&str],
5596                val: impl ::core::convert::Into<#root::core::SqlValue>,
5597                all: bool,
5598                pool: &#root::sql::Pool,
5599            ) -> ::core::result::Result<
5600                ::std::vec::Vec<Self>,
5601                #root::sql::ExecError,
5602            > {
5603                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5604                    .await
5605            }
5606
5607            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5608            /// parity. PK-ordered
5609            /// is NOT guaranteed without an explicit `order_by` —
5610            /// drop into `Self::query()` for that.
5611            ///
5612            /// # Errors
5613            /// As [`FetcherPool::fetch`].
5614            ///
5615            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5616            pub async fn take(
5617                n: i64,
5618                pool: &#root::sql::Pool,
5619            ) -> ::core::result::Result<
5620                ::std::vec::Vec<Self>,
5621                #root::sql::ExecError,
5622            > {
5623                use #root::sql::FetcherPool as _;
5624                #root::query::QuerySet::<Self>::default()
5625                    .limit(n)
5626                    .fetch(pool)
5627                    .await
5628            }
5629
5630            /// Fetch the page-th window of `per_page` rows
5631            /// (1-indexed). Eloquent
5632            /// `Model::query()->forPage($page, $perPage)->get()`
5633            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5634            /// LIMIT per_page`; for large offsets this is O(N) —
5635            /// prefer keyset pagination via PK on hot paths.
5636            ///
5637            /// # Errors
5638            /// As [`FetcherPool::fetch`].
5639            ///
5640            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5641            pub async fn for_page(
5642                page: i64,
5643                per_page: i64,
5644                pool: &#root::sql::Pool,
5645            ) -> ::core::result::Result<
5646                ::std::vec::Vec<Self>,
5647                #root::sql::ExecError,
5648            > {
5649                use #root::sql::FetcherPool as _;
5650                let _offset = #root::list_params::page_offset(page, per_page);
5651                #root::query::QuerySet::<Self>::default()
5652                    .limit(per_page)
5653                    .offset(_offset)
5654                    .fetch(pool)
5655                    .await
5656            }
5657
5658            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5659            /// one page of rows AND the total row count in one
5660            /// call. Returns `(rows, total)`. Two queries: a
5661            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5662            /// for the total.
5663            ///
5664            /// Useful for paginated UIs that need both the visible
5665            /// rows AND a "Page X of Y" / total-count widget. For
5666            /// large tables prefer keyset pagination + cached count;
5667            /// every call to `paginate` re-counts the full table.
5668            ///
5669            /// Same 1-indexed `page` convention as [`Self::for_page`].
5670            ///
5671            /// # Errors
5672            /// As [`Self::for_page`] and [`Self::count`].
5673            pub async fn paginate(
5674                page: i64,
5675                per_page: i64,
5676                pool: &#root::sql::Pool,
5677            ) -> ::core::result::Result<
5678                (::std::vec::Vec<Self>, i64),
5679                #root::sql::ExecError,
5680            > {
5681                // Route through the queryset rather than `Self::count`:
5682                // the `count` inherent method is suppressed on models
5683                // that declare a field named `count` (field/shortcut
5684                // collision guard), and `paginate` must still compile
5685                // for those models.
5686                let total = {
5687                    use #root::sql::CounterPool as _;
5688                    #root::query::QuerySet::<Self>::default()
5689                        .count(pool)
5690                        .await?
5691                };
5692                let rows = Self::for_page(page, per_page, pool).await?;
5693                ::core::result::Result::Ok((rows, total))
5694            }
5695
5696            /// Bulk-update — set `set_col = set_val` on every row
5697            /// matching `where_col = where_val`. Returns affected row
5698            /// count. Eloquent
5699            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5700            /// parity.
5701            ///
5702            /// For multi-column updates drop into the queryset
5703            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5704            ///
5705            /// # Errors
5706            /// As [`UpdaterPool::execute_pool`].
5707            ///
5708            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5709            pub async fn update_where(
5710                where_col: &str,
5711                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5712                set_col: &str,
5713                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5714                pool: &#root::sql::Pool,
5715            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5716                let _query = #root::query::QuerySet::<Self>::default()
5717                    .filter(where_col, where_val)
5718                    .update()
5719                    .set(set_col, set_val)
5720                    .compile()?;
5721                #bulk_update_run
5722            }
5723
5724            /// Bulk-delete — remove every row matching
5725            /// `where_col = where_val`. Returns affected row count.
5726            /// Eloquent
5727            /// `Model::where($where_col, $where_val)->delete()` parity.
5728            ///
5729            /// # Errors
5730            /// As [`#root::sql::delete_pool`].
5731            pub async fn delete_where(
5732                where_col: &str,
5733                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5734                pool: &#root::sql::Pool,
5735            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5736                let _query = #root::query::QuerySet::<Self>::default()
5737                    .filter_op(where_col, #root::core::Op::Eq, where_val)
5738                    .compile_delete()?;
5739                #bulk_delete_run
5740            }
5741
5742            /// Bulk-update — set `set_col = set_val` on EVERY row of
5743            /// this model's table. **No WHERE clause** — use with
5744            /// care. Eloquent `Model::query()->update([$col => $val])`
5745            /// parity.
5746            ///
5747            /// Use for backfills, one-shot reset flows, etc. The
5748            /// returned count is rows affected.
5749            ///
5750            /// # Errors
5751            /// As [`UpdaterPool::execute_pool`].
5752            ///
5753            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5754            pub async fn update_all(
5755                set_col: &str,
5756                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5757                pool: &#root::sql::Pool,
5758            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5759                let _query = #root::query::QuerySet::<Self>::default()
5760                    .update()
5761                    .set(set_col, set_val)
5762                    .compile()?;
5763                #bulk_update_run
5764            }
5765
5766            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5767            /// Eloquent `Model::whereNotLike` parity. Pattern is
5768            /// passed verbatim — caller controls `%` / `_`.
5769            ///
5770            /// # Errors
5771            /// As [`FetcherPool::fetch`].
5772            ///
5773            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5774            pub async fn where_not_like(
5775                col: &str,
5776                pattern: impl ::core::convert::Into<::std::string::String>,
5777                pool: &#root::sql::Pool,
5778            ) -> ::core::result::Result<
5779                ::std::vec::Vec<Self>,
5780                #root::sql::ExecError,
5781            > {
5782                use #root::sql::FetcherPool as _;
5783                let _key = ::std::format!("{}__not_like", col);
5784                #root::query::QuerySet::<Self>::default()
5785                    .filter(
5786                        &_key,
5787                        #root::core::SqlValue::String(pattern.into()),
5788                    )
5789                    .fetch(pool)
5790                    .await
5791            }
5792
5793            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5794            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5795            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5796            ///
5797            /// # Errors
5798            /// As [`FetcherPool::fetch`].
5799            ///
5800            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5801            pub async fn where_not_ilike(
5802                col: &str,
5803                pattern: impl ::core::convert::Into<::std::string::String>,
5804                pool: &#root::sql::Pool,
5805            ) -> ::core::result::Result<
5806                ::std::vec::Vec<Self>,
5807                #root::sql::ExecError,
5808            > {
5809                use #root::sql::FetcherPool as _;
5810                let _key = ::std::format!("{}__not_ilike", col);
5811                #root::query::QuerySet::<Self>::default()
5812                    .filter(
5813                        &_key,
5814                        #root::core::SqlValue::String(pattern.into()),
5815                    )
5816                    .fetch(pool)
5817                    .await
5818            }
5819
5820            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5821            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5822            /// parity.
5823            ///
5824            /// # Errors
5825            /// As [`FetcherPool::fetch`].
5826            ///
5827            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5828            pub async fn where_not_between(
5829                col: &str,
5830                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5831                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5832                pool: &#root::sql::Pool,
5833            ) -> ::core::result::Result<
5834                ::std::vec::Vec<Self>,
5835                #root::sql::ExecError,
5836            > {
5837                use #root::sql::FetcherPool as _;
5838                let _key = ::std::format!("{}__not_between", col);
5839                let _vals = #root::core::SqlValue::List(::std::vec![
5840                    ::core::convert::Into::into(lo),
5841                    ::core::convert::Into::into(hi),
5842                ]);
5843                #root::query::QuerySet::<Self>::default()
5844                    .filter(&_key, _vals)
5845                    .fetch(pool)
5846                    .await
5847            }
5848
5849            /// Returns the SQL table name for this model. Eloquent
5850            /// `$model->getTable()` parity.
5851            #[must_use]
5852            pub fn table_name() -> &'static str {
5853                <Self as #root::core::Model>::SCHEMA.table
5854            }
5855
5856            /// Returns the SQL column name of this model's primary
5857            /// key, or `None` when the model has no
5858            /// `#[rustango(primary_key)]`. Eloquent
5859            /// `$model->getKeyName()` parity.
5860            #[must_use]
5861            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5862                <Self as #root::core::Model>::SCHEMA
5863                    .primary_key()
5864                    .map(|f| f.column)
5865            }
5866
5867            /// Fetch every row where `<col> BETWEEN lo AND hi`
5868            /// (inclusive on both ends — same as SQL). Eloquent
5869            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5870            ///
5871            /// # Errors
5872            /// As [`FetcherPool::fetch`].
5873            ///
5874            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5875            pub async fn where_between(
5876                col: &str,
5877                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5878                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5879                pool: &#root::sql::Pool,
5880            ) -> ::core::result::Result<
5881                ::std::vec::Vec<Self>,
5882                #root::sql::ExecError,
5883            > {
5884                use #root::sql::FetcherPool as _;
5885                let _key = ::std::format!("{}__between", col);
5886                let _vals = #root::core::SqlValue::List(::std::vec![
5887                    ::core::convert::Into::into(lo),
5888                    ::core::convert::Into::into(hi),
5889                ]);
5890                #root::query::QuerySet::<Self>::default()
5891                    .filter(&_key, _vals)
5892                    .fetch(pool)
5893                    .await
5894            }
5895
5896            /// Fetch the first row where `<col> = <val>`. Returns
5897            /// `Ok(None)` when no row matches. Eloquent
5898            /// `Model::firstWhere($col, $val)` parity.
5899            ///
5900            /// Thin wrapper over `QuerySet::<Self>::default()
5901            /// .filter(col, val).first(pool)`. Use this when you
5902            /// want one row identified by a non-PK column (e.g.
5903            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5904            ///
5905            /// `val` accepts any value `Into<SqlValue>` so plain
5906            /// strings, ints, UUIDs, etc. all work.
5907            ///
5908            /// # Errors
5909            /// As `QuerySet::first`.
5910            pub async fn first_where(
5911                col: &str,
5912                val: impl ::core::convert::Into<#root::core::SqlValue>,
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                    .filter(col, val)
5920                    .first(pool)
5921                    .await
5922            }
5923
5924            #value_method
5925
5926            /// Fetch the row with the largest non-NULL `field` value.
5927            /// Returns `Ok(None)` when the table is empty or every
5928            /// `field` is NULL. Thin wrapper over
5929            /// `QuerySet::<Self>::default().latest(field, pool)`.
5930            ///
5931            /// **Field name** is the Rust field ident as a string
5932            /// (not the SQL column). It is a runtime `&str`, so an
5933            /// unknown one compiles and surfaces as
5934            /// `ExecError::Query(QueryError::UnknownField)` when the
5935            /// query runs.
5936            ///
5937            /// # Errors
5938            /// As `QuerySet::latest`.
5939            pub async fn latest(
5940                field: &str,
5941                pool: &#root::sql::Pool,
5942            ) -> ::core::result::Result<
5943                ::core::option::Option<Self>,
5944                #root::sql::ExecError,
5945            > {
5946                #root::query::QuerySet::<Self>::default()
5947                    .latest(field, pool)
5948                    .await
5949            }
5950
5951            /// Sibling of [`Self::latest`]: fetches the row with the
5952            /// smallest non-NULL `field` value.
5953            ///
5954            /// # Errors
5955            /// As [`Self::latest`].
5956            pub async fn earliest(
5957                field: &str,
5958                pool: &#root::sql::Pool,
5959            ) -> ::core::result::Result<
5960                ::core::option::Option<Self>,
5961                #root::sql::ExecError,
5962            > {
5963                #root::query::QuerySet::<Self>::default()
5964                    .earliest(field, pool)
5965                    .await
5966            }
5967
5968            #count_method
5969
5970            /// `true` when the table contains at least one row.
5971            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
5972            /// over `QuerySet::<Self>::default().exists(pool)`.
5973            ///
5974            /// # Errors
5975            /// As [`ExistsPool::exists`].
5976            ///
5977            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5978            pub async fn exists(
5979                pool: &#root::sql::Pool,
5980            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5981                use #root::sql::ExistsPool as _;
5982                #root::query::QuerySet::<Self>::default()
5983                    .exists(pool)
5984                    .await
5985            }
5986
5987            /// Inverse of [`Self::exists`] — returns `true` when
5988            /// the table has zero rows. Eloquent
5989            /// `Model::doesntExist()` parity.
5990            ///
5991            /// # Errors
5992            /// As [`Self::exists`].
5993            pub async fn doesnt_exist(
5994                pool: &#root::sql::Pool,
5995            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5996                Self::exists(pool).await.map(|e| !e)
5997            }
5998
5999            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6000            /// `true` when a row with primary key `pk` exists in
6001            /// the table. Sugar over
6002            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6003            ///
6004            /// Differs from [`Self::exists`] (which checks "any row
6005            /// in the table") by checking a specific PK existence.
6006            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6007            /// because the row is never materialized — the SQL is
6008            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6009            ///
6010            /// # Errors
6011            /// As [`#root::sql::ExistsPool::contains_pk`].
6012            pub async fn contains_pk(
6013                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6014                pool: &#root::sql::Pool,
6015            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6016                use #root::sql::ExistsPool as _;
6017                #root::query::QuerySet::<Self>::default()
6018                    .contains_pk(pool, pk)
6019                    .await
6020            }
6021
6022            #sum_method
6023            #avg_method
6024            #min_method
6025            #max_method
6026
6027            /// Fetch every row of this model from `pool`. Eloquent
6028            /// `Model::all()` parity — a thin wrapper over
6029            /// `QuerySet::<Self>::default().fetch(pool)`.
6030            ///
6031            /// **Use with care on large tables** — there's no
6032            /// pagination or limit; the entire table is materialized
6033            /// into memory. For anything beyond fixture / lookup
6034            /// tables, page through `QuerySet::<Self>::default()
6035            /// .order_by(...).limit(N).offset(M).fetch(pool)`
6036            /// or stream via `.iterator(chunk_size)`.
6037            ///
6038            /// # Errors
6039            /// As [`FetcherPool::fetch`].
6040            ///
6041            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6042            pub async fn all(
6043                pool: &#root::sql::Pool,
6044            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6045            {
6046                use #root::sql::FetcherPool as _;
6047                #root::query::QuerySet::<Self>::default()
6048                    .fetch(pool)
6049                    .await
6050            }
6051
6052            /// Look up every row whose primary key is in `pks`.
6053            /// Returns the matching rows in **inventory** order — NOT
6054            /// the order of `pks`. Empty `pks` returns an empty
6055            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6056            /// with a list) parity.
6057            ///
6058            /// Thin wrapper over `QuerySet::<Self>::default()
6059            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6060            /// Caller-supplied PKs that don't match a row are
6061            /// silently skipped (the returned vec is shorter than
6062            /// the input list). For an order-preserving / "fail
6063            /// when any missing" variant, build the queryset
6064            /// explicitly with `in_bulk` instead.
6065            ///
6066            /// Accepts any iterable whose elements are
6067            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6068            /// `Vec<Uuid>`, etc.
6069            ///
6070            /// # Errors
6071            /// As [`FetcherPool::fetch`].
6072            ///
6073            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6074            pub async fn find_many<V>(
6075                pks: impl ::core::iter::IntoIterator<Item = V>,
6076                pool: &#root::sql::Pool,
6077            ) -> ::core::result::Result<
6078                ::std::vec::Vec<Self>,
6079                #root::sql::ExecError,
6080            >
6081            where
6082                V: ::core::convert::Into<#root::core::SqlValue>,
6083            {
6084                use #root::sql::FetcherPool as _;
6085                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6086                    pks.into_iter().map(::core::convert::Into::into).collect();
6087                if _values.is_empty() {
6088                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6089                }
6090                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6091                let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
6092                let _split = _batches.len() > 1;
6093                let mut _rows = ::std::vec::Vec::new();
6094                for _keys in _batches {
6095                    _rows.extend(
6096                        #root::query::QuerySet::<Self>::default()
6097                            .filter(&_key, #root::core::SqlValue::List(_keys))
6098                            .fetch(pool)
6099                            .await?,
6100                    );
6101                }
6102                if _split {
6103                    _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
6104                }
6105                ::core::result::Result::Ok(_rows)
6106            }
6107
6108            /// Look up the row whose primary key equals `pk`. Returns
6109            /// `Ok(None)` when no row matches, rather than erroring.
6110            /// Eloquent `Model::find`
6111            /// shape — accepts any value `Into<SqlValue>`.
6112            ///
6113            /// One-liner shortcut for the common
6114            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6115            /// .limit(1).fetch(pool).await?.into_iter().next()`
6116            /// dance.
6117            ///
6118            /// # Errors
6119            /// As [`FetcherPool::fetch`].
6120            ///
6121            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6122            pub async fn find(
6123                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6124                pool: &#root::sql::Pool,
6125            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6126            {
6127                use #root::sql::FetcherPool as _;
6128                let _pk_val: #root::core::SqlValue = pk.into();
6129                let mut _rows: ::std::vec::Vec<Self> =
6130                    #root::query::QuerySet::<Self>::default()
6131                        .filter(::core::stringify!(#pk_ident), _pk_val)
6132                        .limit(1)
6133                        .fetch(pool)
6134                        .await?;
6135                ::core::result::Result::Ok(_rows.into_iter().next())
6136            }
6137
6138            /// Look up the row whose primary key equals `pk`. Errors
6139            /// when no row matches — the throwing counterpart of
6140            /// [`Self::find`]. Eloquent `Model::findOrFail`
6141            /// parity.
6142            ///
6143            /// Translates the miss into
6144            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6145            /// so callers can `?`-bubble straight through the typical
6146            /// `ExecError` error chain.
6147            ///
6148            /// # Errors
6149            /// As [`Self::find`]; additionally
6150            /// [`sqlx::Error::RowNotFound`] when no row matches.
6151            ///
6152            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6153            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6154            pub async fn find_or_fail(
6155                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6156                pool: &#root::sql::Pool,
6157            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6158                match Self::find(pk, pool).await? {
6159                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6160                    ::core::option::Option::None => ::core::result::Result::Err(
6161                        #root::sql::ExecError::Driver(
6162                            #root::sql::sqlx::Error::RowNotFound,
6163                        ),
6164                    ),
6165                }
6166            }
6167
6168            /// Look up multiple rows by primary key, **failing** if
6169            /// any requested PK is missing. Eloquent
6170            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6171            /// inventory order (NOT request order — same as
6172            /// [`Self::find_many`]).
6173            ///
6174            /// Empty `pks` returns an empty `Vec` (no rows requested
6175            /// → nothing to fail on).
6176            ///
6177            /// Differs from [`Self::find_many`] in that this method
6178            /// requires every PK to resolve. If even one is missing,
6179            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6180            /// in `ExecError::Driver`). A key counts as found when the
6181            /// database matches it to a row, so repeats and keys its
6182            /// collation folds together (`abc` / `ABC` on MySQL) pass.
6183            ///
6184            /// # Errors
6185            /// As [`Self::find_many`], plus `RowNotFound` when a
6186            /// requested PK matches no row.
6187            pub async fn find_many_or_fail<V>(
6188                pks: impl ::core::iter::IntoIterator<Item = V>,
6189                pool: &#root::sql::Pool,
6190            ) -> ::core::result::Result<
6191                ::std::vec::Vec<Self>,
6192                #root::sql::ExecError,
6193            >
6194            where
6195                V: ::core::convert::Into<#root::core::SqlValue>,
6196            {
6197                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6198                    pks.into_iter().map(::core::convert::Into::into).collect();
6199                if _values.is_empty() {
6200                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6201                }
6202                use #root::sql::ExistsPool as _;
6203                let mut _seen: ::std::collections::HashSet<
6204                    ::std::string::String,
6205                > = ::std::collections::HashSet::new();
6206                let mut _keys: ::std::vec::Vec<#root::core::SqlValue> =
6207                    ::std::vec::Vec::new();
6208                for v in &_values {
6209                    if _seen.insert(v.to_display_string()) {
6210                        _keys.push(::core::clone::Clone::clone(v));
6211                    }
6212                }
6213                let _rows = Self::find_many(_values, pool).await?;
6214                let _found: ::std::collections::HashSet<::std::string::String> = _rows
6215                    .iter()
6216                    .map(|r| r.__rustango_pk_value().to_display_string())
6217                    .collect();
6218                // A key with no exact twin among the rows may still match one
6219                // under the column's collation: the database decides.
6220                for _key in _keys {
6221                    if _found.contains(&_key.to_display_string()) {
6222                        continue;
6223                    }
6224                    let _hit = #root::query::QuerySet::<Self>::default()
6225                        .filter(::core::stringify!(#pk_ident), _key)
6226                        .exists(pool)
6227                        .await?;
6228                    if !_hit {
6229                        return ::core::result::Result::Err(
6230                            #root::sql::ExecError::Driver(
6231                                #root::sql::sqlx::Error::RowNotFound,
6232                            ),
6233                        );
6234                    }
6235                }
6236                ::core::result::Result::Ok(_rows)
6237            }
6238
6239            /// Find by primary key or run `fallback` to produce a
6240            /// default row to return. Eloquent
6241            /// `Model::findOr($pk, fn() => …)` parity.
6242            ///
6243            /// Unlike [`Self::find_or_fail`] (which raises on
6244            /// miss), this is the "give me something sensible"
6245            /// branch: typical use is "fetch the user's row, else
6246            /// fall back to an anonymous/guest stub".
6247            ///
6248            /// `fallback` runs only when no row matches — the DB
6249            /// round-trip happens unconditionally.
6250            ///
6251            /// # Errors
6252            /// As [`Self::find`].
6253            pub async fn find_or<F>(
6254                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6255                pool: &#root::sql::Pool,
6256                fallback: F,
6257            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6258            where
6259                F: ::core::ops::FnOnce() -> Self,
6260            {
6261                ::core::result::Result::Ok(
6262                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6263                )
6264            }
6265
6266            /// Same as [`Self::find_or`] but also returns a `bool`
6267            /// indicating whether the row was found in the DB
6268            /// (`true`) or freshly built from `fallback` (`false`).
6269            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6270            /// in PHP the returned model also exposes `->exists`;
6271            /// here that's surfaced as the second tuple element.
6272            ///
6273            /// Useful for edit-or-create form handlers where the
6274            /// caller needs to know whether to PATCH or POST when
6275            /// the user submits the form.
6276            ///
6277            /// # Errors
6278            /// As [`Self::find`].
6279            pub async fn find_or_new<F>(
6280                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6281                pool: &#root::sql::Pool,
6282                fallback: F,
6283            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6284            where
6285                F: ::core::ops::FnOnce() -> Self,
6286            {
6287                match Self::find(pk, pool).await? {
6288                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6289                    ::core::option::Option::None => {
6290                        ::core::result::Result::Ok((fallback(), false))
6291                    }
6292                }
6293            }
6294
6295            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6296            /// [`Self::find_or_new`] but **persists** the new row
6297            /// when the PK isn't found. Returns `(row, exists: bool)`
6298            /// — `exists=true` when the PK was found,
6299            /// `false` when a fresh row was inserted.
6300            ///
6301            /// ```ignore
6302            /// let (post, found) = Post::find_or_insert(
6303            ///     pk,
6304            ///     &pool,
6305            ///     || Post { id: Auto::default(), title: "new".into() },
6306            /// ).await?;
6307            /// // `found` true → returned existing row;
6308            /// // `found` false → fallback was inserted, `post.id` populated.
6309            /// ```
6310            ///
6311            /// **Caveat**: two concurrent calls that both miss the
6312            /// find can both INSERT, violating uniqueness. Wrap in a
6313            /// transaction or rely on a UNIQUE constraint + handle
6314            /// the conflict error if you need race-free semantics.
6315            ///
6316            /// # Errors
6317            /// As [`Self::find`] and [`Self::save`].
6318            pub async fn find_or_insert<F>(
6319                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6320                pool: &#root::sql::Pool,
6321                fallback: F,
6322            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6323            where
6324                F: ::core::ops::FnOnce() -> Self,
6325            {
6326                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6327                    return ::core::result::Result::Ok((_row, true));
6328                }
6329                let mut _new = fallback();
6330                _new.save(pool).await?;
6331                ::core::result::Result::Ok((_new, false))
6332            }
6333
6334            /// Fetch the first row of the table, or run `fallback`
6335            /// when the table is empty. Eloquent
6336            /// `Model::firstOr(fn() => …)` parity.
6337            ///
6338            /// # Errors
6339            /// As [`Self::first`].
6340            pub async fn first_or<F>(
6341                pool: &#root::sql::Pool,
6342                fallback: F,
6343            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6344            where
6345                F: ::core::ops::FnOnce() -> Self,
6346            {
6347                // Route through the queryset rather than `Self::first`,
6348                // which is suppressed on models with a field named
6349                // `first` (field/shortcut collision guard).
6350                ::core::result::Result::Ok(
6351                    #root::query::QuerySet::<Self>::default()
6352                        .first(pool)
6353                        .await?
6354                        .unwrap_or_else(fallback),
6355                )
6356            }
6357
6358            /// Fetch exactly one row matching `<col> = <val>`. Errors
6359            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6360            /// or more than one matches
6361            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6362            /// Eloquent `Model::sole($col, $val)` parity.
6363            ///
6364            /// # Errors
6365            /// As [`Self::where_`] plus the explicit
6366            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6367            pub async fn sole(
6368                col: &str,
6369                val: impl ::core::convert::Into<#root::core::SqlValue>,
6370                pool: &#root::sql::Pool,
6371            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6372                let mut _rows = Self::where_(col, val, pool).await?;
6373                match _rows.len() {
6374                    0 => ::core::result::Result::Err(
6375                        #root::sql::ExecError::Driver(
6376                            #root::sql::sqlx::Error::RowNotFound,
6377                        ),
6378                    ),
6379                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6380                    n => ::core::result::Result::Err(
6381                        #root::sql::ExecError::MultipleRowsReturned {
6382                            op: "sole",
6383                            table: <Self as #root::core::Model>::SCHEMA.name,
6384                            count: n,
6385                        },
6386                    ),
6387                }
6388            }
6389        }
6390    } else {
6391        quote!()
6392    };
6393
6394    // `_tx` family: the shared `__rustango_*_exec` bodies on an open
6395    // `PoolTx`; an audited model writes its snapshot entry in the same tx.
6396    let tx_emit = |op: TokenStream2| -> TokenStream2 {
6397        if audited_fields.is_some() && primary_key.is_some() {
6398            quote! {
6399                #root::audit::__emit_many_tx(
6400                    tx,
6401                    &[self.__rustango_audit_entry(#root::audit::AuditOp::#op)],
6402                )
6403                .await?;
6404            }
6405        } else {
6406            quote!()
6407        }
6408    };
6409    let tx_insert_method = {
6410        let self_ref = if fields.has_auto {
6411            quote!(&mut self)
6412        } else {
6413            quote!(&self)
6414        };
6415        let emit = tx_emit(quote!(Create));
6416        let refuse_pkless = if audited_fields.is_some() && primary_key.is_none() {
6417            quote! {
6418                return ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
6419                    table: <Self as #root::core::Model>::SCHEMA.table,
6420                });
6421            }
6422        } else {
6423            quote!()
6424        };
6425        quote! {
6426            /// Insert this row inside an open transaction; an audited model
6427            /// writes its audit row there too.
6428            ///
6429            /// # Errors
6430            /// As [`Self::insert`].
6431            #[allow(unreachable_code)]
6432            pub async fn insert_tx(
6433                #self_ref,
6434                tx: &mut #root::sql::PoolTx<'_>,
6435            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6436                #refuse_pkless
6437                self.__rustango_insert_exec(&mut *tx).await?;
6438                #emit
6439                ::core::result::Result::Ok(())
6440            }
6441        }
6442    };
6443    let tx_save_method = if let Some((pk_ident, _)) = primary_key {
6444        let emit_create = tx_emit(quote!(Create));
6445        let emit_update = tx_emit(quote!(Update));
6446        let was_unset = if fields.pk_is_auto {
6447            quote!(matches!(self.#pk_ident, #root::sql::Auto::Unset))
6448        } else {
6449            quote!(false)
6450        };
6451        quote! {
6452            /// Save this row inside an open transaction; an audited model
6453            /// writes its audit row there too.
6454            ///
6455            /// # Errors
6456            /// As [`Self::save`].
6457            pub async fn save_tx(
6458                &mut self,
6459                tx: &mut #root::sql::PoolTx<'_>,
6460            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6461                let _was_unset = #was_unset;
6462                let _n = self.__rustango_save_exec(&mut *tx).await?;
6463                if _n > 0 {
6464                    if _was_unset {
6465                        #emit_create
6466                    } else {
6467                        #emit_update
6468                    }
6469                }
6470                ::core::result::Result::Ok(_n)
6471            }
6472        }
6473    } else {
6474        quote!()
6475    };
6476    let tx_delete_method = if primary_key.is_some() {
6477        let emit = tx_emit(quote!(Delete));
6478        quote! {
6479            /// Delete this row inside an open transaction; an audited model
6480            /// writes its audit row there too.
6481            ///
6482            /// # Errors
6483            /// As [`Self::delete`].
6484            pub async fn delete_tx(
6485                &self,
6486                tx: &mut #root::sql::PoolTx<'_>,
6487            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6488                let _n = self.__rustango_delete_exec(&mut *tx).await?;
6489                if _n > 0 {
6490                    #emit
6491                }
6492                ::core::result::Result::Ok(_n)
6493            }
6494        }
6495    } else {
6496        quote!()
6497    };
6498
6499    // The one statement-level write path: `_on` (refusing audited models)
6500    // and `_tx` share these `__rustango_*_exec` bodies; non-audited `&Pool`
6501    // methods forward to them too.
6502    let refuse = |method: &str| -> TokenStream2 {
6503        let reason = format!("`{method}` cannot audit; use the `&Pool` method");
6504        quote!(#root::sql::__macro_internals::refuse_audited::<Self>(#reason)?;)
6505    };
6506    let refuse_save = refuse("save_on");
6507    let save_method = if let Some((pk_ident, pk_column)) = primary_key {
6508        let pk_column_lit = pk_column.as_str();
6509        let assignments = &fields.update_assignments;
6510        let dispatch_unset = if fields.pk_is_auto {
6511            quote! {
6512                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6513                    return self.__rustango_insert_exec(_executor).await.map(|()| 1u64);
6514                }
6515            }
6516        } else {
6517            quote!()
6518        };
6519        let write = restore_auto_now_unless(
6520            &fields.auto_now_idents,
6521            &save_ret,
6522            &save_keep,
6523            &quote! {
6524                let _query = #root::core::UpdateQuery::new(
6525                    <Self as #root::core::Model>::SCHEMA,
6526                    ::std::vec![ #( #assignments ),* ],
6527                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6528                        #pk_column_lit,
6529                        #root::core::Op::Eq,
6530                        ::core::convert::Into::<#root::core::SqlValue>::into(
6531                            ::core::clone::Clone::clone(&self.#pk_ident)
6532                        ),
6533                    )),
6534                );
6535                #root::sql::__macro_internals::update_on(_executor, &_query).await
6536            },
6537        );
6538        let upsert = if fields.pk_is_auto {
6539            let upsert_cols = &fields.upsert_update_columns;
6540            let upsert_pushes = &fields.insert_pushes;
6541            let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6542                fields.uuid_v7_fields.iter().cloned().unzip();
6543            let upsert_returning = &fields.returning_cols;
6544            let auto_now_idents = &fields.auto_now_idents;
6545            // Prefer the first `unique_together`: a server-assigned PK never
6546            // collides, so a PK-only target would always insert.
6547            let upsert_target_columns: Vec<String> =
6548                upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6549            let upsert_target_lits = upsert_target_columns
6550                .iter()
6551                .map(String::as_str)
6552                .collect::<Vec<_>>();
6553            let conflict_clause = if fields.upsert_update_columns.is_empty() {
6554                quote!(#root::core::ConflictClause::DoNothing)
6555            } else {
6556                quote!(#root::core::ConflictClause::DoUpdate {
6557                    target: ::std::vec![ #( #upsert_target_lits ),* ],
6558                    update_columns: ::std::vec![ #( #upsert_cols ),* ],
6559                })
6560            };
6561            let unit_ret = quote!(::core::result::Result<(), #root::sql::ExecError>);
6562            let unit_keep = quote!(_saved.is_ok());
6563            let exec_body = restore_auto_now_unless(
6564                auto_now_idents,
6565                &unit_ret,
6566                &unit_keep,
6567                &quote! {
6568                    let _query = self.__rustango_upsert_query();
6569                    let _row = #root::sql::__macro_internals::insert_returning_on(
6570                        _executor, &_query,
6571                    )
6572                    .await?;
6573                    #root::sql::apply_auto_pk(_row, self)
6574                },
6575            );
6576            let pool_body = if audited_fields.is_some() {
6577                restore_auto_now_unless(
6578                    auto_now_idents,
6579                    &unit_ret,
6580                    &unit_keep,
6581                    &quote! {
6582                        let _query = self.__rustango_upsert_query();
6583                        #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self, _op| {
6584                            _m.__rustango_audit_entry(_op)
6585                        })
6586                        .await
6587                    },
6588                )
6589            } else {
6590                quote!(self.__rustango_upsert_exec(pool).await)
6591            };
6592            let refuse_upsert = refuse("upsert_on");
6593            quote! {
6594                /// Insert this row, or update the row it conflicts with: one
6595                /// `INSERT … ON CONFLICT` (MySQL: `ON DUPLICATE KEY UPDATE`).
6596                /// The target is the first `unique_together`, else the PK.
6597                /// Fills the `Auto<T>` fields from the written row.
6598                ///
6599                /// # Errors
6600                /// As [`Self::insert`].
6601                pub async fn upsert(
6602                    &mut self,
6603                    pool: &#root::sql::Pool,
6604                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6605                    #pool_body
6606                }
6607
6608                /// [`Self::upsert`] on any [`#root::sql::SqlExecutor`]. An
6609                /// audited model refuses it: use [`Self::upsert`].
6610                ///
6611                /// # Errors
6612                /// As [`Self::upsert`].
6613                pub async fn upsert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6614                    &mut self,
6615                    _executor: _E,
6616                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6617                    #refuse_upsert
6618                    self.__rustango_upsert_exec(_executor).await
6619                }
6620
6621                #[doc(hidden)]
6622                pub async fn __rustango_upsert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6623                    &mut self,
6624                    _executor: _E,
6625                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6626                    #exec_body
6627                }
6628
6629                /// The upsert statement; stamps `auto_now` like `save`.
6630                #[doc(hidden)]
6631                pub fn __rustango_upsert_query(&mut self) -> #root::core::InsertQuery {
6632                    let mut _columns: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
6633                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6634                        ::std::vec::Vec::new();
6635                    #[allow(unused_mut)]
6636                    let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
6637                    #(
6638                        if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
6639                            _generated.push(#gen_cols);
6640                        }
6641                    )*
6642                    #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
6643                    #( #upsert_pushes )*
6644                    #root::core::InsertQuery::new(
6645                        <Self as #root::core::Model>::SCHEMA,
6646                        _columns,
6647                        _values,
6648                    )
6649                    .returning(::std::vec![ #( #upsert_returning ),* ])
6650                    .on_conflict(#conflict_clause)
6651                    .generated(_generated)
6652                }
6653            }
6654        } else {
6655            quote!()
6656        };
6657        Some(quote! {
6658            /// [`Self::save`] on any [`#root::sql::SqlExecutor`]: a held
6659            /// `PoolConn`, a `PoolTx`, a tenant connection or a Postgres sqlx
6660            /// executor. An audited model refuses it: use [`Self::save`].
6661            ///
6662            /// # Errors
6663            /// As [`Self::save`].
6664            pub async fn save_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6665                &mut self,
6666                _executor: _E,
6667            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6668                #refuse_save
6669                self.__rustango_save_exec(_executor).await
6670            }
6671
6672            #[doc(hidden)]
6673            pub async fn __rustango_save_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6674                &mut self,
6675                _executor: _E,
6676            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6677                #dispatch_unset
6678                #write
6679            }
6680
6681            /// Old name of [`Self::save`].
6682            ///
6683            /// # Errors
6684            /// As [`Self::save`].
6685            #[deprecated(since = "0.62.0", note = "use `save`")]
6686            pub async fn save_pool(
6687                &mut self,
6688                pool: &#root::sql::Pool,
6689            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6690                self.save(pool).await
6691            }
6692
6693            #upsert
6694        })
6695    } else {
6696        None
6697    };
6698
6699    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6700        let pk_column_lit = pk_column.as_str();
6701        let refuse_delete = refuse("delete_on");
6702        // Optional `soft_delete_on` / `restore_on` companions when the
6703        // model has a `#[rustango(soft_delete)]` column. They land
6704        // alongside the regular `delete_on` so callers have both
6705        // options — a hard delete (audit-tracked as a real DELETE) and
6706        // a logical delete (audit-tracked as an UPDATE setting the
6707        // deleted_at column to NOW()).
6708        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6709            let col_lit = col;
6710            let sd_field_ident = fields
6711                .soft_delete_field_ident
6712                .clone()
6713                .expect("soft_delete_column without ident");
6714            let refuse_soft_delete = refuse("soft_delete_on");
6715            let refuse_restore = refuse("restore_on");
6716            quote! {
6717                /// Soft-delete this row on any [`#root::sql::SqlExecutor`]: set
6718                /// its `#[rustango(soft_delete)]` column to now. An audited
6719                /// model refuses it: use [`Self::soft_delete`].
6720                ///
6721                /// # Errors
6722                /// As [`Self::delete`].
6723                pub async fn soft_delete_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6724                    &self,
6725                    _executor: _E,
6726                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6727                    #refuse_soft_delete
6728                    let _query = #root::soft_delete::__mark_query(
6729                        <Self as #root::core::Model>::SCHEMA,
6730                        #col_lit,
6731                        #pk_column_lit,
6732                        ::core::convert::Into::<#root::core::SqlValue>::into(
6733                            ::core::clone::Clone::clone(&self.#pk_ident)
6734                        ),
6735                        ::core::option::Option::Some(#root::__chrono::Utc::now()),
6736                    );
6737                    #root::sql::__macro_internals::update_on(_executor, &_query).await
6738                }
6739
6740                /// Inverse of [`Self::soft_delete_on`]: clear the soft-delete
6741                /// column. An audited model refuses it: use [`Self::restore`].
6742                ///
6743                /// # Errors
6744                /// As [`Self::delete`].
6745                pub async fn restore_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6746                    &self,
6747                    _executor: _E,
6748                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6749                    #refuse_restore
6750                    let _query = #root::soft_delete::__mark_query(
6751                        <Self as #root::core::Model>::SCHEMA,
6752                        #col_lit,
6753                        #pk_column_lit,
6754                        ::core::convert::Into::<#root::core::SqlValue>::into(
6755                            ::core::clone::Clone::clone(&self.#pk_ident)
6756                        ),
6757                        ::core::option::Option::None,
6758                    );
6759                    #root::sql::__macro_internals::update_on(_executor, &_query).await
6760                }
6761
6762                /// Soft-delete this row on any backend. Eloquent `Model::delete()` semantics on
6763                /// soft-delete-enabled models (closes #821 partial).
6764                ///
6765                /// Sets the `#[rustango(soft_delete)]` column to
6766                /// `NOW()` on every backend. Query helpers
6767                /// (`QuerySet::active()` / `only_trashed()`,
6768                /// `soft_delete::active_filter` /
6769                /// `compose_with_active`) filter trashed rows out by
6770                /// reading `IS NULL` on the same column. Keyed on the PK;
6771                /// global scopes do not apply.
6772                ///
6773                /// # Errors
6774                /// As [`#root::sql::update_pool`].
6775                pub async fn soft_delete(
6776                    &self,
6777                    pool: &#root::sql::Pool,
6778                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6779                    let _now = #root::__chrono::Utc::now();
6780                    let _query = #root::soft_delete::__mark_query(
6781                        <Self as #root::core::Model>::SCHEMA,
6782                        #col_lit,
6783                        #pk_column_lit,
6784                        ::core::convert::Into::<#root::core::SqlValue>::into(
6785                            ::core::clone::Clone::clone(&self.#pk_ident)
6786                        ),
6787                        ::core::option::Option::Some(_now),
6788                    );
6789                    #pool_softdelete_run
6790                }
6791
6792                /// Tri-dialect counterpart of [`Self::restore_on`].
6793                /// Clears the `#[rustango(soft_delete)]` column back
6794                /// to `NULL`, marking the row live again. Eloquent
6795                /// `Model::restore()` parity. Keyed on the PK; global
6796                /// scopes do not apply.
6797                ///
6798                /// # Errors
6799                /// As [`#root::sql::update_pool`].
6800                pub async fn restore(
6801                    &self,
6802                    pool: &#root::sql::Pool,
6803                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6804                    let _query = #root::soft_delete::__mark_query(
6805                        <Self as #root::core::Model>::SCHEMA,
6806                        #col_lit,
6807                        #pk_column_lit,
6808                        ::core::convert::Into::<#root::core::SqlValue>::into(
6809                            ::core::clone::Clone::clone(&self.#pk_ident)
6810                        ),
6811                        ::core::option::Option::None,
6812                    );
6813                    #pool_restore_run
6814                }
6815
6816                /// Hard-delete this row, ignoring the soft-delete
6817                /// column. Eloquent `Model::forceDelete()` parity —
6818                /// the escape hatch when you need to actually purge
6819                /// data (GDPR, fixture cleanup, etc.).
6820                ///
6821                /// Equivalent to [`Self::delete_pool`] (the framework's
6822                /// non-soft delete) — exposed under the Eloquent name
6823                /// for muscle-memory + so soft-delete-enabled models
6824                /// have all three operations (soft / restore / force)
6825                /// in one place.
6826                ///
6827                /// # Errors
6828                /// As [`Self::delete_pool`].
6829                pub async fn force_delete(
6830                    &self,
6831                    pool: &#root::sql::Pool,
6832                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6833                    Self::delete(self, pool).await
6834                }
6835
6836                /// Returns `true` when this row is soft-deleted (its
6837                /// `#[rustango(soft_delete)]` column is currently
6838                /// set — Eloquent `$model->trashed()` parity).
6839                ///
6840                /// Pure in-memory predicate over `&self`; does not
6841                /// hit the database. Useful in admin/template code
6842                /// like `{% if post.trashed() %}…{% endif %}` and in
6843                /// guard clauses on restore/force-delete flows.
6844                pub fn trashed(&self) -> bool {
6845                    ::core::option::Option::is_some(&self.#sd_field_ident)
6846                }
6847
6848                /// Fetch every row whose `#[rustango(soft_delete)]`
6849                /// column is `NULL` (a.k.a. the live, non-trashed
6850                /// rows). Eloquent's default `Model::all()` behavior on
6851                /// a soft-delete model (Eloquent auto-scopes trashed
6852                /// rows out; rustango doesn't have auto-scoping yet —
6853                /// see issue #820 — so this is the explicit shortcut).
6854                ///
6855                /// One-liner over `QuerySet::<Self>::default()
6856                /// .active().fetch(pool)`. Closes #821 partial.
6857                ///
6858                /// # Errors
6859                /// As [`#root::sql::FetcherPool::fetch`].
6860                pub async fn active(
6861                    pool: &#root::sql::Pool,
6862                ) -> ::core::result::Result<
6863                    ::std::vec::Vec<Self>,
6864                    #root::sql::ExecError,
6865                > {
6866                    use #root::sql::FetcherPool as _;
6867                    #root::query::QuerySet::<Self>::default()
6868                        .active()
6869                        .fetch(pool)
6870                        .await
6871                }
6872
6873                /// Fetch ONLY soft-deleted rows. Eloquent
6874                /// `Model::onlyTrashed()->get()` parity — drives the
6875                /// admin "Trash" page, restore flows, GDPR purge
6876                /// scans, etc.
6877                ///
6878                /// One-liner over `QuerySet::<Self>::default()
6879                /// .only_trashed().fetch(pool)`. Closes #821
6880                /// partial.
6881                ///
6882                /// # Errors
6883                /// As [`#root::sql::FetcherPool::fetch`].
6884                pub async fn only_trashed(
6885                    pool: &#root::sql::Pool,
6886                ) -> ::core::result::Result<
6887                    ::std::vec::Vec<Self>,
6888                    #root::sql::ExecError,
6889                > {
6890                    use #root::sql::FetcherPool as _;
6891                    #root::query::QuerySet::<Self>::default()
6892                        .only_trashed()
6893                        .fetch(pool)
6894                        .await
6895                }
6896
6897                /// Fetch every row, both live and soft-deleted.
6898                /// Eloquent `Model::withTrashed()->get()` parity.
6899                ///
6900                /// Today every queryset already includes trashed rows
6901                /// (rustango has no global-scope tracking yet — issue
6902                /// #820), so this is functionally equivalent to
6903                /// [`Self::all`]. Exposed as a named shortcut so
6904                /// soft-delete-aware code reads `Model::with_trashed_pool`
6905                /// rather than `Model::all_pool` — keeps intent visible
6906                /// in callers and stays correct when auto-scoping lands.
6907                ///
6908                /// Closes #821 partial.
6909                ///
6910                /// # Errors
6911                /// As [`#root::sql::FetcherPool::fetch`].
6912                pub async fn with_trashed(
6913                    pool: &#root::sql::Pool,
6914                ) -> ::core::result::Result<
6915                    ::std::vec::Vec<Self>,
6916                    #root::sql::ExecError,
6917                > {
6918                    use #root::sql::FetcherPool as _;
6919                    #root::query::QuerySet::<Self>::default()
6920                        .with_trashed()
6921                        .fetch(pool)
6922                        .await
6923                }
6924
6925            }
6926        } else {
6927            quote!()
6928        };
6929        quote! {
6930
6931
6932            /// [`Self::delete`] on any [`#root::sql::SqlExecutor`]. An audited
6933            /// model refuses it: use [`Self::delete`].
6934            ///
6935            /// # Errors
6936            /// As [`Self::delete`].
6937            pub async fn delete_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6938                &self,
6939                _executor: _E,
6940            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6941                #refuse_delete
6942                self.__rustango_delete_exec(_executor).await
6943            }
6944
6945            #[doc(hidden)]
6946            pub async fn __rustango_delete_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6947                &self,
6948                _executor: _E,
6949            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6950                let _query = #root::core::DeleteQuery::by_pk(
6951                    <Self as #root::core::Model>::SCHEMA,
6952                    #pk_column_lit,
6953                    ::core::convert::Into::<#root::core::SqlValue>::into(
6954                        ::core::clone::Clone::clone(&self.#pk_ident)
6955                    ),
6956                );
6957                #root::sql::__macro_internals::delete_on(_executor, &_query).await
6958            }
6959
6960            /// Old name of [`Self::delete`].
6961            ///
6962            /// # Errors
6963            /// As [`Self::delete`].
6964            #[deprecated(since = "0.62.0", note = "use `delete`")]
6965            pub async fn delete_pool(
6966                &self,
6967                pool: &#root::sql::Pool,
6968            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6969                self.delete(pool).await
6970            }
6971
6972
6973            #pool_delete_method
6974            #pool_insert_method
6975            #audit_entry_method
6976            #pool_save_method
6977            #refresh_replicate_methods
6978            #tx_delete_method
6979            #tx_insert_method
6980            #tx_save_method
6981            #soft_delete_methods
6982
6983            /// Returns `true` when `other` represents the same DB
6984            /// row as `self` — i.e. their primary keys compare
6985            /// equal. Eloquent `$model->is($other)` parity.
6986            ///
6987            /// Because both arguments are typed `&Self`, the
6988            /// model/table check is automatic — `Post::is` cannot
6989            /// be invoked against a `Comment` at compile time. Only
6990            /// the PK has to be compared at runtime.
6991            pub fn is(&self, other: &Self) -> bool {
6992                self.#pk_ident == other.#pk_ident
6993            }
6994
6995            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
6996            /// parity.
6997            pub fn is_not(&self, other: &Self) -> bool {
6998                self.#pk_ident != other.#pk_ident
6999            }
7000
7001            /// Returns this row's primary-key value as an
7002            /// [`#root::core::SqlValue`]. Eloquent
7003            /// `$model->getKey()` parity.
7004            ///
7005            /// Useful when you need to thread the PK through a
7006            /// generic `Into<SqlValue>`-bound API without knowing the
7007            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7008            #[must_use]
7009            pub fn get_key(&self) -> #root::core::SqlValue {
7010                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7011            }
7012
7013        }
7014    });
7015
7016    let refuse_insert = refuse("insert_on");
7017    // The audited `insert` is built with the PK methods; an audited model
7018    // without a PK has no row to name, so its insert refuses.
7019    let audited_insert = audited_fields.is_some() && primary_key.is_some();
7020    let insert_pool_body = if audited_fields.is_some() {
7021        quote! {
7022            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7023                table: <Self as #root::core::Model>::SCHEMA.table,
7024            })
7025        }
7026    } else {
7027        quote!(self.__rustango_insert_exec(pool).await)
7028    };
7029    let plain_insert_auto = if audited_insert {
7030        quote!()
7031    } else {
7032        quote! {
7033            /// Insert this row on any backend, reading each `Auto` field
7034            /// back from the written row.
7035            ///
7036            /// # Errors
7037            /// Returns [`#root::sql::ExecError`] for SQL-writing or driver failures.
7038            pub async fn insert(
7039                &mut self,
7040                pool: &#root::sql::Pool,
7041            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7042                #insert_pool_body
7043            }
7044        }
7045    };
7046    let plain_insert_plain = if audited_insert {
7047        quote!()
7048    } else {
7049        quote! {
7050            /// Insert this row on any backend.
7051            ///
7052            /// # Errors
7053            /// Returns [`#root::sql::ExecError`] for SQL-writing or driver failures.
7054            pub async fn insert(
7055                &self,
7056                pool: &#root::sql::Pool,
7057            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7058                #insert_pool_body
7059            }
7060        }
7061    };
7062    let insert_method = if fields.has_auto {
7063        let pushes = &fields.insert_pushes;
7064        let returning_cols = &fields.returning_cols;
7065        quote! {
7066            #plain_insert_auto
7067            /// [`Self::insert`] on any [`#root::sql::SqlExecutor`]. An audited
7068            /// model refuses it: use [`Self::insert`].
7069            ///
7070            /// # Errors
7071            /// As [`Self::insert`].
7072            pub async fn insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7073                &mut self,
7074                _executor: _E,
7075            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7076                #refuse_insert
7077                self.__rustango_insert_exec(_executor).await
7078            }
7079
7080            #[doc(hidden)]
7081            pub async fn __rustango_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7082                &mut self,
7083                _executor: _E,
7084            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7085                let _query = self.__rustango_insert_query();
7086                // Every `Auto` filled Rust-side: nothing to read back.
7087                if _query.returning.is_empty() {
7088                    return #root::sql::__macro_internals::insert_on(_executor, &_query).await;
7089                }
7090                let _row = #root::sql::__macro_internals::insert_returning_on(
7091                    _executor, &_query,
7092                )
7093                .await?;
7094                #root::sql::apply_auto_pk(_row, self)
7095            }
7096
7097            /// The INSERT for this row; fills Rust-side `Auto` values first.
7098            #[doc(hidden)]
7099            pub fn __rustango_insert_query(&mut self) -> #root::core::InsertQuery {
7100                let mut _columns: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7101                let mut _values: ::std::vec::Vec<#root::core::SqlValue> = ::std::vec::Vec::new();
7102                #( #pushes )*
7103                #root::core::InsertQuery::new(
7104                    <Self as #root::core::Model>::SCHEMA,
7105                    _columns,
7106                    _values,
7107                )
7108                .returning(::std::vec![ #( #returning_cols ),* ])
7109            }
7110
7111            /// Old name of [`Self::insert`].
7112            ///
7113            /// # Errors
7114            /// As [`Self::insert`].
7115            #[deprecated(since = "0.62.0", note = "use `insert`")]
7116            pub async fn insert_pool(
7117                &mut self,
7118                pool: &#root::sql::Pool,
7119            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7120                self.insert(pool).await
7121            }
7122        }
7123    } else {
7124        let insert_columns = &fields.insert_columns;
7125        let insert_values = &fields.insert_values;
7126        // The audited `insert` takes `&mut self` for its audit helper.
7127        let pool_self = if audited_fields.is_some() {
7128            quote!(&mut self)
7129        } else {
7130            quote!(&self)
7131        };
7132        quote! {
7133            #plain_insert_plain
7134            /// [`Self::insert`] on any [`#root::sql::SqlExecutor`]. An audited
7135            /// model refuses it: use [`Self::insert`].
7136            ///
7137            /// # Errors
7138            /// As [`Self::insert`].
7139            pub async fn insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7140                &self,
7141                _executor: _E,
7142            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7143                #refuse_insert
7144                self.__rustango_insert_exec(_executor).await
7145            }
7146
7147            #[doc(hidden)]
7148            pub async fn __rustango_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7149                &self,
7150                _executor: _E,
7151            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7152                #root::sql::__macro_internals::insert_on(_executor, &self.__rustango_insert_query())
7153                    .await
7154            }
7155
7156            /// The INSERT for this row.
7157            #[doc(hidden)]
7158            pub fn __rustango_insert_query(&self) -> #root::core::InsertQuery {
7159                #root::core::InsertQuery::new(
7160                    <Self as #root::core::Model>::SCHEMA,
7161                    ::std::vec![ #( #insert_columns ),* ],
7162                    ::std::vec![ #( #insert_values ),* ],
7163                )
7164            }
7165
7166            /// Old name of [`Self::insert`].
7167            ///
7168            /// # Errors
7169            /// As [`Self::insert`].
7170            #[deprecated(since = "0.62.0", note = "use `insert`")]
7171            pub async fn insert_pool(
7172                #pool_self,
7173                pool: &#root::sql::Pool,
7174            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7175                self.insert(pool).await
7176            }
7177        }
7178    };
7179
7180    // `bulk_insert(&Pool)`: audited models write their rows and one
7181    // `Create` entry each in one transaction.
7182    let bulk_pool_body = if audited_fields.is_some() {
7183        quote! {
7184            let mut _tx = #root::sql::write_transaction(pool).await?;
7185            Self::__rustango_bulk_insert_exec(rows, &mut _tx).await?;
7186            // MySQL has no RETURNING: a database-filled PK stays unset.
7187            if rows.iter().any(|_row| _row.__rustango_audit_pk().is_empty()) {
7188                return ::core::result::Result::Err(#root::sql::ExecError::AuditUnsupported {
7189                    table: <Self as #root::core::Model>::SCHEMA.table,
7190                    reason: "MySQL has no RETURNING to read the written rows",
7191                });
7192            }
7193            let _entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
7194                .iter()
7195                .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
7196                .collect();
7197            #root::audit::__emit_many_tx(&mut _tx, &_entries).await?;
7198            _tx.commit().await?;
7199            ::core::result::Result::Ok(())
7200        }
7201    } else {
7202        quote!(Self::__rustango_bulk_insert_exec(rows, pool).await)
7203    };
7204    let refuse_bulk = refuse("bulk_insert_on");
7205    let bulk_insert_method = if fields.has_auto {
7206        let cols_no_auto = &fields.bulk_columns_no_auto;
7207        let cols_all = &fields.bulk_columns_all;
7208        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7209        let pushes_all = &fields.bulk_pushes_all;
7210        let returning_cols = &fields.returning_cols;
7211        let uniformity = &fields.bulk_auto_uniformity;
7212        let fill_in_place = &fields.bulk_fill_in_place;
7213        // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7214        let first_auto_ident = fields
7215            .first_db_auto
7216            .as_ref()
7217            .map(|(ident, _)| ident)
7218            .or(fields.first_auto_ident.as_ref())
7219            .expect("has_auto implies first_auto_ident is Some");
7220        quote! {
7221            /// Bulk-insert `rows`: one statement, or one per chunk in one
7222            /// transaction past the bind cap. Every row's `Auto<T>` PK must
7223            /// be `Auto::Unset` (the database fills it) or every one `Set`.
7224            /// Each row's `Auto` fields are read back from `RETURNING`; MySQL
7225            /// has none, so it leaves them unset. An empty slice is a no-op.
7226            ///
7227            /// # Errors
7228            /// Returns [`#root::sql::ExecError`] for validation,
7229            /// SQL-writing, mixed-Auto rejection, or driver failures.
7230            pub async fn bulk_insert(
7231                rows: &mut [Self],
7232                pool: &#root::sql::Pool,
7233            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7234                #bulk_pool_body
7235            }
7236
7237            /// [`Self::bulk_insert`] on any [`#root::sql::SqlExecutor`]. An
7238            /// audited model refuses it: use [`Self::bulk_insert`].
7239            ///
7240            /// # Errors
7241            /// As [`Self::bulk_insert`].
7242            pub async fn bulk_insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7243                rows: &mut [Self],
7244                _executor: _E,
7245            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7246                #refuse_bulk
7247                Self::__rustango_bulk_insert_exec(rows, _executor).await
7248            }
7249
7250            #[doc(hidden)]
7251            pub async fn __rustango_bulk_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7252                rows: &mut [Self],
7253                _executor: _E,
7254            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7255                if rows.is_empty() {
7256                    return ::core::result::Result::Ok(());
7257                }
7258                let _first_unset = matches!(
7259                    rows[0].#first_auto_ident,
7260                    #root::sql::Auto::Unset
7261                );
7262                #( #uniformity )*
7263                for _row in rows.iter_mut() {
7264                    #( #fill_in_place )*
7265                }
7266                let mut _all_rows: ::std::vec::Vec<
7267                    ::std::vec::Vec<#root::core::SqlValue>,
7268                > = ::std::vec::Vec::with_capacity(rows.len());
7269                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7270                    for _row in rows.iter() {
7271                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7272                            ::std::vec::Vec::new();
7273                        #( #pushes_no_auto )*
7274                        _all_rows.push(_row_vals);
7275                    }
7276                    ::std::vec![ #( #cols_no_auto ),* ]
7277                } else {
7278                    for _row in rows.iter() {
7279                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7280                            ::std::vec::Vec::new();
7281                        #( #pushes_all )*
7282                        _all_rows.push(_row_vals);
7283                    }
7284                    ::std::vec![ #( #cols_all ),* ]
7285                };
7286                let _query = #root::core::BulkInsertQuery::new(
7287                    <Self as #root::core::Model>::SCHEMA,
7288                    _columns,
7289                    _all_rows,
7290                )
7291                .returning(::std::vec![ #( #returning_cols ),* ]);
7292                let _returned =
7293                    #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7294                if _returned.is_empty() {
7295                    return ::core::result::Result::Ok(());
7296                }
7297                if _returned.len() != rows.len() {
7298                    return ::core::result::Result::Err(#root::sql::ExecError::Sql(
7299                        #root::sql::SqlError::BulkInsertReturningMismatch {
7300                            expected: rows.len(),
7301                            actual: _returned.len(),
7302                        },
7303                    ));
7304                }
7305                for (_row, _row_mut) in _returned.into_iter().zip(rows.iter_mut()) {
7306                    #root::sql::apply_auto_pk(_row, _row_mut)?;
7307                }
7308                ::core::result::Result::Ok(())
7309            }
7310        }
7311    } else {
7312        let cols_all = &fields.bulk_columns_all;
7313        let pushes_all = &fields.bulk_pushes_all;
7314        quote! {
7315            /// Bulk-insert `rows`: one statement, or one per chunk in one
7316            /// transaction past the bind cap. An empty slice is a no-op.
7317            ///
7318            /// # Errors
7319            /// Returns [`#root::sql::ExecError`] for validation,
7320            /// SQL-writing, or driver failures.
7321            pub async fn bulk_insert(
7322                rows: &[Self],
7323                pool: &#root::sql::Pool,
7324            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7325                #bulk_pool_body
7326            }
7327
7328            /// [`Self::bulk_insert`] on any [`#root::sql::SqlExecutor`]. An
7329            /// audited model refuses it: use [`Self::bulk_insert`].
7330            ///
7331            /// # Errors
7332            /// As [`Self::bulk_insert`].
7333            pub async fn bulk_insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7334                rows: &[Self],
7335                _executor: _E,
7336            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7337                #refuse_bulk
7338                Self::__rustango_bulk_insert_exec(rows, _executor).await
7339            }
7340
7341            #[doc(hidden)]
7342            pub async fn __rustango_bulk_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7343                rows: &[Self],
7344                _executor: _E,
7345            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7346                if rows.is_empty() {
7347                    return ::core::result::Result::Ok(());
7348                }
7349                let mut _all_rows: ::std::vec::Vec<
7350                    ::std::vec::Vec<#root::core::SqlValue>,
7351                > = ::std::vec::Vec::with_capacity(rows.len());
7352                for _row in rows.iter() {
7353                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7354                        ::std::vec::Vec::new();
7355                    #( #pushes_all )*
7356                    _all_rows.push(_row_vals);
7357                }
7358                let _query = #root::core::BulkInsertQuery::new(
7359                    <Self as #root::core::Model>::SCHEMA,
7360                    ::std::vec![ #( #cols_all ),* ],
7361                    _all_rows,
7362                );
7363                #root::sql::__macro_internals::bulk_insert_on(_executor, &_query)
7364                    .await
7365                    .map(drop)
7366            }
7367        }
7368    };
7369
7370    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7371    // (no postgres-feature gate); routes through the existing
7372    // `bulk_insert_pool` + per-dialect conflict writer.
7373    //
7374    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7375    // sequence picks the PK for fresh inserts; the UPDATE branch never
7376    // touches the Auto column.
7377    // Audited models audit each written row; MySQL, with no RETURNING,
7378    // is refused there (#1747, #1795).
7379    let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7380        (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7381        (true, true) => quote! {
7382            #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7383                _r.__rustango_audit_entry(_op)
7384            })
7385            .await
7386        },
7387        (true, false) => quote! {
7388            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7389                table: <Self as #root::core::Model>::SCHEMA.table,
7390            })
7391        },
7392    };
7393    let bulk_upsert_pool_method = {
7394        let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7395            fields.uuid_v7_fields.iter().cloned().unzip();
7396        // Pick the "no Auto" columns when the model has Auto fields,
7397        // else every column.
7398        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7399            (
7400                fields.bulk_columns_no_auto.clone(),
7401                fields.bulk_pushes_no_auto.clone(),
7402            )
7403        } else {
7404            (
7405                fields.bulk_columns_all.clone(),
7406                fields.bulk_pushes_all.clone(),
7407            )
7408        };
7409        quote! {
7410            /// Tri-dialect bulk insert that updates on conflict — the
7411            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7412            ///
7413            /// Per-row values are extracted and lowered into a
7414            /// [`#root::core::BulkInsertQuery`] with
7415            /// `on_conflict = DoUpdate { target, update_columns }`. The
7416            /// writer dispatches per-dialect:
7417            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7418            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7419            ///   `target` is not the only unique key, since MySQL matches every UNIQUE index
7420            ///
7421            /// `target` names the column(s) whose unique constraint
7422            /// defines the conflict (typically a `unique` or
7423            /// `unique_together` natural-key column, NOT the `Auto<T>`
7424            /// PK). `update_cols` names the columns to overwrite on
7425            /// conflict — every other column is left untouched on the
7426            /// existing row.
7427            ///
7428            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7429            /// picks the PK on insert; the update path never touches
7430            /// the Auto column). Auto-set rows trigger a hard error.
7431            /// Rows repeating a `target` key: the last one wins.
7432            /// Empty slice is a no-op. On an audited model each written
7433            /// row gets a `create` or `update` audit row; MySQL refuses it.
7434            ///
7435            /// # Errors
7436            /// Returns [`#root::sql::ExecError`] for validation,
7437            /// SQL-writing, or driver failures.
7438            pub async fn bulk_upsert_pool(
7439                rows: &[Self],
7440                target: &[&'static str],
7441                update_cols: &[&'static str],
7442                pool: &#root::sql::Pool,
7443            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7444                if rows.is_empty() {
7445                    return ::core::result::Result::Ok(());
7446                }
7447                let mut _all_rows: ::std::vec::Vec<
7448                    ::std::vec::Vec<#root::core::SqlValue>,
7449                > = ::std::vec::Vec::with_capacity(rows.len());
7450                for _row in rows.iter() {
7451                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7452                        ::std::vec::Vec::new();
7453                    #( #upsert_pushes )*
7454                    _all_rows.push(_row_vals);
7455                }
7456                #[allow(unused_mut)]
7457                let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7458                #(
7459                    if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7460                        _generated.push(#gen_cols);
7461                    }
7462                )*
7463                let _query = #root::core::BulkInsertQuery::new(
7464                    <Self as #root::core::Model>::SCHEMA,
7465                    ::std::vec![ #( #upsert_cols ),* ],
7466                    _all_rows,
7467                )
7468                .on_conflict_do_update(target, update_cols)
7469                .generated(_generated);
7470                #run_conflict_bulk_insert
7471            }
7472
7473            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7474            /// skip rows that would violate a unique constraint. Issue
7475            /// #267 / T1.5. Same per-dialect dispatch as
7476            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7477            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7478            /// <pivot> = <pivot>` (MySQL no-op write).
7479            ///
7480            /// # Errors
7481            /// As [`Self::bulk_upsert_pool`].
7482            pub async fn bulk_insert_or_ignore_pool(
7483                rows: &[Self],
7484                pool: &#root::sql::Pool,
7485            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7486                if rows.is_empty() {
7487                    return ::core::result::Result::Ok(());
7488                }
7489                let mut _all_rows: ::std::vec::Vec<
7490                    ::std::vec::Vec<#root::core::SqlValue>,
7491                > = ::std::vec::Vec::with_capacity(rows.len());
7492                for _row in rows.iter() {
7493                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7494                        ::std::vec::Vec::new();
7495                    #( #upsert_pushes )*
7496                    _all_rows.push(_row_vals);
7497                }
7498                let _query = #root::core::BulkInsertQuery::new(
7499                    <Self as #root::core::Model>::SCHEMA,
7500                    ::std::vec![ #( #upsert_cols ),* ],
7501                    _all_rows,
7502                )
7503                .on_conflict_do_nothing();
7504                #run_conflict_bulk_insert
7505            }
7506        }
7507    };
7508
7509    // Ergonomic `Model::bulk_update(objs, fields)`.
7510    // The SQL/IR/executor stack
7511    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7512    // `write_bulk_update_*` writers) already existed; what was missing
7513    // was the per-model constructor that maps `&[Self]` + a runtime
7514    // column list into rows of `[pk, col_vals…]` so callers don't
7515    // hand-build the IR. Emitted only when the model has a primary key
7516    // (the PK is the join key and can't itself be updated).
7517    let bulk_update_rows_run = if audited_fields.is_some() {
7518        quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7519            _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7520        })
7521        .await)
7522    } else {
7523        quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7524    };
7525    let bulk_update_method = match &fields.primary_key {
7526        None => quote! {},
7527        Some((pk_ident, pk_col)) => {
7528            // One pair of match arms per non-PK column: a validation arm
7529            // resolving the runtime name to its `&'static str` column,
7530            // and a value arm pushing that field off the row.
7531            let mut col_arms: Vec<TokenStream2> = Vec::new();
7532            let mut val_arms: Vec<TokenStream2> = Vec::new();
7533            for entry in &fields.column_entries {
7534                if &entry.column == pk_col {
7535                    continue;
7536                }
7537                let col = &entry.column;
7538                let ident = &entry.ident;
7539                col_arms.push(quote! { #col => #col, });
7540                val_arms.push(quote! {
7541                    #col => _row_vals.push(
7542                        ::core::convert::Into::<#root::core::SqlValue>::into(
7543                            ::core::clone::Clone::clone(&_o.#ident)
7544                        )
7545                    ),
7546                });
7547            }
7548            quote! {
7549                /// Write per-row-different values for the named `fields`
7550                /// across every object in `objs`, matched by primary key.
7551                /// Past the backend's bind cap the rows go in batches inside
7552                /// one transaction, so the update is all or none.
7553                ///
7554                /// `fields` names the **columns** to update. The primary
7555                /// key identifies each row and cannot itself be updated
7556                /// (pass it and you get
7557                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7558                /// Empty `objs` or `fields` is a no-op returning `0`.
7559                /// Objects whose PK matches no row are simply not updated.
7560                /// Returns the number of rows affected.
7561                ///
7562                /// Tri-dialect: lowers to a
7563                /// [`#root::core::BulkUpdateQuery`] and dispatches
7564                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7565                /// a CTE + correlated subquery on SQLite, an inner
7566                /// `JOIN (VALUES …)` on MySQL.
7567                ///
7568                /// # Errors
7569                /// [`#root::core::QueryError::UnknownField`] for a name
7570                /// that isn't a column on this model,
7571                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7572                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7573                /// SQL-writing / driver failures.
7574                pub async fn bulk_update(
7575                    objs: &[Self],
7576                    fields: &[&str],
7577                    pool: &#root::sql::Pool,
7578                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7579                    if objs.is_empty() || fields.is_empty() {
7580                        return ::core::result::Result::Ok(0);
7581                    }
7582                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7583                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7584                        ::std::vec::Vec::with_capacity(fields.len());
7585                    for &_f in fields {
7586                        let _col: &'static str = match _f {
7587                            #pk_col => {
7588                                return ::core::result::Result::Err(
7589                                    ::core::convert::Into::into(
7590                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7591                                            model: _model_name,
7592                                            field: ::std::string::ToString::to_string(_f),
7593                                        }
7594                                    )
7595                                );
7596                            }
7597                            #( #col_arms )*
7598                            _ => {
7599                                return ::core::result::Result::Err(
7600                                    ::core::convert::Into::into(
7601                                        #root::core::QueryError::UnknownField {
7602                                            model: _model_name,
7603                                            field: ::std::string::ToString::to_string(_f),
7604                                        }
7605                                    )
7606                                );
7607                            }
7608                        };
7609                        // A model whose only field is the primary key emits no
7610                        // non-PK arms, so every arm above diverges and this push
7611                        // is genuinely unreachable — correct, since such a model
7612                        // has nothing to bulk-update. Scoped to the statement
7613                        // rather than the fn so a real unreachability elsewhere
7614                        // in this body still warns. Without it every PK-only
7615                        // model warns in the *user's* crate, which they cannot
7616                        // act on (#1290).
7617                        #[allow(unreachable_code)]
7618                        _update_columns.push(_col);
7619                    }
7620                    let mut _rows: ::std::vec::Vec<
7621                        ::std::vec::Vec<#root::core::SqlValue>,
7622                    > = ::std::vec::Vec::with_capacity(objs.len());
7623                    for _o in objs.iter() {
7624                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7625                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7626                        // PK first — the writers expect `[pk, …update cols]`.
7627                        _row_vals.push(
7628                            ::core::convert::Into::<#root::core::SqlValue>::into(
7629                                ::core::clone::Clone::clone(&_o.#pk_ident)
7630                            )
7631                        );
7632                        for &_f in fields {
7633                            match _f {
7634                                #( #val_arms )*
7635                                // Unreachable: every name was validated
7636                                // against the same arm set above.
7637                                _ => {}
7638                            }
7639                        }
7640                        _rows.push(_row_vals);
7641                    }
7642                    let _query = #root::core::BulkUpdateQuery::new(
7643                        <Self as #root::core::Model>::SCHEMA,
7644                        _update_columns,
7645                        _rows,
7646                    );
7647                    #bulk_update_rows_run
7648                }
7649            }
7650        }
7651    };
7652
7653    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7654        quote! {
7655            /// Hidden runtime accessor for the primary-key value as a
7656            /// [`SqlValue`]. Used by reverse-relation helpers
7657            /// (`<parent>::<child>_set`) emitted from sibling models'
7658            /// FK fields. Not part of the public API.
7659            #[doc(hidden)]
7660            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7661                ::core::convert::Into::<#root::core::SqlValue>::into(
7662                    ::core::clone::Clone::clone(&self.#pk_ident)
7663                )
7664            }
7665        }
7666    });
7667
7668    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7669        let column_arms = fields.column_entries.iter().map(|entry| {
7670            let col = &entry.column;
7671            let ident = &entry.ident;
7672            quote! {
7673                #col => ::core::option::Option::Some(
7674                    ::core::convert::Into::<#root::core::SqlValue>::into(
7675                        ::core::clone::Clone::clone(&self.#ident)
7676                    )
7677                ),
7678            }
7679        });
7680        quote! {
7681            impl #root::sql::HasPkValue for #struct_name {
7682                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7683                    ::core::convert::Into::<#root::core::SqlValue>::into(
7684                        ::core::clone::Clone::clone(&self.#pk_ident)
7685                    )
7686                }
7687                fn __rustango_column_value(
7688                    &self,
7689                    column: &str,
7690                ) -> ::core::option::Option<#root::core::SqlValue> {
7691                    match column {
7692                        #( #column_arms )*
7693                        _ => ::core::option::Option::None,
7694                    }
7695                }
7696            }
7697        }
7698    });
7699
7700    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7701
7702    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7703    // dispatch to the right per-backend body without the macro emitting
7704    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7705    // so audited models with non-Auto PKs (which still go through
7706    // `insert_one_with_audit` → `apply_auto_pk`) link.
7707    let assign_auto_pk_pool_impl = {
7708        let auto_assigns = &fields.auto_assigns;
7709        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7710        // so the body is structurally identical to `auto_assigns` —
7711        // only the helper name swaps from `try_get_returning` to
7712        // `try_get_returning_sqlite` so the closure typechecks against
7713        // a `SqliteRow` instead of a `PgRow`.
7714        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7715            .auto_field_idents
7716            .iter()
7717            .map(|(ident, column, uuid)| {
7718                // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
7719                if *uuid {
7720                    return quote! {
7721                        self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
7722                            _returning_row, #column
7723                        )?);
7724                    };
7725                }
7726                quote! {
7727                    self.#ident = #root::sql::try_get_returning_sqlite(
7728                        _returning_row, #column
7729                    )?;
7730                }
7731            })
7732            .collect();
7733        // #1028 — decode each `generated_as` column from the same
7734        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7735        // own type drives the decode (no `Auto<T>` wrapper).
7736        let generated_assigns: Vec<TokenStream2> = fields
7737            .generated_field_idents
7738            .iter()
7739            .map(|(ident, column)| {
7740                quote! {
7741                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7742                }
7743            })
7744            .collect();
7745        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7746            .generated_field_idents
7747            .iter()
7748            .map(|(ident, column)| {
7749                quote! {
7750                    self.#ident = #root::sql::try_get_returning_sqlite(
7751                        _returning_row, #column
7752                    )?;
7753                }
7754            })
7755            .collect();
7756        let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
7757            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7758            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7759            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7760            // those models can't use AUTO_INCREMENT). The trait is only
7761            // touched on the MySQL arm at runtime, so PG-only consumers
7762            // never see the bound failure.
7763            //
7764            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7765            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7766            // runtime with "multi-column RETURNING". MySQL has no
7767            // multi-column RETURNING semantic and a follow-up SELECT
7768            // would need cross-trait plumbing. Pragmatic shape: succeed
7769            // with the FIRST Auto field populated from LAST_INSERT_ID();
7770            // any other Auto fields stay `Auto::Unset`. Callers that
7771            // need the DB-defaulted timestamp / UUID can re-fetch the
7772            // row by PK after `save_pool`. Fixes the cookbook chapter
7773            // 12 dialect divergence.
7774            quote! {
7775                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7776                    ::rustango_from_mysql_auto_id(_id)?;
7777                self.#first = #root::sql::Auto::Set(_converted);
7778                ::core::result::Result::Ok(())
7779            }
7780        } else {
7781            quote! {
7782                let _ = _id;
7783                ::core::result::Result::Ok(())
7784            }
7785        };
7786        quote! {
7787            impl #root::sql::AssignAutoPkPool for #struct_name {
7788                fn __rustango_assign_from_pg_row(
7789                    &mut self,
7790                    _returning_row: &#root::sql::PgReturningRow,
7791                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7792                    #( #auto_assigns )*
7793                    #( #generated_assigns )*
7794                    ::core::result::Result::Ok(())
7795                }
7796                fn __rustango_assign_from_mysql_id(
7797                    &mut self,
7798                    _id: i64,
7799                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7800                    #mysql_body
7801                }
7802                fn __rustango_assign_from_sqlite_row(
7803                    &mut self,
7804                    _returning_row: &#root::sql::SqliteReturningRow,
7805                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7806                    #( #auto_assigns_sqlite )*
7807                    #( #generated_assigns_sqlite )*
7808                    ::core::result::Result::Ok(())
7809                }
7810            }
7811        }
7812    };
7813
7814    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7815    let aliased_row_helper = quote! {
7816        /// Decode a row's aliased target columns (produced by
7817        /// `select_related`'s LEFT JOIN) into a fresh instance of
7818        /// this model. Reads each column via
7819        /// `format!("{prefix}__{col}")`, matching the alias the
7820        /// SELECT writer emitted. Slice 9.0d.
7821        #[doc(hidden)]
7822        #[cfg(feature = "postgres")]
7823        pub fn __rustango_from_aliased_row(
7824            row: &#root::sql::sqlx::postgres::PgRow,
7825            prefix: &str,
7826        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7827            ::core::result::Result::Ok(Self {
7828                #( #from_aliased_row_inits ),*
7829            })
7830        }
7831    };
7832    // v0.23.0-batch8 — MySQL counterpart, gated through the
7833    // cfg-aware macro_rules so PG-only builds expand to nothing.
7834    let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
7835    let aliased_row_helper_my = quote! {
7836        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7837            #( #from_aliased_row_inits_my ),*
7838        });
7839    };
7840
7841    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7842    // pattern + cfg gate on the `sqlite` feature.
7843    let aliased_row_helper_sqlite = quote! {
7844        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7845            #( #from_aliased_row_inits ),*
7846        });
7847    };
7848
7849    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7850    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7851    let load_related_impl_sqlite =
7852        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7853
7854    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7855    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7856    // default `Self::objects()`. Each accessor returns a fresh
7857    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7858    // methods the user defined.
7859    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7860        .iter()
7861        .map(|fn_ident| {
7862            let model_name_str = struct_name.to_string();
7863            let fn_name_str = fn_ident.to_string();
7864            let doc = format!(
7865                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7866                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7867                 equivalent to `Self::objects()`. Chains with any \
7868                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7869                 extension methods."
7870            );
7871            quote! {
7872                #[doc = #doc]
7873                #[must_use]
7874                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7875                    #root::query::QuerySet::new()
7876                }
7877            }
7878        })
7879        .collect();
7880
7881    quote! {
7882        impl #struct_name {
7883            /// Start a new `QuerySet` over this model.
7884            #[must_use]
7885            pub fn objects() -> #root::query::QuerySet<#struct_name> {
7886                #root::query::QuerySet::new()
7887            }
7888
7889            /// Eloquent-shape alias of [`Self::objects`]. Returns
7890            /// a fresh `QuerySet<Self>` ready for `.filter()` /
7891            /// `.where_()` / etc. Matches Laravel muscle-memory:
7892            ///
7893            /// ```ignore
7894            /// // Eloquent:    Post::query()->where('published', true)
7895            /// // rustango:    Post::query().filter("published", true)
7896            /// //         or:  Post::objects().filter("published", true)
7897            /// ```
7898            ///
7899            /// Both names point at the same underlying constructor;
7900            /// neither is preferred.
7901            #[must_use]
7902            pub fn query() -> #root::query::QuerySet<#struct_name> {
7903                #root::query::QuerySet::new()
7904            }
7905
7906            #( #extra_manager_fns )*
7907
7908            #insert_method
7909
7910            #bulk_insert_method
7911
7912            #bulk_upsert_pool_method
7913
7914            #bulk_update_method
7915
7916            #save_method
7917
7918            #pk_methods
7919
7920            #pk_value_helper
7921
7922            #aliased_row_helper
7923
7924            #column_consts
7925        }
7926
7927        #aliased_row_helper_my
7928
7929        #aliased_row_helper_sqlite
7930
7931        #load_related_impl
7932
7933        #load_related_impl_my
7934
7935        #load_related_impl_sqlite
7936
7937        #has_pk_value_impl
7938
7939        #fk_pk_access_impl
7940
7941        #assign_auto_pk_pool_impl
7942    }
7943}
7944
7945/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7946fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7947    let lines = entries.iter().map(|e| {
7948        let ident = &e.ident;
7949        let col_ty = column_type_ident(ident);
7950        quote! {
7951            #[allow(non_upper_case_globals)]
7952            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7953        }
7954    });
7955    quote! { #(#lines)* }
7956}
7957
7958/// Emit a hidden per-model module carrying one zero-sized type per field,
7959/// each with a `Column` impl pointing back at the model.
7960fn column_module_tokens(
7961    module_ident: &syn::Ident,
7962    struct_name: &syn::Ident,
7963    entries: &[ColumnEntry],
7964) -> TokenStream2 {
7965    let root = rustango_root();
7966    let items = entries.iter().map(|e| {
7967        let col_ty = column_type_ident(&e.ident);
7968        let value_ty = &e.value_ty;
7969        let name = &e.name;
7970        let column = &e.column;
7971        let field_type_tokens = &e.field_type_tokens;
7972        quote! {
7973            #[derive(::core::clone::Clone, ::core::marker::Copy)]
7974            pub struct #col_ty;
7975
7976            impl #root::core::Column for #col_ty {
7977                type Model = super::#struct_name;
7978                type Value = #value_ty;
7979                const NAME: &'static str = #name;
7980                const COLUMN: &'static str = #column;
7981                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7982            }
7983        }
7984    });
7985    quote! {
7986        #[doc(hidden)]
7987        #[allow(non_camel_case_types, non_snake_case)]
7988        pub mod #module_ident {
7989            // Re-import the parent scope so field types referencing
7990            // sibling models (e.g. `ForeignKey<Author>`) resolve
7991            // inside this submodule. Without this we'd hit
7992            // `proc_macro_derive_resolution_fallback` warnings.
7993            #[allow(unused_imports)]
7994            use super::*;
7995            #(#items)*
7996        }
7997    }
7998}
7999
8000fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8001    let name = ident_name(field_ident);
8002    syn::Ident::new(&format!("{name}_col"), field_ident.span())
8003}
8004
8005/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8006/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8007fn restore_auto_now_unless(
8008    idents: &[syn::Ident],
8009    ret: &TokenStream2,
8010    keep: &TokenStream2,
8011    body: &TokenStream2,
8012) -> TokenStream2 {
8013    if idents.is_empty() {
8014        return body.clone();
8015    }
8016    let prev: Vec<syn::Ident> = (0..idents.len())
8017        .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8018        .collect();
8019    quote! {
8020        #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8021        let _saved: #ret = async { #body }.await;
8022        if !(#keep) {
8023            #( self.#idents = #prev; )*
8024        }
8025        _saved
8026    }
8027}
8028
8029/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8030fn ident_name(ident: &syn::Ident) -> String {
8031    syn::ext::IdentExt::unraw(ident).to_string()
8032}
8033
8034/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8035fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8036    match syn::parse_str::<syn::Ident>(name) {
8037        Ok(mut ident) => {
8038            ident.set_span(span);
8039            ident
8040        }
8041        Err(_) => syn::Ident::new_raw(name, span),
8042    }
8043}
8044
8045fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8046    syn::Ident::new(
8047        &format!("__rustango_cols_{struct_name}"),
8048        struct_name.span(),
8049    )
8050}
8051
8052fn from_row_impl_tokens(
8053    struct_name: &syn::Ident,
8054    from_row_inits: &[TokenStream2],
8055    from_row_inits_my: &[TokenStream2],
8056) -> TokenStream2 {
8057    let root = rustango_root();
8058    // The Postgres impl is always emitted — every rustango build pulls in
8059    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8060    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8061    // macro_rules whose body collapses to nothing when rustango is built
8062    // without the `mysql` feature. No user-facing feature shim required.
8063    //
8064    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8065    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8066    // back into a comma-separated list inside square brackets. Since each
8067    // entry is already in `field: expr` shape, the existing tokens slot in.
8068    quote! {
8069        #[cfg(feature = "postgres")]
8070        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8071            for #struct_name
8072        {
8073            fn from_row(
8074                row: &'r #root::sql::sqlx::postgres::PgRow,
8075            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8076                ::core::result::Result::Ok(Self {
8077                    #( #from_row_inits ),*
8078                })
8079            }
8080        }
8081
8082        #root::__impl_my_from_row!(#struct_name, |row| {
8083            #( #from_row_inits_my ),*
8084        });
8085
8086        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8087            #( #from_row_inits ),*
8088        });
8089    }
8090}
8091
8092struct ContainerAttrs {
8093    table: Option<String>,
8094    display: Option<(String, proc_macro2::Span)>,
8095    /// Explicit app label from `#[rustango(app = "blog")]`.
8096    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8097    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8098    /// at runtime — this attribute is the override for cases where
8099    /// the inference is wrong (e.g. a model that conceptually belongs
8100    /// to one app but is physically in another module).
8101    app: Option<String>,
8102    /// Per-model admin knobs from
8103    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8104    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8105    /// admin code falls back to `AdminConfig::DEFAULT`.
8106    admin: Option<AdminAttrs>,
8107    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8108    /// `None` when the model isn't audited — write paths emit no
8109    /// audit entries. When present, single-row writes capture
8110    /// before/after for the listed fields and bulk writes batch
8111    /// snapshots into one INSERT into `rustango_audit_log`.
8112    audit: Option<AuditAttrs>,
8113    /// `true` when `#[rustango(permissions)]` is present. Signals that
8114    /// `auto_create_permissions` should seed the four CRUD codenames for
8115    /// this model.
8116    permissions: bool,
8117    /// Many-to-many relations declared via
8118    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8119    ///                 src = "post_id", dst = "tag_id"))]`.
8120    m2m: Vec<M2MAttr>,
8121    /// Polymorphic M2M relations declared via
8122    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8123    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8124    ///   related_column = "tag_id"))]` (issue #818).
8125    generic_m2m: Vec<GenericM2MAttr>,
8126    /// Composite indexes declared via
8127    /// `#[rustango(index("col1, col2"))]` or
8128    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8129    /// Single-column indexes from `#[rustango(index)]` on fields are
8130    /// accumulated here during field collection.
8131    indexes: Vec<IndexAttr>,
8132    /// Table-level CHECK constraints declared via
8133    /// `#[rustango(check(name = "…", expr = "…"))]`.
8134    checks: Vec<CheckAttr>,
8135    /// Table-level PG `EXCLUDE` constraints declared via
8136    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8137    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8138    /// migration writer renders them on Postgres and skips with a
8139    /// warning on MySQL/SQLite. Issue #319.
8140    excludes: Vec<ExcludeAttr>,
8141    /// Composite (multi-column) FKs declared via
8142    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8143    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8144    composite_fks: Vec<CompositeFkAttr>,
8145    /// Generic ("any model") FKs declared via
8146    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8147    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8148    generic_fks: Vec<GenericFkAttr>,
8149    /// Where this model lives in a tenancy deployment, declared via
8150    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8151    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8152    /// to partition diff output between registry-scoped and
8153    /// tenant-scoped migration files.
8154    scope: Option<String>,
8155    /// Extra QuerySet accessor names from
8156    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8157    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8158    /// the default `Self::objects()`. Multiple attributes allowed.
8159    manager_fns: Vec<syn::Ident>,
8160    /// Default ordering declared via `#[rustango(default_order =
8161    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8162    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8163    /// `-` prefix means descending; the `+` prefix or no prefix means
8164    /// ascending.
8165    default_order: Vec<(String, bool, proc_macro2::Span)>,
8166    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8167    /// Routes the emitted schema's `is_view = true` so the migration
8168    /// snapshot skips this model (its underlying SQL view is operator-
8169    /// managed, not rustango-managed).
8170    is_view: bool,
8171    /// `#[rustango(managed = false)]` — who owns the table.
8172    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8173    /// the migration snapshot skips this model so `makemigrations` /
8174    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8175    /// TABLE` against it (operator-managed schema).
8176    managed: bool,
8177    /// Name of the model's base manager, from
8178    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8179    /// `ModelSchema::base_manager_name`. Declarative-only today.
8180    base_manager_name: Option<String>,
8181    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8182    /// field this model's instances are ordered relative to.
8183    /// Declarative-only today; threaded onto
8184    /// `ModelSchema::order_with_respect_to`.
8185    order_with_respect_to: Option<String>,
8186    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8187    /// Marks the model as a proxy that
8188    /// shares its DB table with another struct. Threaded into
8189    /// `ModelSchema::proxy` so future codegen can skip table-owning
8190    /// behavior for proxies.
8191    proxy: bool,
8192    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8193    /// Each comma-separated capability token surfaces on
8194    /// `ModelSchema::required_db_features` so `manage check --deploy`
8195    /// can warn when the active dialect lacks one.
8196    required_db_features: Vec<String>,
8197    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8198    /// Normalized to the dialect name `manage check --deploy`
8199    /// compares against `Settings.database.backend`. Aliases
8200    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8201    /// stored under the canonical name.
8202    required_db_vendor: Option<String>,
8203    /// `#[rustango(default_related_name = "...")]`. Threaded into
8204    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8205    /// name to use when an FK / M2M field doesn't override it.
8206    /// Today rustango doesn't auto-emit reverse managers; the
8207    /// metadata is the foundation for that work.
8208    default_related_name: Option<String>,
8209    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8210    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8211    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8212    /// inline `COMMENT='...'`, SQLite: no-op).
8213    db_table_comment: Option<String>,
8214    /// `#[rustango(get_latest_by = "created_at")]` /
8215    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8216    /// `(column, descending)` where `descending = true` when the
8217    /// attribute value starts with `-`. Threaded into
8218    /// `ModelSchema::get_latest_by`.
8219    get_latest_by: Option<(String, bool, proc_macro2::Span)>,
8220    /// Extra `(codename, label)` permissions
8221    /// from `#[rustango(extra_permissions = "approve:Can approve,
8222    /// archive:Can archive")]`. Comma-separated `codename:label`
8223    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8224    extra_permissions: Vec<(String, String)>,
8225    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8226    /// `"view"`) the framework
8227    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8228    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8229    /// Validated at parse time; unknown actions fail with a span-pointing
8230    /// error.
8231    default_permissions: Vec<String>,
8232    /// `#[rustango(verbose_name = "blog post")]` — the
8233    /// human-readable singular label for the model. Threaded into
8234    /// `ModelSchema::verbose_name` so admin section headers /
8235    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8236    /// over the Rust struct identifier.
8237    verbose_name: Option<String>,
8238    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8239    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8240    /// When unset, `display_label_plural()` auto-suffixes `s`.
8241    verbose_name_plural: Option<String>,
8242    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8243    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8244    /// a name (used by `QuerySet::without_global_scope`) with a
8245    /// `fn() -> WhereExpr` path that the macro emits into
8246    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8247    global_scopes: Vec<GlobalScopeAttr>,
8248    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8249    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8250    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8251    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8252    /// entry emits an inherent `<name>_through(&self)` accessor that
8253    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8254    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8255    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8256    through_relations: Vec<ThroughAttr>,
8257    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8258    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8259    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8260    /// Each entry emits two associated functions on the parent —
8261    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8262    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8263    /// over a correlated subquery against the child table. Users
8264    /// drop the result into `QuerySet::where_raw(...)`.
8265    reverse_has_relations: Vec<ReverseHasAttr>,
8266    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8267    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8268    /// entry so the relation-existence family resolves polymorphic
8269    /// children by name.
8270    generic_has_relations: Vec<GenericHasAttr>,
8271}
8272
8273/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8274/// declaration. Each entry becomes one `core::GlobalScope` in the
8275/// emitted schema literal; `apply` resolves at macro-expand time
8276/// against the consumer's scope so the function must be in scope at
8277/// the use site. Issue #820.
8278struct GlobalScopeAttr {
8279    name: String,
8280    apply: syn::Path,
8281}
8282
8283/// Parsed `#[rustango(through(...))]` declaration. Issue
8284/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8285/// `hasManyThrough` / `hasOneThrough` parity.
8286///
8287/// `Country hasManyThrough Post via User` declares as:
8288///
8289/// ```ignore
8290/// #[rustango(through(
8291///     name                   = "posts",
8292///     far                    = "Post",
8293///     far_fk_column          = "author_id",
8294///     intermediate           = "User",
8295///     intermediate_fk_column = "country_id",
8296/// ))]
8297/// struct Country { ... }
8298/// ```
8299///
8300/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8301/// which returns a queryset filtered via a correlated subquery —
8302/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8303/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8304/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8305/// because the subquery lives inside a `WhereExpr::InSubquery` node
8306/// and the outer queryset's pending list stays empty.
8307///
8308/// All four required arguments use **SQL column / table names**
8309/// (not Rust field names) to sidestep the multi-hop-filter substrate
8310/// gap. Once that substrate lands, a higher-level Rust-field-name
8311/// shorthand can be added without breaking this surface.
8312struct ThroughAttr {
8313    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8314    name: String,
8315    /// Far model type identifier. `far = "Post"` → returns
8316    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8317    /// the derive expands.
8318    far: syn::Ident,
8319    /// SQL column on the far model's table that references the
8320    /// intermediate model's primary key. For `Post`'s
8321    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8322    /// default `<field>_id` convention) or whatever the user
8323    /// declared via `#[rustango(db_column = "...")]`.
8324    far_fk_column: String,
8325    /// Intermediate model type identifier. Needed to look up its
8326    /// `SCHEMA` so the subquery's `FROM` clause points at the
8327    /// intermediate table. `intermediate = "User"`.
8328    intermediate: syn::Ident,
8329    /// SQL column on the intermediate's table that references the
8330    /// source (this) model's primary key. For `User`'s
8331    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8332    intermediate_fk_column: String,
8333    /// SQL primary-key column on the intermediate's table — the
8334    /// column the subquery projects. Optional; defaults to `"id"`
8335    /// (rustango's default PK column name). Override when the
8336    /// intermediate declares a custom PK column.
8337    intermediate_pk_column: String,
8338}
8339
8340/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8341/// child_fk_column = "..."))]` declaration. Issue
8342/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8343/// `whereHas` / `whereDoesntHave` parity.
8344///
8345/// `Post hasMany Comment` declares as:
8346///
8347/// ```ignore
8348/// #[rustango(reverse_has(
8349///     name             = "comments",
8350///     child            = "Comment",
8351///     child_fk_column  = "post_id",
8352/// ))]
8353/// struct Post { ... }
8354/// ```
8355///
8356/// The macro emits two associated functions on `Post`:
8357///
8358/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8359///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8360///   <outer>.<self_pk_column>)`.
8361/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8362///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8363///
8364/// User code:
8365///
8366/// ```ignore
8367/// // Posts with at least one comment:
8368/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8369/// // Posts with no comments:
8370/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8371/// ```
8372///
8373/// As with #817, all column / table identifiers are **SQL names**
8374/// (not Rust field names) so the substrate is independent of the
8375/// outstanding multi-hop filter resolver gap. The emitted
8376/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8377/// SQL-emit time via the writer's scope stack.
8378struct ReverseHasAttr {
8379    /// Accessor name. `name = "comments"` → emits
8380    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8381    name: String,
8382    /// Child model type identifier. `child = "Comment"` — needed to
8383    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8384    /// points at the child table.
8385    child: syn::Ident,
8386    /// SQL column on the child's table that references this model's
8387    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8388    /// column is `"post_id"`.
8389    child_fk_column: String,
8390    /// Column on **this** model's table the child FK references — the
8391    /// `OuterRef` target. Defaults to the PK column (`"id"` without one).
8392    self_pk_column: Option<String>,
8393    /// Where an explicit `self_pk_column` was written, for its error.
8394    self_pk_span: Option<proc_macro2::Span>,
8395}
8396
8397impl ReverseHasAttr {
8398    fn self_pk_column(&self) -> &str {
8399        self.self_pk_column.as_deref().unwrap_or("id")
8400    }
8401}
8402
8403/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8404/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8405/// relation-existence family (issue #830). The child is a polymorphic,
8406/// content-type-discriminated model; emits a `Model::
8407/// generic_reverse_relations()` entry the queryset resolves by name.
8408struct GenericHasAttr {
8409    /// Accessor name, e.g. `name = "tags"`.
8410    name: String,
8411    /// Child model type identifier (`child = "Tag"`) — looked up for its
8412    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8413    child: syn::Ident,
8414    /// Column on the child table holding the parent's content-type id.
8415    /// Optional; defaults to `"content_type_id"`.
8416    ct_column: String,
8417    /// Column on the child table holding the parent's PK value.
8418    /// Optional; defaults to `"object_pk"`.
8419    pk_column: String,
8420    /// SQL primary-key column on **this** (parent) model's table.
8421    /// Defaults to the PK column (`"id"` without one).
8422    self_pk_column: Option<String>,
8423    /// Where an explicit `self_pk_column` was written, for its error.
8424    self_pk_span: Option<proc_macro2::Span>,
8425}
8426
8427impl GenericHasAttr {
8428    fn self_pk_column(&self) -> &str {
8429        self.self_pk_column.as_deref().unwrap_or("id")
8430    }
8431}
8432
8433/// Parsed form of one index declaration (field-level or container-level).
8434struct IndexAttr {
8435    /// The declaring attribute, for diagnostics.
8436    span: proc_macro2::Span,
8437    /// Index name; auto-derived when `None` at parse time.
8438    name: Option<String>,
8439    /// Column names in the index.
8440    columns: Vec<String>,
8441    /// `true` for `CREATE UNIQUE INDEX`.
8442    unique: bool,
8443    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8444    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8445    /// `"btree"` when the attribute is absent — the DDL writer omits
8446    /// the `USING` clause and the backend uses its own default
8447    /// (btree on every supported dialect).
8448    method: String,
8449    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8450    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8451    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8452    where_clause: Option<String>,
8453    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8454    /// Empty `Vec` (the default) means "no covering columns".
8455    include: Vec<String>,
8456}
8457
8458/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8459struct CheckAttr {
8460    name: String,
8461    expr: String,
8462}
8463
8464/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8465/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8466/// PG-only — surfaced on every backend in the macro emit; the migration
8467/// writer skips the op on MySQL/SQLite. Issue #319.
8468struct ExcludeAttr {
8469    /// Constraint name (free-form Rust identifier).
8470    name: String,
8471    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8472    using: String,
8473    /// Comma-separated `(column, operator)` pairs, in declaration
8474    /// order. Parsed from `"col WITH op, col WITH op"`.
8475    elements: Vec<(String, String)>,
8476    /// Optional `WHERE` predicate (raw SQL).
8477    where_clause: Option<String>,
8478}
8479
8480/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8481/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8482/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8483/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8484/// the model, not the field.
8485struct CompositeFkAttr {
8486    /// Logical relation name (free-form Rust identifier).
8487    name: String,
8488    /// SQL table name of the target.
8489    to: String,
8490    /// Source-side column names, in declaration order.
8491    from: Vec<String>,
8492    /// Target-side column names, same length / order as `from`.
8493    on: Vec<String>,
8494}
8495
8496/// Parsed form of one `#[rustango(generic_fk(name = "target",
8497/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8498/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8499/// generic ("any model") FKs live on the model, not the field.
8500struct GenericFkAttr {
8501    /// Logical relation name (free-form Rust identifier).
8502    name: String,
8503    /// Source-side column carrying the `content_type_id` value.
8504    ct_column: String,
8505    /// Source-side column carrying the target row's primary key.
8506    pk_column: String,
8507}
8508
8509/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8510struct M2MAttr {
8511    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8512    name: String,
8513    /// Target table (e.g. `"app_tags"`).
8514    to: String,
8515    /// Junction table (e.g. `"post_tags"`).
8516    through: String,
8517    /// Source FK column in the junction table (e.g. `"post_id"`).
8518    src: String,
8519    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8520    dst: String,
8521    /// Whether the migration writer should auto-create the junction
8522    /// table. Default `true`. Set `auto_create = false` (#324) when
8523    /// the operator declares the through table as its own
8524    /// `#[derive(Model)]` struct with extra columns.
8525    auto_create: bool,
8526}
8527
8528/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8529/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8530/// junction carries a ContentType discriminator so unrelated models
8531/// share one pivot + related set.
8532struct GenericM2MAttr {
8533    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8534    name: String,
8535    /// Polymorphic junction table (e.g. `"taggables"`).
8536    through: String,
8537    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8538    pk_column: String,
8539    /// Junction column holding the owning model's `content_type_id`
8540    /// discriminator (e.g. `"taggable_type"`).
8541    ct_column: String,
8542    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8543    related_column: String,
8544}
8545
8546/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8547/// "user"))]`. `track` is a comma-separated list of field names whose
8548/// before/after values land in the JSONB `changes` column. `source`
8549/// is informational only — it pins a default source when the model
8550/// is written outside any `audit::with_source(...)` scope (rare).
8551#[derive(Default)]
8552struct AuditAttrs {
8553    /// Field names to capture in the `changes` JSONB. Validated
8554    /// against declared scalar fields at compile time. Empty means
8555    /// "track every scalar field".
8556    track: Option<(Vec<String>, proc_macro2::Span)>,
8557}
8558
8559/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8560/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8561/// comma-separated strings; we validate each ident against the model's
8562/// declared fields at compile time.
8563#[derive(Default)]
8564struct AdminAttrs {
8565    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8566    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8567    list_per_page: Option<usize>,
8568    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8569    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8570    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8571    /// Bulk action names. No field-validation against model fields —
8572    /// these are action handlers, not column references.
8573    actions: Option<(Vec<String>, proc_macro2::Span)>,
8574    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8575    /// sections, comma-separated fields per section, optional
8576    /// `Title:` prefix. Empty title omits the `<legend>`.
8577    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8578    /// `admin(list_display_links = "title")`. Names
8579    /// from `list_display` whose cells should link to detail/edit.
8580    /// Issue #350.
8581    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8582    /// `admin(search_help_text = "...")`. Short
8583    /// caption rendered beside the admin list view's search box.
8584    /// Issue #353.
8585    search_help_text: Option<String>,
8586    /// `admin(actions_on_top = false)`. Hides the
8587    /// action-bar above the table. Default `true`. Issue #354.
8588    actions_on_top: Option<bool>,
8589    /// `admin(actions_on_bottom = true)`. Renders an
8590    /// additional action-bar below the table. Default `false`.
8591    /// Issue #354.
8592    actions_on_bottom: Option<bool>,
8593    /// `admin(date_hierarchy = "created_at")`. Name of
8594    /// a date / datetime field whose values render as a clickable
8595    /// year / month / day drill-down strip above the list table.
8596    /// Empty / unset disables the strip. Issue #355.
8597    date_hierarchy: Option<String>,
8598    /// `admin(prepopulated_fields = "slug:title")`.
8599    /// Each entry is `target:source[+source2]`; multiple entries are
8600    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8601    /// The admin change-form emits JS that slugifies the source values
8602    /// into the target field on every keystroke. Issue #356.
8603    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8604    /// `admin(raw_id_fields = "parent, owner")`. Names
8605    /// of FK fields whose change-form widget renders a lookup link
8606    /// next to the input. Issue #357.
8607    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8608    /// `admin(autocomplete_fields = "author_id")`.
8609    /// Names of FK fields whose change-form widget renders an
8610    /// Ajax-driven typeahead populated from a `__autocomplete`
8611    /// endpoint on the target model. Issue #358.
8612    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8613    /// `admin(list_select_related = "all" | "none" | "author, …")`
8614    /// — tunes the admin list view's FK auto-JOIN
8615    /// policy. Default `"all"` matches rustango's join-everything
8616    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8617    list_select_related: Option<String>,
8618    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8619    /// Each entry is `field_name:widget_name`; multiple
8620    /// entries comma-separated. Empty / unset → no overrides. The
8621    /// list of widget names supported is documented on
8622    /// `AdminConfig::formfield_overrides`. Issue #359.
8623    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8624}
8625
8626/// The names `#[derive(Model)]` reads its attributes under: `rgo` is a
8627/// short alias of `rustango` with the same meaning.
8628#[derive(Clone, Copy, PartialEq, Eq)]
8629enum ModelAttrName {
8630    Rustango,
8631    Rgo,
8632}
8633
8634impl ModelAttrName {
8635    fn of(attr: &syn::Attribute) -> Option<Self> {
8636        let path = attr.path();
8637        if path.is_ident("rustango") {
8638            Some(Self::Rustango)
8639        } else if path.is_ident("rgo") {
8640            Some(Self::Rgo)
8641        } else {
8642            None
8643        }
8644    }
8645
8646    fn as_str(self) -> &'static str {
8647        match self {
8648            Self::Rustango => "rustango",
8649            Self::Rgo => "rgo",
8650        }
8651    }
8652}
8653
8654/// The model attributes in `attrs`, under either name. One item or field
8655/// uses one name, so a mix reads as a mistake rather than a merge.
8656fn model_attrs(attrs: &[syn::Attribute]) -> syn::Result<Vec<&syn::Attribute>> {
8657    let mut first: Option<ModelAttrName> = None;
8658    let mut out = Vec::new();
8659    for attr in attrs {
8660        let Some(name) = ModelAttrName::of(attr) else {
8661            continue;
8662        };
8663        match first {
8664            Some(seen) if seen != name => {
8665                return Err(syn::Error::new_spanned(
8666                    attr.path(),
8667                    format!(
8668                        "`#[{}(...)]` and `#[{}(...)]` are the same attribute; \
8669                         use one name on an item or field",
8670                        seen.as_str(),
8671                        name.as_str(),
8672                    ),
8673                ));
8674            }
8675            _ => first = Some(name),
8676        }
8677        out.push(attr);
8678    }
8679    Ok(out)
8680}
8681
8682fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8683    let mut out = ContainerAttrs {
8684        table: None,
8685        display: None,
8686        app: None,
8687        admin: None,
8688        audit: None,
8689        // Default `permissions = true` so every `#[derive(Model)]`
8690        // gets the four CRUD codenames seeded by `auto_create_permissions`
8691        // and is visible to non-superusers in the tenant admin without
8692        // manual per-model annotation. Models that intentionally don't
8693        // want permission rows (registry-internal types, framework
8694        // tables operators shouldn't manage directly) opt out via
8695        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8696        // out-of-the-box admin invisibility regression (#62).
8697        permissions: true,
8698        m2m: Vec::new(),
8699        generic_m2m: Vec::new(),
8700        indexes: Vec::new(),
8701        checks: Vec::new(),
8702        excludes: Vec::new(),
8703        composite_fks: Vec::new(),
8704        generic_fks: Vec::new(),
8705        scope: None,
8706        manager_fns: Vec::new(),
8707        default_order: Vec::new(),
8708        is_view: false,
8709        managed: true,
8710        verbose_name: None,
8711        verbose_name_plural: None,
8712        base_manager_name: None,
8713        order_with_respect_to: None,
8714        proxy: false,
8715        required_db_features: Vec::new(),
8716        required_db_vendor: None,
8717        default_related_name: None,
8718        db_table_comment: None,
8719        get_latest_by: None,
8720        extra_permissions: Vec::new(),
8721        default_permissions: Vec::new(),
8722        global_scopes: Vec::new(),
8723        through_relations: Vec::new(),
8724        reverse_has_relations: Vec::new(),
8725        generic_has_relations: Vec::new(),
8726    };
8727    for attr in model_attrs(&input.attrs)? {
8728        // The name the user wrote, for hints that quote the attribute.
8729        let written = ModelAttrName::of(attr).map_or("rustango", ModelAttrName::as_str);
8730        attr.parse_nested_meta(|meta| {
8731            if meta.path.is_ident("table") {
8732                let s: LitStr = meta.value()?.parse()?;
8733                let name = s.value();
8734                // v0.27.3 (#65) — macro-time guard against table names
8735                // that compile but break SQL downstream. Hyphens are
8736                // the common footgun: PostgreSQL accepts a quoted
8737                // `"intermediate-region"` in CREATE TABLE, but the
8738                // FK / index name derivation in `migrate::ddl`
8739                // emits `intermediate-region_field_fkey` unquoted,
8740                // which then fails the SQL parser. Same shape rule
8741                // as Postgres regular identifiers so the safe path
8742                // is the only path.
8743                validate_table_name(&name, s.span())?;
8744                out.table = Some(name);
8745                return Ok(());
8746            }
8747            if meta.path.is_ident("display") {
8748                let s: LitStr = meta.value()?.parse()?;
8749                out.display = Some((s.value(), s.span()));
8750                return Ok(());
8751            }
8752            if meta.path.is_ident("app") {
8753                let s: LitStr = meta.value()?.parse()?;
8754                out.app = Some(s.value());
8755                return Ok(());
8756            }
8757            if meta.path.is_ident("scope") {
8758                let s: LitStr = meta.value()?.parse()?;
8759                let val = s.value();
8760                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8761                    return Err(meta.error(format!(
8762                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8763                    )));
8764                }
8765                out.scope = Some(val);
8766                return Ok(());
8767            }
8768            if meta.path.is_ident("admin") {
8769                let mut admin = AdminAttrs::default();
8770                meta.parse_nested_meta(|inner| {
8771                    if inner.path.is_ident("list_display") {
8772                        let s: LitStr = inner.value()?.parse()?;
8773                        admin.list_display =
8774                            Some((split_field_list(&s.value()), s.span()));
8775                        return Ok(());
8776                    }
8777                    if inner.path.is_ident("search_fields") {
8778                        let s: LitStr = inner.value()?.parse()?;
8779                        admin.search_fields =
8780                            Some((split_field_list(&s.value()), s.span()));
8781                        return Ok(());
8782                    }
8783                    if inner.path.is_ident("readonly_fields") {
8784                        let s: LitStr = inner.value()?.parse()?;
8785                        admin.readonly_fields =
8786                            Some((split_field_list(&s.value()), s.span()));
8787                        return Ok(());
8788                    }
8789                    if inner.path.is_ident("list_per_page") {
8790                        let lit: syn::LitInt = inner.value()?.parse()?;
8791                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8792                        return Ok(());
8793                    }
8794                    if inner.path.is_ident("ordering") {
8795                        let s: LitStr = inner.value()?.parse()?;
8796                        admin.ordering = Some((
8797                            parse_ordering_list(&s.value()),
8798                            s.span(),
8799                        ));
8800                        return Ok(());
8801                    }
8802                    if inner.path.is_ident("list_filter") {
8803                        let s: LitStr = inner.value()?.parse()?;
8804                        admin.list_filter =
8805                            Some((split_field_list(&s.value()), s.span()));
8806                        return Ok(());
8807                    }
8808                    if inner.path.is_ident("actions") {
8809                        let s: LitStr = inner.value()?.parse()?;
8810                        admin.actions =
8811                            Some((split_field_list(&s.value()), s.span()));
8812                        return Ok(());
8813                    }
8814                    if inner.path.is_ident("fieldsets") {
8815                        let s: LitStr = inner.value()?.parse()?;
8816                        admin.fieldsets =
8817                            Some((parse_fieldset_list(&s.value()), s.span()));
8818                        return Ok(());
8819                    }
8820                    if inner.path.is_ident("list_display_links") {
8821                        let s: LitStr = inner.value()?.parse()?;
8822                        admin.list_display_links =
8823                            Some((split_field_list(&s.value()), s.span()));
8824                        return Ok(());
8825                    }
8826                    if inner.path.is_ident("search_help_text") {
8827                        let s: LitStr = inner.value()?.parse()?;
8828                        admin.search_help_text = Some(s.value());
8829                        return Ok(());
8830                    }
8831                    if inner.path.is_ident("actions_on_top") {
8832                        let lit: syn::LitBool = inner.value()?.parse()?;
8833                        admin.actions_on_top = Some(lit.value);
8834                        return Ok(());
8835                    }
8836                    if inner.path.is_ident("actions_on_bottom") {
8837                        let lit: syn::LitBool = inner.value()?.parse()?;
8838                        admin.actions_on_bottom = Some(lit.value);
8839                        return Ok(());
8840                    }
8841                    if inner.path.is_ident("date_hierarchy") {
8842                        let s: LitStr = inner.value()?.parse()?;
8843                        admin.date_hierarchy = Some(s.value());
8844                        return Ok(());
8845                    }
8846                    if inner.path.is_ident("prepopulated_fields") {
8847                        let s: LitStr = inner.value()?.parse()?;
8848                        admin.prepopulated_fields =
8849                            Some((parse_prepopulated_list(&s.value()), s.span()));
8850                        return Ok(());
8851                    }
8852                    if inner.path.is_ident("raw_id_fields") {
8853                        let s: LitStr = inner.value()?.parse()?;
8854                        admin.raw_id_fields =
8855                            Some((split_field_list(&s.value()), s.span()));
8856                        return Ok(());
8857                    }
8858                    if inner.path.is_ident("autocomplete_fields") {
8859                        let s: LitStr = inner.value()?.parse()?;
8860                        admin.autocomplete_fields =
8861                            Some((split_field_list(&s.value()), s.span()));
8862                        return Ok(());
8863                    }
8864                    if inner.path.is_ident("list_select_related") {
8865                        let s: LitStr = inner.value()?.parse()?;
8866                        admin.list_select_related = Some(s.value());
8867                        return Ok(());
8868                    }
8869                    if inner.path.is_ident("formfield_overrides") {
8870                        let s: LitStr = inner.value()?.parse()?;
8871                        admin.formfield_overrides =
8872                            Some((parse_formfield_overrides(&s.value()), s.span()));
8873                        return Ok(());
8874                    }
8875                    Err(inner.error(
8876                        "unknown admin attribute (supported: \
8877                         `list_display`, `list_display_links`, \
8878                         `search_fields`, `search_help_text`, \
8879                         `readonly_fields`, \
8880                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8881                         `actions_on_top`, `actions_on_bottom`, \
8882                         `date_hierarchy`, \
8883                         `prepopulated_fields`, \
8884                         `raw_id_fields`, \
8885                         `autocomplete_fields`, \
8886                         `list_select_related`, \
8887                         `formfield_overrides`, \
8888                         `fieldsets`)",
8889                    ))
8890                })?;
8891                out.admin = Some(admin);
8892                return Ok(());
8893            }
8894            if meta.path.is_ident("manager") {
8895                // Its empty trait could not take methods (#2132).
8896                return Err(meta.error(
8897                    "`manager(ext = ...)` was removed (#2132): declare your own \
8898                     `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
8899                ));
8900            }
8901            if meta.path.is_ident("manager_fn") {
8902                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8903                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8904                // next to the default `Self::objects()`. Multiple
8905                // attributes accumulate.
8906                let s: LitStr = meta.value()?.parse()?;
8907                let name = s.value();
8908                if name.is_empty() {
8909                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8910                }
8911                if name == "objects" {
8912                    return Err(meta.error(
8913                        "`manager_fn = \"objects\"` collides with the default \
8914                         accessor — pick a different name",
8915                    ));
8916                }
8917                let ident = syn::Ident::new(&name, s.span());
8918                if out.manager_fns.iter().any(|prev| *prev == ident) {
8919                    return Err(meta.error(format!(
8920                        "duplicate `manager_fn = \"{name}\"`"
8921                    )));
8922                }
8923                out.manager_fns.push(ident);
8924                return Ok(());
8925            }
8926            if meta.path.is_ident("default_order") {
8927                // `#[rustango(default_order = "-created_at, status")]`
8928                // — issue #291 / T2.5. Comma-separated list; `-prefix`
8929                // means descending, `+prefix` or bare name means ascending.
8930                // Per-query opt-in via `QuerySet::with_default_order()`.
8931                let s: LitStr = meta.value()?.parse()?;
8932                let raw = s.value();
8933                let span = s.span();
8934                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8935                    Vec::new();
8936                for entry in raw.split(',') {
8937                    let trimmed = entry.trim();
8938                    if trimmed.is_empty() {
8939                        return Err(syn::Error::new(
8940                            span,
8941                            "`default_order = \"...\"` has an empty entry — \
8942                             check for a stray comma",
8943                        ));
8944                    }
8945                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8946                        (true, rest.trim().to_owned())
8947                    } else if let Some(rest) = trimmed.strip_prefix('+') {
8948                        (false, rest.trim().to_owned())
8949                    } else {
8950                        (false, trimmed.to_owned())
8951                    };
8952                    if name.is_empty() {
8953                        return Err(syn::Error::new(
8954                            span,
8955                            "`default_order` entry has no column name after the prefix",
8956                        ));
8957                    }
8958                    if parsed.iter().any(|(n, _, _)| *n == name) {
8959                        return Err(syn::Error::new(
8960                            span,
8961                            format!("duplicate column `{name}` in `default_order`"),
8962                        ));
8963                    }
8964                    parsed.push((name, desc, span));
8965                }
8966                if parsed.is_empty() {
8967                    return Err(syn::Error::new(
8968                        span,
8969                        "`default_order = \"...\"` cannot be empty",
8970                    ));
8971                }
8972                out.default_order = parsed;
8973                return Ok(());
8974            }
8975            if meta.path.is_ident("global_scope") {
8976                // `#[rustango(global_scope(name = "active", apply =
8977                //  path::to::fn))]` — issue #820. The apply function
8978                // path resolves at macro-expand time in the consumer's
8979                // scope; the macro re-emits it verbatim into the
8980                // `ModelSchema::global_scopes` slice literal.
8981                let span = meta.path.span();
8982                let mut scope_name: Option<String> = None;
8983                let mut apply_path: Option<syn::Path> = None;
8984                meta.parse_nested_meta(|inner| {
8985                    if inner.path.is_ident("name") {
8986                        let s: LitStr = inner.value()?.parse()?;
8987                        let raw = s.value();
8988                        if raw.trim().is_empty() {
8989                            return Err(syn::Error::new(
8990                                s.span(),
8991                                "`global_scope(name = \"...\")` must not be empty",
8992                            ));
8993                        }
8994                        scope_name = Some(raw);
8995                        return Ok(());
8996                    }
8997                    if inner.path.is_ident("apply") {
8998                        let p: syn::Path = inner.value()?.parse()?;
8999                        apply_path = Some(p);
9000                        return Ok(());
9001                    }
9002                    Err(inner.error(
9003                        "unknown `global_scope` attribute (supported: \
9004                         `name`, `apply`)",
9005                    ))
9006                })?;
9007                let Some(name) = scope_name else {
9008                    return Err(syn::Error::new(
9009                        span,
9010                        "`global_scope` requires `name = \"...\"`",
9011                    ));
9012                };
9013                let Some(apply) = apply_path else {
9014                    return Err(syn::Error::new(
9015                        span,
9016                        "`global_scope` requires `apply = fn_path`",
9017                    ));
9018                };
9019                if out.global_scopes.iter().any(|s| s.name == name) {
9020                    return Err(syn::Error::new(
9021                        span,
9022                        format!(
9023                            "duplicate global scope name `{name}` — \
9024                             pick a unique identifier so \
9025                             `QuerySet::without_global_scope(\"{name}\")` \
9026                             is unambiguous"
9027                        ),
9028                    ));
9029                }
9030                out.global_scopes.push(GlobalScopeAttr { name, apply });
9031                return Ok(());
9032            }
9033            if meta.path.is_ident("through") {
9034                // `#[rustango(through(name = "posts", far = "Post",
9035                //  far_fk_column = "author_id", intermediate = "User",
9036                //  intermediate_fk_column = "country_id"
9037                //  [, intermediate_pk_column = "id"]))]` — issue #817.
9038                let span = meta.path.span();
9039                let mut accessor_name: Option<String> = None;
9040                let mut far_ident: Option<syn::Ident> = None;
9041                let mut far_fk_column: Option<String> = None;
9042                let mut intermediate_ident: Option<syn::Ident> = None;
9043                let mut intermediate_fk_column: Option<String> = None;
9044                let mut intermediate_pk_column: Option<String> = None;
9045                fn parse_nonempty_string(
9046                    inner: &syn::meta::ParseNestedMeta<'_>,
9047                    field: &str,
9048                ) -> syn::Result<String> {
9049                    let s: LitStr = inner.value()?.parse()?;
9050                    let raw = s.value();
9051                    let trimmed = raw.trim();
9052                    if trimmed.is_empty() {
9053                        return Err(syn::Error::new(
9054                            s.span(),
9055                            format!("`through({field} = \"...\")` must not be empty"),
9056                        ));
9057                    }
9058                    Ok(trimmed.to_owned())
9059                }
9060                meta.parse_nested_meta(|inner| {
9061                    if inner.path.is_ident("name") {
9062                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9063                        return Ok(());
9064                    }
9065                    if inner.path.is_ident("far") {
9066                        // Far model name accepted as a string literal
9067                        // so the attribute fits inside the existing
9068                        // `parse_nested_meta` shape; parsed back into a
9069                        // `syn::Ident` so the emitted accessor can
9070                        // reference the type directly.
9071                        let s: LitStr = inner.value()?.parse()?;
9072                        let raw = s.value();
9073                        let trimmed = raw.trim();
9074                        if trimmed.is_empty() {
9075                            return Err(syn::Error::new(
9076                                s.span(),
9077                                "`through(far = \"...\")` must not be empty",
9078                            ));
9079                        }
9080                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9081                        return Ok(());
9082                    }
9083                    if inner.path.is_ident("far_fk_column") {
9084                        far_fk_column =
9085                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9086                        return Ok(());
9087                    }
9088                    if inner.path.is_ident("intermediate") {
9089                        let s: LitStr = inner.value()?.parse()?;
9090                        let raw = s.value();
9091                        let trimmed = raw.trim();
9092                        if trimmed.is_empty() {
9093                            return Err(syn::Error::new(
9094                                s.span(),
9095                                "`through(intermediate = \"...\")` must not be empty",
9096                            ));
9097                        }
9098                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9099                        return Ok(());
9100                    }
9101                    if inner.path.is_ident("intermediate_fk_column") {
9102                        intermediate_fk_column =
9103                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9104                        return Ok(());
9105                    }
9106                    if inner.path.is_ident("intermediate_pk_column") {
9107                        intermediate_pk_column =
9108                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9109                        return Ok(());
9110                    }
9111                    Err(inner.error(
9112                        "unknown `through` attribute (supported: \
9113                         `name`, `far`, `far_fk_column`, \
9114                         `intermediate`, `intermediate_fk_column`, \
9115                         `intermediate_pk_column`)",
9116                    ))
9117                })?;
9118                let Some(name) = accessor_name else {
9119                    return Err(syn::Error::new(
9120                        span,
9121                        "`through` requires `name = \"...\"`",
9122                    ));
9123                };
9124                let Some(far) = far_ident else {
9125                    return Err(syn::Error::new(
9126                        span,
9127                        "`through` requires `far = \"FarModelType\"`",
9128                    ));
9129                };
9130                let Some(far_fk_column) = far_fk_column else {
9131                    return Err(syn::Error::new(
9132                        span,
9133                        "`through` requires `far_fk_column = \"<column>\"`",
9134                    ));
9135                };
9136                let Some(intermediate) = intermediate_ident else {
9137                    return Err(syn::Error::new(
9138                        span,
9139                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9140                    ));
9141                };
9142                let Some(intermediate_fk_column) = intermediate_fk_column else {
9143                    return Err(syn::Error::new(
9144                        span,
9145                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9146                    ));
9147                };
9148                let intermediate_pk_column =
9149                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9150                if out.through_relations.iter().any(|t| t.name == name) {
9151                    return Err(syn::Error::new(
9152                        span,
9153                        format!(
9154                            "duplicate `through(name = \"{name}\")` — \
9155                             pick a unique accessor name"
9156                        ),
9157                    ));
9158                }
9159                out.through_relations.push(ThroughAttr {
9160                    name,
9161                    far,
9162                    far_fk_column,
9163                    intermediate,
9164                    intermediate_fk_column,
9165                    intermediate_pk_column,
9166                });
9167                return Ok(());
9168            }
9169            if meta.path.is_ident("reverse_has") {
9170                // `#[rustango(reverse_has(name = "comments",
9171                //  child = "Comment", child_fk_column = "post_id"
9172                //  [, self_pk_column = "id"]))]` — issue #830.
9173                let span = meta.path.span();
9174                let mut accessor_name: Option<String> = None;
9175                let mut child_ident: Option<syn::Ident> = None;
9176                let mut child_fk_column: Option<String> = None;
9177                let mut self_pk_column: Option<String> = None;
9178                let mut self_pk_span: Option<proc_macro2::Span> = None;
9179                meta.parse_nested_meta(|inner| {
9180                    if inner.path.is_ident("name") {
9181                        let s: LitStr = inner.value()?.parse()?;
9182                        let raw = s.value();
9183                        if raw.trim().is_empty() {
9184                            return Err(syn::Error::new(
9185                                s.span(),
9186                                "`reverse_has(name = \"...\")` must not be empty",
9187                            ));
9188                        }
9189                        accessor_name = Some(raw);
9190                        return Ok(());
9191                    }
9192                    if inner.path.is_ident("child") {
9193                        let s: LitStr = inner.value()?.parse()?;
9194                        let raw = s.value();
9195                        let trimmed = raw.trim();
9196                        if trimmed.is_empty() {
9197                            return Err(syn::Error::new(
9198                                s.span(),
9199                                "`reverse_has(child = \"...\")` must not be empty",
9200                            ));
9201                        }
9202                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9203                        return Ok(());
9204                    }
9205                    if inner.path.is_ident("child_fk_column") {
9206                        let s: LitStr = inner.value()?.parse()?;
9207                        let raw = s.value();
9208                        let trimmed = raw.trim();
9209                        if trimmed.is_empty() {
9210                            return Err(syn::Error::new(
9211                                s.span(),
9212                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9213                            ));
9214                        }
9215                        child_fk_column = Some(trimmed.to_owned());
9216                        return Ok(());
9217                    }
9218                    if inner.path.is_ident("self_pk_column") {
9219                        let s: LitStr = inner.value()?.parse()?;
9220                        let raw = s.value();
9221                        let trimmed = raw.trim();
9222                        if trimmed.is_empty() {
9223                            return Err(syn::Error::new(
9224                                s.span(),
9225                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9226                            ));
9227                        }
9228                        self_pk_column = Some(trimmed.to_owned());
9229                        self_pk_span = Some(s.span());
9230                        return Ok(());
9231                    }
9232                    Err(inner.error(
9233                        "unknown `reverse_has` attribute (supported: \
9234                         `name`, `child`, `child_fk_column`, \
9235                         `self_pk_column`)",
9236                    ))
9237                })?;
9238                let Some(name) = accessor_name else {
9239                    return Err(syn::Error::new(
9240                        span,
9241                        "`reverse_has` requires `name = \"...\"`",
9242                    ));
9243                };
9244                let Some(child) = child_ident else {
9245                    return Err(syn::Error::new(
9246                        span,
9247                        "`reverse_has` requires `child = \"ChildModelType\"`",
9248                    ));
9249                };
9250                let Some(child_fk_column) = child_fk_column else {
9251                    return Err(syn::Error::new(
9252                        span,
9253                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9254                    ));
9255                };
9256                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9257                    return Err(syn::Error::new(
9258                        span,
9259                        format!(
9260                            "duplicate `reverse_has(name = \"{name}\")` — \
9261                             pick a unique accessor name"
9262                        ),
9263                    ));
9264                }
9265                out.reverse_has_relations.push(ReverseHasAttr {
9266                    name,
9267                    child,
9268                    child_fk_column,
9269                    self_pk_column,
9270                    self_pk_span,
9271                });
9272                return Ok(());
9273            }
9274            if meta.path.is_ident("generic_has") {
9275                // `#[rustango(generic_has(name = "tags",
9276                //  child = "Tag", ct_column = "content_type_id",
9277                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9278                //  — issue #830, the reverse generic-FK (GFK) arm of the
9279                //  relation-existence family.
9280                let span = meta.path.span();
9281                let mut accessor_name: Option<String> = None;
9282                let mut child_ident: Option<syn::Ident> = None;
9283                let mut ct_column: Option<String> = None;
9284                let mut pk_column: Option<String> = None;
9285                let mut self_pk_column: Option<String> = None;
9286                let mut self_pk_span: Option<proc_macro2::Span> = None;
9287                meta.parse_nested_meta(|inner| {
9288                    if inner.path.is_ident("name") {
9289                        let s: LitStr = inner.value()?.parse()?;
9290                        let raw = s.value();
9291                        if raw.trim().is_empty() {
9292                            return Err(syn::Error::new(
9293                                s.span(),
9294                                "`generic_has(name = \"...\")` must not be empty",
9295                            ));
9296                        }
9297                        accessor_name = Some(raw);
9298                        return Ok(());
9299                    }
9300                    if inner.path.is_ident("child") {
9301                        let s: LitStr = inner.value()?.parse()?;
9302                        let trimmed = s.value().trim().to_owned();
9303                        if trimmed.is_empty() {
9304                            return Err(syn::Error::new(
9305                                s.span(),
9306                                "`generic_has(child = \"...\")` must not be empty",
9307                            ));
9308                        }
9309                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9310                        return Ok(());
9311                    }
9312                    if inner.path.is_ident("ct_column") {
9313                        let s: LitStr = inner.value()?.parse()?;
9314                        let trimmed = s.value().trim().to_owned();
9315                        if trimmed.is_empty() {
9316                            return Err(syn::Error::new(
9317                                s.span(),
9318                                "`generic_has(ct_column = \"...\")` must not be empty",
9319                            ));
9320                        }
9321                        ct_column = Some(trimmed);
9322                        return Ok(());
9323                    }
9324                    if inner.path.is_ident("pk_column") {
9325                        let s: LitStr = inner.value()?.parse()?;
9326                        let trimmed = s.value().trim().to_owned();
9327                        if trimmed.is_empty() {
9328                            return Err(syn::Error::new(
9329                                s.span(),
9330                                "`generic_has(pk_column = \"...\")` must not be empty",
9331                            ));
9332                        }
9333                        pk_column = Some(trimmed);
9334                        return Ok(());
9335                    }
9336                    if inner.path.is_ident("self_pk_column") {
9337                        let s: LitStr = inner.value()?.parse()?;
9338                        let trimmed = s.value().trim().to_owned();
9339                        if trimmed.is_empty() {
9340                            return Err(syn::Error::new(
9341                                s.span(),
9342                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9343                            ));
9344                        }
9345                        self_pk_column = Some(trimmed);
9346                        self_pk_span = Some(s.span());
9347                        return Ok(());
9348                    }
9349                    Err(inner.error(
9350                        "unknown `generic_has` attribute (supported: \
9351                         `name`, `child`, `ct_column`, `pk_column`, \
9352                         `self_pk_column`)",
9353                    ))
9354                })?;
9355                let Some(name) = accessor_name else {
9356                    return Err(syn::Error::new(
9357                        span,
9358                        "`generic_has` requires `name = \"...\"`",
9359                    ));
9360                };
9361                let Some(child) = child_ident else {
9362                    return Err(syn::Error::new(
9363                        span,
9364                        "`generic_has` requires `child = \"ChildModelType\"`",
9365                    ));
9366                };
9367                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9368                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9369                if out.generic_has_relations.iter().any(|r| r.name == name) {
9370                    return Err(syn::Error::new(
9371                        span,
9372                        format!(
9373                            "duplicate `generic_has(name = \"{name}\")` — \
9374                             pick a unique accessor name"
9375                        ),
9376                    ));
9377                }
9378                out.generic_has_relations.push(GenericHasAttr {
9379                    name,
9380                    child,
9381                    ct_column,
9382                    pk_column,
9383                    self_pk_column,
9384                    self_pk_span,
9385                });
9386                return Ok(());
9387            }
9388            if meta.path.is_ident("audit") {
9389                let mut audit = AuditAttrs::default();
9390                meta.parse_nested_meta(|inner| {
9391                    if inner.path.is_ident("track") {
9392                        let s: LitStr = inner.value()?.parse()?;
9393                        audit.track =
9394                            Some((split_field_list(&s.value()), s.span()));
9395                        return Ok(());
9396                    }
9397                    Err(inner.error(
9398                        "unknown audit attribute (supported: `track`)",
9399                    ))
9400                })?;
9401                out.audit = Some(audit);
9402                return Ok(());
9403            }
9404            if meta.path.is_ident("permissions") {
9405                // Two forms accepted:
9406                //   #[rustango(permissions)]          — flag form, true
9407                //   #[rustango(permissions = false)]  — explicit opt-out
9408                //   #[rustango(permissions = true)]   — explicit opt-in
9409                if let Ok(v) = meta.value() {
9410                    let lit: syn::LitBool = v.parse()?;
9411                    out.permissions = lit.value;
9412                } else {
9413                    out.permissions = true;
9414                }
9415                return Ok(());
9416            }
9417            if meta.path.is_ident("view") {
9418                // Issue #293 / T2.10. Two forms accepted, matching
9419                // the `permissions` flag pattern:
9420                //   #[rustango(view)]          — flag form, true
9421                //   #[rustango(view = false)]  — explicit opt-out
9422                //   #[rustango(view = true)]   — explicit opt-in
9423                if let Ok(v) = meta.value() {
9424                    let lit: syn::LitBool = v.parse()?;
9425                    out.is_view = lit.value;
9426                } else {
9427                    out.is_view = true;
9428                }
9429                return Ok(());
9430            }
9431            if meta.path.is_ident("managed") {
9432                // Who owns the table. Issue #321.
9433                //   #[rustango(managed = false)]  — operator-managed table
9434                //   #[rustango(managed = true)]   — rustango-managed (the default)
9435                // Bare-flag form is intentionally not accepted: writing
9436                // `#[rustango(managed)]` reads as "yes please manage it"
9437                // which is already the default. The opt-out is the only
9438                // useful state, so it must be explicit.
9439                let v = meta.value()?;
9440                let lit: syn::LitBool = v.parse()?;
9441                out.managed = lit.value;
9442                return Ok(());
9443            }
9444            if meta.path.is_ident("verbose_name") {
9445                let s: LitStr = meta.value()?.parse()?;
9446                out.verbose_name = Some(s.value());
9447                return Ok(());
9448            }
9449            if meta.path.is_ident("verbose_name_plural") {
9450                let s: LitStr = meta.value()?.parse()?;
9451                out.verbose_name_plural = Some(s.value());
9452                return Ok(());
9453            }
9454            if meta.path.is_ident("db_table_comment") {
9455                // Free-form table-level comment attached to the DB
9456                // catalog.
9457                let s: LitStr = meta.value()?.parse()?;
9458                out.db_table_comment = Some(s.value());
9459                return Ok(());
9460            }
9461            if meta.path.is_ident("proxy") {
9462                // Declarative flag
9463                // marking the struct as a proxy of another model that
9464                // shares its DB table. Stored on `ModelSchema::proxy`
9465                // so future codegen can skip `CreateTable` emission
9466                // for proxies (parent owns the table) and route
9467                // per-instance method resolution to the proxy class.
9468                //
9469                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9470                let value = if meta.input.peek(syn::Token![=]) {
9471                    meta.value()?.parse::<syn::LitBool>()?.value
9472                } else {
9473                    true
9474                };
9475                out.proxy = value;
9476                return Ok(());
9477            }
9478            if meta.path.is_ident("order_with_respect_to") {
9479                // `order_with_respect_to = "parent_fk"` — the model's
9480                // instances are intrinsically ordered relative to
9481                // their parent FK.
9482                //
9483                // rustango stores the FK field name on
9484                // `ModelSchema::order_with_respect_to`. Declarative-only
9485                // today: the migration writer + admin surfaces still
9486                // treat every model identically. Future codegen will
9487                // key off the metadata to auto-emit the `_order`
9488                // column and reorder helpers.
9489                //
9490                // Validated as a Rust-shape identifier so the macro
9491                // can reject typos at derive time.
9492                let s: LitStr = meta.value()?.parse()?;
9493                let raw = s.value();
9494                if raw.is_empty() {
9495                    return Err(syn::Error::new(
9496                        s.span(),
9497                        "`order_with_respect_to` must be a non-empty FK field name",
9498                    ));
9499                }
9500                let valid = raw
9501                    .chars()
9502                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9503                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9504                if !valid {
9505                    return Err(syn::Error::new(
9506                        s.span(),
9507                        format!(
9508                            "`order_with_respect_to` must be a valid Rust \
9509                             identifier (letters / digits / underscores, \
9510                             not starting with a digit); got `{raw}`"
9511                        ),
9512                    ));
9513                }
9514                out.order_with_respect_to = Some(raw);
9515                return Ok(());
9516            }
9517            if meta.path.is_ident("required_db_features") {
9518                // Capability tokens the model needs (e.g.
9519                // `"json_extract"`, `"window_functions"`,
9520                // `"row_security"`). Comma-separated.
9521                // `manage check --deploy` walks every model and warns
9522                // when the active backend doesn't advertise the
9523                // capability.
9524                //
9525                // rustango ships a small registry of capability tokens
9526                // each dialect supports — see `Dialect::supports`.
9527                // Unknown tokens still parse (they end up on the
9528                // schema and show up in the warning) so projects can
9529                // declare aspirational capabilities and the check
9530                // verb will keep nagging until the dialect implements
9531                // them.
9532                let s: LitStr = meta.value()?.parse()?;
9533                let raw = s.value();
9534                let features: Vec<String> = raw
9535                    .split(',')
9536                    .map(str::trim)
9537                    .filter(|s| !s.is_empty())
9538                    .map(str::to_owned)
9539                    .collect();
9540                if features.is_empty() {
9541                    return Err(syn::Error::new(
9542                        s.span(),
9543                        "`required_db_features` must list at least one \
9544                         comma-separated capability token",
9545                    ));
9546                }
9547                out.required_db_features = features;
9548                return Ok(());
9549            }
9550            if meta.path.is_ident("required_db_vendor") {
9551                // The model is only meant to run against the named DB
9552                // backend. `manage check --deploy` flags a mismatch so
9553                // ops catches "I forgot to switch DATABASE_URL" at
9554                // deploy time rather than runtime.
9555                //
9556                // Restricted to the three backends rustango ships
9557                // dialects for so the check verb can compare reliably.
9558                let s: LitStr = meta.value()?.parse()?;
9559                let raw = s.value().to_ascii_lowercase();
9560                match raw.as_str() {
9561                    "postgresql" | "postgres" | "pg" => {
9562                        out.required_db_vendor = Some("postgres".to_owned());
9563                    }
9564                    "mysql" | "mariadb" => {
9565                        out.required_db_vendor = Some("mysql".to_owned());
9566                    }
9567                    "sqlite" | "sqlite3" => {
9568                        out.required_db_vendor = Some("sqlite".to_owned());
9569                    }
9570                    _ => {
9571                        return Err(syn::Error::new(
9572                            s.span(),
9573                            format!(
9574                                "unknown required_db_vendor `{raw}` — \
9575                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9576                                 `mysql` (alias: `mariadb`), or `sqlite` \
9577                                 (alias: `sqlite3`)"
9578                            ),
9579                        ));
9580                    }
9581                }
9582                return Ok(());
9583            }
9584            if meta.path.is_ident("base_manager_name") {
9585                // Name of the manager that `<instance>.<relation>_set`
9586                // uses when resolving reverse-relation managers.
9587                // Distinct from `default_manager_name` (what
9588                // `Self::objects()` returns). Stored on
9589                // `ModelSchema::base_manager_name`.
9590                //
9591                // Validated as a Rust identifier so it stays safe to
9592                // re-emit as code in future reverse-manager codegen.
9593                let s: LitStr = meta.value()?.parse()?;
9594                let raw = s.value();
9595                if raw.is_empty() {
9596                    return Err(syn::Error::new(
9597                        s.span(),
9598                        "`base_manager_name` must be a non-empty string",
9599                    ));
9600                }
9601                let valid = raw
9602                    .chars()
9603                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9604                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9605                if !valid {
9606                    return Err(syn::Error::new(
9607                        s.span(),
9608                        format!(
9609                            "`base_manager_name` must be a valid Rust \
9610                             identifier (letters / digits / underscores, \
9611                             not starting with a digit); got `{raw}`"
9612                        ),
9613                    ));
9614                }
9615                out.base_manager_name = Some(raw);
9616                return Ok(());
9617            }
9618            if meta.path.is_ident("default_related_name") {
9619                // The name reverse-relation accessors use when callers
9620                // don't override `related_name = "..."` on the FK / M2M
9621                // field. Stored on `ModelSchema::default_related_name`
9622                // so future reverse-manager codegen / schema emit /
9623                // admin templates can pick the right accessor name
9624                // (today rustango doesn't auto-emit reverse managers;
9625                // the metadata is the foundation for that work).
9626                //
9627                // Enforced non-empty + ASCII identifier-shape so the
9628                // string is safe to use as a Rust ident later.
9629                let s: LitStr = meta.value()?.parse()?;
9630                let raw = s.value();
9631                if raw.is_empty() {
9632                    return Err(syn::Error::new(
9633                        s.span(),
9634                        "`default_related_name` must be a non-empty string",
9635                    ));
9636                }
9637                let valid = raw
9638                    .chars()
9639                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9640                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9641                if !valid {
9642                    return Err(syn::Error::new(
9643                        s.span(),
9644                        format!(
9645                            "`default_related_name` must be snake_case ASCII \
9646                             (lowercase letters / digits / underscores, not \
9647                             starting with a digit); got `{raw}`"
9648                        ),
9649                    ));
9650                }
9651                out.default_related_name = Some(raw);
9652                return Ok(());
9653            }
9654            if meta.path.is_ident("extra_permissions") {
9655                // Comma-separated `codename:label` pairs.
9656                let s: LitStr = meta.value()?.parse()?;
9657                let raw = s.value();
9658                let mut pairs = Vec::new();
9659                for entry in raw.split(',') {
9660                    let entry = entry.trim();
9661                    if entry.is_empty() {
9662                        continue;
9663                    }
9664                    let (codename, label) = match entry.split_once(':') {
9665                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9666                        None => (entry.to_owned(), entry.to_owned()),
9667                    };
9668                    if codename.is_empty() {
9669                        return Err(meta.error(
9670                            "`extra_permissions` entries must be `codename:label` pairs",
9671                        ));
9672                    }
9673                    pairs.push((codename, label));
9674                }
9675                if pairs.is_empty() {
9676                    return Err(meta
9677                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9678                }
9679                out.extra_permissions = pairs;
9680                return Ok(());
9681            }
9682            if meta.path.is_ident("default_permissions") {
9683                // Comma-separated subset of the CRUD action set.
9684                // Empty means all four (the framework default).
9685                let s: LitStr = meta.value()?.parse()?;
9686                let raw = s.value();
9687                let mut actions: Vec<String> = Vec::new();
9688                for entry in raw.split(',') {
9689                    let action = entry.trim().to_ascii_lowercase();
9690                    if action.is_empty() {
9691                        continue;
9692                    }
9693                    match action.as_str() {
9694                        "add" | "change" | "delete" | "view" => {}
9695                        other => {
9696                            return Err(syn::Error::new(
9697                                s.span(),
9698                                format!(
9699                                    "unknown default_permissions action `{other}` — \
9700                                     expected one of `add`, `change`, `delete`, `view`"
9701                                ),
9702                            ));
9703                        }
9704                    }
9705                    if !actions.contains(&action) {
9706                        actions.push(action);
9707                    }
9708                }
9709                if actions.is_empty() {
9710                    return Err(syn::Error::new(
9711                        s.span(),
9712                        "`default_permissions = \"…\"` must list at least one action; \
9713                         use `permissions = false` on the container if you want NO \
9714                         permissions seeded for this model.",
9715                    ));
9716                }
9717                out.default_permissions = actions;
9718                return Ok(());
9719            }
9720            if meta.path.is_ident("get_latest_by") {
9721                // The `-` prefix selects descending order.
9722                let s: LitStr = meta.value()?.parse()?;
9723                let raw = s.value();
9724                let trimmed = raw.trim();
9725                if trimmed.is_empty() {
9726                    return Err(meta.error("`get_latest_by` must name a column"));
9727                }
9728                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9729                    (stripped.to_owned(), true)
9730                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9731                    (stripped.to_owned(), false)
9732                } else {
9733                    (trimmed.to_owned(), false)
9734                };
9735                if col.is_empty() {
9736                    return Err(meta.error("`get_latest_by` must name a column"));
9737                }
9738                out.get_latest_by = Some((col, desc, s.span()));
9739                return Ok(());
9740            }
9741            if meta.path.is_ident("unique_together") {
9742                // Composite UNIQUE index. Two syntaxes:
9743                //
9744                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9745                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9746                //
9747                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9748                // (col1, col2)`, where <name> defaults to
9749                // `<table>_<col1>_<col2>_uq` when not supplied.
9750                let (columns, name) = parse_together_attr(&meta, "unique_together", written)?;
9751                out.indexes.push(IndexAttr {
9752                    span: meta.path.span(),
9753                    name,
9754                    columns,
9755                    unique: true,
9756                    method: "btree".to_owned(),
9757                    where_clause: None,
9758                include: Vec::new(),
9759                });
9760                return Ok(());
9761            }
9762            if meta.path.is_ident("index_together") {
9763                // Composite (non-unique) index. Two syntaxes
9764                // mirroring `unique_together`.
9765                //
9766                //   #[rustango(index_together = "created_at, status")]
9767                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9768                let (columns, name) = parse_together_attr(&meta, "index_together", written)?;
9769                out.indexes.push(IndexAttr {
9770                    span: meta.path.span(),
9771                    name,
9772                    columns,
9773                    unique: false,
9774                    method: "btree".to_owned(),
9775                    where_clause: None,
9776                include: Vec::new(),
9777                });
9778                return Ok(());
9779            }
9780            if meta.path.is_ident("unique_when") {
9781                // Partial unique index. Issue #265 / T1.3.
9782                //
9783                //   #[rustango(unique_when(
9784                //       columns   = "email",
9785                //       condition = "deleted_at IS NULL",
9786                //       name      = "unique_active_email"
9787                //   ))]
9788                //
9789                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9790                // on PG / SQLite (both ship partial indexes natively).
9791                // MySQL falls back to a plain UNIQUE index — the
9792                // condition is lost; document the limitation in the
9793                // generated migration.
9794                let mut columns: Option<Vec<String>> = None;
9795                let mut condition: Option<String> = None;
9796                let mut name: Option<String> = None;
9797                let mut include: Vec<String> = Vec::new();
9798                meta.parse_nested_meta(|inner| {
9799                    if inner.path.is_ident("columns") {
9800                        let s: LitStr = inner.value()?.parse()?;
9801                        columns = Some(split_field_list(&s.value()));
9802                        return Ok(());
9803                    }
9804                    if inner.path.is_ident("condition") {
9805                        let s: LitStr = inner.value()?.parse()?;
9806                        condition = Some(s.value());
9807                        return Ok(());
9808                    }
9809                    if inner.path.is_ident("name") {
9810                        let s: LitStr = inner.value()?.parse()?;
9811                        name = Some(s.value());
9812                        return Ok(());
9813                    }
9814                    if inner.path.is_ident("include") {
9815                        // PG 11+ covering-index columns. Non-key columns
9816                        // travel with the index leaf for index-only
9817                        // scans. Dropped on MySQL/SQLite by the writer.
9818                        let s: LitStr = inner.value()?.parse()?;
9819                        include = split_field_list(&s.value());
9820                        return Ok(());
9821                    }
9822                    Err(inner.error(
9823                        "unknown unique_when attribute (supported: \
9824                         `columns = \"...\"`, `condition = \"...\"`, \
9825                         `name = \"...\"`, `include = \"...\"`)",
9826                    ))
9827                })?;
9828                let columns = columns.ok_or_else(|| {
9829                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9830                })?;
9831                let condition = condition.ok_or_else(|| {
9832                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9833                })?;
9834                if columns.is_empty() {
9835                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9836                }
9837                out.indexes.push(IndexAttr {
9838                    span: meta.path.span(),
9839                    name,
9840                    columns,
9841                    unique: true,
9842                    method: "btree".to_owned(),
9843                    where_clause: Some(condition),
9844                    include,
9845                });
9846                return Ok(());
9847            }
9848            if meta.path.is_ident("index_when") {
9849                // Non-unique partial index. Sibling of `unique_when`
9850                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9851                //
9852                //   #[rustango(index_when(
9853                //       columns   = "status, created_at",
9854                //       condition = "deleted_at IS NULL",
9855                //       name      = "active_status_created_idx"
9856                //   ))]
9857                //
9858                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9859                // on PG / SQLite (both ship partial indexes natively).
9860                // MySQL has no native partial-index support — the writer
9861                // emits a plain CREATE INDEX and the condition is lost;
9862                // operators wanting that selectivity on MySQL should
9863                // declare a covering index plus an application-level
9864                // filter.
9865                let mut columns: Option<Vec<String>> = None;
9866                let mut condition: Option<String> = None;
9867                let mut name: Option<String> = None;
9868                let mut method: String = "btree".to_owned();
9869                let mut include: Vec<String> = Vec::new();
9870                meta.parse_nested_meta(|inner| {
9871                    if inner.path.is_ident("columns") {
9872                        let s: LitStr = inner.value()?.parse()?;
9873                        columns = Some(split_field_list(&s.value()));
9874                        return Ok(());
9875                    }
9876                    if inner.path.is_ident("condition") {
9877                        let s: LitStr = inner.value()?.parse()?;
9878                        condition = Some(s.value());
9879                        return Ok(());
9880                    }
9881                    if inner.path.is_ident("name") {
9882                        let s: LitStr = inner.value()?.parse()?;
9883                        name = Some(s.value());
9884                        return Ok(());
9885                    }
9886                    if inner.path.is_ident("method") {
9887                        let s: LitStr = inner.value()?.parse()?;
9888                        method = s.value();
9889                        return Ok(());
9890                    }
9891                    if inner.path.is_ident("include") {
9892                        // PG 11+ covering-index columns; non-key
9893                        // columns travel with the index leaf. Dropped
9894                        // on MySQL/SQLite.
9895                        let s: LitStr = inner.value()?.parse()?;
9896                        include = split_field_list(&s.value());
9897                        return Ok(());
9898                    }
9899                    Err(inner.error(
9900                        "unknown index_when attribute (supported: \
9901                         `columns = \"...\"`, `condition = \"...\"`, \
9902                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9903                         `include = \"...\"`)",
9904                    ))
9905                })?;
9906                let columns = columns
9907                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9908                let condition = condition.ok_or_else(|| {
9909                    meta.error("`index_when(...)` requires `condition = \"...\"`")
9910                })?;
9911                if columns.is_empty() {
9912                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
9913                }
9914                out.indexes.push(IndexAttr {
9915                    span: meta.path.span(),
9916                    name,
9917                    columns,
9918                    unique: false,
9919                    method,
9920                    where_clause: Some(condition),
9921                    include,
9922                });
9923                return Ok(());
9924            }
9925            if meta.path.is_ident("index") {
9926                // Container-level composite index. Two syntaxes:
9927                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
9928                //   #[rustango(index("col1, col2"))]    — call form (same result)
9929                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9930                // The call form takes the column list as a leading string
9931                // literal, then optional `unique` / `name = "..."` /
9932                // `method = "..."` flags (a leading literal can't compose
9933                // under `parse_nested_meta`, so the paren body is parsed by
9934                // hand). `unique_together` / `index_together` remain
9935                // aliases for the same feature.
9936                let cols_lit: LitStr;
9937                let mut unique = false;
9938                let mut name: Option<String> = None;
9939                let mut method = "btree".to_owned();
9940                if meta.input.peek(syn::token::Paren) {
9941                    let content;
9942                    syn::parenthesized!(content in meta.input);
9943                    cols_lit = content.parse()?;
9944                    while content.peek(syn::Token![,]) {
9945                        content.parse::<syn::Token![,]>()?;
9946                        if content.is_empty() {
9947                            break;
9948                        }
9949                        let flag: syn::Ident = content.parse()?;
9950                        if flag == "unique" {
9951                            unique = true;
9952                        } else if flag == "name" {
9953                            content.parse::<syn::Token![=]>()?;
9954                            let s: LitStr = content.parse()?;
9955                            name = Some(s.value());
9956                        } else if flag == "method" {
9957                            content.parse::<syn::Token![=]>()?;
9958                            let s: LitStr = content.parse()?;
9959                            let v = s.value();
9960                            match v.as_str() {
9961                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9962                                | "bloom" => method = v,
9963                                other => {
9964                                    return Err(syn::Error::new(
9965                                        s.span(),
9966                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9967                                    ));
9968                                }
9969                            }
9970                        } else {
9971                            return Err(syn::Error::new(
9972                                flag.span(),
9973                                "unknown index flag (supported: `unique`, `name`, `method`)",
9974                            ));
9975                        }
9976                    }
9977                } else {
9978                    cols_lit = meta.value()?.parse()?;
9979                }
9980                let columns = split_field_list(&cols_lit.value());
9981                out.indexes.push(IndexAttr {
9982                    span: meta.path.span(),
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(unknown_key_error(&meta, "container", CONTAINER_ATTR_KEYS))
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    written: &str,
10346) -> syn::Result<(Vec<String>, Option<String>)> {
10347    // Disambiguate by whether the next token is `=` (key-value) or
10348    // `(` (parenthesized).
10349    if meta.input.peek(syn::Token![=]) {
10350        let cols_lit: LitStr = meta.value()?.parse()?;
10351        let columns = split_field_list(&cols_lit.value());
10352        check_together_columns(meta, attr, written, &columns)?;
10353        return Ok((columns, None));
10354    }
10355    let mut columns: Option<Vec<String>> = None;
10356    let mut name: Option<String> = None;
10357    meta.parse_nested_meta(|inner| {
10358        if inner.path.is_ident("columns") {
10359            let s: LitStr = inner.value()?.parse()?;
10360            columns = Some(split_field_list(&s.value()));
10361            return Ok(());
10362        }
10363        if inner.path.is_ident("name") {
10364            let s: LitStr = inner.value()?.parse()?;
10365            name = Some(s.value());
10366            return Ok(());
10367        }
10368        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10369    })?;
10370    let columns = columns.ok_or_else(|| {
10371        meta.error(format!(
10372            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10373        ))
10374    })?;
10375    check_together_columns(meta, attr, written, &columns)?;
10376    Ok((columns, name))
10377}
10378
10379fn check_together_columns(
10380    meta: &syn::meta::ParseNestedMeta<'_>,
10381    attr: &str,
10382    written: &str,
10383    columns: &[String],
10384) -> syn::Result<()> {
10385    if columns.len() < 2 {
10386        let flag = if attr == "unique_together" {
10387            "unique"
10388        } else {
10389            "index"
10390        };
10391        let single = format!("#[{written}({flag})] on the field");
10392        return Err(meta.error(format!(
10393            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10394        )));
10395    }
10396    Ok(())
10397}
10398
10399/// Parse the fieldsets DSL: pipe-separated sections, optional
10400/// `"Title:"` prefix on each, comma-separated field names after.
10401/// Examples:
10402/// * `"name, office"` → one untitled section with two fields
10403/// * `"Identity: name, office | Metadata: created_at"` → two titled
10404///   sections
10405///
10406/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10407fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10408    raw.split('|')
10409        .map(str::trim)
10410        .filter(|s| !s.is_empty())
10411        .map(|section| {
10412            // Split off an optional `Title:` prefix (first colon).
10413            let (title, rest) = match section.split_once(':') {
10414                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10415                _ => (String::new(), section),
10416            };
10417            let fields = split_field_list(rest);
10418            (title, fields)
10419        })
10420        .collect()
10421}
10422
10423/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10424/// comma-separated entry maps a target field to one or more source
10425/// fields joined with `+`. Whitespace around tokens is trimmed.
10426/// Entries missing `:` or with empty target/source lists are dropped.
10427fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10428    raw.split(',')
10429        .map(str::trim)
10430        .filter(|s| !s.is_empty())
10431        .filter_map(|entry| {
10432            let (target, sources_raw) = entry.split_once(':')?;
10433            let target = target.trim().to_owned();
10434            if target.is_empty() {
10435                return None;
10436            }
10437            let sources: Vec<String> = sources_raw
10438                .split('+')
10439                .map(|s| s.trim().to_owned())
10440                .filter(|s| !s.is_empty())
10441                .collect();
10442            if sources.is_empty() {
10443                return None;
10444            }
10445            Some((target, sources))
10446        })
10447        .collect()
10448}
10449
10450/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10451/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10452/// and empty halves drop silently — the macro layer only enforces shape,
10453/// not field-name vs. widget-name validity (those checks happen at
10454/// `AdminConfig` consumption time). Issue #359.
10455fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10456    raw.split(',')
10457        .map(str::trim)
10458        .filter(|s| !s.is_empty())
10459        .filter_map(|entry| {
10460            let (field, widget) = entry.split_once(':')?;
10461            let field = field.trim().to_owned();
10462            let widget = widget.trim().to_owned();
10463            if field.is_empty() || widget.is_empty() {
10464                return None;
10465            }
10466            Some((field, widget))
10467        })
10468        .collect()
10469}
10470
10471/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10472/// Returns `(field_name, desc)` pairs in the same order as the input.
10473fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10474    raw.split(',')
10475        .map(str::trim)
10476        .filter(|s| !s.is_empty())
10477        .map(|spec| {
10478            spec.strip_prefix('-')
10479                .map_or((spec.to_owned(), false), |rest| {
10480                    (rest.trim().to_owned(), true)
10481                })
10482        })
10483        .collect()
10484}
10485
10486struct FieldAttrs {
10487    column: Option<String>,
10488    primary_key: bool,
10489    fk: Option<String>,
10490    o2o: Option<String>,
10491    on: Option<String>,
10492    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10493    /// "set_default" | "no_action")]` — what happens to this row when
10494    /// the row it points at is deleted.
10495    /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10496    /// set; the macro errors at compile time if applied to a
10497    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10498    /// DDL writer renders `ON DELETE <action>` after the constraint
10499    /// clause when this is `Some`; `None` falls back to the database
10500    /// default (NO ACTION on every backend rustango supports).
10501    on_delete: Option<String>,
10502    /// `#[rustango(related_name = "...")]` — per-FK
10503    /// reverse-accessor override. When set, the derive emits
10504    /// `Parent::<related_name>[_pool]` instead of the container-level
10505    /// `default_related_name` or the `<child_snake>_set[_pool]`
10506    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10507    /// silently ignored on non-FK fields. Follow-up to #816.
10508    /// `"+"` emits none, for a parent defined in another crate.
10509    related_name: Option<String>,
10510    max_length: Option<u32>,
10511    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10512    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10513    /// unconstrained `vector` column.
10514    vector_dims: Option<u32>,
10515    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10516    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10517    /// unconstrained `geometry(Point)` column (SRID 0).
10518    geometry_srid: Option<u32>,
10519    min: Option<i64>,
10520    max: Option<i64>,
10521    default: Option<String>,
10522    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10523    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10524    /// "gen_random_uuid()"`. The Rust field type must be
10525    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10526    /// INSERTs so the DB DEFAULT fires.
10527    auto_uuid: bool,
10528    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10529    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10530    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10531    /// the field is `Auto::Unset`, then bound as a normal parameter
10532    /// rather than relying on a per-dialect DB function. Issue #823
10533    /// (Eloquent `HasUuids`).
10534    ///
10535    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10536    /// Composes with every backend (PG / MySQL / SQLite) — no
10537    /// `gen_random_uuid()` requirement on the database.
10538    default_uuid_v7: bool,
10539    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10540    /// Server-set on insert, immutable from app code afterwards.
10541    /// Implies `auto + default = "now()"`. Field type must be
10542    /// `DateTime<Utc>`.
10543    auto_now_add: bool,
10544    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10545    /// every insert AND every update. Implies `auto + default =
10546    /// "now()"`; the macro additionally rewrites `update_on` /
10547    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10548    /// field value.
10549    auto_now: bool,
10550    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10551    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10552    /// `soft_delete_on(executor)` and `restore_on(executor)`
10553    /// methods on the model.
10554    soft_delete: bool,
10555    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10556    /// the column in the generated DDL.
10557    unique: bool,
10558    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10559    /// generates a `CREATE INDEX` for this column. `unique` here means
10560    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10561    index: bool,
10562    index_unique: bool,
10563    index_name: Option<String>,
10564    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10565    /// `"btree"`. Issue #34.
10566    index_method: String,
10567    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10568    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10569    /// the macro skips this column from every INSERT and UPDATE
10570    /// path, so the database always recomputes the value from
10571    /// `EXPR`. Backlog item #35.
10572    generated_as: Option<String>,
10573    /// `#[rustango(help_text = "…")]` — help text
10574    /// rendered below the admin form's input. Threaded into
10575    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10576    /// layers can read it.
10577    help_text: Option<String>,
10578    /// `#[rustango(choices = "value:Label, value:Label")]` — the
10579    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10580    /// as a `&'static [(&'static str, &'static str)]` slice. When
10581    /// present, the admin form renders a `<select>` instead of `<input>`
10582    /// and the validator rejects values not in the list. Only meaningful
10583    /// for `FieldType::String`; the macro errors at compile time if
10584    /// applied to a non-string field.
10585    choices: Option<Vec<(String, String)>>,
10586    /// `#[rustango(db_comment = "…")]` — DB-side column
10587    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10588    /// the comment in CREATE TABLE; Postgres emits a separate
10589    /// `COMMENT ON COLUMN` statement after the table is created;
10590    /// SQLite silently drops the value (no native column comments).
10591    db_comment: Option<String>,
10592    /// `#[rustango(verbose_name = "…")]` — human-readable
10593    /// label for the field. Threaded into `FieldSchema::verbose_name`
10594    /// so admin column headers, form labels, and other display
10595    /// surfaces can prefer the friendly caption over the Rust
10596    /// identifier. `None` means renderers fall back to the field name.
10597    verbose_name: Option<String>,
10598    /// `#[rustango(editable = false)]` — opt out of
10599    /// auto-generated form rendering. Defaults to `true` so existing
10600    /// fields keep their current admin / form behavior; setting
10601    /// `false` removes the field from the admin change-form entirely
10602    /// (the value is still visible on detail / list views, just not
10603    /// editable).
10604    editable: bool,
10605    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10606    /// "form may submit empty even when DB is NOT NULL". Threaded into
10607    /// `FieldSchema::blank`. Defaults to `false`.
10608    blank: bool,
10609    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10610    /// case-insensitive text column. Threaded into
10611    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10612    /// fields; the macro errors at derive time if applied elsewhere.
10613    case_insensitive: bool,
10614    /// `#[rustango(validators = "email,url")]` — the
10615    /// model-level validator chain. Comma-separated names that
10616    /// dispatch to the `validators::*` family in `validate_value`.
10617    /// Empty by default; fires on every typed INSERT/UPDATE. Each is
10618    /// spanned on the attribute string, so an unknown name errors there.
10619    validators: Vec<syn::Ident>,
10620    /// `#[rustango(secret)]` / `secret = false`; `None` defers to the name rules.
10621    secret: Option<bool>,
10622}
10623
10624/// Keys `parse_field_attrs` accepts, listed in its unknown-key error.
10625const FIELD_ATTR_KEYS: &[&str] = &[
10626    "column",
10627    "primary_key",
10628    "fk",
10629    "o2o",
10630    "on",
10631    "on_delete",
10632    "related_name",
10633    "max_length",
10634    "vector",
10635    "geometry",
10636    "min",
10637    "max",
10638    "default",
10639    "generated_as",
10640    "help_text",
10641    "choices",
10642    "db_comment",
10643    "verbose_name",
10644    "editable",
10645    "secret",
10646    "blank",
10647    "citext",
10648    "validators",
10649    "auto_uuid",
10650    "default_uuid_v7",
10651    "auto_now_add",
10652    "auto_now",
10653    "soft_delete",
10654    "unique",
10655    "index",
10656];
10657
10658/// Keys `parse_container_attrs` accepts, listed in its unknown-key error.
10659const CONTAINER_ATTR_KEYS: &[&str] = &[
10660    "table",
10661    "display",
10662    "app",
10663    "scope",
10664    "admin",
10665    "manager",
10666    "manager_fn",
10667    "default_order",
10668    "global_scope",
10669    "through",
10670    "reverse_has",
10671    "generic_has",
10672    "audit",
10673    "permissions",
10674    "view",
10675    "managed",
10676    "verbose_name",
10677    "verbose_name_plural",
10678    "db_table_comment",
10679    "proxy",
10680    "order_with_respect_to",
10681    "required_db_features",
10682    "required_db_vendor",
10683    "base_manager_name",
10684    "default_related_name",
10685    "extra_permissions",
10686    "default_permissions",
10687    "get_latest_by",
10688    "unique_together",
10689    "index_together",
10690    "unique_when",
10691    "index_when",
10692    "index",
10693    "check",
10694    "exclude",
10695    "generic_fk",
10696    "fk_composite",
10697    "m2m",
10698    "generic_m2m",
10699];
10700
10701/// A flag attribute: `#[rustango(key)]` or `#[rustango(key = true)]`.
10702/// `= false` is refused: off is the default, so the attribute is a no-op.
10703fn parse_flag(meta: &syn::meta::ParseNestedMeta<'_>, key: &str) -> syn::Result<()> {
10704    if meta.input.peek(syn::Token![=]) {
10705        let lit: syn::LitBool = meta.value()?.parse()?;
10706        if !lit.value {
10707            return Err(syn::Error::new(
10708                lit.span(),
10709                format!("`{key} = false` is the default; remove the attribute"),
10710            ));
10711        }
10712    }
10713    Ok(())
10714}
10715
10716/// "unknown rustango <what> attribute `key`", with the nearest key and the full list.
10717fn unknown_key_error(
10718    meta: &syn::meta::ParseNestedMeta<'_>,
10719    what: &str,
10720    keys: &[&str],
10721) -> syn::Error {
10722    let path = &meta.path;
10723    let got = path
10724        .get_ident()
10725        .map_or_else(|| quote!(#path).to_string(), ToString::to_string);
10726    let mut msg = format!("unknown rustango {what} attribute `{got}`");
10727    if let Some(near) = keys
10728        .iter()
10729        .map(|k| (edit_distance(&got, k), *k))
10730        .filter(|(d, k)| *d <= 2.max(k.len() / 4))
10731        .min_by_key(|(d, _)| *d)
10732        .map(|(_, k)| k)
10733    {
10734        msg.push_str(&format!("; did you mean `{near}`?"));
10735    }
10736    msg.push_str(&format!(" Supported: {}", keys.join(", ")));
10737    meta.error(msg)
10738}
10739
10740/// Levenshtein distance, for "did you mean" hints.
10741fn edit_distance(a: &str, b: &str) -> usize {
10742    let b: Vec<char> = b.chars().collect();
10743    let mut prev: Vec<usize> = (0..=b.len()).collect();
10744    for (i, ca) in a.chars().enumerate() {
10745        let mut cur = vec![i + 1];
10746        for (j, cb) in b.iter().enumerate() {
10747            let sub = prev[j] + usize::from(ca != *cb);
10748            cur.push(sub.min(prev[j + 1] + 1).min(cur[j] + 1));
10749        }
10750        prev = cur;
10751    }
10752    prev[b.len()]
10753}
10754
10755fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10756    let mut out = FieldAttrs {
10757        column: None,
10758        primary_key: false,
10759        fk: None,
10760        o2o: None,
10761        on: None,
10762        on_delete: None,
10763        related_name: None,
10764        max_length: None,
10765        vector_dims: None,
10766        geometry_srid: None,
10767        min: None,
10768        max: None,
10769        default: None,
10770        auto_uuid: false,
10771        default_uuid_v7: false,
10772        auto_now_add: false,
10773        auto_now: false,
10774        soft_delete: false,
10775        unique: false,
10776        index: false,
10777        index_unique: false,
10778        index_name: None,
10779        index_method: "btree".to_owned(),
10780        generated_as: None,
10781        help_text: None,
10782        choices: None,
10783        db_comment: None,
10784        verbose_name: None,
10785        editable: true,
10786        blank: false,
10787        case_insensitive: false,
10788        validators: Vec::new(),
10789        secret: None,
10790    };
10791    for attr in model_attrs(&field.attrs)? {
10792        attr.parse_nested_meta(|meta| {
10793            if meta.path.is_ident("column") {
10794                let s: LitStr = meta.value()?.parse()?;
10795                let name = s.value();
10796                validate_sql_identifier(&name, "column", s.span())?;
10797                out.column = Some(name);
10798                return Ok(());
10799            }
10800            if meta.path.is_ident("primary_key") {
10801                parse_flag(&meta, "primary_key")?;
10802                out.primary_key = true;
10803                return Ok(());
10804            }
10805            if meta.path.is_ident("fk") {
10806                let s: LitStr = meta.value()?.parse()?;
10807                out.fk = Some(s.value());
10808                return Ok(());
10809            }
10810            if meta.path.is_ident("o2o") {
10811                let s: LitStr = meta.value()?.parse()?;
10812                out.o2o = Some(s.value());
10813                return Ok(());
10814            }
10815            if meta.path.is_ident("on") {
10816                let s: LitStr = meta.value()?.parse()?;
10817                out.on = Some(s.value());
10818                return Ok(());
10819            }
10820            if meta.path.is_ident("on_delete") {
10821                let s: LitStr = meta.value()?.parse()?;
10822                let raw = s.value();
10823                let normalized = raw.trim().to_ascii_lowercase();
10824                // Validate at parse time so the user gets a clear span
10825                // rather than a downstream compile error in the emit.
10826                match normalized.as_str() {
10827                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10828                    _ => {
10829                        return Err(syn::Error::new(
10830                            s.span(),
10831                            format!(
10832                                "unknown on_delete action `{raw}`; expected one of \
10833                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10834                            ),
10835                        ));
10836                    }
10837                }
10838                out.on_delete = Some(normalized);
10839                return Ok(());
10840            }
10841            if meta.path.is_ident("related_name") {
10842                let s: LitStr = meta.value()?.parse()?;
10843                let raw = s.value();
10844                if raw.trim().is_empty() {
10845                    return Err(syn::Error::new(
10846                        s.span(),
10847                        "`related_name` must be a non-empty identifier",
10848                    ));
10849                }
10850                // Validate as a Rust-ident shape — the value becomes
10851                // a method name on the parent type. Same rule as
10852                // `default_related_name` so the two surfaces match.
10853                // `"+"` emits no accessor at all.
10854                let snake = raw
10855                    .chars()
10856                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10857                    && !raw.starts_with(char::is_numeric);
10858                if !snake && raw != NO_REVERSE_ACCESSOR {
10859                    return Err(syn::Error::new(
10860                        s.span(),
10861                        "`related_name` must be snake_case ASCII (lowercase letters, \
10862                         digits, underscores; no leading digit), or \"+\" for none",
10863                    ));
10864                }
10865                out.related_name = Some(raw);
10866                return Ok(());
10867            }
10868            if meta.path.is_ident("max_length") {
10869                let lit: syn::LitInt = meta.value()?.parse()?;
10870                out.max_length = Some(lit.base10_parse::<u32>()?);
10871                return Ok(());
10872            }
10873            // `#[rustango(vector(dims = N))]` — pgvector column
10874            // dimension (#824). Nested-meta form so it reads like the
10875            // other typed-column attrs.
10876            if meta.path.is_ident("vector") {
10877                meta.parse_nested_meta(|inner| {
10878                    if inner.path.is_ident("dims") {
10879                        let lit: syn::LitInt = inner.value()?.parse()?;
10880                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10881                        return Ok(());
10882                    }
10883                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10884                })?;
10885                return Ok(());
10886            }
10887            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10888            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10889            if meta.path.is_ident("geometry") {
10890                meta.parse_nested_meta(|inner| {
10891                    if inner.path.is_ident("srid") {
10892                        let lit: syn::LitInt = inner.value()?.parse()?;
10893                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10894                        return Ok(());
10895                    }
10896                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10897                })?;
10898                return Ok(());
10899            }
10900            if meta.path.is_ident("min") {
10901                out.min = Some(parse_signed_i64(&meta)?);
10902                return Ok(());
10903            }
10904            if meta.path.is_ident("max") {
10905                out.max = Some(parse_signed_i64(&meta)?);
10906                return Ok(());
10907            }
10908            if meta.path.is_ident("default") {
10909                let s: LitStr = meta.value()?.parse()?;
10910                out.default = Some(s.value());
10911                return Ok(());
10912            }
10913            if meta.path.is_ident("generated_as") {
10914                let s: LitStr = meta.value()?.parse()?;
10915                out.generated_as = Some(s.value());
10916                return Ok(());
10917            }
10918            if meta.path.is_ident("help_text") {
10919                let s: LitStr = meta.value()?.parse()?;
10920                out.help_text = Some(s.value());
10921                return Ok(());
10922            }
10923            if meta.path.is_ident("choices") {
10924                let s: LitStr = meta.value()?.parse()?;
10925                let raw = s.value();
10926                let mut pairs: Vec<(String, String)> = Vec::new();
10927                for chunk in raw.split(',') {
10928                    let chunk = chunk.trim();
10929                    if chunk.is_empty() {
10930                        continue;
10931                    }
10932                    let (value, label) = match chunk.split_once(':') {
10933                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10934                        None => (chunk.to_owned(), chunk.to_owned()),
10935                    };
10936                    if value.is_empty() {
10937                        return Err(syn::Error::new(
10938                            s.span(),
10939                            "`choices` entry has empty value before `:`",
10940                        ));
10941                    }
10942                    pairs.push((value, label));
10943                }
10944                if pairs.is_empty() {
10945                    return Err(syn::Error::new(
10946                        s.span(),
10947                        "`choices = \"…\"` must contain at least one value",
10948                    ));
10949                }
10950                out.choices = Some(pairs);
10951                return Ok(());
10952            }
10953            if meta.path.is_ident("db_comment") {
10954                let s: LitStr = meta.value()?.parse()?;
10955                out.db_comment = Some(s.value());
10956                return Ok(());
10957            }
10958            if meta.path.is_ident("verbose_name") {
10959                let s: LitStr = meta.value()?.parse()?;
10960                out.verbose_name = Some(s.value());
10961                return Ok(());
10962            }
10963            if meta.path.is_ident("secret") {
10964                // `#[rustango(secret)]` or `#[rustango(secret = false)]`.
10965                out.secret = Some(if let Ok(v) = meta.value() {
10966                    v.parse::<syn::LitBool>()?.value
10967                } else {
10968                    true
10969                });
10970                return Ok(());
10971            }
10972            if meta.path.is_ident("editable") {
10973                // Two forms accepted:
10974                //   #[rustango(editable = false)] / true — explicit
10975                //   #[rustango(editable)] — flag form (= true, the
10976                //   default, so harmless; included for symmetry)
10977                if let Ok(v) = meta.value() {
10978                    let lit: syn::LitBool = v.parse()?;
10979                    out.editable = lit.value;
10980                } else {
10981                    out.editable = true;
10982                }
10983                return Ok(());
10984            }
10985            if meta.path.is_ident("blank") {
10986                // Two forms accepted:
10987                //   #[rustango(blank)] — flag form, true
10988                //   #[rustango(blank = false)] / true — explicit
10989                if let Ok(v) = meta.value() {
10990                    let lit: syn::LitBool = v.parse()?;
10991                    out.blank = lit.value;
10992                } else {
10993                    out.blank = true;
10994                }
10995                return Ok(());
10996            }
10997            if meta.path.is_ident("citext") {
10998                // Case-insensitive text column (#344). Two forms:
10999                //   #[rustango(citext)]          — flag form, true
11000                //   #[rustango(citext = true)]   — explicit
11001                //   #[rustango(citext = false)]  — explicit opt-out
11002                // String-only validation lives in the field-type
11003                // emitter (the FieldType discriminant is computed in
11004                // `detect_type`); the macro records the flag and the
11005                // DDL writer emits dialect-specific COLLATE / CITEXT.
11006                if let Ok(v) = meta.value() {
11007                    let lit: syn::LitBool = v.parse()?;
11008                    out.case_insensitive = lit.value;
11009                } else {
11010                    out.case_insensitive = true;
11011                }
11012                return Ok(());
11013            }
11014            if meta.path.is_ident("validators") {
11015                let s: LitStr = meta.value()?.parse()?;
11016                let raw = s.value();
11017                out.validators = raw
11018                    .split(',')
11019                    .map(str::trim)
11020                    .filter(|s| !s.is_empty())
11021                    .map(|name| {
11022                        let mut ident = syn::parse_str::<syn::Ident>(name).map_err(|_| {
11023                            syn::Error::new(s.span(), format!("unknown validator `{name}`"))
11024                        })?;
11025                        ident.set_span(s.span());
11026                        Ok(ident)
11027                    })
11028                    .collect::<syn::Result<_>>()?;
11029                if out.validators.is_empty() {
11030                    return Err(syn::Error::new(
11031                        s.span(),
11032                        "`validators = \"…\"` must list at least one name",
11033                    ));
11034                }
11035                return Ok(());
11036            }
11037            if meta.path.is_ident("auto_uuid") {
11038                parse_flag(&meta, "auto_uuid")?;
11039                out.auto_uuid = true;
11040                // Implied: PK + auto + DEFAULT gen_random_uuid().
11041                // Each is also explicitly settable; the explicit
11042                // value wins if conflicting.
11043                out.primary_key = true;
11044                if out.default.is_none() {
11045                    out.default = Some("gen_random_uuid()".into());
11046                }
11047                return Ok(());
11048            }
11049            if meta.path.is_ident("default_uuid_v7") {
11050                parse_flag(&meta, "default_uuid_v7")?;
11051                // Backend-neutral counterpart of `auto_uuid` — issue #823.
11052                // No SQL DEFAULT (the macro fills the value Rust-side
11053                // before binding); just mark the field as PK + Auto
11054                // so the insert path is the `Auto::Unset → generate`
11055                // branch.
11056                out.default_uuid_v7 = true;
11057                out.primary_key = true;
11058                return Ok(());
11059            }
11060            if meta.path.is_ident("auto_now_add") {
11061                parse_flag(&meta, "auto_now_add")?;
11062                out.auto_now_add = true;
11063                if out.default.is_none() {
11064                    out.default = Some("now()".into());
11065                }
11066                return Ok(());
11067            }
11068            if meta.path.is_ident("auto_now") {
11069                parse_flag(&meta, "auto_now")?;
11070                out.auto_now = true;
11071                if out.default.is_none() {
11072                    out.default = Some("now()".into());
11073                }
11074                return Ok(());
11075            }
11076            if meta.path.is_ident("soft_delete") {
11077                parse_flag(&meta, "soft_delete")?;
11078                out.soft_delete = true;
11079                return Ok(());
11080            }
11081            if meta.path.is_ident("unique") {
11082                parse_flag(&meta, "unique")?;
11083                out.unique = true;
11084                return Ok(());
11085            }
11086            if meta.path.is_ident("index") {
11087                parse_flag(&meta, "index")?;
11088                out.index = true;
11089                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11090                if meta.input.peek(syn::token::Paren) {
11091                    meta.parse_nested_meta(|inner| {
11092                        if inner.path.is_ident("unique") {
11093                            out.index_unique = true;
11094                            return Ok(());
11095                        }
11096                        if inner.path.is_ident("name") {
11097                            let s: LitStr = inner.value()?.parse()?;
11098                            out.index_name = Some(s.value());
11099                            return Ok(());
11100                        }
11101                        if inner.path.is_ident("method") {
11102                            let s: LitStr = inner.value()?.parse()?;
11103                            let v = s.value();
11104                            match v.as_str() {
11105                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11106                                    out.index_method = v;
11107                                }
11108                                other => {
11109                                    return Err(inner.error(format!(
11110                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11111                                    )));
11112                                }
11113                            }
11114                            return Ok(());
11115                        }
11116                        Err(inner.error(
11117                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11118                        ))
11119                    })?;
11120                }
11121                return Ok(());
11122            }
11123            Err(unknown_key_error(&meta, "field", FIELD_ATTR_KEYS))
11124        })?;
11125    }
11126    Ok(out)
11127}
11128
11129/// Parse a signed integer literal, accepting optional leading `-`.
11130fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11131    let expr: syn::Expr = meta.value()?.parse()?;
11132    match expr {
11133        syn::Expr::Lit(syn::ExprLit {
11134            lit: syn::Lit::Int(lit),
11135            ..
11136        }) => lit.base10_parse::<i64>(),
11137        syn::Expr::Unary(syn::ExprUnary {
11138            op: syn::UnOp::Neg(_),
11139            expr,
11140            ..
11141        }) => {
11142            if let syn::Expr::Lit(syn::ExprLit {
11143                lit: syn::Lit::Int(lit),
11144                ..
11145            }) = *expr
11146            {
11147                let v: i64 = lit.base10_parse()?;
11148                Ok(-v)
11149            } else {
11150                Err(syn::Error::new_spanned(expr, "expected integer literal"))
11151            }
11152        }
11153        other => Err(syn::Error::new_spanned(
11154            other,
11155            "expected integer literal (signed)",
11156        )),
11157    }
11158}
11159
11160struct FieldInfo<'a> {
11161    ident: &'a syn::Ident,
11162    column: String,
11163    primary_key: bool,
11164    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11165    /// skip this column when `Auto::Unset` and emit it under
11166    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11167    auto: bool,
11168    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11169    /// the `Column::Value` associated type for typed-column tokens.
11170    value_ty: &'a Type,
11171    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11172    field_type_tokens: TokenStream2,
11173    schema: TokenStream2,
11174    from_row_init: TokenStream2,
11175    /// Variant of [`Self::from_row_init`] that reads the column via
11176    /// `format!("{prefix}__{col}")` so a model can be decoded out of
11177    /// the aliased columns of a JOINed row. Drives slice 9.0d's
11178    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11179    /// helper that `select_related` calls when stitching loaded FKs.
11180    from_aliased_row_init: TokenStream2,
11181    /// MySQL twins of the two inits above. They differ only for a UUID
11182    /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11183    from_row_init_my: TokenStream2,
11184    from_aliased_row_init_my: TokenStream2,
11185    /// A bare or optional `Uuid` field.
11186    uuid_column: bool,
11187    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11188    /// relation helper emit (`Author::<child>_set`) needs to know `T`
11189    /// to point the generated method at the right child model.
11190    fk_inner: Option<Type>,
11191    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11192    /// `kind` (since ForeignKey detection sets `kind` to K's
11193    /// underlying type) but stored separately for clarity at the
11194    /// `FkRelation` construction site, which only sees the FK's
11195    /// surface fields.
11196    fk_pk_kind: DetectedKind,
11197    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11198    /// the bare `ForeignKey<T, K>`. Routes the load_related and
11199    /// fk_pk_access emitters to wrap assignments / accessors in
11200    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11201    /// FK column compiles end-to-end. The DDL writer reads this off
11202    /// the field schema (`nullable` flag); the macro just needs to
11203    /// keep the Rust-side codegen consistent.
11204    nullable: bool,
11205    /// `true` when this column was marked `#[rustango(auto_now)]` —
11206    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11207    /// column instead of the user-supplied value, so `updated_at`
11208    /// always reflects the latest write without the caller having
11209    /// to remember to set it.
11210    auto_now: bool,
11211    /// `true` when this column was marked `#[rustango(auto_now_add)]`
11212    /// — the column is server-set on INSERT (DB DEFAULT) and
11213    /// **immutable** afterwards. `update_on` / `save_on` skip the
11214    /// column entirely so a stale `created_at` value in memory never
11215    /// rewrites the persisted timestamp.
11216    auto_now_add: bool,
11217    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11218    /// Triggers emission of `soft_delete_on(executor)` and
11219    /// `restore_on(executor)` on the model's inherent impl. There is
11220    /// at most one such column per model — emission asserts this.
11221    soft_delete: bool,
11222    /// `Some` when this column was marked
11223    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11224    /// every INSERT and UPDATE path; the database recomputes the
11225    /// value from `EXPR`. Backlog item #35.
11226    generated_as: Option<String>,
11227    /// `true` when this column was marked
11228    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11229    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11230    /// with `Uuid::now_v7()` before binding, so the PK is generated
11231    /// Rust-side and the column is always present in the INSERT
11232    /// statement (no DB DEFAULT requirement). Issue #823.
11233    default_uuid_v7: bool,
11234    /// `Some` when this FK field carried `#[rustango(related_name =
11235    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11236    /// consumed by [`reverse_helper_tokens`] to override the default
11237    /// `<child_snake>_set` accessor name. Follow-up to #816.
11238    related_name: Option<String>,
11239}
11240
11241/// Reject table names that won't survive SQL identifier
11242/// derivation downstream. Postgres' regular-identifier rule
11243/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11244/// through the framework's unquoted FK / index / constraint name
11245/// emission without surprises. We also disallow leading-digit and
11246/// the empty string for clarity.
11247///
11248/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11249/// here — those produce a runtime error from the SQL parser,
11250/// which is loud enough; statically enumerating reserved words
11251/// across the three supported dialects is more friction than help.
11252///
11253/// Backlog item #65.
11254fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11255    validate_sql_identifier(name, "table", span)
11256}
11257
11258/// Reject SQL identifiers that compile but break downstream SQL
11259/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11260/// `kind` is "table" / "column" — used for the error message so users
11261/// see which attribute caused the failure.
11262fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11263    if name.is_empty() {
11264        return Err(syn::Error::new(
11265            span,
11266            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11267        ));
11268    }
11269    let mut chars = name.chars();
11270    let first = chars.next().unwrap();
11271    if !(first.is_ascii_alphabetic() || first == '_') {
11272        return Err(syn::Error::new(
11273            span,
11274            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11275        ));
11276    }
11277    for c in chars {
11278        if !(c.is_ascii_alphanumeric() || c == '_') {
11279            return Err(syn::Error::new(
11280                span,
11281                format!(
11282                    "{kind} name `{name}` contains invalid character {c:?} — \
11283                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11284                     Hyphens in particular break FK / index name derivation \
11285                     downstream; use underscores instead (e.g. `{}`)",
11286                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11287                ),
11288            ));
11289        }
11290    }
11291    Ok(())
11292}
11293
11294fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11295    let root = rustango_root();
11296    let attrs = parse_field_attrs(field)?;
11297    let ident = field
11298        .ident
11299        .as_ref()
11300        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11301    let name = ident_name(ident);
11302    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11303    let primary_key = attrs.primary_key;
11304    let DetectedType {
11305        kind,
11306        nullable,
11307        auto: detected_auto,
11308        fk_inner,
11309    } = detect_type(&field.ty)?;
11310    check_bound_compatibility(field, &attrs, kind)?;
11311    let auto = detected_auto;
11312    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11313    // INSERT path: the user must wrap the field in `Auto<T>`, which
11314    // marks the column as DB-default-supplied. The mixin attrs then
11315    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11316    // for `auto_now`, force the value on UPDATE too.
11317    if attrs.auto_uuid {
11318        if kind != DetectedKind::Uuid {
11319            return Err(syn::Error::new_spanned(
11320                field,
11321                "`#[rustango(auto_uuid)]` requires the field type to be \
11322                 `Auto<uuid::Uuid>`",
11323            ));
11324        }
11325        if !detected_auto {
11326            return Err(syn::Error::new_spanned(
11327                field,
11328                "`#[rustango(auto_uuid)]` requires the field type to be \
11329                 wrapped in `Auto<...>` so the macro skips the column on \
11330                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11331            ));
11332        }
11333    }
11334    if attrs.default_uuid_v7 {
11335        if kind != DetectedKind::Uuid {
11336            return Err(syn::Error::new_spanned(
11337                field,
11338                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11339                 `Auto<uuid::Uuid>`",
11340            ));
11341        }
11342        if !detected_auto {
11343            return Err(syn::Error::new_spanned(
11344                field,
11345                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11346                 wrapped in `Auto<...>` so the macro can detect the \
11347                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11348            ));
11349        }
11350        if attrs.auto_uuid {
11351            return Err(syn::Error::new_spanned(
11352                field,
11353                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11354                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11355                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11356                 Pick one.",
11357            ));
11358        }
11359    }
11360    if attrs.auto_now_add || attrs.auto_now {
11361        if kind != DetectedKind::DateTime {
11362            return Err(syn::Error::new_spanned(
11363                field,
11364                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11365                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11366            ));
11367        }
11368        if !detected_auto {
11369            return Err(syn::Error::new_spanned(
11370                field,
11371                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11372                 the field type to be wrapped in `Auto<...>` so the macro skips \
11373                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11374            ));
11375        }
11376    }
11377    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11378        return Err(syn::Error::new_spanned(
11379            field,
11380            "`#[rustango(soft_delete)]` requires the field type to be \
11381             `Option<chrono::DateTime<chrono::Utc>>`",
11382        ));
11383    }
11384    let is_mixin_auto =
11385        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11386    if detected_auto && !primary_key && !is_mixin_auto {
11387        return Err(syn::Error::new_spanned(
11388            field,
11389            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11390             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11391             `auto_now`",
11392        ));
11393    }
11394    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11395        return Err(syn::Error::new_spanned(
11396            field,
11397            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11398             SERIAL / BIGSERIAL already supplies a default sequence.",
11399        ));
11400    }
11401    if fk_inner.is_some() && primary_key {
11402        return Err(syn::Error::new_spanned(
11403            field,
11404            "`ForeignKey<T>` is not allowed on a primary-key field — \
11405             a row's PK is its own identity, not a reference to a parent.",
11406        ));
11407    }
11408    if attrs.generated_as.is_some() {
11409        if primary_key {
11410            return Err(syn::Error::new_spanned(
11411                field,
11412                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11413                 primary-key field — a PK must be writable so the row \
11414                 has an identity at INSERT time.",
11415            ));
11416        }
11417        if attrs.default.is_some() {
11418            return Err(syn::Error::new_spanned(
11419                field,
11420                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11421                 `default = \"…\"` — Postgres rejects DEFAULT on \
11422                 generated columns. The expression IS the default.",
11423            ));
11424        }
11425        if detected_auto {
11426            return Err(syn::Error::new_spanned(
11427                field,
11428                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11429                 an `Auto<T>` field — generated columns are computed \
11430                 by the DB, not server-assigned via a sequence. Use a \
11431                 plain Rust type (e.g. `f64`).",
11432            ));
11433        }
11434        if fk_inner.is_some() {
11435            return Err(syn::Error::new_spanned(
11436                field,
11437                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11438                 ForeignKey field.",
11439            ));
11440        }
11441    }
11442    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11443    let column_lit = column.as_str();
11444    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11445    // dimension that the bare `Vector` Rust type can't carry, so emit
11446    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11447    // fallback of `Vector(0)`.
11448    let field_type_tokens = if kind == DetectedKind::Vector {
11449        let root = rustango_root();
11450        let dims = attrs.vector_dims.unwrap_or(0);
11451        quote!(#root::core::FieldType::Vector(#dims))
11452    } else if kind == DetectedKind::Geometry {
11453        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11454        // SRID that the bare `Point` Rust type can't carry, so emit
11455        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11456        let root = rustango_root();
11457        let srid = attrs.geometry_srid.unwrap_or(0);
11458        quote!(#root::core::FieldType::Geometry(#srid))
11459    } else {
11460        kind.variant_tokens()
11461    };
11462    let max_length = optional_u32(attrs.max_length);
11463    let min = optional_i64(attrs.min);
11464    let max = optional_i64(attrs.max);
11465    let default = optional_str(attrs.default.as_deref());
11466
11467    let unique = attrs.unique;
11468    let generated_as = optional_str(attrs.generated_as.as_deref());
11469    let help_text = optional_str(attrs.help_text.as_deref());
11470    let choices = optional_choices(attrs.choices.as_deref());
11471    let db_comment = optional_str(attrs.db_comment.as_deref());
11472    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11473    let editable = attrs.editable;
11474    let secret = match attrs.secret {
11475        Some(v) => quote!(::core::option::Option::Some(#v)),
11476        None => quote!(::core::option::Option::None),
11477    };
11478    let blank = attrs.blank;
11479    let case_insensitive = attrs.case_insensitive;
11480    // A path per name: one the runtime does not know fails to resolve.
11481    let validators = &attrs.validators;
11482    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11483    {
11484        return Err(syn::Error::new_spanned(
11485            field,
11486            "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11487             or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11488             has no meaning on a non-FK column.",
11489        ));
11490    }
11491    if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11492        return Err(syn::Error::new_spanned(
11493            field,
11494            "`on_delete = \"set_null\"` needs a nullable column: use \
11495             `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11496             alias for `Option` is not seen).",
11497        ));
11498    }
11499    let fk_on_delete = match attrs.on_delete.as_deref() {
11500        None => quote!(::core::option::Option::None),
11501        Some(action) => {
11502            let variant = match action {
11503                "cascade" => quote!(Cascade),
11504                "restrict" => quote!(Restrict),
11505                "set_null" => quote!(SetNull),
11506                "set_default" => quote!(SetDefault),
11507                "no_action" => quote!(NoAction),
11508                // parse_field_attrs already validated this — guard against future drift.
11509                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11510            };
11511            quote!(::core::option::Option::Some(
11512                #root::core::OnDeleteAction::#variant
11513            ))
11514        }
11515    };
11516    let auto_now_flag = attrs.auto_now;
11517    let cast = match cast_type(&field.ty) {
11518        Some(ty) => quote!(::core::option::Option::Some(<#ty>::FIELD_CAST)),
11519        None => quote!(::core::option::Option::None),
11520    };
11521    // A sealed cast is always a credential: `secret = false` cannot opt it out.
11522    let sealed_guard = match (attrs.secret, cast_type(&field.ty)) {
11523        (Some(false), Some(ty)) => quote! {
11524            const _: () = ::core::assert!(
11525                !::core::matches!(<#ty>::FIELD_CAST, #root::core::FieldCast::Sealed),
11526                "`#[rustango(secret = false)]` on a sealed cast: it is always a credential",
11527            );
11528        },
11529        _ => quote!(),
11530    };
11531    let schema = quote! {{
11532        #sealed_guard
11533        #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11534            name: #name,
11535            column: #column_lit,
11536            ty: #field_type_tokens,
11537            nullable: #nullable,
11538            primary_key: #primary_key,
11539            relation: #relation,
11540            max_length: #max_length,
11541            min: #min,
11542            max: #max,
11543            default: #default,
11544            auto: #auto,
11545            auto_now: #auto_now_flag,
11546            unique: #unique,
11547            generated_as: #generated_as,
11548            help_text: #help_text,
11549            choices: #choices,
11550            db_comment: #db_comment,
11551            verbose_name: #verbose_name,
11552            editable: #editable,
11553            blank: #blank,
11554            case_insensitive: #case_insensitive,
11555            fk_on_delete: #fk_on_delete,
11556            validators: &[ #( #root::core::__validator_names::#validators ),* ],
11557            cast: #cast,
11558            secret: #secret,
11559        })
11560    }};
11561
11562    // sqlx-sqlite has no `Decimal` decode: it goes through `FlatScalar`'s cell.
11563    let decimal_column = kind == DetectedKind::Decimal && !detected_auto && fk_inner.is_none();
11564    let (from_row_init, from_aliased_row_init) = if decimal_column {
11565        (
11566            quote! { #ident: #root::sql::try_get_flat(row, #column_lit)? },
11567            quote! {
11568                #ident: #root::sql::try_get_flat(
11569                    row,
11570                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11571                )?
11572            },
11573        )
11574    } else {
11575        (
11576            quote! { #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)? },
11577            quote! {
11578                #ident: #root::sql::sqlx::Row::try_get(
11579                    row,
11580                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11581                )?
11582            },
11583        )
11584    };
11585    // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11586    // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11587    let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11588    let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11589        (
11590            quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11591            quote! {
11592                #ident: #root::sql::try_get_flat_my(
11593                    row,
11594                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11595                )?
11596            },
11597        )
11598    } else {
11599        (from_row_init.clone(), from_aliased_row_init.clone())
11600    };
11601
11602    Ok(FieldInfo {
11603        ident,
11604        column,
11605        primary_key,
11606        auto,
11607        value_ty: &field.ty,
11608        field_type_tokens,
11609        schema,
11610        from_row_init,
11611        from_aliased_row_init,
11612        from_row_init_my,
11613        from_aliased_row_init_my,
11614        uuid_column,
11615        fk_inner: fk_inner.cloned(),
11616        fk_pk_kind: kind,
11617        nullable,
11618        auto_now: attrs.auto_now,
11619        auto_now_add: attrs.auto_now_add,
11620        soft_delete: attrs.soft_delete,
11621        generated_as: attrs.generated_as.clone(),
11622        default_uuid_v7: attrs.default_uuid_v7,
11623        related_name: attrs.related_name.clone(),
11624    })
11625}
11626
11627fn check_bound_compatibility(
11628    field: &syn::Field,
11629    attrs: &FieldAttrs,
11630    kind: DetectedKind,
11631) -> syn::Result<()> {
11632    if attrs.max_length.is_some() && kind != DetectedKind::String {
11633        return Err(syn::Error::new_spanned(
11634            field,
11635            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11636        ));
11637    }
11638    // These were silently dropped on the wrong type (#1937).
11639    if attrs.case_insensitive && kind != DetectedKind::String {
11640        return Err(syn::Error::new_spanned(
11641            field,
11642            "`citext` is only valid on `String` fields (or `Option<String>`)",
11643        ));
11644    }
11645    if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11646        return Err(syn::Error::new_spanned(
11647            field,
11648            "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11649        ));
11650    }
11651    if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11652        return Err(syn::Error::new_spanned(
11653            field,
11654            "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11655        ));
11656    }
11657    if attrs.choices.is_some() && kind != DetectedKind::String {
11658        return Err(syn::Error::new_spanned(
11659            field,
11660            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11661             integer-valued enumerations should be modeled with a Rust enum and \
11662             custom (de)serializer for now",
11663        ));
11664    }
11665    // They run only on string values, so elsewhere they would never fire.
11666    if !attrs.validators.is_empty() && kind != DetectedKind::String {
11667        return Err(syn::Error::new_spanned(
11668            field,
11669            "`validators` is only valid on `String` fields (or `Option<String>`)",
11670        ));
11671    }
11672    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11673        return Err(syn::Error::new_spanned(
11674            field,
11675            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11676        ));
11677    }
11678    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11679        if min > max {
11680            return Err(syn::Error::new_spanned(
11681                field,
11682                format!("`min` ({min}) is greater than `max` ({max})"),
11683            ));
11684        }
11685    }
11686    if let Some(default) = &attrs.default {
11687        check_default_expr(default, kind).map_err(|msg| syn::Error::new_spanned(field, msg))?;
11688    }
11689    Ok(())
11690}
11691
11692/// SQL words a `default` may be on its own; any other bare word is a
11693/// column reference, which Postgres and MySQL refuse in a DEFAULT.
11694const DEFAULT_KEYWORDS: &[&str] = &[
11695    "NULL",
11696    "TRUE",
11697    "FALSE",
11698    "CURRENT_TIMESTAMP",
11699    "CURRENT_DATE",
11700    "CURRENT_TIME",
11701    "LOCALTIMESTAMP",
11702    "LOCALTIME",
11703];
11704
11705/// `default` is raw SQL. Refuse the shapes only SQLite accepts: a bare
11706/// word (read as a column on PG/MySQL) and a non-numeric literal on a
11707/// numeric field.
11708fn check_default_expr(default: &str, kind: DetectedKind) -> Result<(), String> {
11709    let expr = default.trim();
11710    let is_word = expr.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
11711        && expr
11712            .chars()
11713            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ' ');
11714    let upper = expr.to_ascii_uppercase();
11715    if is_word && ["CURRENT_USER", "SESSION_USER", "USER"].contains(&upper.as_str()) {
11716        return Err(format!(
11717            "`default = \"{expr}\"` works on Postgres only: MySQL refuses it as a default \
11718             and SQLite stores the text `{expr}`; set the value in code instead"
11719        ));
11720    }
11721    if is_word && !DEFAULT_KEYWORDS.contains(&upper.as_str()) {
11722        return Err(format!(
11723            "`default = \"{expr}\"` is raw SQL, so `{expr}` is read as a column name; \
11724             for the text '{expr}' write `default = \"'{expr}'\"`"
11725        ));
11726    }
11727    let numeric = kind.is_integer()
11728        || matches!(
11729            kind,
11730            DetectedKind::F32 | DetectedKind::F64 | DetectedKind::Decimal
11731        );
11732    if numeric {
11733        let unquoted = expr
11734            .strip_prefix('\'')
11735            .and_then(|e| e.strip_suffix('\''))
11736            .unwrap_or(expr);
11737        let is_number = if kind.is_integer() {
11738            unquoted.parse::<i64>().is_ok()
11739        } else {
11740            unquoted.parse::<f64>().is_ok_and(f64::is_finite)
11741        };
11742        // Keep expressions (`(1 + 2)`, `abs(-1)`) and NULL; refuse a
11743        // literal that is no number. `""` renders as `''`.
11744        let is_literal = is_word || expr.is_empty() || expr.starts_with('\'');
11745        if is_literal && !is_number && upper != "NULL" {
11746            return Err(format!(
11747                "`default = \"{expr}\"` is not a number, but the field is numeric"
11748            ));
11749        }
11750    }
11751    Ok(())
11752}
11753
11754/// A `&'static [ty]` of `const fn` results. The slice needs its own
11755/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11756fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11757    quote!({
11758        const ITEMS: &[#ty] = &[ #(#items),* ];
11759        ITEMS
11760    })
11761}
11762
11763fn optional_u32(value: Option<u32>) -> TokenStream2 {
11764    if let Some(v) = value {
11765        quote!(::core::option::Option::Some(#v))
11766    } else {
11767        quote!(::core::option::Option::None)
11768    }
11769}
11770
11771fn optional_i64(value: Option<i64>) -> TokenStream2 {
11772    if let Some(v) = value {
11773        quote!(::core::option::Option::Some(#v))
11774    } else {
11775        quote!(::core::option::Option::None)
11776    }
11777}
11778
11779fn optional_str(value: Option<&str>) -> TokenStream2 {
11780    if let Some(v) = value {
11781        quote!(::core::option::Option::Some(#v))
11782    } else {
11783        quote!(::core::option::Option::None)
11784    }
11785}
11786
11787fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11788    let Some(pairs) = pairs else {
11789        return quote!(::core::option::Option::None);
11790    };
11791    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11792    quote!(::core::option::Option::Some(&[#(#entries),*]))
11793}
11794
11795fn relation_tokens(
11796    field: &syn::Field,
11797    attrs: &FieldAttrs,
11798    fk_inner: Option<&syn::Type>,
11799    table: &str,
11800) -> syn::Result<TokenStream2> {
11801    let root = rustango_root();
11802    if let Some(inner) = fk_inner {
11803        if attrs.fk.is_some() || attrs.o2o.is_some() {
11804            return Err(syn::Error::new_spanned(
11805                field,
11806                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11807                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11808            ));
11809        }
11810        // No `on`: the target's PK column, which may be renamed.
11811        let on = match attrs.on.as_deref() {
11812            Some(on) => quote!(#on),
11813            None => quote! {
11814                match <#inner as #root::core::Model>::SCHEMA.pk_column() {
11815                    ::core::option::Option::Some(c) => c,
11816                    ::core::option::Option::None => "id",
11817                }
11818            },
11819        };
11820        return Ok(quote! {
11821            ::core::option::Option::Some(#root::core::Relation::fk(
11822                <#inner as #root::core::Model>::SCHEMA.table,
11823                #on,
11824            ))
11825        });
11826    }
11827    match (&attrs.fk, &attrs.o2o) {
11828        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11829            field,
11830            "`fk` and `o2o` are mutually exclusive",
11831        )),
11832        (Some(to), None) => {
11833            let on = attrs.on.as_deref().unwrap_or("id");
11834            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11835            // the model's own table. Threaded as a literal string at
11836            // macro-expansion time to sidestep the const-eval cycle
11837            // that `Self::SCHEMA.table` would create when referenced
11838            // inside Self::SCHEMA's own initializer.
11839            let resolved = if to == "self" { table } else { to };
11840            Ok(quote! {
11841                ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11842            })
11843        }
11844        (None, Some(to)) => {
11845            let on = attrs.on.as_deref().unwrap_or("id");
11846            let resolved = if to == "self" { table } else { to };
11847            Ok(quote! {
11848                ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11849            })
11850        }
11851        (None, None) => {
11852            if attrs.on.is_some() {
11853                return Err(syn::Error::new_spanned(
11854                    field,
11855                    "`on` requires `fk` or `o2o`",
11856                ));
11857            }
11858            Ok(quote!(::core::option::Option::None))
11859        }
11860    }
11861}
11862
11863/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11864/// about kinds without depending on `rustango-core` (which would require a
11865/// proc-macro/normal split it doesn't have today).
11866#[derive(Clone, Copy, PartialEq, Eq)]
11867enum DetectedKind {
11868    I16,
11869    I32,
11870    I64,
11871    F32,
11872    F64,
11873    Bool,
11874    String,
11875    DateTime,
11876    Date,
11877    Time,
11878    Uuid,
11879    Json,
11880    Decimal,
11881    Binary,
11882    /// `Array<String>` → PG `text[]` (#341).
11883    ArrayText,
11884    /// `Array<i32>` → PG `integer[]` (#341).
11885    ArrayInt,
11886    /// `Array<i64>` → PG `bigint[]` (#341).
11887    ArrayBigInt,
11888    /// `Range<i32>` → PG `int4range` (#343).
11889    RangeInt,
11890    /// `Range<i64>` → PG `int8range` (#343).
11891    RangeBigInt,
11892    /// `Range<Decimal>` → PG `numrange` (#343).
11893    RangeNumeric,
11894    /// `Range<NaiveDate>` → PG `daterange` (#343).
11895    RangeDate,
11896    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11897    RangeDateTime,
11898    /// `HStore` → PG `hstore` (#342).
11899    HStore,
11900    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11901    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11902    /// in at the `FieldType` emission site (not carried on this enum).
11903    Vector,
11904    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11905    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11906    /// threaded in at the `FieldType` emission site.
11907    Geometry,
11908}
11909
11910impl DetectedKind {
11911    fn variant_tokens(self) -> TokenStream2 {
11912        let root = rustango_root();
11913        match self {
11914            Self::I16 => quote!(#root::core::FieldType::I16),
11915            Self::I32 => quote!(#root::core::FieldType::I32),
11916            Self::I64 => quote!(#root::core::FieldType::I64),
11917            Self::F32 => quote!(#root::core::FieldType::F32),
11918            Self::F64 => quote!(#root::core::FieldType::F64),
11919            Self::Bool => quote!(#root::core::FieldType::Bool),
11920            Self::String => quote!(#root::core::FieldType::String),
11921            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11922            Self::Date => quote!(#root::core::FieldType::Date),
11923            Self::Time => quote!(#root::core::FieldType::Time),
11924            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11925            Self::Json => quote!(#root::core::FieldType::Json),
11926            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11927            Self::Binary => quote!(#root::core::FieldType::Binary),
11928            Self::ArrayText => {
11929                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11930            }
11931            Self::ArrayInt => {
11932                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11933            }
11934            Self::ArrayBigInt => {
11935                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11936            }
11937            Self::RangeInt => {
11938                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11939            }
11940            Self::RangeBigInt => {
11941                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11942            }
11943            Self::RangeNumeric => {
11944                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11945            }
11946            Self::RangeDate => {
11947                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11948            }
11949            Self::RangeDateTime => {
11950                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11951            }
11952            Self::HStore => quote!(#root::core::FieldType::HStore),
11953            // Dimension comes from the `vector(dims = N)` attribute,
11954            // applied at the emission site; `0` here is just a fallback.
11955            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11956            // SRID comes from the `geometry(srid = N)` attribute, applied
11957            // at the emission site; `0` here is just a fallback.
11958            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11959        }
11960    }
11961
11962    fn is_integer(self) -> bool {
11963        matches!(self, Self::I16 | Self::I32 | Self::I64)
11964    }
11965}
11966
11967/// Result of walking a field's Rust type. `kind` is the underlying
11968/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11969/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11970/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11971/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11972#[derive(Clone, Copy)]
11973struct DetectedType<'a> {
11974    kind: DetectedKind,
11975    nullable: bool,
11976    auto: bool,
11977    fk_inner: Option<&'a syn::Type>,
11978}
11979
11980/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11981/// non-`Auto` types — the caller should already have routed Auto-only
11982/// codegen through this helper, so a `None` indicates a macro-internal
11983/// invariant break.
11984fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11985    let Type::Path(TypePath { path, qself: None }) = ty else {
11986        return None;
11987    };
11988    let last = path.segments.last()?;
11989    if last.ident != "Auto" {
11990        return None;
11991    }
11992    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11993        return None;
11994    };
11995    args.args.iter().find_map(|a| match a {
11996        syn::GenericArgument::Type(t) => Some(t),
11997        _ => None,
11998    })
11999}
12000
12001/// The `Cast<C>` inside `ty` (bare or in an `Option`), for its schema record.
12002fn cast_type(ty: &syn::Type) -> Option<&syn::Type> {
12003    let Type::Path(TypePath { path, qself: None }) = ty else {
12004        return None;
12005    };
12006    let last = path.segments.last()?;
12007    if last.ident == "Cast" {
12008        return Some(ty);
12009    }
12010    if last.ident != "Option" {
12011        return None;
12012    }
12013    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12014        return None;
12015    };
12016    match args.args.first()? {
12017        syn::GenericArgument::Type(inner) => cast_type(inner).filter(|_| args.args.len() == 1),
12018        _ => None,
12019    }
12020}
12021
12022fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
12023    let Type::Path(TypePath { path, qself: None }) = ty else {
12024        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
12025    };
12026    let last = path
12027        .segments
12028        .last()
12029        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
12030
12031    if last.ident == "Option" {
12032        let inner = generic_inner(ty, &last.arguments, "Option")?;
12033        let inner_det = detect_type(inner)?;
12034        if inner_det.nullable {
12035            return Err(syn::Error::new_spanned(
12036                ty,
12037                "nested Option is not supported",
12038            ));
12039        }
12040        if inner_det.auto {
12041            return Err(syn::Error::new_spanned(
12042                ty,
12043                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12044            ));
12045        }
12046        return Ok(DetectedType {
12047            nullable: true,
12048            ..inner_det
12049        });
12050    }
12051
12052    if last.ident == "Auto" {
12053        let inner = generic_inner(ty, &last.arguments, "Auto")?;
12054        let inner_det = detect_type(inner)?;
12055        if inner_det.auto {
12056            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
12057        }
12058        if inner_det.nullable {
12059            return Err(syn::Error::new_spanned(
12060                ty,
12061                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12062            ));
12063        }
12064        if inner_det.fk_inner.is_some() {
12065            return Err(syn::Error::new_spanned(
12066                ty,
12067                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
12068            ));
12069        }
12070        if !matches!(
12071            inner_det.kind,
12072            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
12073        ) {
12074            return Err(syn::Error::new_spanned(
12075                ty,
12076                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
12077                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
12078                 (DEFAULT now())",
12079            ));
12080        }
12081        return Ok(DetectedType {
12082            auto: true,
12083            ..inner_det
12084        });
12085    }
12086
12087    if last.ident == "ForeignKey" {
12088        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
12089        // Resolve the FK column's underlying SQL type from `K`. When the
12090        // user wrote `ForeignKey<T>` without a key parameter, the type
12091        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
12092        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
12093        // recurse into K so the column DDL emits the right SQL type
12094        // (VARCHAR for String, UUID for Uuid, …) and the load_related
12095        // emitter knows which `SqlValue` variant to match.
12096        let kind = match key_ty {
12097            Some(k) => detect_type(k)?.kind,
12098            None => DetectedKind::I64,
12099        };
12100        return Ok(DetectedType {
12101            kind,
12102            nullable: false,
12103            auto: false,
12104            fk_inner: Some(inner),
12105        });
12106    }
12107
12108    let kind = match last.ident.to_string().as_str() {
12109        "i16" => DetectedKind::I16,
12110        "i32" => DetectedKind::I32,
12111        "i64" => DetectedKind::I64,
12112        "f32" => DetectedKind::F32,
12113        "f64" => DetectedKind::F64,
12114        "bool" => DetectedKind::Bool,
12115        "String" => DetectedKind::String,
12116        "DateTime" => DetectedKind::DateTime,
12117        "NaiveDate" => DetectedKind::Date,
12118        "NaiveTime" => DetectedKind::Time,
12119        "Uuid" => DetectedKind::Uuid,
12120        "Value" => DetectedKind::Json,
12121        "Decimal" => DetectedKind::Decimal,
12122        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
12123        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
12124        // — that would emit Binary DDL and decode-fail at runtime.
12125        "Vec" => {
12126            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
12127            if let Type::Path(TypePath { path, qself: None }) = inner {
12128                if let Some(seg) = path.segments.last() {
12129                    if seg.ident == "u8" && seg.arguments.is_empty() {
12130                        return Ok(DetectedType {
12131                            kind: DetectedKind::Binary,
12132                            nullable: false,
12133                            auto: false,
12134                            fk_inner: None,
12135                        });
12136                    }
12137                }
12138            }
12139            return Err(syn::Error::new_spanned(
12140                ty,
12141                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
12142                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
12143            ));
12144        }
12145        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
12146        // `integer[]` / `bigint[]` (#341).
12147        "Array" => {
12148            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
12149            let elem = match inner {
12150                Type::Path(TypePath { path, qself: None }) => {
12151                    path.segments.last().map(|s| s.ident.to_string())
12152                }
12153                _ => None,
12154            };
12155            let kind = match elem.as_deref() {
12156                Some("String") => DetectedKind::ArrayText,
12157                Some("i32") => DetectedKind::ArrayInt,
12158                Some("i64") => DetectedKind::ArrayBigInt,
12159                _ => {
12160                    return Err(syn::Error::new_spanned(
12161                        ty,
12162                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12163                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12164                    ));
12165                }
12166            };
12167            return Ok(DetectedType {
12168                kind,
12169                nullable: false,
12170                auto: false,
12171                fk_inner: None,
12172            });
12173        }
12174        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12175        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12176        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12177        "Range" => {
12178            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12179            let elem = match inner {
12180                Type::Path(TypePath { path, qself: None }) => {
12181                    path.segments.last().map(|s| s.ident.to_string())
12182                }
12183                _ => None,
12184            };
12185            let kind = match elem.as_deref() {
12186                Some("i32") => DetectedKind::RangeInt,
12187                Some("i64") => DetectedKind::RangeBigInt,
12188                Some("Decimal") => DetectedKind::RangeNumeric,
12189                Some("NaiveDate") => DetectedKind::RangeDate,
12190                Some("DateTime") => DetectedKind::RangeDateTime,
12191                _ => {
12192                    return Err(syn::Error::new_spanned(
12193                        ty,
12194                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12195                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12196                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12197                         (→ tstzrange) are supported (#343)",
12198                    ));
12199                }
12200            };
12201            return Ok(DetectedType {
12202                kind,
12203                nullable: false,
12204                auto: false,
12205                fk_inner: None,
12206            });
12207        }
12208        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12209        // (the `CastValue` impl bridges logical↔stored); the field's own
12210        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12211        // schema just needs `FieldType::String`.
12212        "Cast" => {
12213            return Ok(DetectedType {
12214                kind: DetectedKind::String,
12215                nullable: false,
12216                auto: false,
12217                fk_inner: None,
12218            });
12219        }
12220        // `HStore` → PG `hstore` (#342). No generic
12221        // parameter — always a string→string map.
12222        "HStore" => {
12223            return Ok(DetectedType {
12224                kind: DetectedKind::HStore,
12225                nullable: false,
12226                auto: false,
12227                fk_inner: None,
12228            });
12229        }
12230        // `Vector` → pgvector `vector(N)` (#824). The dimension is
12231        // supplied by `#[rustango(vector(dims = N))]`, not the type.
12232        "Vector" => {
12233            return Ok(DetectedType {
12234                kind: DetectedKind::Vector,
12235                nullable: false,
12236                auto: false,
12237                fk_inner: None,
12238            });
12239        }
12240        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12241        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12242        "Point" => {
12243            return Ok(DetectedType {
12244                kind: DetectedKind::Geometry,
12245                nullable: false,
12246                auto: false,
12247                fk_inner: None,
12248            });
12249        }
12250        other => {
12251            return Err(syn::Error::new_spanned(
12252                ty,
12253                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)"),
12254            ));
12255        }
12256    };
12257    Ok(DetectedType {
12258        kind,
12259        nullable: false,
12260        auto: false,
12261        fk_inner: None,
12262    })
12263}
12264
12265fn generic_inner<'a>(
12266    ty: &'a Type,
12267    arguments: &'a PathArguments,
12268    wrapper: &str,
12269) -> syn::Result<&'a Type> {
12270    let PathArguments::AngleBracketed(args) = arguments else {
12271        return Err(syn::Error::new_spanned(
12272            ty,
12273            format!("{wrapper} requires a generic argument"),
12274        ));
12275    };
12276    args.args
12277        .iter()
12278        .find_map(|a| match a {
12279            GenericArgument::Type(t) => Some(t),
12280            _ => None,
12281        })
12282        .ok_or_else(|| {
12283            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12284        })
12285}
12286
12287/// Like [`generic_inner`] but pulls *two* type args — the first is
12288/// required, the second is optional. Used by the `ForeignKey<T, K>`
12289/// detection where K defaults to `i64` when omitted.
12290fn generic_pair<'a>(
12291    ty: &'a Type,
12292    arguments: &'a PathArguments,
12293    wrapper: &str,
12294) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12295    let PathArguments::AngleBracketed(args) = arguments else {
12296        return Err(syn::Error::new_spanned(
12297            ty,
12298            format!("{wrapper} requires a generic argument"),
12299        ));
12300    };
12301    let mut types = args.args.iter().filter_map(|a| match a {
12302        GenericArgument::Type(t) => Some(t),
12303        _ => None,
12304    });
12305    let first = types.next().ok_or_else(|| {
12306        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12307    })?;
12308    let second = types.next();
12309    Ok((first, second))
12310}
12311
12312fn to_snake_case(s: &str) -> String {
12313    let mut out = String::with_capacity(s.len() + 4);
12314    for (i, ch) in s.chars().enumerate() {
12315        if ch.is_ascii_uppercase() {
12316            if i > 0 {
12317                out.push('_');
12318            }
12319            out.push(ch.to_ascii_lowercase());
12320        } else {
12321            out.push(ch);
12322        }
12323    }
12324    out
12325}
12326
12327// ============================================================
12328//  #[derive(Form)]  —  slice 8.4B
12329// ============================================================
12330
12331/// Per-field `#[form(...)]` attributes recognised by the derive.
12332#[derive(Default)]
12333struct FormFieldAttrs {
12334    min: Option<i64>,
12335    max: Option<i64>,
12336    min_length: Option<u32>,
12337    max_length: Option<u32>,
12338    /// `#[form(clean = "fn_name")]` — per-field clean hook.
12339    /// The named static method on the form struct is called after the
12340    /// field's typed parse + length/range checks; it gets the parsed
12341    /// value by reference and returns `Result<<FieldType>, String>`.
12342    /// On Ok, the returned value replaces the parsed one; on Err, the
12343    /// message is attached to the field error list. Issue #372.
12344    clean: Option<syn::Ident>,
12345}
12346
12347/// Container-level `#[form(...)]` attributes. Currently only the
12348/// cross-field `validate` hook (issue #373).
12349#[derive(Default)]
12350struct FormContainerAttrs {
12351    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12352    /// After every per-field parse succeeds, the named method on the
12353    /// form struct is called with `&self` and may return
12354    /// `Result<(), FormErrors>`. Errors merge into the field error
12355    /// list. Issue #373.
12356    validate: Option<syn::Ident>,
12357}
12358
12359/// Detected shape of a form field's Rust type.
12360#[derive(Clone, Copy)]
12361enum FormFieldKind {
12362    String,
12363    I16,
12364    I32,
12365    I64,
12366    F32,
12367    F64,
12368    Bool,
12369}
12370
12371impl FormFieldKind {
12372    fn parse_method(self) -> &'static str {
12373        match self {
12374            Self::I16 => "i16",
12375            Self::I32 => "i32",
12376            Self::I64 => "i64",
12377            Self::F32 => "f32",
12378            Self::F64 => "f64",
12379            // String + Bool don't go through `str::parse`; the codegen
12380            // handles them inline.
12381            Self::String | Self::Bool => "",
12382        }
12383    }
12384}
12385
12386/// A generated `derive(Form)` local; mixed-site so a same-named field can't shadow it.
12387fn form_local(name: &str) -> syn::Ident {
12388    syn::Ident::new(name, proc_macro2::Span::mixed_site())
12389}
12390
12391fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12392    let root = rustango_root();
12393    let (errs, cleaned, data_param) = (
12394        form_local("__errors"),
12395        form_local("__cleaned"),
12396        form_local("data"),
12397    );
12398    let struct_name = &input.ident;
12399
12400    let Data::Struct(data) = &input.data else {
12401        return Err(syn::Error::new_spanned(
12402            struct_name,
12403            "Form can only be derived on structs",
12404        ));
12405    };
12406    let Fields::Named(named) = &data.fields else {
12407        return Err(syn::Error::new_spanned(
12408            struct_name,
12409            "Form requires a struct with named fields",
12410        ));
12411    };
12412
12413    // #373 — container-level `#[form(validate = "fn")]` hook.
12414    let container = parse_form_container_attrs(input)?;
12415    let post_field_clean: Vec<TokenStream2> = Vec::new();
12416    let _ = post_field_clean;
12417
12418    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12419    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12420
12421    for field in &named.named {
12422        let ident = field
12423            .ident
12424            .as_ref()
12425            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12426        let attrs = parse_form_field_attrs(field)?;
12427        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12428        let is_string = matches!(kind, FormFieldKind::String);
12429        let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12430        if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12431            return Err(syn::Error::new(
12432                field.span(),
12433                "`min_length` / `max_length` need a String field",
12434            ));
12435        }
12436        if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12437            return Err(syn::Error::new(
12438                field.span(),
12439                "`min` / `max` need a numeric field",
12440            ));
12441        }
12442
12443        let name_lit = ident_name(ident);
12444        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12445        // #372 — append the per-field `clean_<field>` call right after
12446        // the parse block when the attribute is set. The clean fn
12447        // takes &T and returns Result<T, String>; on Err we attach
12448        // the message to the field error list without aborting, so
12449        // the caller sees every field error at once.
12450        let clean_block = if let Some(clean_fn) = &attrs.clean {
12451            quote! {
12452                if #errs.fields().get(#name_lit).is_none() {
12453                    match Self::#clean_fn(&#ident) {
12454                        ::core::result::Result::Ok(#cleaned) => { #ident = #cleaned; }
12455                        ::core::result::Result::Err(__msg) => {
12456                            #errs.add(#name_lit, __msg);
12457                        }
12458                    }
12459                }
12460            }
12461        } else {
12462            quote! {}
12463        };
12464        field_blocks.push(quote! {
12465            #parse_block
12466            #clean_block
12467        });
12468        field_idents.push(ident);
12469    }
12470
12471    // #373 — after every per-field parse + clean succeeds, call the
12472    // cross-field validator if declared. Errors merge into the
12473    // outgoing FormErrors via the existing `FormErrors::merge` helper
12474    // (same primitive the serializer cross-field hook uses).
12475    let cross_field_call = if let Some(validate_fn) = &container.validate {
12476        quote! {
12477            if #errs.is_empty() {
12478                let __candidate = Self { #( #field_idents ),* };
12479                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12480                    #errs.merge(__other);
12481                }
12482                if !#errs.is_empty() {
12483                    return ::core::result::Result::Err(#errs);
12484                }
12485                return ::core::result::Result::Ok(__candidate);
12486            }
12487        }
12488    } else {
12489        quote! {}
12490    };
12491
12492    Ok(quote! {
12493        impl #root::forms::Form for #struct_name {
12494            fn parse(
12495                #data_param: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12496            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12497                let mut #errs = #root::forms::FormErrors::default();
12498                #( #field_blocks )*
12499                #cross_field_call
12500                if !#errs.is_empty() {
12501                    return ::core::result::Result::Err(#errs);
12502                }
12503                ::core::result::Result::Ok(Self {
12504                    #( #field_idents ),*
12505                })
12506            }
12507        }
12508    })
12509}
12510
12511fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12512    let mut out = FormContainerAttrs::default();
12513    for attr in &input.attrs {
12514        if !attr.path().is_ident("form") {
12515            continue;
12516        }
12517        attr.parse_nested_meta(|meta| {
12518            if meta.path.is_ident("validate") {
12519                let s: LitStr = meta.value()?.parse()?;
12520                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12521                return Ok(());
12522            }
12523            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12524        })?;
12525    }
12526    Ok(out)
12527}
12528
12529fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12530    let mut out = FormFieldAttrs::default();
12531    for attr in &field.attrs {
12532        if !attr.path().is_ident("form") {
12533            continue;
12534        }
12535        attr.parse_nested_meta(|meta| {
12536            if meta.path.is_ident("min") {
12537                let lit: syn::LitInt = meta.value()?.parse()?;
12538                out.min = Some(lit.base10_parse::<i64>()?);
12539                return Ok(());
12540            }
12541            if meta.path.is_ident("max") {
12542                let lit: syn::LitInt = meta.value()?.parse()?;
12543                out.max = Some(lit.base10_parse::<i64>()?);
12544                return Ok(());
12545            }
12546            if meta.path.is_ident("min_length") {
12547                let lit: syn::LitInt = meta.value()?.parse()?;
12548                out.min_length = Some(lit.base10_parse::<u32>()?);
12549                return Ok(());
12550            }
12551            if meta.path.is_ident("max_length") {
12552                let lit: syn::LitInt = meta.value()?.parse()?;
12553                out.max_length = Some(lit.base10_parse::<u32>()?);
12554                return Ok(());
12555            }
12556            if meta.path.is_ident("clean") {
12557                let s: LitStr = meta.value()?.parse()?;
12558                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12559                return Ok(());
12560            }
12561            Err(meta.error(
12562                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12563            ))
12564        })?;
12565    }
12566    Ok(out)
12567}
12568
12569fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12570    let Type::Path(TypePath { path, qself: None }) = ty else {
12571        return Err(syn::Error::new(
12572            span,
12573            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12574        ));
12575    };
12576    let last = path
12577        .segments
12578        .last()
12579        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12580
12581    if last.ident == "Option" {
12582        let inner = generic_inner(ty, &last.arguments, "Option")?;
12583        let (kind, nested) = detect_form_field(inner, span)?;
12584        if nested {
12585            return Err(syn::Error::new(
12586                span,
12587                "nested Option in Form fields is not supported",
12588            ));
12589        }
12590        return Ok((kind, true));
12591    }
12592
12593    let kind = match last.ident.to_string().as_str() {
12594        "String" => FormFieldKind::String,
12595        "i16" => FormFieldKind::I16,
12596        "i32" => FormFieldKind::I32,
12597        "i64" => FormFieldKind::I64,
12598        "f32" => FormFieldKind::F32,
12599        "f64" => FormFieldKind::F64,
12600        "bool" => FormFieldKind::Bool,
12601        other => {
12602            return Err(syn::Error::new(
12603                span,
12604                format!(
12605                    "Form field type `{other}` is not supported in v0.8 — use String / \
12606                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12607                ),
12608            ));
12609        }
12610    };
12611    Ok((kind, false))
12612}
12613
12614#[allow(clippy::too_many_lines)]
12615fn render_form_field_parse(
12616    ident: &syn::Ident,
12617    name_lit: &str,
12618    kind: FormFieldKind,
12619    nullable: bool,
12620    attrs: &FormFieldAttrs,
12621) -> TokenStream2 {
12622    let (errs, data_param) = (form_local("__errors"), form_local("data"));
12623    // Pull the raw &str from the payload `Form::parse(data: &HashMap<…>)`.
12624    let lookup = quote! {
12625        let __raw: ::core::option::Option<&::std::string::String> = #data_param.get(#name_lit);
12626    };
12627
12628    let root = rustango_root();
12629    // Strict, as ModelForm: an unknown spelling is an error, not `true`.
12630    let checkbox = quote! {
12631        match #root::forms::checkbox(__s) {
12632            ::core::option::Option::Some(__b) => __b,
12633            ::core::option::Option::None => {
12634                #errs.add(#name_lit, "Enter true or false.");
12635                false
12636            }
12637        }
12638    };
12639    let parsed_value = match kind {
12640        // `Option<bool>`: absent or blank is `None`, as for the other types.
12641        FormFieldKind::Bool if nullable => quote! {
12642            let __v: ::core::option::Option<bool> = match __raw {
12643                ::core::option::Option::None => ::core::option::Option::None,
12644                ::core::option::Option::Some(__s) if __s.is_empty() => {
12645                    ::core::option::Option::None
12646                }
12647                ::core::option::Option::Some(__s) => ::core::option::Option::Some(#checkbox),
12648            };
12649        },
12650        FormFieldKind::Bool => quote! {
12651            let __v: bool = match __raw {
12652                ::core::option::Option::None => false,
12653                ::core::option::Option::Some(__s) => #checkbox,
12654            };
12655        },
12656        FormFieldKind::String => {
12657            if nullable {
12658                quote! {
12659                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12660                        ::core::option::Option::None => ::core::option::Option::None,
12661                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12662                            ::core::option::Option::None
12663                        }
12664                        ::core::option::Option::Some(__s) => {
12665                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12666                        }
12667                    };
12668                }
12669            } else {
12670                quote! {
12671                    let __v: ::std::string::String = match __raw {
12672                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12673                            ::core::clone::Clone::clone(__s)
12674                        }
12675                        _ => {
12676                            #errs.add(#name_lit, "This field is required.");
12677                            ::std::string::String::new()
12678                        }
12679                    };
12680                }
12681            }
12682        }
12683        FormFieldKind::I16
12684        | FormFieldKind::I32
12685        | FormFieldKind::I64
12686        | FormFieldKind::F32
12687        | FormFieldKind::F64 => {
12688            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12689            let ty_lit = kind.parse_method();
12690            let default_val = match kind {
12691                FormFieldKind::I16 => quote! { 0i16 },
12692                FormFieldKind::I32 => quote! { 0i32 },
12693                FormFieldKind::I64 => quote! { 0i64 },
12694                FormFieldKind::F32 => quote! { 0f32 },
12695                FormFieldKind::F64 => quote! { 0f64 },
12696                _ => quote! { Default::default() },
12697            };
12698            // NaN and inf parse, then pass every min/max compare.
12699            let finite = if matches!(kind, FormFieldKind::F32 | FormFieldKind::F64) {
12700                quote! {
12701                    .and_then(|__n| if __n.is_finite() {
12702                        ::core::result::Result::Ok(__n)
12703                    } else {
12704                        ::core::result::Result::Err(::std::string::String::from("not a finite number"))
12705                    })
12706                }
12707            } else {
12708                quote! {}
12709            };
12710            if nullable {
12711                quote! {
12712                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12713                        ::core::option::Option::None => ::core::option::Option::None,
12714                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12715                            ::core::option::Option::None
12716                        }
12717                        ::core::option::Option::Some(__s) => {
12718                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12719                                ::core::result::Result::Ok(__n) => {
12720                                    ::core::option::Option::Some(__n)
12721                                }
12722                                ::core::result::Result::Err(__e) => {
12723                                    #errs.add(
12724                                        #name_lit,
12725                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12726                                    );
12727                                    ::core::option::Option::None
12728                                }
12729                            }
12730                        }
12731                    };
12732                }
12733            } else {
12734                quote! {
12735                    let __v: #parse_ty = match __raw {
12736                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12737                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12738                                ::core::result::Result::Ok(__n) => __n,
12739                                ::core::result::Result::Err(__e) => {
12740                                    #errs.add(
12741                                        #name_lit,
12742                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12743                                    );
12744                                    #default_val
12745                                }
12746                            }
12747                        }
12748                        _ => {
12749                            #errs.add(#name_lit, "This field is required.");
12750                            #default_val
12751                        }
12752                    };
12753                }
12754            }
12755        }
12756    };
12757
12758    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12759
12760    quote! {
12761        // `mut` so the per-field `clean` hook (#372) can rewrite the
12762        // parsed value in-place when it returns Ok with a normalized
12763        // form (e.g. trim / lowercase).
12764        let mut #ident = {
12765            #lookup
12766            #parsed_value
12767            #validators
12768            __v
12769        };
12770    }
12771}
12772
12773fn render_form_validators(
12774    name_lit: &str,
12775    kind: FormFieldKind,
12776    nullable: bool,
12777    attrs: &FormFieldAttrs,
12778) -> TokenStream2 {
12779    let errs = form_local("__errors");
12780    let mut checks: Vec<TokenStream2> = Vec::new();
12781
12782    let val_ref = if nullable {
12783        quote! { __v.as_ref() }
12784    } else {
12785        quote! { ::core::option::Option::Some(&__v) }
12786    };
12787
12788    let is_string = matches!(kind, FormFieldKind::String);
12789    let is_numeric = matches!(
12790        kind,
12791        FormFieldKind::I16
12792            | FormFieldKind::I32
12793            | FormFieldKind::I64
12794            | FormFieldKind::F32
12795            | FormFieldKind::F64
12796    );
12797
12798    if is_string {
12799        if let Some(min_len) = attrs.min_length {
12800            let min_len_usize = min_len as usize;
12801            checks.push(quote! {
12802                if let ::core::option::Option::Some(__s) = #val_ref {
12803                    if __s.chars().count() < #min_len_usize {
12804                        #errs.add(
12805                            #name_lit,
12806                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12807                        );
12808                    }
12809                }
12810            });
12811        }
12812        if let Some(max_len) = attrs.max_length {
12813            let max_len_usize = max_len as usize;
12814            checks.push(quote! {
12815                if let ::core::option::Option::Some(__s) = #val_ref {
12816                    if __s.chars().count() > #max_len_usize {
12817                        #errs.add(
12818                            #name_lit,
12819                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12820                        );
12821                    }
12822                }
12823            });
12824        }
12825    }
12826
12827    if is_numeric {
12828        if let Some(min) = attrs.min {
12829            checks.push(quote! {
12830                if let ::core::option::Option::Some(__n) = #val_ref {
12831                    if (*__n as f64) < (#min as f64) {
12832                        #errs.add(
12833                            #name_lit,
12834                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12835                        );
12836                    }
12837                }
12838            });
12839        }
12840        if let Some(max) = attrs.max {
12841            checks.push(quote! {
12842                if let ::core::option::Option::Some(__n) = #val_ref {
12843                    if (*__n as f64) > (#max as f64) {
12844                        #errs.add(
12845                            #name_lit,
12846                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12847                        );
12848                    }
12849                }
12850            });
12851        }
12852    }
12853
12854    quote! { #( #checks )* }
12855}
12856
12857// ============================================================
12858//  #[derive(ViewSet)]
12859// ============================================================
12860
12861struct ViewSetAttrs {
12862    model: syn::Path,
12863    fields: Vec<String>,
12864    filter_fields: Vec<String>,
12865    search_fields: Vec<String>,
12866    /// (field_name, desc)
12867    ordering: Vec<(String, bool)>,
12868    page_size: Option<usize>,
12869    read_only: bool,
12870    allow_anonymous: bool,
12871    perms: ViewSetPermsAttrs,
12872    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12873    /// retrieve / create responses through this `#[derive(Serializer)]`
12874    /// type instead of the default field-level projection (requires the
12875    /// `serializer` feature). Tri-dialect.
12876    serializer: Option<syn::Path>,
12877}
12878
12879#[derive(Default)]
12880struct ViewSetPermsAttrs {
12881    list: Vec<String>,
12882    retrieve: Vec<String>,
12883    create: Vec<String>,
12884    update: Vec<String>,
12885    destroy: Vec<String>,
12886}
12887
12888fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12889    let root = rustango_root();
12890    let struct_name = &input.ident;
12891
12892    // Must be a unit struct or an empty named struct.
12893    match &input.data {
12894        Data::Struct(s) => match &s.fields {
12895            Fields::Unit | Fields::Named(_) => {}
12896            Fields::Unnamed(_) => {
12897                return Err(syn::Error::new_spanned(
12898                    struct_name,
12899                    "ViewSet can only be derived on a unit struct or an empty named struct",
12900                ));
12901            }
12902        },
12903        _ => {
12904            return Err(syn::Error::new_spanned(
12905                struct_name,
12906                "ViewSet can only be derived on a struct",
12907            ));
12908        }
12909    }
12910
12911    let attrs = parse_viewset_attrs(input)?;
12912    let model_path = &attrs.model;
12913
12914    let field_lits = attrs.fields.iter().map(|f| f.as_str());
12915
12916    let filter_fields_call = if attrs.filter_fields.is_empty() {
12917        quote!()
12918    } else {
12919        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12920        quote!(.filter_fields(&[ #(#lits),* ]))
12921    };
12922
12923    let search_fields_call = if attrs.search_fields.is_empty() {
12924        quote!()
12925    } else {
12926        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12927        quote!(.search_fields(&[ #(#lits),* ]))
12928    };
12929
12930    let ordering_call = if attrs.ordering.is_empty() {
12931        quote!()
12932    } else {
12933        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12934            let f = f.as_str();
12935            quote!((#f, #desc))
12936        });
12937        quote!(.ordering(&[ #(#pairs),* ]))
12938    };
12939
12940    let page_size_call = if let Some(n) = attrs.page_size {
12941        quote!(.page_size(#n))
12942    } else {
12943        quote!()
12944    };
12945
12946    let read_only_call = if attrs.read_only {
12947        quote!(.read_only())
12948    } else {
12949        quote!()
12950    };
12951    let allow_anonymous_call = if attrs.allow_anonymous {
12952        quote!(.allow_anonymous())
12953    } else {
12954        quote!()
12955    };
12956
12957    // `.serializer::<S>()` — reshape responses through a derived
12958    // serializer. Requires the downstream crate to enable the
12959    // `serializer` feature (the method is gated on it).
12960    let serializer_call = if let Some(ref ser) = attrs.serializer {
12961        quote!(.serializer::<#ser>())
12962    } else {
12963        quote!()
12964    };
12965
12966    let perms = &attrs.perms;
12967    let perms_call = if perms.list.is_empty()
12968        && perms.retrieve.is_empty()
12969        && perms.create.is_empty()
12970        && perms.update.is_empty()
12971        && perms.destroy.is_empty()
12972    {
12973        quote!()
12974    } else {
12975        let list_lits = perms.list.iter().map(|s| s.as_str());
12976        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12977        let create_lits = perms.create.iter().map(|s| s.as_str());
12978        let update_lits = perms.update.iter().map(|s| s.as_str());
12979        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12980        quote! {
12981            .permissions(#root::viewset::ViewSetPerms {
12982                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
12983                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12984                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
12985                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
12986                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
12987            })
12988        }
12989    };
12990
12991    Ok(quote! {
12992        impl #struct_name {
12993            /// Build an `axum::Router` with the six standard REST endpoints
12994            /// for this ViewSet, mounted at `prefix`.
12995            ///
12996            /// Accepts any backend's pool — a `sqlx::PgPool`,
12997            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12998            /// enum — because they all `Into<Pool>` (#1273). This is why
12999            /// the derive no longer names `PgPool`, which would not
13000            /// exist on a project built without the `postgres` feature.
13001            pub fn router(
13002                prefix: &str,
13003                pool: impl ::core::convert::Into<#root::sql::Pool>,
13004            ) -> #root::__axum::Router {
13005                #root::viewset::ViewSet::for_model(
13006                    <#model_path as #root::core::Model>::SCHEMA,
13007                    &[ #(#field_lits),* ],
13008                )
13009                    #filter_fields_call
13010                    #search_fields_call
13011                    #ordering_call
13012                    #page_size_call
13013                    #perms_call
13014                    #read_only_call
13015                    #allow_anonymous_call
13016                    #serializer_call
13017                    .router(prefix, pool.into())
13018            }
13019        }
13020    })
13021}
13022
13023fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
13024    let mut model: Option<syn::Path> = None;
13025    let mut fields: Option<Vec<String>> = None;
13026    let mut filter_fields: Vec<String> = Vec::new();
13027    let mut search_fields: Vec<String> = Vec::new();
13028    let mut ordering: Vec<(String, bool)> = Vec::new();
13029    let mut page_size: Option<usize> = None;
13030    let mut read_only = false;
13031    let mut allow_anonymous = false;
13032    let mut perms = ViewSetPermsAttrs::default();
13033    let mut serializer: Option<syn::Path> = None;
13034
13035    for attr in &input.attrs {
13036        if !attr.path().is_ident("viewset") {
13037            continue;
13038        }
13039        attr.parse_nested_meta(|meta| {
13040            if meta.path.is_ident("model") {
13041                let path: syn::Path = meta.value()?.parse()?;
13042                model = Some(path);
13043                return Ok(());
13044            }
13045            if meta.path.is_ident("serializer") {
13046                let path: syn::Path = meta.value()?.parse()?;
13047                serializer = Some(path);
13048                return Ok(());
13049            }
13050            if meta.path.is_ident("fields") {
13051                let s: LitStr = meta.value()?.parse()?;
13052                fields = Some(split_field_list(&s.value()));
13053                return Ok(());
13054            }
13055            if meta.path.is_ident("filter_fields") {
13056                let s: LitStr = meta.value()?.parse()?;
13057                filter_fields = split_field_list(&s.value());
13058                return Ok(());
13059            }
13060            if meta.path.is_ident("search_fields") {
13061                let s: LitStr = meta.value()?.parse()?;
13062                search_fields = split_field_list(&s.value());
13063                return Ok(());
13064            }
13065            if meta.path.is_ident("ordering") {
13066                let s: LitStr = meta.value()?.parse()?;
13067                ordering = parse_ordering_list(&s.value());
13068                return Ok(());
13069            }
13070            if meta.path.is_ident("page_size") {
13071                let lit: syn::LitInt = meta.value()?.parse()?;
13072                page_size = Some(lit.base10_parse::<usize>()?);
13073                return Ok(());
13074            }
13075            if meta.path.is_ident("read_only") {
13076                read_only = true;
13077                return Ok(());
13078            }
13079            if meta.path.is_ident("allow_anonymous") {
13080                allow_anonymous = true;
13081                return Ok(());
13082            }
13083            if meta.path.is_ident("permissions") {
13084                meta.parse_nested_meta(|inner| {
13085                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
13086                        let s: LitStr = inner.value()?.parse()?;
13087                        Ok(split_field_list(&s.value()))
13088                    };
13089                    if inner.path.is_ident("list") {
13090                        perms.list = parse_codenames(&inner)?;
13091                    } else if inner.path.is_ident("retrieve") {
13092                        perms.retrieve = parse_codenames(&inner)?;
13093                    } else if inner.path.is_ident("create") {
13094                        perms.create = parse_codenames(&inner)?;
13095                    } else if inner.path.is_ident("update") {
13096                        perms.update = parse_codenames(&inner)?;
13097                    } else if inner.path.is_ident("destroy") {
13098                        perms.destroy = parse_codenames(&inner)?;
13099                    } else {
13100                        return Err(inner.error(
13101                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
13102                        ));
13103                    }
13104                    Ok(())
13105                })?;
13106                return Ok(());
13107            }
13108            Err(meta.error(
13109                "unknown viewset attribute (supported: model, fields, filter_fields, \
13110                 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
13111                 permissions(...))",
13112            ))
13113        })?;
13114    }
13115
13116    let model = model.ok_or_else(|| {
13117        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
13118    })?;
13119    // The field list is the write allow-list, so it is never implied.
13120    let fields = fields.ok_or_else(|| {
13121        syn::Error::new_spanned(
13122            &input.ident,
13123            "`#[viewset(fields = \"id, title\")]` is required: the fields the API renders and writes",
13124        )
13125    })?;
13126
13127    Ok(ViewSetAttrs {
13128        model,
13129        fields,
13130        filter_fields,
13131        search_fields,
13132        ordering,
13133        page_size,
13134        read_only,
13135        allow_anonymous,
13136        perms,
13137        serializer,
13138    })
13139}
13140
13141// ============================================================ #[derive(Serializer)]
13142
13143struct SerializerContainerAttrs {
13144    model: syn::Path,
13145    /// `#[serializer(validate = "fn_name")]` on the struct — the
13146    /// cross-field validation hook (#436). The named inherent method
13147    /// must take `&self` and return
13148    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
13149    /// `validate()` runs every per-field validator first; then calls
13150    /// the cross-field method if declared; aggregates all errors into
13151    /// one `FormErrors`.
13152    cross_validate: Option<syn::Ident>,
13153}
13154
13155#[derive(Default)]
13156struct SerializerFieldAttrs {
13157    read_only: bool,
13158    write_only: bool,
13159    source: Option<String>,
13160    skip: bool,
13161    /// `#[serializer(method = "fn_name")]` — computed field. The macro
13162    /// emits a `from_model` initializer that calls
13163    /// `Self::fn_name(&model)` and stores the return value.
13164    method: Option<String>,
13165    /// `#[serializer(validate = "fn_name")]` — per-field validator
13166    /// callable run by `Self::validate(&self)`. Must return
13167    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
13168    /// the field name.
13169    validate: Option<String>,
13170    /// `#[serializer(nested)]` on a field whose type is another
13171    /// `Serializer` — the macro emits `from_model` initializer that
13172    /// reads the parent via `model.<source>.value()` then calls the
13173    /// child serializer's `from_model(parent)`. When the FK is
13174    /// unloaded the field falls back to `Default::default()` (does
13175    /// NOT panic) so a missing prefetch in prod degrades gracefully.
13176    /// Source field on the model defaults to the field name; override
13177    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
13178    /// panic-on-unloaded behavior for tests.
13179    nested: bool,
13180    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
13181    /// strict behavior: panic when the FK isn't loaded. Useful in
13182    /// test code where forgetting select_related must trip a hard
13183    /// failure rather than render a blank nested object.
13184    nested_strict: bool,
13185    /// `#[serializer(many = TagSerializer)]` — declare the field as
13186    /// a list of nested serializers. Field type must be `Vec<S>`
13187    /// where `S` is the inner serializer. The macro initializes the
13188    /// field to `Vec::new()` in `from_model` and emits a typed
13189    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13190    /// maps each model row through `S::from_model`. Auto-load isn't
13191    /// possible (the M2M / one-to-many accessor is async); callers
13192    /// fetch the children + call the setter post-from_model.
13193    many: Option<syn::Type>,
13194    /// `#[serializer(slug = "name")]` — render a relation as one of
13195    /// its fields.
13196    /// Source field on the model must be a `ForeignKey<T>`; the
13197    /// macro emits `from_model` glue that walks
13198    /// `model.<source>.value()?.<slug>` and clones it. Field type on
13199    /// the serializer is typically `String` (whatever type the slug
13200    /// column has). When the FK is unloaded the field falls back to
13201    /// `Default::default()`, same graceful-degrade contract as
13202    /// `nested`. Source defaults to the field name; override with
13203    /// `source = "..."`. v0.44.
13204    slug: Option<String>,
13205    /// `#[serializer(max_length = N)]` — caps
13206    /// the character count of a string field on write. Overrides the
13207    /// model's `max_length`; when absent the model value is inherited.
13208    max_length: Option<u64>,
13209    /// `#[serializer(min_length = N)]` — minimum character count.
13210    /// Serializer-only (the model has no `min_length` column).
13211    min_length: Option<u64>,
13212    /// `#[serializer(min = N)]` — inclusive
13213    /// integer lower bound; overrides the model's `min` when given.
13214    min: Option<i64>,
13215    /// `#[serializer(max = N)]` — inclusive
13216    /// integer upper bound; overrides the model's `max` when given.
13217    max: Option<i64>,
13218}
13219
13220fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13221    let mut model: Option<syn::Path> = None;
13222    let mut cross_validate: Option<syn::Ident> = None;
13223    for attr in &input.attrs {
13224        if !attr.path().is_ident("serializer") {
13225            continue;
13226        }
13227        attr.parse_nested_meta(|meta| {
13228            if meta.path.is_ident("model") {
13229                let _eq: syn::Token![=] = meta.input.parse()?;
13230                model = Some(meta.input.parse()?);
13231                return Ok(());
13232            }
13233            if meta.path.is_ident("validate") {
13234                // #436 — container-level `validate = "fn_name"` for
13235                // cross-field validation. Field-level
13236                // `#[serializer(validate = "...")]` on a field is
13237                // parsed separately in `parse_serializer_field_attrs`.
13238                let s: LitStr = meta.value()?.parse()?;
13239                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13240                return Ok(());
13241            }
13242            Err(meta.error(
13243                "unknown serializer container attribute \
13244                 (supported: `model`, `validate`)",
13245            ))
13246        })?;
13247    }
13248    let model = model.ok_or_else(|| {
13249        syn::Error::new_spanned(
13250            &input.ident,
13251            "`#[serializer(model = SomeModel)]` is required",
13252        )
13253    })?;
13254    Ok(SerializerContainerAttrs {
13255        model,
13256        cross_validate,
13257    })
13258}
13259
13260/// The `#[serde(...)]` keys the Serializer derive honours. Its own
13261/// `Serialize` impl would otherwise drop them silently.
13262#[derive(Default)]
13263struct SerdeWireAttrs {
13264    rename_ser: Option<String>,
13265    rename_de: Option<String>,
13266    skip_ser: bool,
13267    skip_de: bool,
13268    skip_ser_if: Option<syn::ExprPath>,
13269}
13270
13271/// A serde `rename_all` rule, applied to a `snake_case` field name.
13272#[derive(Clone, Copy)]
13273enum SerdeRenameRule {
13274    Lower,
13275    Upper,
13276    Pascal,
13277    Camel,
13278    ScreamingSnake,
13279    Kebab,
13280    ScreamingKebab,
13281}
13282
13283impl SerdeRenameRule {
13284    fn parse(s: &LitStr) -> syn::Result<Self> {
13285        Ok(match s.value().as_str() {
13286            "lowercase" | "snake_case" => Self::Lower,
13287            "UPPERCASE" => Self::Upper,
13288            "PascalCase" => Self::Pascal,
13289            "camelCase" => Self::Camel,
13290            "SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
13291            "kebab-case" => Self::Kebab,
13292            "SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
13293            _ => {
13294                return Err(syn::Error::new_spanned(
13295                    s,
13296                    "unknown serde `rename_all` rule",
13297                ))
13298            }
13299        })
13300    }
13301
13302    /// Same output as serde's own field renaming.
13303    fn apply(self, field: &str) -> String {
13304        match self {
13305            Self::Lower => field.to_owned(),
13306            Self::Upper | Self::ScreamingSnake => field.to_ascii_uppercase(),
13307            Self::Pascal | Self::Camel => {
13308                let mut out = String::new();
13309                let mut cap = matches!(self, Self::Pascal);
13310                for ch in field.chars() {
13311                    if ch == '_' {
13312                        cap = !out.is_empty() || matches!(self, Self::Pascal);
13313                    } else if cap {
13314                        out.push(ch.to_ascii_uppercase());
13315                        cap = false;
13316                    } else {
13317                        out.push(ch);
13318                    }
13319                }
13320                out
13321            }
13322            Self::Kebab => field.replace('_', "-"),
13323            Self::ScreamingKebab => field.to_ascii_uppercase().replace('_', "-"),
13324        }
13325    }
13326}
13327
13328/// Consume the value of a `serde` key the derive does not act on.
13329fn skip_serde_meta_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
13330    if meta.input.peek(syn::Token![=]) {
13331        let _: syn::Expr = meta.value()?.parse()?;
13332    } else if meta.input.peek(syn::token::Paren) {
13333        let _: proc_macro2::Group = meta.input.parse()?;
13334    }
13335    Ok(())
13336}
13337
13338/// `rename = "x"` or `rename(serialize = "a", deserialize = "b")`.
13339fn parse_serde_rename(
13340    meta: &syn::meta::ParseNestedMeta,
13341    mut set: impl FnMut(bool, LitStr) -> syn::Result<()>,
13342) -> syn::Result<()> {
13343    if meta.input.peek(syn::Token![=]) {
13344        let s: LitStr = meta.value()?.parse()?;
13345        set(true, s.clone())?;
13346        return set(false, s);
13347    }
13348    meta.parse_nested_meta(|inner| {
13349        let is_ser = inner.path.is_ident("serialize");
13350        if !is_ser && !inner.path.is_ident("deserialize") {
13351            return Err(inner.error("expected `serialize` or `deserialize`"));
13352        }
13353        set(is_ser, inner.value()?.parse()?)
13354    })
13355}
13356
13357/// Container `rename_all` rules: (serialize, deserialize).
13358fn parse_serde_container_rules(
13359    input: &DeriveInput,
13360) -> syn::Result<(Option<SerdeRenameRule>, Option<SerdeRenameRule>)> {
13361    let (mut ser, mut de) = (None, None);
13362    for attr in input.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13363        attr.parse_nested_meta(|meta| {
13364            if meta.path.is_ident("rename_all") {
13365                return parse_serde_rename(&meta, |is_ser, s| {
13366                    let rule = Some(SerdeRenameRule::parse(&s)?);
13367                    if is_ser {
13368                        ser = rule;
13369                    } else {
13370                        de = rule;
13371                    }
13372                    Ok(())
13373                });
13374            }
13375            skip_serde_meta_value(&meta)
13376        })?;
13377    }
13378    Ok((ser, de))
13379}
13380
13381fn parse_serde_field_attrs(field: &syn::Field) -> syn::Result<SerdeWireAttrs> {
13382    let mut out = SerdeWireAttrs::default();
13383    for attr in field.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13384        attr.parse_nested_meta(|meta| {
13385            if meta.path.is_ident("rename") {
13386                return parse_serde_rename(&meta, |is_ser, s| {
13387                    if is_ser {
13388                        out.rename_ser = Some(s.value());
13389                    } else {
13390                        out.rename_de = Some(s.value());
13391                    }
13392                    Ok(())
13393                });
13394            }
13395            if meta.path.is_ident("skip") {
13396                out.skip_ser = true;
13397                out.skip_de = true;
13398                return Ok(());
13399            }
13400            if meta.path.is_ident("skip_serializing") {
13401                out.skip_ser = true;
13402                return Ok(());
13403            }
13404            if meta.path.is_ident("skip_deserializing") {
13405                out.skip_de = true;
13406                return Ok(());
13407            }
13408            if meta.path.is_ident("skip_serializing_if") {
13409                let s: LitStr = meta.value()?.parse()?;
13410                out.skip_ser_if = Some(s.parse()?);
13411                return Ok(());
13412            }
13413            skip_serde_meta_value(&meta)
13414        })?;
13415    }
13416    Ok(out)
13417}
13418
13419fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13420    let mut out = SerializerFieldAttrs::default();
13421    for attr in &field.attrs {
13422        if !attr.path().is_ident("serializer") {
13423            continue;
13424        }
13425        attr.parse_nested_meta(|meta| {
13426            if meta.path.is_ident("read_only") {
13427                out.read_only = true;
13428                return Ok(());
13429            }
13430            if meta.path.is_ident("write_only") {
13431                out.write_only = true;
13432                return Ok(());
13433            }
13434            if meta.path.is_ident("skip") {
13435                out.skip = true;
13436                return Ok(());
13437            }
13438            if meta.path.is_ident("source") {
13439                let s: LitStr = meta.value()?.parse()?;
13440                out.source = Some(s.value());
13441                return Ok(());
13442            }
13443            if meta.path.is_ident("method") {
13444                let s: LitStr = meta.value()?.parse()?;
13445                out.method = Some(s.value());
13446                return Ok(());
13447            }
13448            if meta.path.is_ident("validate") {
13449                let s: LitStr = meta.value()?.parse()?;
13450                out.validate = Some(s.value());
13451                return Ok(());
13452            }
13453            if meta.path.is_ident("many") {
13454                let _eq: syn::Token![=] = meta.input.parse()?;
13455                out.many = Some(meta.input.parse()?);
13456                return Ok(());
13457            }
13458            if meta.path.is_ident("nested") {
13459                out.nested = true;
13460                // Optional strict flag inside parentheses:
13461                //   #[serializer(nested(strict))]
13462                if meta.input.peek(syn::token::Paren) {
13463                    meta.parse_nested_meta(|inner| {
13464                        if inner.path.is_ident("strict") {
13465                            out.nested_strict = true;
13466                            return Ok(());
13467                        }
13468                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13469                    })?;
13470                }
13471                return Ok(());
13472            }
13473            if meta.path.is_ident("slug") {
13474                let s: LitStr = meta.value()?.parse()?;
13475                out.slug = Some(s.value());
13476                return Ok(());
13477            }
13478            if meta.path.is_ident("max_length") {
13479                let lit: syn::LitInt = meta.value()?.parse()?;
13480                out.max_length = Some(lit.base10_parse::<u64>()?);
13481                return Ok(());
13482            }
13483            if meta.path.is_ident("min_length") {
13484                let lit: syn::LitInt = meta.value()?.parse()?;
13485                out.min_length = Some(lit.base10_parse::<u64>()?);
13486                return Ok(());
13487            }
13488            if meta.path.is_ident("min") {
13489                let lit: syn::LitInt = meta.value()?.parse()?;
13490                out.min = Some(lit.base10_parse::<i64>()?);
13491                return Ok(());
13492            }
13493            if meta.path.is_ident("max") {
13494                let lit: syn::LitInt = meta.value()?.parse()?;
13495                out.max = Some(lit.base10_parse::<i64>()?);
13496                return Ok(());
13497            }
13498            Err(meta.error(
13499                "unknown serializer field attribute (supported: \
13500                 `read_only`, `write_only`, `source`, `skip`, `method`, \
13501                 `validate`, `nested`, `many`, `slug`, `max_length`, \
13502                 `min_length`, `min`, `max`)",
13503            ))
13504        })?;
13505    }
13506    // Validate: read_only + write_only is nonsensical
13507    if out.read_only && out.write_only {
13508        return Err(syn::Error::new_spanned(
13509            field,
13510            "a field cannot be both `read_only` and `write_only`",
13511        ));
13512    }
13513    if out.method.is_some() && out.source.is_some() {
13514        return Err(syn::Error::new_spanned(
13515            field,
13516            "`method` and `source` are mutually exclusive — `method` computes \
13517             the value from a method, `source` reads it from a different model field",
13518        ));
13519    }
13520    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13521        return Err(syn::Error::new_spanned(
13522            field,
13523            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13524             — pick one strategy for populating the field",
13525        ));
13526    }
13527    Ok(out)
13528}
13529
13530fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13531    let root = rustango_root();
13532    let struct_name = &input.ident;
13533    let struct_name_lit = struct_name.to_string();
13534
13535    let Data::Struct(data) = &input.data else {
13536        return Err(syn::Error::new_spanned(
13537            struct_name,
13538            "Serializer can only be derived on structs",
13539        ));
13540    };
13541    let Fields::Named(named) = &data.fields else {
13542        return Err(syn::Error::new_spanned(
13543            struct_name,
13544            "Serializer requires a struct with named fields",
13545        ));
13546    };
13547
13548    let container = parse_serializer_container_attrs(input)?;
13549    let model_path = &container.model;
13550
13551    // Classify each field. `ty` is only consumed by the
13552    // `#[cfg(feature = "openapi")]` block below, but we always
13553    // capture it to keep the field-info build a single pass.
13554    #[allow(dead_code)]
13555    struct FieldInfo {
13556        ident: syn::Ident,
13557        ty: syn::Type,
13558        attrs: SerializerFieldAttrs,
13559        serde: SerdeWireAttrs,
13560        /// JSON key in the output / in a request body.
13561        ser_name: String,
13562        de_name: String,
13563    }
13564    let (ser_rule, de_rule) = parse_serde_container_rules(input)?;
13565    let mut fields_info: Vec<FieldInfo> = Vec::new();
13566    for field in &named.named {
13567        let ident = field.ident.clone().expect("named field has ident");
13568        let attrs = parse_serializer_field_attrs(field)?;
13569        let serde = parse_serde_field_attrs(field)?;
13570        let name = ident_name(&ident);
13571        let wire = |rename: &Option<String>, rule: Option<SerdeRenameRule>| {
13572            rename
13573                .clone()
13574                .unwrap_or_else(|| rule.map_or_else(|| name.clone(), |r| r.apply(&name)))
13575        };
13576        fields_info.push(FieldInfo {
13577            ser_name: wire(&serde.rename_ser, ser_rule),
13578            de_name: wire(&serde.rename_de, de_rule),
13579            ident,
13580            ty: field.ty.clone(),
13581            attrs,
13582            serde,
13583        });
13584    }
13585
13586    // Generate from_model body: struct literal with each field assigned.
13587    let from_model_fields = fields_info.iter().map(|fi| {
13588        let ident = &fi.ident;
13589        if let Some(_inner) = &fi.attrs.many {
13590            // Many — collection field. Initialize empty; caller
13591            // populates via the macro-emitted set_<field> helper
13592            // after fetching the M2M children.
13593            quote! { #ident: ::std::vec::Vec::new() }
13594        } else if let Some(method) = &fi.attrs.method {
13595            // SerializerMethodField: call Self::<method>(&model) to
13596            // compute the value. Method signature must be
13597            // `fn <method>(model: &T) -> <field type>`.
13598            let method_ident = syn::Ident::new(method, ident.span());
13599            quote! { #ident: Self::#method_ident(model) }
13600        } else if let Some(slug_field) = &fi.attrs.slug {
13601            // v0.44 — SlugRelatedField. Source defaults to the field
13602            // name on this struct; override via `source = "..."`. The
13603            // source field on the model is expected to be a
13604            // `ForeignKey<T>`; the slug field on the parent is named
13605            // by the attribute value. When the FK is unloaded the
13606            // field falls back to `Default::default()` — same
13607            // graceful-degrade contract as `nested`; a NULL
13608            // `Option<ForeignKey>` renders `None`.
13609            let src_name = fi
13610                .attrs
13611                .source
13612                .as_deref()
13613                .unwrap_or(&ident_name(&fi.ident))
13614                .to_owned();
13615            let src_ident = ident_from_name(&src_name, ident.span());
13616            let slug_ident = syn::Ident::new(slug_field, ident.span());
13617            quote! {
13618                #ident: #root::serializer::LoadedRelation::render(
13619                    &model.#src_ident,
13620                    |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
13621                    ::core::default::Default::default,
13622                )
13623            }
13624        } else if fi.attrs.nested {
13625            // Nested serializer. Source defaults to the field name on
13626            // this struct; override via `source = "..."`. The source
13627            // field on the model is a `ForeignKey<T>` or
13628            // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
13629            //
13630            // Behavior matrix (tweakable per-field):
13631            //   * FK loaded   → nested object materializes via
13632            //                   ChildSerializer::from_model(parent).
13633            //   * FK unloaded → fall back to ChildSerializer::default()
13634            //                   (so prod doesn't crash on a missing
13635            //                   prefetch — just renders a blank nested
13636            //                   object). Add `#[serializer(nested(
13637            //                   strict))]` to keep the v0.18.1
13638            //                   panic-on-unloaded behavior for tests
13639            //                   that want hard guardrails.
13640            //   * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
13641            //                   field (JSON null), strict or not.
13642            let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13643            let src_ident = ident_from_name(&src_name, ident.span());
13644            let unloaded = if fi.attrs.nested_strict {
13645                let panic_msg = format!(
13646                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13647                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13648                );
13649                quote! { || ::core::panic!("{}", #panic_msg) }
13650            } else {
13651                quote! { ::core::default::Default::default }
13652            };
13653            quote! {
13654                #ident: #root::serializer::LoadedRelation::render(
13655                    &model.#src_ident,
13656                    |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
13657                    #unloaded,
13658                )
13659            }
13660        } else if fi.attrs.write_only || fi.attrs.skip {
13661            // Not read from model — use default
13662            quote! { #ident: ::core::default::Default::default() }
13663        } else if let Some(src) = &fi.attrs.source {
13664            let src_ident = ident_from_name(src, ident.span());
13665            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13666        } else {
13667            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13668        }
13669    });
13670
13671    // is_writable predicate (also used by writable_lits /
13672    // writable_source_lits below).
13673    let is_writable = |fi: &&FieldInfo| {
13674        !fi.attrs.read_only
13675            && !fi.attrs.skip
13676            && !fi.serde.skip_de
13677            && fi.attrs.method.is_none()
13678            && !fi.attrs.nested
13679            && fi.attrs.many.is_none()
13680            && fi.attrs.slug.is_none()
13681    };
13682
13683    // Declarative field constraints: one block
13684    // per writable field, run inside `validate()`. Each resolves its
13685    // bounds as the serializer attr when given (`#[serializer(max_length
13686    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13687    // `max` / `choices`), then dispatches on the field's JSON value via
13688    // `forms::validators::check_value` (string → length/choices, integer
13689    // → min/max).
13690    let opt_usize = |v: Option<u64>| match v {
13691        Some(n) => {
13692            let n = n as usize;
13693            quote!(::core::option::Option::Some(#n))
13694        }
13695        None => quote!(::core::option::Option::None),
13696    };
13697    #[cfg(feature = "openapi")]
13698    let opt_u64 = |v: Option<u64>| match v {
13699        Some(n) => quote!(::core::option::Option::Some(#n)),
13700        None => quote!(::core::option::Option::None),
13701    };
13702    let opt_i64 = |v: Option<i64>| match v {
13703        Some(n) => quote!(::core::option::Option::Some(#n)),
13704        None => quote!(::core::option::Option::None),
13705    };
13706    let constraint_blocks: Vec<_> = fields_info
13707        .iter()
13708        .filter(is_writable)
13709        .map(|fi| {
13710            let ident = &fi.ident;
13711            let fname = &fi.de_name;
13712            let mname = fi
13713                .attrs
13714                .source
13715                .clone()
13716                .unwrap_or_else(|| ident_name(&fi.ident));
13717            let attr_max_len = opt_usize(fi.attrs.max_length);
13718            let attr_min_len = opt_usize(fi.attrs.min_length);
13719            let attr_min = opt_i64(fi.attrs.min);
13720            let attr_max = opt_i64(fi.attrs.max);
13721            // A bound the field's type cannot take is a compile error, not a no-op.
13722            let ty = &fi.ty;
13723            let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
13724                .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
13725            let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
13726                .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
13727            quote! {
13728                #len_check
13729                #num_check
13730                if __sent(#fname) {
13731                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13732                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13733                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13734                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13735                    let __min: ::core::option::Option<i64> =
13736                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13737                    let __max: ::core::option::Option<i64> =
13738                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13739                    let __choices = __sf.and_then(|__f| __f.choices);
13740                    let __v = #root::__serde_json::to_value(&self.#ident)
13741                        .unwrap_or(#root::__serde_json::Value::Null);
13742                    #root::forms::validators::check_value(
13743                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13744                        &mut __errors,
13745                    );
13746                }
13747            }
13748        })
13749        .collect();
13750    let has_constraints = !constraint_blocks.is_empty();
13751
13752    // Per-field validators. Emit a
13753    // `validate(&self)` method that runs each user-defined validator
13754    // and aggregates errors into `FormErrors`.
13755    let validator_calls: Vec<_> = fields_info
13756        .iter()
13757        .filter_map(|fi| {
13758            let ident = &fi.ident;
13759            let name_lit = &fi.de_name;
13760            let method = fi.attrs.validate.as_ref()?;
13761            let method_ident = syn::Ident::new(method, ident.span());
13762            Some(quote! {
13763                if __sent(#name_lit) {
13764                    if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13765                        __errors.add(#name_lit.to_owned(), __e);
13766                    }
13767                }
13768            })
13769        })
13770        .collect();
13771    // #436 — cross-field `validate(&self)`. If the
13772    // container declared `#[serializer(validate = "fn_name")]`,
13773    // the macro-generated `validate(&self)` runs every per-field
13774    // validator first, then calls the user's cross-field method,
13775    // merging its `FormErrors` into the per-field errors. Either
13776    // alone is enough to emit the wrapper.
13777    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13778        quote! {
13779            // Merge cross-field errors into the per-field bucket so
13780            // a single .validate() call surfaces both layers.
13781            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13782                __errors.merge(__cross);
13783            }
13784        }
13785    });
13786    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13787    // Anything to run? Declarative constraints (for any writable field) +
13788    // per-field validators + cross-field hook.
13789    let has_run_validations = has_validators || has_constraints;
13790    // Shared body: declarative field constraints first (length / range /
13791    // choices, serializer-attr-or-model), then per-field validators, then
13792    // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13793    let checks = quote! {
13794        let mut __errors = #root::forms::FormErrors::default();
13795        #( #constraint_blocks )*
13796        #( #validator_calls )*
13797        #cross_validate_call
13798        if __errors.is_empty() {
13799            ::core::result::Result::Ok(())
13800        } else {
13801            ::core::result::Result::Err(__errors)
13802        }
13803    };
13804    let validate_body = quote! {
13805        let __sent = |_: &str| true;
13806        #checks
13807    };
13808    // PATCH: only the keys the body carries.
13809    let validate_patch_body = quote! {
13810        let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13811        #checks
13812    };
13813    // Inherent `validate(&self)` — kept for back-compat with direct
13814    // `serializer.validate()` calls that don't import `ModelSerializer`.
13815    // Emitted only when the serializer declares per-field/cross-field
13816    // validators (unchanged rule) so it never collides with a
13817    // hand-written inherent `validate`.
13818    let validate_method = if has_validators {
13819        quote! {
13820            impl #struct_name {
13821                /// Run the declarative field constraints, every
13822                /// `#[serializer(validate = "...")]` per-field validator,
13823                /// and (when declared) the container-level cross-field
13824                /// validator. Aggregates errors into `FormErrors` keyed by
13825                /// the field name. Returns `Ok(())` when all pass.
13826                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13827                    #validate_body
13828                }
13829            }
13830        }
13831    } else {
13832        quote! {}
13833    };
13834    // Trait-method override so the type-erased ViewSet write path can
13835    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13836    // there's anything to run (declarative constraints inherited from the
13837    // model count); otherwise the trait's default no-op is used.
13838    let trait_validate_override = if has_run_validations {
13839        quote! {
13840            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13841                #validate_body
13842            }
13843        }
13844    } else {
13845        quote! {}
13846    };
13847
13848    // For every `#[serializer(many = S)]` field, emit a
13849    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13850    // helper that maps the parents through `S::from_model`.
13851    let many_setters: Vec<_> = fields_info
13852        .iter()
13853        .filter_map(|fi| {
13854            let many_ty = fi.attrs.many.as_ref()?;
13855            let ident = &fi.ident;
13856            let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13857            Some(quote! {
13858                /// Populate this `many` field by mapping each parent model
13859                /// through the inner serializer's `from_model`. Call after
13860                /// fetching the M2M / one-to-many children since
13861                /// `from_model` itself can't await an SQL query.
13862                pub fn #setter(
13863                    &mut self,
13864                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13865                ) -> &mut Self {
13866                    self.#ident = models.iter()
13867                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13868                        .collect();
13869                    self
13870                }
13871            })
13872        })
13873        .collect();
13874    let many_setters_impl = if many_setters.is_empty() {
13875        quote! {}
13876    } else {
13877        quote! {
13878            impl #struct_name {
13879                #( #many_setters )*
13880            }
13881        }
13882    };
13883
13884    // Generate custom Serialize: skip write_only and serde-skipped fields
13885    let output_fields: Vec<_> = fields_info
13886        .iter()
13887        .filter(|fi| !fi.attrs.write_only && !fi.serde.skip_ser)
13888        .collect();
13889    // `skip_serializing_if` fields count only when rendered.
13890    let output_field_count = output_fields.iter().map(|fi| {
13891        let ident = &fi.ident;
13892        if let Some(pred) = &fi.serde.skip_ser_if {
13893            quote! { + if #pred(&self.#ident) { 0 } else { 1 } }
13894        } else {
13895            quote! { + 1 }
13896        }
13897    });
13898    let output_field_count = quote! { 0usize #( #output_field_count )* };
13899    let serialize_fields = output_fields.iter().map(|fi| {
13900        let ident = &fi.ident;
13901        let name_lit = &fi.ser_name;
13902        if let Some(pred) = &fi.serde.skip_ser_if {
13903            quote! {
13904                if #pred(&self.#ident) {
13905                    __state.skip_field(#name_lit)?;
13906                } else {
13907                    __state.serialize_field(#name_lit, &self.#ident)?;
13908                }
13909            }
13910        } else {
13911            quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13912        }
13913    });
13914
13915    // writable_fields: normal + write_only.
13916    // Exclude:
13917    //   - `read_only` — server-computed.
13918    //   - `skip` — caller sets manually post-from_model.
13919    //   - `method` — computed from a Self::fn(&model) call; accepting
13920    //     it on write is meaningless.
13921    //   - `nested` / `many` — populated from related-model data, not
13922    //     from a field on the wire body.
13923    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13924    // `many` in `writable_fields()`, which made the ViewSet write
13925    // path accept those fields from the JSON body and try to bind
13926    // them to the SQL UPDATE — a silent no-op at best, a type
13927    // mismatch at worst.
13928    // (`is_writable` is defined above, near the constraint codegen.)
13929    let writable_lits: Vec<_> = fields_info
13930        .iter()
13931        .filter(is_writable)
13932        .map(|fi| fi.de_name.clone())
13933        .collect();
13934
13935    // `writable_source_fields`: the MODEL field names of writable
13936    // serializer fields (source-resolved). The ViewSet write path skips
13937    // every model column NOT in this set, so `read_only` / `method` /
13938    // computed fields a client posts are ignored instead of written.
13939    let writable_source_lits: Vec<String> = fields_info
13940        .iter()
13941        .filter(is_writable)
13942        .map(|fi| {
13943            fi.attrs
13944                .source
13945                .clone()
13946                .unwrap_or_else(|| ident_name(&fi.ident))
13947        })
13948        .collect();
13949
13950    // `readable_source_fields`: model columns the JSON output copies
13951    // straight from the model, so `?ordering=` cannot sort on a column
13952    // the API never shows (#1845).
13953    let readable: Vec<&FieldInfo> = fields_info
13954        .iter()
13955        .filter(|fi| {
13956            !fi.attrs.write_only
13957                && !fi.serde.skip_ser
13958                && !fi.attrs.skip
13959                && fi.attrs.method.is_none()
13960                && !fi.attrs.nested
13961                && fi.attrs.many.is_none()
13962                && fi.attrs.slug.is_none()
13963        })
13964        .collect();
13965    let readable_source_lits: Vec<String> = readable
13966        .iter()
13967        .map(|fi| {
13968            fi.attrs
13969                .source
13970                .clone()
13971                .unwrap_or_else(|| ident_name(&fi.ident))
13972        })
13973        .collect();
13974    // Their JSON keys, so the ViewSet finds a renamed cursor column.
13975    let readable_lits: Vec<String> = readable.iter().map(|fi| fi.ser_name.clone()).collect();
13976
13977    // `from_writable_json`: build a partial instance for input
13978    // validation. Writable fields are parsed from the JSON body (keyed
13979    // by serializer field name); every other field defaults. Per-field
13980    // type errors collect into `FormErrors` keyed by the field name.
13981    // Construction is field-by-field (not `Self::default()`) so it works
13982    // for serializers regardless of a struct-level `Default`.
13983    let from_writable_json_inits: Vec<_> = fields_info
13984        .iter()
13985        .map(|fi| {
13986            let ident = &fi.ident;
13987            let fname = &fi.de_name;
13988            let ty = &fi.ty;
13989            if is_writable(&fi) {
13990                quote! {
13991                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13992                        ::core::option::Option::Some(__v) => {
13993                            match #root::__serde_json::from_value::<#ty>(
13994                                ::core::clone::Clone::clone(__v),
13995                            ) {
13996                                ::core::result::Result::Ok(__x) => __x,
13997                                ::core::result::Result::Err(__e) => {
13998                                    __errors.add(#fname.to_owned(), __e.to_string());
13999                                    ::core::default::Default::default()
14000                                }
14001                            }
14002                        }
14003                        ::core::option::Option::None => ::core::default::Default::default(),
14004                    }
14005                }
14006            } else {
14007                quote! { #ident: ::core::default::Default::default() }
14008            }
14009        })
14010        .collect();
14011    // `validate_patch`: overlay each sent writable field on the stored row.
14012    let patch_overlays: Vec<_> = fields_info
14013        .iter()
14014        .filter(is_writable)
14015        .map(|fi| {
14016            let ident = &fi.ident;
14017            let fname = &fi.de_name;
14018            let ty = &fi.ty;
14019            quote! {
14020                if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
14021                    match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
14022                        ::core::result::Result::Ok(__x) => __merged.#ident = __x,
14023                        ::core::result::Result::Err(__e) => {
14024                            __errors.add(#fname.to_owned(), __e.to_string());
14025                        }
14026                    }
14027                }
14028            }
14029        })
14030        .collect();
14031    let patch_validate_call = if has_run_validations {
14032        quote! { __merged.__rustango_validate_patch(__obj) }
14033    } else {
14034        quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
14035    };
14036    let patch_validate_helper = if has_run_validations {
14037        quote! {
14038            impl #struct_name {
14039                #[doc(hidden)]
14040                fn __rustango_validate_patch(
14041                    &self,
14042                    __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
14043                ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14044                    #validate_patch_body
14045                }
14046            }
14047        }
14048    } else {
14049        quote! {}
14050    };
14051
14052    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
14053    // Only includes fields shown in JSON output (skips write_only). For each
14054    // `Option<T>` field, omit from `required` and add `.nullable()`.
14055    let openapi_impl = {
14056        #[cfg(feature = "openapi")]
14057        {
14058            let property_calls = output_fields.iter().map(|fi| {
14059                let name_lit = &fi.ser_name;
14060                let ty = &fi.ty;
14061                let nullable_call = if is_option(ty) {
14062                    quote! { .nullable() }
14063                } else {
14064                    quote! {}
14065                };
14066                // The bounds `validate()` enforces on a written field.
14067                let limits_call = is_writable(fi).then(|| {
14068                    let mname = fi
14069                        .attrs
14070                        .source
14071                        .clone()
14072                        .unwrap_or_else(|| ident_name(&fi.ident));
14073                    let (a, b, c, d) = (
14074                        opt_u64(fi.attrs.max_length),
14075                        opt_u64(fi.attrs.min_length),
14076                        opt_i64(fi.attrs.min),
14077                        opt_i64(fi.attrs.max),
14078                    );
14079                    quote! {
14080                        .field_limits(
14081                            <#model_path as #root::core::Model>::SCHEMA.field(#mname),
14082                            #a, #b, #c, #d,
14083                        )
14084                    }
14085                });
14086                quote! {
14087                    .property(
14088                        #name_lit,
14089                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
14090                            #limits_call
14091                            #nullable_call,
14092                    )
14093                }
14094            });
14095            let required_lits: Vec<_> = output_fields
14096                .iter()
14097                .filter(|fi| !is_option(&fi.ty) && fi.serde.skip_ser_if.is_none())
14098                .map(|fi| fi.ser_name.clone())
14099                .collect();
14100            quote! {
14101                impl #root::openapi::OpenApiSchema for #struct_name {
14102                    fn openapi_schema() -> #root::openapi::Schema {
14103                        #root::openapi::Schema::object()
14104                            #( #property_calls )*
14105                            .required([ #( #required_lits ),* ])
14106                    }
14107                }
14108            }
14109        }
14110        #[cfg(not(feature = "openapi"))]
14111        {
14112            quote! {}
14113        }
14114    };
14115
14116    // The other tables a `nested`, `many` or `slug` field renders rows of.
14117    let related_tables = fields_info.iter().filter_map(|fi| {
14118        let name = &fi.ser_name;
14119        if let Some(inner) = &fi.attrs.many {
14120            return Some(quote! {
14121                (#name, <<#inner as #root::serializer::ModelSerializer>::Model
14122                    as #root::core::Model>::SCHEMA.table)
14123            });
14124        }
14125        if !(fi.attrs.nested || fi.attrs.slug.is_some()) {
14126            return None;
14127        }
14128        let src_name = fi
14129            .attrs
14130            .source
14131            .as_deref()
14132            .unwrap_or(&ident_name(&fi.ident))
14133            .to_owned();
14134        let src_ident = ident_from_name(&src_name, fi.ident.span());
14135        Some(quote! {
14136            (#name, #root::serializer::relation_table(|__m: &#model_path| &__m.#src_ident))
14137        })
14138    });
14139
14140    Ok(quote! {
14141        impl #root::serializer::ModelSerializer for #struct_name {
14142            type Model = #model_path;
14143
14144            fn related_tables() -> ::std::vec::Vec<(&'static str, &'static str)> {
14145                ::std::vec![ #( #related_tables ),* ]
14146            }
14147
14148            fn from_model(model: &Self::Model) -> Self {
14149                Self {
14150                    #( #from_model_fields ),*
14151                }
14152            }
14153
14154            fn writable_fields() -> &'static [&'static str] {
14155                &[ #( #writable_lits ),* ]
14156            }
14157
14158            fn writable_source_fields() -> &'static [&'static str] {
14159                &[ #( #writable_source_lits ),* ]
14160            }
14161
14162            fn readable_source_fields() -> &'static [&'static str] {
14163                &[ #( #readable_source_lits ),* ]
14164            }
14165
14166            fn readable_fields() -> &'static [&'static str] {
14167                &[ #( #readable_lits ),* ]
14168            }
14169
14170            fn from_writable_json(
14171                __body: &#root::__serde_json::Value,
14172            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
14173                let mut __errors = #root::forms::FormErrors::default();
14174                let __obj = __body.as_object();
14175                let __out = Self {
14176                    #( #from_writable_json_inits ),*
14177                };
14178                if __errors.is_empty() {
14179                    ::core::result::Result::Ok(__out)
14180                } else {
14181                    ::core::result::Result::Err(__errors)
14182                }
14183            }
14184
14185            fn validate_patch(
14186                __stored: &Self::Model,
14187                __body: &#root::__serde_json::Value,
14188            ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14189                let mut __errors = #root::forms::FormErrors::default();
14190                let __obj = __body.as_object();
14191                #[allow(unused_mut)]
14192                let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
14193                #( #patch_overlays )*
14194                if !__errors.is_empty() {
14195                    return ::core::result::Result::Err(__errors);
14196                }
14197                #patch_validate_call
14198            }
14199
14200            #trait_validate_override
14201        }
14202
14203        #patch_validate_helper
14204
14205        impl #root::__serde::Serialize for #struct_name {
14206            fn serialize<S>(&self, serializer: S)
14207                -> ::core::result::Result<S::Ok, S::Error>
14208            where
14209                S: #root::__serde::Serializer,
14210            {
14211                use #root::__serde::ser::SerializeStruct;
14212                let mut __state = serializer.serialize_struct(
14213                    #struct_name_lit,
14214                    #output_field_count,
14215                )?;
14216                #( #serialize_fields )*
14217                __state.end()
14218            }
14219        }
14220
14221        #openapi_impl
14222
14223        #validate_method
14224
14225        #many_setters_impl
14226    })
14227}
14228
14229/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
14230/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
14231/// when the feature is off.
14232#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
14233fn is_option(ty: &syn::Type) -> bool {
14234    if let syn::Type::Path(p) = ty {
14235        if let Some(last) = p.path.segments.last() {
14236            return last.ident == "Option";
14237        }
14238    }
14239    false
14240}
14241
14242#[cfg(test)]
14243mod main_attr_tests {
14244    use super::*;
14245
14246    fn expand(args: &str) -> String {
14247        let body = quote! { async fn main() { run().await } };
14248        expand_main(args.parse().unwrap(), body)
14249            .expect("expansion failed")
14250            .to_string()
14251    }
14252
14253    /// Asserts on the expansion rather than on `parse_main_args`,
14254    /// because the defect was a subscriber reaching the output, not
14255    /// an argument being misread (#1465).
14256    #[test]
14257    fn logging_false_emits_no_subscriber() {
14258        let out = expand("logging = false");
14259        assert!(
14260            !out.contains("logging :: Setup"),
14261            "`logging = false` must not install a subscriber:\n{out}",
14262        );
14263        assert!(
14264            out.contains("block_on"),
14265            "the runtime must still be built:\n{out}",
14266        );
14267    }
14268
14269    #[test]
14270    fn default_still_installs_a_subscriber() {
14271        // The control. Without it, an `expand_main` that dropped the
14272        // prologue unconditionally would pass the test above while
14273        // leaving every default project with no logging at all.
14274        let out = expand("");
14275        assert!(
14276            out.contains("logging :: Setup"),
14277            "the default entrypoint must still install one:\n{out}",
14278        );
14279        // `./app serve` or `./app --port 8080` must keep logging to stdout:
14280        // only `Cli`, which knows its verbs, moves logs to stderr.
14281        assert!(
14282            !out.contains("args ()"),
14283            "the entrypoint must not route logs by argv:\n{out}",
14284        );
14285    }
14286
14287    #[test]
14288    fn logging_false_composes_with_flavor() {
14289        let out = expand("flavor = \"current_thread\", logging = false");
14290        assert!(!out.contains("logging :: Setup"), "{out}");
14291        assert!(out.contains("new_current_thread"), "{out}");
14292    }
14293
14294    #[test]
14295    fn logging_true_is_the_default_shape() {
14296        assert!(
14297            parse_main_args(&"logging = true".parse().unwrap())
14298                .unwrap()
14299                .logging
14300        );
14301        assert!(
14302            !parse_main_args(&"logging=false".parse().unwrap())
14303                .unwrap()
14304                .logging
14305        );
14306    }
14307}
14308
14309#[cfg(test)]
14310mod main_arg_tests {
14311    use super::*;
14312
14313    fn expand(args: &str) -> syn::Result<TokenStream2> {
14314        let body = quote! { async fn main() { run().await } };
14315        expand_main(args.parse().unwrap(), body)
14316    }
14317
14318    /// A typo or unknown key is a compile error, not silently ignored.
14319    #[test]
14320    fn unknown_or_misspelt_args_are_errors() {
14321        for args in [
14322            "logging = flase",
14323            "multi_thread",
14324            "workers = 4",
14325            r#"flavor = "curent_thread""#,
14326            "worker_threads = 4",
14327        ] {
14328            let err = expand(args).expect_err(args).to_string();
14329            assert!(err.contains("#[rustango::main]"), "{args}: {err}");
14330        }
14331    }
14332
14333    #[test]
14334    fn supported_args_expand() {
14335        for args in [
14336            "",
14337            r#"flavor = "current_thread""#,
14338            r#"flavor = "multi_thread", logging = false"#,
14339            "logging = true",
14340        ] {
14341            assert!(expand(args).is_ok(), "{args}");
14342        }
14343    }
14344}
14345
14346#[cfg(test)]
14347mod embed_migrations_tests {
14348    use super::*;
14349
14350    fn expand(path: &str) -> String {
14351        expand_embed_migrations(quote! { #path })
14352            .expect("expansion failed")
14353            .to_string()
14354    }
14355
14356    #[test]
14357    fn missing_dir_warns() {
14358        let out = expand("./no_such_migrations_dir");
14359        assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
14360        assert!(out.contains("no_such_migrations_dir"), "{out}");
14361    }
14362
14363    #[test]
14364    fn existing_dir_does_not_warn() {
14365        let out = expand("./src");
14366        assert!(!out.contains("deprecated"), "{out}");
14367    }
14368}
14369
14370#[cfg(test)]
14371mod field_attr_tests {
14372    use super::*;
14373
14374    fn error_for(field: syn::Field) -> Option<String> {
14375        let attrs = parse_field_attrs(&field).unwrap();
14376        let kind = detect_type(&field.ty).unwrap().kind;
14377        check_bound_compatibility(&field, &attrs, kind)
14378            .err()
14379            .map(|e| e.to_string())
14380    }
14381
14382    /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
14383    #[test]
14384    fn type_specific_attrs_reject_other_types() {
14385        let f: syn::FieldsNamed = syn::parse_quote!({
14386            #[rustango(citext)] a: i64,
14387            #[rustango(vector(dims = 3))] b: String,
14388            #[rustango(geometry(srid = 4326))] c: String,
14389            #[rustango(citext)] d: String,
14390        });
14391        let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
14392        assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
14393        assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
14394        assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
14395        assert_eq!(errs[3], None);
14396    }
14397
14398    /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
14399    #[test]
14400    fn manager_ext_is_refused() {
14401        let input: DeriveInput = syn::parse_quote! {
14402            #[rustango(table = "p", manager(ext = "PostManagerExt"))]
14403            struct Post { id: i64 }
14404        };
14405        let err = parse_container_attrs(&input).err().expect("refused");
14406        assert!(err.to_string().contains("#2132"), "{err}");
14407    }
14408
14409    /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
14410    #[test]
14411    fn set_null_needs_an_optional_foreign_key() {
14412        let input: DeriveInput = syn::parse_quote! {
14413            #[rustango(table = "c")]
14414            struct Child {
14415                #[rustango(primary_key)] id: i64,
14416                #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
14417            }
14418        };
14419        let err = expand(&input).err().expect("refused");
14420        assert!(err.to_string().contains("set_null"), "{err}");
14421        let input: DeriveInput = syn::parse_quote! {
14422            #[rustango(table = "c")]
14423            struct Child {
14424                #[rustango(primary_key)] id: i64,
14425                #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
14426            }
14427        };
14428        assert!(expand(&input).is_ok());
14429    }
14430
14431    /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
14432    #[test]
14433    fn set_null_needs_a_nullable_fk_column() {
14434        let input: DeriveInput = syn::parse_quote! {
14435            #[rustango(table = "c")]
14436            struct Child {
14437                #[rustango(primary_key)] id: i64,
14438                #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
14439                #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
14440            }
14441        };
14442        let err = expand(&input).err().expect("refused");
14443        assert!(err.to_string().contains("set_null"), "{err}");
14444        let input: DeriveInput = syn::parse_quote! {
14445            #[rustango(table = "c")]
14446            struct Child {
14447                #[rustango(primary_key)] id: i64,
14448                #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
14449                #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
14450                #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
14451            }
14452        };
14453        assert!(expand(&input).is_ok());
14454    }
14455
14456    /// An index or `get_latest_by` naming no column is a compile error,
14457    /// one per name.
14458    #[test]
14459    fn unknown_index_columns_are_errors() {
14460        let input: DeriveInput = syn::parse_quote! {
14461            #[rustango(
14462                table = "w",
14463                unique_together = "a, nope",
14464                index_together = "a, b_typo",
14465                index("missing"),
14466                get_latest_by = "-when_typo"
14467            )]
14468            struct W { #[rustango(primary_key)] id: i64, a: i64, b: i64 }
14469        };
14470        let err = expand(&input).expect_err("must not compile");
14471        let all: Vec<String> = err.into_iter().map(|e| e.to_string()).collect();
14472        for name in ["nope", "b_typo", "missing", "when_typo"] {
14473            assert!(
14474                all.iter().any(|m| m.contains(&format!("`{name}`"))),
14475                "{name}: {all:?}"
14476            );
14477        }
14478    }
14479
14480    /// Real columns, field names for `get_latest_by` and expression
14481    /// indexes compile.
14482    #[test]
14483    fn known_index_columns_compile() {
14484        let input: DeriveInput = syn::parse_quote! {
14485            #[rustango(
14486                table = "w",
14487                unique_together = "a, b_col",
14488                index("lower(a)"),
14489                get_latest_by = "-b"
14490            )]
14491            struct W {
14492                #[rustango(primary_key)] id: i64,
14493                a: i64,
14494                #[rustango(column = "b_col")] b: i64,
14495            }
14496        };
14497        expand(&input).expect("compiles");
14498    }
14499
14500    /// Two FKs to one parent would emit the same reverse accessor twice.
14501    #[test]
14502    fn reverse_accessor_clash_names_related_name() {
14503        let input: DeriveInput = syn::parse_quote! {
14504            #[rustango(table = "m")]
14505            struct Message {
14506                #[rustango(primary_key)] id: i64,
14507                sender: ForeignKey<User>,
14508                recipient: ForeignKey<User>,
14509            }
14510        };
14511        let err = expand(&input).err().expect("refused");
14512        assert!(err.to_string().contains("related_name"), "{err}");
14513    }
14514
14515    /// `related_name = "+"` emits no reverse accessor, so a parent from
14516    /// another crate, which cannot take an inherent impl, still derives.
14517    #[test]
14518    fn related_name_plus_skips_the_reverse_accessor() {
14519        let input: DeriveInput = syn::parse_quote! {
14520            #[rustango(table = "m")]
14521            struct Message {
14522                #[rustango(primary_key)] id: i64,
14523                sender: ForeignKey<User>,
14524                #[rustango(related_name = "+")] recipient: ForeignKey<User>,
14525                #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
14526            }
14527        };
14528        let out = expand(&input).expect("derives").to_string();
14529        assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
14530        assert!(!out.contains("impl other :: Account {"), "{out}");
14531    }
14532
14533    fn field_error(src: &str) -> Option<String> {
14534        let f: syn::FieldsNamed = syn::parse_str(&format!("{{ {src} }}")).unwrap();
14535        let field = f.named.into_iter().next().unwrap();
14536        match parse_field_attrs(&field) {
14537            Err(e) => Some(e.to_string()),
14538            Ok(_) => error_for(field),
14539        }
14540    }
14541
14542    /// Flag attrs take `= true` like `blank`; `= false` is refused by name.
14543    #[test]
14544    fn flag_attrs_accept_eq_true() {
14545        for key in [
14546            "primary_key",
14547            "unique",
14548            "soft_delete",
14549            "index",
14550            "auto_now",
14551            "auto_now_add",
14552        ] {
14553            let f: syn::FieldsNamed =
14554                syn::parse_str(&format!("{{ #[rustango({key} = true)] a: i64 }}")).unwrap();
14555            let field = f.named.into_iter().next().unwrap();
14556            assert!(parse_field_attrs(&field).is_ok(), "{key} = true");
14557            let err = field_error(&format!("#[rustango({key} = false)] a: i64")).unwrap();
14558            assert!(err.contains(&format!("`{key} = false`")), "{err}");
14559        }
14560        let f: syn::Field =
14561            syn::parse_quote!(#[rustango(primary_key = true, unique = true)] a: i64);
14562        let attrs = parse_field_attrs(&f).unwrap();
14563        assert!(attrs.primary_key && attrs.unique);
14564    }
14565
14566    /// An unknown key is named, the nearest key suggested and the list shown.
14567    #[test]
14568    fn unknown_key_is_named_with_a_suggestion() {
14569        let err = field_error("#[rustango(max_lenght = 5)] a: String").unwrap();
14570        assert!(err.contains("`max_lenght`"), "{err}");
14571        assert!(err.contains("did you mean `max_length`"), "{err}");
14572        assert!(err.contains("Supported: column"), "{err}");
14573        let input: DeriveInput = syn::parse_quote! {
14574            #[rustango(tabel = "p")]
14575            struct P { id: i64 }
14576        };
14577        let err = parse_container_attrs(&input).err().unwrap().to_string();
14578        assert!(
14579            err.contains("`tabel`") && err.contains("did you mean `table`"),
14580            "{err}"
14581        );
14582        let err = field_error("#[rustango(zzzzzz)] a: i64").unwrap();
14583        assert!(!err.contains("did you mean"), "{err}");
14584    }
14585
14586    /// The listed keys are the parsed keys; a stale list would mislead.
14587    #[test]
14588    fn listed_keys_are_all_known() {
14589        for key in FIELD_ATTR_KEYS {
14590            let err = field_error(&format!("#[rustango({key})] a: i64")).unwrap_or_default();
14591            assert!(!err.contains("unknown rustango field"), "{key}: {err}");
14592        }
14593        for key in CONTAINER_ATTR_KEYS {
14594            let input: DeriveInput =
14595                syn::parse_str(&format!("#[rustango({key})] struct P {{ id: i64 }}")).unwrap();
14596            let err = parse_container_attrs(&input)
14597                .err()
14598                .map(|e| e.to_string())
14599                .unwrap_or_default();
14600            assert!(!err.contains("unknown rustango container"), "{key}: {err}");
14601        }
14602    }
14603
14604    /// `default` is raw SQL: a bare word or a non-number on a numeric field is refused.
14605    #[test]
14606    fn default_shapes_only_sqlite_takes_are_refused() {
14607        for bad in [
14608            r#"#[rustango(default = "draft")] s: String"#,
14609            r#"#[rustango(default = "draft")] s: Option<String>"#,
14610            r#"#[rustango(default = "abc")] n: i32"#,
14611            r#"#[rustango(default = "'abc'")] n: i64"#,
14612            r#"#[rustango(default = "true")] n: i32"#,
14613            r#"#[rustango(default = "")] n: i32"#,
14614            r#"#[rustango(default = "'x'")] n: f64"#,
14615        ] {
14616            assert!(field_error(bad).is_some(), "{bad} must be refused");
14617        }
14618        let err = field_error(r#"#[rustango(default = "draft")] s: String"#).unwrap();
14619        assert!(err.contains(r#"default = "'draft'""#), "{err}");
14620        // Valid on Postgres alone, so the message says that, not "a column".
14621        let err = field_error(r#"#[rustango(default = "current_user")] s: String"#).unwrap();
14622        assert!(
14623            err.contains("Postgres only") && !err.contains("column"),
14624            "{err}"
14625        );
14626        for good in [
14627            r#"#[rustango(default = "'draft'")] s: String"#,
14628            r#"#[rustango(default = "")] s: String"#,
14629            r#"#[rustango(default = "now()")] s: String"#,
14630            r#"#[rustango(default = "CURRENT_TIMESTAMP")] s: String"#,
14631            r#"#[rustango(default = "NULL")] s: Option<String>"#,
14632            r#"#[rustango(default = "0")] n: i32"#,
14633            r#"#[rustango(default = "-1")] n: i64"#,
14634            r#"#[rustango(default = "'5'")] n: i64"#,
14635            r#"#[rustango(default = "(1 + 2)")] n: i64"#,
14636            r#"#[rustango(default = "1.5")] n: f64"#,
14637            r#"#[rustango(default = "true")] b: bool"#,
14638        ] {
14639            assert_eq!(field_error(good), None, "{good}");
14640        }
14641    }
14642
14643    /// A raw ident derives names without `r#`; this used to panic (#1937).
14644    #[test]
14645    fn raw_idents_derive_plain_names() {
14646        let ident: syn::Ident = syn::parse_quote!(r#type);
14647        assert_eq!(column_type_ident(&ident).to_string(), "type_col");
14648        assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
14649    }
14650}
14651
14652#[cfg(test)]
14653mod model_attr_name_tests {
14654    use super::*;
14655
14656    fn container_error(item: proc_macro2::TokenStream) -> Option<String> {
14657        let input: DeriveInput = syn::parse2(item).unwrap();
14658        parse_container_attrs(&input).err().map(|e| e.to_string())
14659    }
14660
14661    fn field_error(field: proc_macro2::TokenStream) -> Option<String> {
14662        let f: syn::FieldsNamed = syn::parse2(quote!({ #field })).unwrap();
14663        let field = f.named.into_iter().next().unwrap();
14664        parse_field_attrs(&field).err().map(|e| e.to_string())
14665    }
14666
14667    /// `#[rgo(...)]` reads exactly as `#[rustango(...)]`.
14668    #[test]
14669    fn rgo_is_read_like_rustango() {
14670        let long: DeriveInput = syn::parse_quote! {
14671            #[rustango(table = "t", display = "name")] struct S;
14672        };
14673        let short: DeriveInput = syn::parse_quote! {
14674            #[rgo(table = "t", display = "name")] struct S;
14675        };
14676        let (long, short) = (
14677            parse_container_attrs(&long).unwrap(),
14678            parse_container_attrs(&short).unwrap(),
14679        );
14680        assert_eq!(long.table, short.table);
14681        assert_eq!(short.table.as_deref(), Some("t"));
14682        assert_eq!(long.display.map(|d| d.0), short.display.map(|d| d.0));
14683    }
14684
14685    #[test]
14686    fn mixing_names_on_one_item_or_field_is_refused() {
14687        let err = container_error(quote! {
14688            #[rustango(table = "t")] #[rgo(display = "name")] struct S;
14689        })
14690        .expect("a mix on an item must fail");
14691        assert!(
14692            err.contains("`#[rustango(...)]` and `#[rgo(...)]`"),
14693            "{err}"
14694        );
14695        let err = field_error(quote! { #[rgo(primary_key)] #[rustango(column = "c")] a: i64 })
14696            .expect("a mix on a field must fail");
14697        assert!(
14698            err.contains("`#[rgo(...)]` and `#[rustango(...)]`"),
14699            "{err}"
14700        );
14701        // Either name repeated alone is fine.
14702        assert_eq!(
14703            container_error(quote! { #[rgo(table = "t")] #[rgo(display = "name")] struct S; }),
14704            None
14705        );
14706        assert_eq!(
14707            field_error(quote! { #[rustango(primary_key)] #[rustango(column = "c")] a: i64 }),
14708            None
14709        );
14710    }
14711
14712    /// A hint quotes the attribute under the name the user wrote.
14713    #[test]
14714    fn hints_use_the_written_name() {
14715        let err = container_error(quote! { #[rgo(table = "t", unique_together = "a")] struct S; })
14716            .expect("one column must fail");
14717        assert!(err.contains("#[rgo(unique)]"), "{err}");
14718        let err =
14719            container_error(quote! { #[rustango(table = "t", index_together = "a")] struct S; })
14720                .expect("one column must fail");
14721        assert!(err.contains("#[rustango(index)]"), "{err}");
14722    }
14723}