Skip to main content

rustango_macros/
lib.rs

1//! Proc-macros for rustango.
2//!
3//! v0.1 ships `#[derive(Model)]`, which emits:
4//! * a `Model` impl carrying a static `ModelSchema`,
5//! * an `inventory::submit!` so the model is discoverable from the registry,
6//! * an inherent `objects()` returning a `QuerySet<Self>`,
7//! * a `sqlx::FromRow` impl so query results decode into the struct.
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13    parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14    PathArguments, Type, TypePath,
15};
16
17/// Resolve the consumer's local name for the `rustango` crate so the
18/// macro-emitted code keeps compiling when a downstream `Cargo.toml`
19/// renames the dep (`[dependencies] orm = { package = "rustango", … }`)
20/// or when the standalone `rustango-orm` crate ships in a future
21/// slice of the [orm-extract epic](https://github.com/ujeenet/rustango/issues/149).
22///
23/// Returns one of:
24/// - `quote!(::rustango)` — the consumer IS the `rustango` crate
25///   itself. We emit the absolute path `::rustango` (NOT `crate`)
26///   because examples and integration tests inside the rustango
27///   package compile as separate binaries — their own `crate::`
28///   namespace is the example/test file, not rustango's lib root.
29///   The absolute `::rustango` resolves correctly in both contexts
30///   (rustango lib code AND rustango/examples/*.rs).
31/// - `quote!(::ident)` — the consumer renamed the dep; emit the
32///   user's chosen ident.
33/// - `quote!(::rustango)` — fallback when `proc-macro-crate` can't
34///   read the manifest (rare; preserves today's behavior).
35///
36/// Issue [#142](https://github.com/ujeenet/rustango/issues/142).
37fn rustango_root() -> TokenStream2 {
38    use proc_macro_crate::{crate_name, FoundCrate};
39    match crate_name("rustango") {
40        Ok(FoundCrate::Itself) => quote!(::rustango),
41        Ok(FoundCrate::Name(name)) => {
42            let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
43            quote!(::#ident)
44        }
45        Err(_) => quote!(::rustango),
46    }
47}
48
49/// Derive a `Model` impl. See crate docs for the supported attributes.
50#[proc_macro_derive(Model, attributes(rustango))]
51pub fn derive_model(input: TokenStream) -> TokenStream {
52    let input = parse_macro_input!(input as DeriveInput);
53    expand(&input)
54        .unwrap_or_else(syn::Error::into_compile_error)
55        .into()
56}
57
58/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
59/// marker struct, wiring the full CRUD ViewSet in one annotation.
60///
61/// ```ignore
62/// #[derive(ViewSet)]
63/// #[viewset(
64///     model        = Post,
65///     fields       = "id, title, body, author_id",
66///     filter_fields = "author_id",
67///     search_fields = "title, body",
68///     ordering     = "-published_at",
69///     page_size    = 20,
70/// )]
71/// pub struct PostViewSet;
72///
73/// // Mount into your app:
74/// let app = Router::new()
75///     .merge(PostViewSet::router("/api/posts", pool.clone()));
76/// ```
77///
78/// Attributes:
79/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
80///   `SCHEMA` constant drives the endpoints.
81/// * `fields = "a, b, c"` — scalar fields included in list/retrieve JSON
82///   and accepted on create/update (default: all scalar fields).
83/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
84/// * `search_fields = "a, b"` — fields searched by `?search=...`.
85/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
86/// * `page_size = N` — default page size (default: 20, max: 1000).
87/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
88/// * `serializer = SomeSerializer` — render list / retrieve / create
89///   responses through a `#[derive(Serializer)]` type instead of the
90///   default field-level projection (`read_only` / `source` / `method`
91///   / `write_only` overrides apply). Tri-dialect. Requires the
92///   `serializer` feature.
93/// * `permissions(list = "...", retrieve = "...", create = "...",
94///   update = "...", destroy = "...")` — codenames required per action.
95#[proc_macro_derive(ViewSet, attributes(viewset))]
96pub fn derive_viewset(input: TokenStream) -> TokenStream {
97    let input = parse_macro_input!(input as DeriveInput);
98    expand_viewset(&input)
99        .unwrap_or_else(syn::Error::into_compile_error)
100        .into()
101}
102
103/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
104/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
105/// that walks every named field and:
106///
107/// * Parses the string value into the field's Rust type (`String`,
108///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
109///   nullable case).
110/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
111///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
112///   validators in declaration order, returning `FormError::Parse`
113///   on the first failure.
114///
115/// Example:
116///
117/// ```ignore
118/// #[derive(Form)]
119/// pub struct CreateItemForm {
120///     #[form(min_length = 1, max_length = 64)]
121///     pub name: String,
122///     #[form(min = 0, max = 150)]
123///     pub age: i32,
124///     pub active: bool,
125///     pub email: Option<String>,
126/// }
127///
128/// let parsed = CreateItemForm::parse(&form_map)?;
129/// ```
130#[proc_macro_derive(Form, attributes(form))]
131pub fn derive_form(input: TokenStream) -> TokenStream {
132    let input = parse_macro_input!(input as DeriveInput);
133    expand_form(&input)
134        .unwrap_or_else(syn::Error::into_compile_error)
135        .into()
136}
137
138/// Derive `rustango::serializer::ModelSerializer` for a struct.
139/// (intra-doc link disabled — the macro crate doesn't depend on
140/// `rustango` itself, so rustdoc can't resolve the path.)
141///
142/// # Container attribute (required)
143/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
144///
145/// # Field attributes
146/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
147/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
148/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
149/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
150/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
151/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
152/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
153/// - `#[serializer(slug = "name")]` — render a relation as one of its fields: clones `model.<source>.value()?.name`; excluded from `writable_fields()` (v0.44)
154/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
155/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
156///   write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
157///   no attr is given, override it when given (`min_length` is serializer-only)
158///
159/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
160#[proc_macro_derive(Serializer, attributes(serializer))]
161pub fn derive_serializer(input: TokenStream) -> TokenStream {
162    let input = parse_macro_input!(input as DeriveInput);
163    expand_serializer(&input)
164        .unwrap_or_else(syn::Error::into_compile_error)
165        .into()
166}
167
168/// Bake every `*.json` migration file in a directory into the binary
169/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
170/// of `(name, json_content)` pairs, lex-sorted by file stem.
171///
172/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
173/// behaviour as `migrate(pool, dir)` but with no filesystem access.
174/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
175/// (i.e. the crate that invokes the macro). Default is
176/// `"./migrations"` if no argument is supplied.
177///
178/// ```ignore
179/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
180/// // or:
181/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
182///
183/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
184/// ```
185///
186/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
187/// file's `name` field must equal its file stem, every `prev`
188/// reference must point to another migration in the same directory,
189/// and the JSON must parse. A broken chain — orphan `prev`, missing
190/// predecessor, malformed file — fails at macro-expansion time with
191/// a clear `compile_error!`. *No other Rust web framework
192/// validates migration chains at compile time*: Cot's migrations are
193/// imperative Rust code (no static chain), Loco's are SeaORM
194/// up/down (same), Rwf's are raw SQL (no chain at all).
195///
196/// Each migration is included via `include_str!` so cargo's rebuild
197/// detection picks up file *content* changes. **Caveat:** cargo
198/// doesn't watch directory listings, so adding or removing a
199/// migration file inside the dir won't auto-trigger a rebuild — run
200/// `cargo clean` (or just bump any other source file) when you add
201/// new migrations during embedded development.
202#[proc_macro]
203pub fn embed_migrations(input: TokenStream) -> TokenStream {
204    expand_embed_migrations(input.into())
205        .unwrap_or_else(syn::Error::into_compile_error)
206        .into()
207}
208
209/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
210/// compile time against typed columns. Issue #269 / T1.7.
211///
212/// Each invocation lowers to the equivalent typed-column method call:
213///
214/// ```ignore
215/// // These expand identically:
216/// Q!(User.email__icontains = "alice")
217/// User::email.ilike("%alice%")
218/// ```
219///
220/// Field-name typos fail the build (the macro emits `User::no_such_field`
221/// which doesn't exist) — the headline ergonomic win of this slice over
222/// filters keyed by a plain string.
223///
224/// # Supported lookup suffixes
225///
226/// * bare `=` / `__exact` → `.eq(value)`
227/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
228/// * `__ne` → `.ne(value)`
229/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
230/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
231/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
232/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
233/// * `__in` → `.is_in(iterable)`
234/// * `__not_in` → `.not_in(iterable)`
235/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
236/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
237/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
238///
239/// Unknown suffixes fail the build with a `compile_error!` pointing at
240/// the lookup token.
241///
242/// # Combine
243///
244/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
245/// `.and()` / `.or()` / `.not()` methods:
246///
247/// ```ignore
248/// User::objects()
249///     .where_(
250///         Q!(User.active = true)
251///             .and(Q!(User.email__icontains = "alice"))
252///     )
253///     .fetch(&pool).await?;
254/// ```
255///
256/// All emitted code routes through existing per-dialect writers — no new
257/// SQL emission machinery. Tri-dialect support is inherent.
258#[allow(non_snake_case)]
259#[proc_macro]
260pub fn Q(input: TokenStream) -> TokenStream {
261    expand_q(input.into())
262        .unwrap_or_else(syn::Error::into_compile_error)
263        .into()
264}
265
266/// `#[rustango::main]` — the runserver entrypoint. Wraps
267/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
268/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
269/// functions are zero-boilerplate:
270///
271/// ```ignore
272/// #[rustango::main]
273/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
274///     rustango::server::Builder::from_env().await?
275///         .migrate("migrations").await?
276///         .api(my_app::urls::api())
277///         .seed_with(my_app::seed::run).await?
278///         .serve("0.0.0.0:8080").await
279/// }
280/// ```
281///
282/// Optional `flavor = "current_thread"` passes through to
283/// `#[tokio::main]`; default is the multi-threaded runtime.
284///
285/// `logging = false` suppresses the default subscriber, for apps
286/// that install their own. Needed whenever the body reaches
287/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
288/// macro installs gets there first, and `tracing` keeps the first
289/// one, so the `[logging]` section would otherwise be read and
290/// discarded (#1465).
291///
292/// ```ignore
293/// #[rustango::main(logging = false)]
294/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
295///     rustango::manage::Cli::new()
296///         .with_settings_from_env()
297///         .with_logging()          // now the first installer
298///         .run().await
299/// }
300/// ```
301///
302/// Pulls `tracing-subscriber` into the rustango crate behind the
303/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
304/// out get plain `#[tokio::main]` ergonomics without the dependency.
305#[proc_macro_attribute]
306pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
307    expand_main(args.into(), item.into())
308        .unwrap_or_else(syn::Error::into_compile_error)
309        .into()
310}
311
312fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
313    let mut input: syn::ItemFn = syn::parse2(item)?;
314    if input.sig.asyncness.is_none() {
315        return Err(syn::Error::new(
316            input.sig.ident.span(),
317            "`#[rustango::main]` must wrap an `async fn`",
318        ));
319    }
320
321    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
322    // to `#[tokio::main]`. Tokio's proc-macro internally emits
323    // `::tokio::*` paths that resolve against the user crate's deps,
324    // so calling it through the rustango re-export still requires the
325    // user to add tokio to their own Cargo.toml. Building the
326    // runtime ourselves keeps the dep transitive through the
327    // `runtime` feature on rustango.
328    //
329    // Parse optional `flavor = "current_thread"` / `flavor =
330    // "multi_thread"` from the attribute args. Unknown args are
331    // tolerated (forward-compat with tokio's own arg surface).
332    let root = rustango_root();
333    let flavor = parse_flavor(&args);
334    let builder_call = match flavor {
335        Flavor::CurrentThread => quote! {
336            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
337        },
338        Flavor::MultiThread => quote! {
339            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
340        },
341    };
342
343    // Detach the user body and rewrite `main` as a sync fn that
344    // builds the runtime and blocks on the async body.
345    let user_body = input.block.clone();
346    input.sig.asyncness = None;
347
348    // `logging = false` drops the block entirely, so a later
349    // `Cli::with_logging()` is the first installer and its
350    // `[logging]` settings actually take effect (#1465).
351    let logging_prologue = if parse_logging(&args) {
352        quote! {
353            use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
354            // Colour only when stdout is a terminal, and never under
355            // `NO_COLOR` — `Color::Auto`'s rule, called rather than
356            // copied.
357            //
358            // This was inlined at first, on a comment claiming
359            // `rustango::logging` is gated on `admin` + `tenancy`. It is
360            // not: `pub mod logging` is ungated and its contents are
361            // `runtime`-gated — the same feature that gates this macro
362            // (`pub use rustango_macros::main` is `#[cfg(feature =
363            // "runtime")]`). So wherever this expands, `Color` is
364            // reachable, and the copy was justified by a gate that does
365            // not exist. `should_colour` carries a deliberate
366            // `NO_COLOR`-set-but-empty subtlety that the inline happened
367            // to match today and nothing kept matching tomorrow.
368            //
369            // Without any of it the subscriber inherits
370            // tracing-subscriber's default, `cfg!(feature = "ansi")` —
371            // true since the framework turned that feature on — so
372            // `./app > app.log` and every container redirecting stdout
373            // collected escape codes. `Setup::install` has its own
374            // check; this is the default entrypoint and had none.
375            let __ansi = #root::logging::Color::Auto.should_colour();
376            // `try_init` so duplicate installers (e.g. tests already
377            // holding a subscriber) don't panic.
378            let _ = tracing_subscriber::fmt()
379                .with_env_filter(
380                    EnvFilter::try_from_default_env()
381                        .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
382                )
383                .with_ansi(__ansi)
384                .try_init();
385        }
386    } else {
387        quote! {}
388    };
389
390    input.block = syn::parse2(quote! {{
391        { #logging_prologue }
392        let __rt = #builder_call
393            .enable_all()
394            .build()
395            .expect("failed to build tokio runtime");
396        __rt.block_on(async move #user_body)
397    }})?;
398
399    Ok(quote! {
400        #input
401    })
402}
403
404enum Flavor {
405    MultiThread,
406    CurrentThread,
407}
408
409fn parse_flavor(args: &TokenStream2) -> Flavor {
410    // Cheap parser: look for the literal token sequence
411    // `flavor = "current_thread"`. Everything else (including
412    // bare `multi_thread` or no args) defaults to multi-thread.
413    let s = args.to_string();
414    if s.contains("current_thread") {
415        Flavor::CurrentThread
416    } else {
417        Flavor::MultiThread
418    }
419}
420
421/// `false` when the attribute carries `logging = false` — the caller
422/// installs their own subscriber, usually via `Cli::with_logging`
423/// (#1465). Same cheap token match as `parse_flavor`.
424fn parse_logging(args: &TokenStream2) -> bool {
425    !args.to_string().replace(' ', "").contains("logging=false")
426}
427
428/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
429struct QInput {
430    base_path: syn::Path,
431    field: syn::Ident,
432    value: syn::Expr,
433}
434
435impl syn::parse::Parse for QInput {
436    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
437        let base_path: syn::Path = input.parse()?;
438        input.parse::<syn::Token![.]>()?;
439        let field: syn::Ident = input.parse()?;
440        input.parse::<syn::Token![=]>()?;
441        let value: syn::Expr = input.parse()?;
442        Ok(QInput {
443            base_path,
444            field,
445            value,
446        })
447    }
448}
449
450fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
451    let q: QInput = syn::parse2(input)?;
452    let root = rustango_root();
453    let field_str = q.field.to_string();
454    let field_span = q.field.span();
455    let (base, suffix) = match field_str.find("__") {
456        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
457        None => (field_str.as_str(), ""),
458    };
459    if base.is_empty() {
460        return Err(syn::Error::new(
461            field_span,
462            "Q!(): field name is empty before `__` suffix",
463        ));
464    }
465    let base_ident = syn::Ident::new(base, field_span);
466    let value = &q.value;
467    let path = &q.base_path;
468
469    // Most suffixes map directly to a Column method with the value
470    // forwarded unchanged. Some need value-shape massaging (wildcards
471    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
472    // ISNULL). Unknown suffixes fail the build.
473    let expanded = match suffix {
474        "" | "exact" => quote! {
475            #root::core::Column::eq(#path::#base_ident, #value)
476        },
477        "ne" => quote! {
478            #root::core::Column::ne(#path::#base_ident, #value)
479        },
480        "gt" => quote! {
481            #root::core::Column::gt(#path::#base_ident, #value)
482        },
483        "gte" => quote! {
484            #root::core::Column::gte(#path::#base_ident, #value)
485        },
486        "lt" => quote! {
487            #root::core::Column::lt(#path::#base_ident, #value)
488        },
489        "lte" => quote! {
490            #root::core::Column::lte(#path::#base_ident, #value)
491        },
492        // The escaped-LIKE lookups delegate to the typed Column
493        // methods, which escape the value and pair it with the
494        // *Escaped ops so `%` / `_` match literally on every dialect
495        // (#1257) — the macro no longer owns any pattern wrapping.
496        "iexact" => quote! {
497            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
498        },
499        "contains" => quote! {
500            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
501        },
502        "icontains" => quote! {
503            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
504        },
505        "startswith" => quote! {
506            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
507        },
508        "istartswith" => quote! {
509            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
510        },
511        "endswith" => quote! {
512            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
513        },
514        "iendswith" => quote! {
515            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
516        },
517        "in" => quote! {
518            #root::core::Column::is_in(#path::#base_ident, #value)
519        },
520        "not_in" => quote! {
521            #root::core::Column::not_in(#path::#base_ident, #value)
522        },
523        "isnull" => {
524            // Must be a bool literal at macro time so we can route to
525            // is_null vs is_not_null without a runtime branch.
526            let b = match value {
527                syn::Expr::Lit(syn::ExprLit {
528                    lit: syn::Lit::Bool(b),
529                    ..
530                }) => b.value(),
531                _ => {
532                    return Err(syn::Error::new_spanned(
533                        value,
534                        "Q!(): `__isnull` requires a `true` or `false` literal",
535                    ));
536                }
537            };
538            if b {
539                quote! { #root::core::Column::is_null(#path::#base_ident) }
540            } else {
541                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
542            }
543        }
544        "between" => {
545            // Accept a tuple literal `(lo, hi)`.
546            let tuple = match value {
547                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
548                _ => {
549                    return Err(syn::Error::new_spanned(
550                        value,
551                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
552                    ));
553                }
554            };
555            let lo = &tuple.elems[0];
556            let hi = &tuple.elems[1];
557            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
558        }
559        "regex" => quote! {
560            #root::core::Column::regex(#path::#base_ident, #value)
561        },
562        "iregex" => quote! {
563            #root::core::Column::iregex(#path::#base_ident, #value)
564        },
565        _ => {
566            return Err(syn::Error::new(
567                field_span,
568                format!(
569                    "Q!(): unknown lookup suffix `__{}`. Supported: __exact / __iexact / __ne / __gt / __gte / __lt / __lte / __contains / __icontains / __startswith / __istartswith / __endswith / __iendswith / __in / __not_in / __isnull / __between / __regex / __iregex",
570                    suffix
571                ),
572            ));
573        }
574    };
575    Ok(expanded)
576}
577
578fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
579    // Default to "./migrations" if invoked without args.
580    let path_str = if input.is_empty() {
581        "./migrations".to_string()
582    } else {
583        let lit: LitStr = syn::parse2(input)?;
584        lit.value()
585    };
586
587    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
588        syn::Error::new(
589            proc_macro2::Span::call_site(),
590            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
591        )
592    })?;
593    let abs = std::path::Path::new(&manifest).join(&path_str);
594
595    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
596    if abs.is_dir() {
597        let read = std::fs::read_dir(&abs).map_err(|e| {
598            syn::Error::new(
599                proc_macro2::Span::call_site(),
600                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
601            )
602        })?;
603        for entry in read.flatten() {
604            let path = entry.path();
605            if !path.is_file() {
606                continue;
607            }
608            if path.extension().and_then(|s| s.to_str()) != Some("json") {
609                continue;
610            }
611            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
612                continue;
613            };
614            entries.push((stem.to_owned(), path));
615        }
616    }
617    entries.sort_by(|a, b| a.0.cmp(&b.0));
618
619    // Compile-time chain validation: read each migration's JSON,
620    // pull `name` and `prev` (file-stem-keyed for the chain check),
621    // and verify every `prev` points to another migration in the
622    // slice. Mismatches between the file stem and the embedded
623    // `name` field — and broken `prev` chains — fail at MACRO
624    // EXPANSION time so a misshapen migration set never compiles.
625    //
626    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
627    // migrations + a Rust proc-macro that reads them is a combo no
628    // other Rust web framework matches (Cot's are imperative Rust
629    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
630    // a static chain to validate).
631    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
632    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
633    for (stem, path) in &entries {
634        let raw = std::fs::read_to_string(path).map_err(|e| {
635            syn::Error::new(
636                proc_macro2::Span::call_site(),
637                format!(
638                    "embed_migrations!: cannot read {} for chain validation: {e}",
639                    path.display()
640                ),
641            )
642        })?;
643        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
644            syn::Error::new(
645                proc_macro2::Span::call_site(),
646                format!(
647                    "embed_migrations!: {} is not valid JSON: {e}",
648                    path.display()
649                ),
650            )
651        })?;
652        let name = json
653            .get("name")
654            .and_then(|v| v.as_str())
655            .ok_or_else(|| {
656                syn::Error::new(
657                    proc_macro2::Span::call_site(),
658                    format!(
659                        "embed_migrations!: {} is missing the `name` field",
660                        path.display()
661                    ),
662                )
663            })?
664            .to_owned();
665        if name != *stem {
666            return Err(syn::Error::new(
667                proc_macro2::Span::call_site(),
668                format!(
669                    "embed_migrations!: file stem `{stem}` does not match the migration's \
670                     `name` field `{name}` — rename the file or fix the JSON",
671                ),
672            ));
673        }
674        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
675        chain_names.push(name.clone());
676        prev_refs.push((name, prev));
677    }
678
679    let name_set: std::collections::HashSet<&str> =
680        chain_names.iter().map(String::as_str).collect();
681    for (name, prev) in &prev_refs {
682        if let Some(p) = prev {
683            if !name_set.contains(p.as_str()) {
684                return Err(syn::Error::new(
685                    proc_macro2::Span::call_site(),
686                    format!(
687                        "embed_migrations!: broken migration chain — `{name}` declares \
688                         prev=`{p}` but no migration with that name exists in {}",
689                        abs.display()
690                    ),
691                ));
692            }
693        }
694    }
695
696    let pairs: Vec<TokenStream2> = entries
697        .iter()
698        .map(|(name, path)| {
699            let path_lit = path.display().to_string();
700            quote! { (#name, ::core::include_str!(#path_lit)) }
701        })
702        .collect();
703
704    Ok(quote! {
705        {
706            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
707            __RUSTANGO_EMBEDDED
708        }
709    })
710}
711
712fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
713    let root = rustango_root();
714    let struct_name = &input.ident;
715
716    let Data::Struct(data) = &input.data else {
717        return Err(syn::Error::new_spanned(
718            struct_name,
719            "Model can only be derived on structs",
720        ));
721    };
722    let Fields::Named(named) = &data.fields else {
723        return Err(syn::Error::new_spanned(
724            struct_name,
725            "Model requires a struct with named fields",
726        ));
727    };
728
729    let container = parse_container_attrs(input)?;
730    let table = container
731        .table
732        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
733    let model_name = struct_name.to_string();
734
735    let collected = collect_fields(named, &table)?;
736
737    // Validate that #[rustango(display = "…")] names a real field.
738    if let Some((ref display, span)) = container.display {
739        if !collected.field_names.iter().any(|n| n == display) {
740            return Err(syn::Error::new(
741                span,
742                format!("`display = \"{display}\"` does not match any field on this struct"),
743            ));
744        }
745    }
746    let display = container.display.map(|(name, _)| name);
747    let app_label = container.app.clone();
748
749    // Validate admin field-name lists against declared field names.
750    // Note: `list_display` is intentionally NOT validated here. As of
751    // v0.32 it may also reference inventory-registered computed
752    // fields (via `register_admin_computed!`) whose existence the
753    // macro can't see at compile time — they're submitted from any
754    // crate that depends on rustango. The runtime list-view resolves
755    // unknown names against the inventory + silently drops the
756    // truly-bogus ones, which is the cheaper trade-off versus
757    // forcing a per-Model attr to opt out.
758    if let Some(admin) = &container.admin {
759        for (label, list) in [
760            ("search_fields", &admin.search_fields),
761            ("readonly_fields", &admin.readonly_fields),
762            ("list_filter", &admin.list_filter),
763        ] {
764            if let Some((names, span)) = list {
765                for name in names {
766                    if !collected.field_names.iter().any(|n| n == name) {
767                        return Err(syn::Error::new(
768                            *span,
769                            format!(
770                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
771                            ),
772                        ));
773                    }
774                }
775            }
776        }
777        if let Some((pairs, span)) = &admin.ordering {
778            for (name, _) in pairs {
779                if !collected.field_names.iter().any(|n| n == name) {
780                    return Err(syn::Error::new(
781                        *span,
782                        format!(
783                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
784                        ),
785                    ));
786                }
787            }
788        }
789        if let Some((groups, span)) = &admin.fieldsets {
790            for (_, fields) in groups {
791                for name in fields {
792                    if !collected.field_names.iter().any(|n| n == name) {
793                        return Err(syn::Error::new(
794                            *span,
795                            format!(
796                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
797                            ),
798                        ));
799                    }
800                }
801            }
802        }
803    }
804    if let Some(audit) = &container.audit {
805        if let Some((names, span)) = &audit.track {
806            for name in names {
807                if !collected.field_names.iter().any(|n| n == name) {
808                    return Err(syn::Error::new(
809                        *span,
810                        format!(
811                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
812                        ),
813                    ));
814                }
815            }
816        }
817    }
818
819    // Issue #291 / T2.5 — validate each `default_order` column name
820    // against the model's collected fields. Typos fail at macro-expand
821    // time, not at the database.
822    for (col, _desc, span) in &container.default_order {
823        if !collected.field_names.iter().any(|n| n == col) {
824            return Err(syn::Error::new(
825                *span,
826                format!(
827                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
828                ),
829            ));
830        }
831    }
832
833    // Build the audit_track list for ModelSchema: None when no audit attr,
834    // Some(empty) when audit present without track, Some(names) when explicit.
835    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
836        audit
837            .track
838            .as_ref()
839            .map(|(names, _)| names.clone())
840            .unwrap_or_default()
841    });
842
843    // Merge field-level indexes into the container's index list.
844    let mut all_indexes: Vec<IndexAttr> = container.indexes;
845    for field in &named.named {
846        let ident = field.ident.as_ref().expect("named");
847        let col = to_snake_case(&ident.to_string()); // column name fallback
848                                                     // Re-parse field attrs to check for index flag
849        if let Ok(fa) = parse_field_attrs(field) {
850            if fa.index {
851                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
852                let auto_name = if fa.index_unique {
853                    format!("{table}_{col_name}_uq_idx")
854                } else {
855                    format!("{table}_{col_name}_idx")
856                };
857                all_indexes.push(IndexAttr {
858                    name: fa.index_name.or(Some(auto_name)),
859                    columns: vec![col_name],
860                    unique: fa.index_unique,
861                    method: fa.index_method,
862                    where_clause: None,
863                    include: Vec::new(),
864                });
865            }
866        }
867    }
868
869    let model_impl = model_impl_tokens(
870        struct_name,
871        &model_name,
872        &table,
873        display.as_deref(),
874        app_label.as_deref(),
875        container.admin.as_ref(),
876        &container.default_order,
877        &collected.field_schemas,
878        collected.soft_delete_column.as_deref(),
879        container.permissions,
880        audit_track_names.as_deref(),
881        &container.m2m,
882        &all_indexes,
883        &container.checks,
884        &container.excludes,
885        &container.composite_fks,
886        &container.generic_fks,
887        container.scope.as_deref(),
888        container.is_view,
889        container.verbose_name.as_deref(),
890        container.verbose_name_plural.as_deref(),
891        container.managed,
892        container.base_manager_name.as_deref(),
893        container.order_with_respect_to.as_deref(),
894        container.proxy,
895        &container.required_db_features,
896        container.required_db_vendor.as_deref(),
897        container.default_related_name.as_deref(),
898        container.db_table_comment.as_deref(),
899        container
900            .get_latest_by
901            .as_ref()
902            .map(|(c, d)| (c.as_str(), *d)),
903        &container.extra_permissions,
904        &container.default_permissions,
905        &container.global_scopes,
906        &container.reverse_has_relations,
907        &container.generic_has_relations,
908    );
909    let module_ident = column_module_ident(struct_name);
910    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
911    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
912        let track_set: Option<std::collections::HashSet<&str>> = audit
913            .track
914            .as_ref()
915            .map(|(names, _)| names.iter().map(String::as_str).collect());
916        collected
917            .column_entries
918            .iter()
919            .filter(|c| {
920                track_set
921                    .as_ref()
922                    .map_or(true, |s| s.contains(c.name.as_str()))
923            })
924            .collect()
925    });
926    let inherent_impl = inherent_impl_tokens(
927        struct_name,
928        &collected,
929        collected.primary_key.as_ref(),
930        &column_consts,
931        audited_fields.as_deref(),
932        &all_indexes,
933        &container.manager_fns,
934    );
935    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
936    let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
937    let reverse_helpers = reverse_helper_tokens(
938        struct_name,
939        &collected.fk_relations,
940        container.default_related_name.as_deref(),
941    );
942    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
943    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
944    // Issue #817 — `#[rustango(through(...))]` accessors.
945    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
946    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
947    let reverse_has_accessors =
948        reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
949    let generic_fk_accessors = generic_fk_accessor_tokens(
950        struct_name,
951        &container.generic_fks,
952        &collected.column_entries,
953    );
954
955    // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
956    // emits an empty extension trait so users can add methods via
957    // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
958    // trait declaration. See `crates/rustango/src/manager.rs` for the
959    // pattern this replaces.
960    let manager_trait = container.manager_ext.as_ref().map(|name| {
961        let model_name_str = struct_name.to_string();
962        let doc = format!(
963            "Custom-Manager extension trait for [`{model_name_str}`]. \
964             Generated by `#[rustango(manager(ext = ...))]`. Add methods \
965             via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
966        );
967        quote! {
968            #[doc = #doc]
969            pub trait #name: ::core::marker::Sized {}
970        }
971    });
972
973    Ok(quote! {
974        #model_impl
975        #inherent_impl
976        #from_row_impl
977        #column_module
978        #reverse_helpers
979        #m2m_accessors
980        #generic_m2m_accessors
981        #through_accessors
982        #reverse_has_accessors
983        #generic_fk_accessors
984        #manager_trait
985
986        #root::core::inventory::submit! {
987            // `module_path!()` evaluates at the registration site,
988            // so a Model declared in `crate::blog::models` records
989            // `"<crate>::blog::models"` and `resolved_app_label()`
990            // can infer "blog" without an explicit attribute.
991            #root::core::ModelEntry::new(
992                <#struct_name as #root::core::Model>::SCHEMA,
993                ::core::module_path!(),
994            )
995        }
996    })
997}
998
999/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1000/// matches `field_name` against the model's FK fields and, for a
1001/// match, decodes the FK target via the parent's macro-generated
1002/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
1003/// `ForeignKey::Loaded` on `self`.
1004///
1005/// Always emitted (with empty arms for FK-less models, which
1006/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1007/// trait bound on `fetch_on` is universally satisfied — users
1008/// never have to think about implementing it.
1009fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1010    let root = rustango_root();
1011    let arms = fk_relations.iter().map(|rel| {
1012        let parent_ty = &rel.parent_type;
1013        let fk_col = rel.fk_column.as_str();
1014        // FK field's Rust ident matches its SQL column name in v0.8
1015        // (no `column = "..."` rename ships on FK fields).
1016        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1017        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1018        let assign = if rel.nullable {
1019            quote! {
1020                self.#field_ident = ::core::option::Option::Some(
1021                    #root::sql::ForeignKey::loaded(_pk, _parent),
1022                );
1023            }
1024        } else {
1025            quote! {
1026                self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1027            }
1028        };
1029        quote! {
1030            #fk_col => {
1031                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1032                // Audit #451 — multi-hop `select_related("a__b__c")`:
1033                // stitch the deeper relation onto this parent first,
1034                // decoding it at the accumulated `__next_alias`. The
1035                // parent type also impls `LoadRelated`, so this recurses
1036                // the FK chain to arbitrary depth.
1037                if let ::core::option::Option::Some(__r) = __rest {
1038                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1039                        &mut _parent, row, __r, &__next_alias,
1040                    )?;
1041                }
1042                // Loud-in-debug, default-in-release: a divergence
1043                // between the FK field's declared `K` (drives the
1044                // expected `SqlValue::<Variant>`) and the parent's
1045                // `__rustango_pk_value` output is a macro-internal
1046                // invariant break — surfacing the panic in dev
1047                // catches it before users hit silent PK=0 corruption.
1048                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1049                    #root::core::SqlValue::#variant_ident(v) => v,
1050                    _other => {
1051                        ::core::debug_assert!(
1052                            false,
1053                            "rustango macro bug: load_related on FK `{}` expected \
1054                             SqlValue::{} from parent's __rustango_pk_value but got \
1055                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1056                            #fk_col,
1057                            ::core::stringify!(#variant_ident),
1058                            _other,
1059                        );
1060                        #default_expr
1061                    }
1062                };
1063                #assign
1064                ::core::result::Result::Ok(true)
1065            }
1066        }
1067    });
1068    quote! {
1069        #[cfg(feature = "postgres")]
1070        impl #root::sql::LoadRelated for #struct_name {
1071            #[allow(unused_variables)]
1072            fn __rustango_load_related(
1073                &mut self,
1074                row: &#root::sql::sqlx::postgres::PgRow,
1075                field_name: &str,
1076                alias: &str,
1077            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1078                // Audit #451 — split the multi-hop path: `base` is the FK
1079                // on THIS model, `__rest` (if any) is the remaining chain
1080                // to stitch onto the loaded parent. `__next_alias` is the
1081                // accumulated join alias the parent's columns live under
1082                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1083                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1084                    match field_name.split_once("__") {
1085                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1086                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1087                    };
1088                let __next_alias: ::std::string::String = match __rest {
1089                    ::core::option::Option::Some(__r) => {
1090                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1091                        ::std::format!("{}__{}", alias, __rb)
1092                    }
1093                    ::core::option::Option::None => ::std::string::String::new(),
1094                };
1095                match __base {
1096                    #( #arms )*
1097                    _ => ::core::result::Result::Ok(false),
1098                }
1099            }
1100        }
1101    }
1102}
1103
1104/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1105/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1106/// which expands to a `LoadRelatedMy` impl when rustango is built with
1107/// the `mysql` feature, and to nothing otherwise. The decoded parent
1108/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1109/// decoder, also batch8) so the dual emission is symmetric across
1110/// backends.
1111fn load_related_impl_my_tokens(
1112    struct_name: &syn::Ident,
1113    fk_relations: &[FkRelation],
1114) -> TokenStream2 {
1115    let root = rustango_root();
1116    let arms = fk_relations.iter().map(|rel| {
1117        let parent_ty = &rel.parent_type;
1118        let fk_col = rel.fk_column.as_str();
1119        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1120        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1121        let assign = if rel.nullable {
1122            quote! {
1123                __self.#field_ident = ::core::option::Option::Some(
1124                    #root::sql::ForeignKey::loaded(_pk, _parent),
1125                );
1126            }
1127        } else {
1128            quote! {
1129                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1130            }
1131        };
1132        // `self` IS hygiene-tracked through macro_rules — emitted from
1133        // a different context than the `&mut self` parameter inside
1134        // the macro_rules-expanded fn. Pass it through as `__self`
1135        // and let the macro_rules rebind it to the receiver.
1136        quote! {
1137            #fk_col => {
1138                let mut _parent: #parent_ty =
1139                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1140                // Audit #451 — multi-hop: stitch the deeper relation onto
1141                // the parent at the accumulated alias (see PG twin).
1142                if let ::core::option::Option::Some(__r) = __rest {
1143                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1144                        &mut _parent, row, __r, &__next_alias,
1145                    )?;
1146                }
1147                // See note in `load_related_impl_tokens` (PG twin) —
1148                // the same loud-in-debug invariant guard.
1149                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1150                    #root::core::SqlValue::#variant_ident(v) => v,
1151                    _other => {
1152                        ::core::debug_assert!(
1153                            false,
1154                            "rustango macro bug: load_related on FK `{}` expected \
1155                             SqlValue::{} from parent's __rustango_pk_value but got \
1156                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1157                            #fk_col,
1158                            ::core::stringify!(#variant_ident),
1159                            _other,
1160                        );
1161                        #default_expr
1162                    }
1163                };
1164                #assign
1165                ::core::result::Result::Ok(true)
1166            }
1167        }
1168    });
1169    quote! {
1170        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1171            #( #arms )*
1172        });
1173    }
1174}
1175
1176/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1177/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1178/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1179fn load_related_impl_sqlite_tokens(
1180    struct_name: &syn::Ident,
1181    fk_relations: &[FkRelation],
1182) -> TokenStream2 {
1183    let root = rustango_root();
1184    let arms = fk_relations.iter().map(|rel| {
1185        let parent_ty = &rel.parent_type;
1186        let fk_col = rel.fk_column.as_str();
1187        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1188        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1189        let assign = if rel.nullable {
1190            quote! {
1191                __self.#field_ident = ::core::option::Option::Some(
1192                    #root::sql::ForeignKey::loaded(_pk, _parent),
1193                );
1194            }
1195        } else {
1196            quote! {
1197                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1198            }
1199        };
1200        quote! {
1201            #fk_col => {
1202                let mut _parent: #parent_ty =
1203                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1204                // Audit #451 — multi-hop: stitch the deeper relation onto
1205                // the parent at the accumulated alias (see PG twin).
1206                if let ::core::option::Option::Some(__r) = __rest {
1207                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1208                        &mut _parent, row, __r, &__next_alias,
1209                    )?;
1210                }
1211                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1212                    #root::core::SqlValue::#variant_ident(v) => v,
1213                    _other => {
1214                        ::core::debug_assert!(
1215                            false,
1216                            "rustango macro bug: load_related on FK `{}` expected \
1217                             SqlValue::{} from parent's __rustango_pk_value but got \
1218                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1219                            #fk_col,
1220                            ::core::stringify!(#variant_ident),
1221                            _other,
1222                        );
1223                        #default_expr
1224                    }
1225                };
1226                #assign
1227                ::core::result::Result::Ok(true)
1228            }
1229        }
1230    });
1231    quote! {
1232        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1233            #( #arms )*
1234        });
1235    }
1236}
1237
1238/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1239/// matches `field_name` against the model's FK fields and returns
1240/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1241/// group children by parent PK.
1242///
1243/// Always emitted (with `_ => None` for FK-less models) so the
1244/// trait bound on `fetch_with_prefetch` is universally satisfied.
1245fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1246    let root = rustango_root();
1247    let arms = fk_relations.iter().map(|rel| {
1248        let fk_col = rel.fk_column.as_str();
1249        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1250        if rel.pk_kind == DetectedKind::I64 {
1251            // i64 FK — return the stored PK so prefetch_related can
1252            // group children by it. Nullable variant unwraps via
1253            // `as_ref().map(...)`: an unset (NULL) FK column yields
1254            // `None` and that child sits out of the grouping (correct
1255            // semantics — it has no parent to attach to).
1256            if rel.nullable {
1257                quote! {
1258                    #fk_col => self.#field_ident
1259                        .as_ref()
1260                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1261                }
1262            } else {
1263                quote! {
1264                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1265                }
1266            }
1267        } else {
1268            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1269            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1270            // i64-keyed grouping path — the trait signature is
1271            // `Option<i64>` and a non-i64 PK can't lower into it.
1272            // The FK still works for everything else (CRUD, lazy
1273            // load via `.get()`, select_related JOINs); only the
1274            // bulk prefetch grouper needs the integer key.
1275            quote! {
1276                #fk_col => ::core::option::Option::None,
1277            }
1278        }
1279    });
1280    // PK-type-agnostic version: every FK arm emits an
1281    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1282    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1283    // opt OUT of the legacy i64 method (it returns None) but opt IN
1284    // here.
1285    let value_arms = fk_relations.iter().map(|rel| {
1286        let fk_col = rel.fk_column.as_str();
1287        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1288        if rel.nullable {
1289            quote! {
1290                #fk_col => self.#field_ident
1291                    .as_ref()
1292                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1293                        #root::sql::ForeignKey::pk(fk)
1294                    )),
1295            }
1296        } else {
1297            quote! {
1298                #fk_col => ::core::option::Option::Some(
1299                    ::core::convert::Into::<#root::core::SqlValue>::into(
1300                        self.#field_ident.pk()
1301                    )
1302                ),
1303            }
1304        }
1305    });
1306    quote! {
1307        impl #root::sql::FkPkAccess for #struct_name {
1308            #[allow(unused_variables)]
1309            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1310                match field_name {
1311                    #( #arms )*
1312                    _ => ::core::option::Option::None,
1313                }
1314            }
1315            #[allow(unused_variables)]
1316            fn __rustango_fk_pk_value(
1317                &self,
1318                field_name: &str,
1319            ) -> ::core::option::Option<#root::core::SqlValue> {
1320                match field_name {
1321                    #( #value_arms )*
1322                    _ => ::core::option::Option::None,
1323                }
1324            }
1325        }
1326    }
1327}
1328
1329/// For every `ForeignKey<Parent>` field on `Child`, emit
1330/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1331/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1332/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1333/// — the canonical reverse-FK fetch. One round trip, no N+1.
1334///
1335/// **PG-only emission**: the accessor is bounded on
1336/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1337/// both of which are gated behind the `postgres` cargo feature. The
1338/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1339/// model derive itself compiles on tri-dialect / sqlite-only
1340/// downstream builds — the accessor just isn't materialised. A tri-
1341/// dialect `_set_pool` variant is a separate follow-up.
1342fn reverse_helper_tokens(
1343    child_ident: &syn::Ident,
1344    fk_relations: &[FkRelation],
1345    default_related_name: Option<&str>,
1346) -> TokenStream2 {
1347    let root = rustango_root();
1348    if fk_relations.is_empty() {
1349        return TokenStream2::new();
1350    }
1351    // Method-name resolution per FK (issue #816 + follow-up):
1352    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1353    //      itself — wins over everything else.
1354    //   2. Container-level `default_related_name = "..."` on the
1355    //      child. Applies to every FK on this model that didn't
1356    //      override.
1357    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1358    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1359    //      edge cases.
1360    //
1361    // The PG-on-executor variant keeps the resolved name; the
1362    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1363    // framework's convention for the `&Pool` flavor of every helper).
1364    let default_pg_suffix = default_related_name
1365        .map(str::to_owned)
1366        .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1367    let impls = fk_relations.iter().map(|rel| {
1368        let pg_suffix = rel
1369            .related_name
1370            .clone()
1371            .unwrap_or_else(|| default_pg_suffix.clone());
1372        let pool_suffix = format!("{}_pool", pg_suffix);
1373        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1374        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1375        let parent_ty = &rel.parent_type;
1376        let fk_col = rel.fk_column.as_str();
1377        let doc = format!(
1378            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1379             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1380             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1381             further `{child_ident}::objects()` filters via direct queryset use."
1382        );
1383        let pool_doc = format!(
1384            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1385             [`#root::sql::Pool`] and dispatches per backend so the \
1386             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1387             Use this from framework code that holds a `&Pool` (admin, \
1388             tenancy resolver, viewset handlers); reach for the executor- \
1389             bound variant when you already have a typed `sqlx::Executor`."
1390        );
1391        quote! {
1392            #[cfg(feature = "postgres")]
1393            impl #parent_ty {
1394                #[doc = #doc]
1395                ///
1396                /// # Errors
1397                /// Returns [`#root::sql::ExecError`] for SQL-writing
1398                /// or driver failures.
1399                pub async fn #pg_method_ident<'_c, _E>(
1400                    &self,
1401                    _executor: _E,
1402                ) -> ::core::result::Result<
1403                    ::std::vec::Vec<#child_ident>,
1404                    #root::sql::ExecError,
1405                >
1406                where
1407                    _E: #root::sql::sqlx::Executor<
1408                        '_c,
1409                        Database = #root::sql::sqlx::Postgres,
1410                    >,
1411                {
1412                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1413                    #root::query::QuerySet::<#child_ident>::new()
1414                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1415                        .fetch_on(_executor)
1416                        .await
1417                }
1418            }
1419
1420            impl #parent_ty {
1421                #[doc = #pool_doc]
1422                ///
1423                /// # Errors
1424                /// Returns [`#root::sql::ExecError`] for SQL-writing
1425                /// or driver failures.
1426                pub async fn #pool_method_ident(
1427                    &self,
1428                    pool: &#root::sql::Pool,
1429                ) -> ::core::result::Result<
1430                    ::std::vec::Vec<#child_ident>,
1431                    #root::sql::ExecError,
1432                > {
1433                    use #root::sql::FetcherPool as _;
1434                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1435                    #root::query::QuerySet::<#child_ident>::new()
1436                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1437                        .fetch(pool)
1438                        .await
1439                }
1440            }
1441        }
1442    });
1443    quote! { #( #impls )* }
1444}
1445
1446/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1447/// relation declared on the model.
1448/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1449/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1450///
1451/// Closes #239 + #240 — `comment.content_object` /
1452/// `comment.content_object = post` ergonomics on top of the existing
1453/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1454///
1455/// `column_entries` is passed so we can resolve each `ct_column` /
1456/// `pk_column` SQL name back to its Rust field ident — the macro
1457/// only sees the column-side strings in the attribute, but the
1458/// emitted accessor needs to read the actual struct field.
1459fn generic_fk_accessor_tokens(
1460    struct_name: &syn::Ident,
1461    generic_fks: &[GenericFkAttr],
1462    column_entries: &[ColumnEntry],
1463) -> TokenStream2 {
1464    let root = rustango_root();
1465    if generic_fks.is_empty() {
1466        return TokenStream2::new();
1467    }
1468    let methods = generic_fks.iter().filter_map(|gfk| {
1469        // Resolve `ct_column` + `pk_column` to the struct's Rust
1470        // field idents. A typo (column name doesn't match any field)
1471        // emits no method for that registration — the user will see
1472        // the compiler reject the SCHEMA literal anyway, so there's
1473        // a clear error path without us double-reporting.
1474        let ct_ident = column_entries
1475            .iter()
1476            .find(|c| c.column == gfk.ct_column)
1477            .map(|c| c.ident.clone())?;
1478        let pk_ident = column_entries
1479            .iter()
1480            .find(|c| c.column == gfk.pk_column)
1481            .map(|c| c.ident.clone())?;
1482
1483        let accessor_ident =
1484            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1485        let setter_ident =
1486            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1487        let name_literal = gfk.name.as_str();
1488
1489        Some(quote! {
1490            #[doc = concat!(
1491                "Resolve the polymorphic `",
1492                #name_literal,
1493                "` relation. Reads `self.",
1494                stringify!(#ct_ident),
1495                "` + `self.",
1496                stringify!(#pk_ident),
1497                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1498                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1499                #name_literal,
1500                "\", ...))]`."
1501            )]
1502            pub async fn #accessor_ident(
1503                &self,
1504                pool: &#root::sql::Pool,
1505            ) -> ::core::result::Result<
1506                ::core::option::Option<#root::__serde_json::Value>,
1507                #root::sql::ExecError,
1508            > {
1509                let gfk = #root::contenttypes::GenericForeignKey::new(
1510                    self.#ct_ident as i64,
1511                    self.#pk_ident as i64,
1512                );
1513                gfk.get_object(pool).await
1514            }
1515
1516            #[doc = concat!(
1517                "Set the polymorphic `",
1518                #name_literal,
1519                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1520                stringify!(#ct_ident),
1521                "` and `self.",
1522                stringify!(#pk_ident),
1523                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1524                #name_literal,
1525                "\", ...))]`."
1526            )]
1527            pub async fn #setter_ident<T: #root::core::Model>(
1528                &mut self,
1529                pool: &#root::sql::Pool,
1530                target_pk: i64,
1531            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1532                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1533                    pool,
1534                    target_pk,
1535                ).await?;
1536                self.#ct_ident = gfk.content_type_id as _;
1537                self.#pk_ident = gfk.object_pk as _;
1538                ::core::result::Result::Ok(())
1539            }
1540        })
1541    });
1542    quote! {
1543        impl #struct_name {
1544            #( #methods )*
1545        }
1546    }
1547}
1548
1549fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1550    let root = rustango_root();
1551    if m2m_relations.is_empty() {
1552        return TokenStream2::new();
1553    }
1554    let methods = m2m_relations.iter().map(|rel| {
1555        let method_name = format!("{}_m2m", rel.name);
1556        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1557        let through = rel.through.as_str();
1558        let src_col = rel.src.as_str();
1559        let dst_col = rel.dst.as_str();
1560        quote! {
1561            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1562                #root::sql::M2MManager {
1563                    src_pk: self.__rustango_pk_value(),
1564                    through: #through,
1565                    src_col: #src_col,
1566                    dst_col: #dst_col,
1567                }
1568            }
1569        }
1570    });
1571    quote! {
1572        impl #struct_name {
1573            #( #methods )*
1574        }
1575    }
1576}
1577
1578/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1579/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1580fn generic_m2m_accessor_tokens(
1581    struct_name: &syn::Ident,
1582    relations: &[GenericM2MAttr],
1583) -> TokenStream2 {
1584    let root = rustango_root();
1585    if relations.is_empty() {
1586        return TokenStream2::new();
1587    }
1588    let methods = relations.iter().map(|rel| {
1589        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1590        let through = rel.through.as_str();
1591        let pk_col = rel.pk_column.as_str();
1592        let ct_col = rel.ct_column.as_str();
1593        let related_col = rel.related_column.as_str();
1594        quote! {
1595            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1596                #root::sql::GenericM2MManager {
1597                    src_pk: self.__rustango_pk_value(),
1598                    src_schema: <Self as #root::core::Model>::SCHEMA,
1599                    through: #through,
1600                    pk_col: #pk_col,
1601                    ct_col: #ct_col,
1602                    dst_col: #related_col,
1603                }
1604            }
1605        }
1606    });
1607    quote! {
1608        impl #struct_name {
1609            #( #methods )*
1610        }
1611    }
1612}
1613
1614/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1615/// associated functions for each `#[rustango(reverse_has(...))]`
1616/// attribute. Issue #830.
1617///
1618/// The two emitted functions return ready-to-use `WhereExpr` nodes
1619/// that downstream callers drop into
1620/// `QuerySet::<Self>::where_raw(...)`:
1621///
1622/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1623///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1624///   `whereHas` parity (without the closure-style sub-predicate
1625///   refinement — that's a follow-up; users can layer additional
1626///   predicates by constructing the SelectQuery themselves and
1627///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1628/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1629///   `whereDoesntHave` parity.
1630///
1631/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1632/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1633/// machinery threads the outer-table reference through automatically
1634/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1635fn reverse_has_accessor_tokens(
1636    struct_name: &syn::Ident,
1637    reverse_has_relations: &[ReverseHasAttr],
1638) -> TokenStream2 {
1639    let root = rustango_root();
1640    if reverse_has_relations.is_empty() {
1641        return TokenStream2::new();
1642    }
1643    let methods = reverse_has_relations.iter().map(|rel| {
1644        let exists_name = format!("{}_exists_expr", rel.name);
1645        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1646        let count_name = format!("{}_count", rel.name);
1647        let fetch_name = format!("{}_fetch", rel.name);
1648        let first_name = format!("{}_first", rel.name);
1649        let pluck_name = format!("{}_pluck", rel.name);
1650        let accessor_name = rel.name.as_str();
1651        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1652        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1653        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1654        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1655        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1656        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1657        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1658        let child = &rel.child;
1659        let child_fk_column = rel.child_fk_column.as_str();
1660        let self_pk_column = rel.self_pk_column.as_str();
1661        let exists_doc = format!(
1662            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1663             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1664             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1665             filter to {struct_name}s with at least one matching child.",
1666        );
1667        let not_exists_doc = format!(
1668            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1669             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1670             <outer>.{self_pk_column})`. Drop into \
1671             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1672             {struct_name}s with **no** matching child.",
1673        );
1674        let count_doc = format!(
1675            "Eloquent `$model->{name}->count()` analog — returns \
1676             the number of `{child}` rows whose `{child_fk_column}` \
1677             matches this `{struct_name}` instance's primary key. \
1678             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1679             {child_fk_column} = <self.pk>`.",
1680            name = rel.name,
1681        );
1682        let accessor_doc = format!(
1683            "Eloquent `$model->{name}` accessor — returns a \
1684             `QuerySet<{child}>` filtered to rows whose \
1685             `{child_fk_column}` matches this `{struct_name}` \
1686             instance's primary key. **Chainable**: compose `.filter()` \
1687             / `.order_by()` / `.limit()` etc. on top, then call \
1688             `.fetch(&pool)` (the QuerySet trait method) when \
1689             done. For the simple \"fetch all\" hot path with no \
1690             further composition, prefer the bare-name \
1691             `{name}_fetch(&pool)` companion.",
1692            name = rel.name,
1693        );
1694        let fetch_doc = format!(
1695            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1696             over `{name}(&self).fetch(&pool)`. Use this when \
1697             you don't need further `.filter()` / `.order_by()` \
1698             composition; falls back to the chainable accessor when \
1699             you do. Avoids the `_pool` suffix on the most common \
1700             call-site shape.",
1701            name = rel.name,
1702        );
1703        quote! {
1704            #[doc = #accessor_doc]
1705            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1706                #root::query::QuerySet::<#child>::new()
1707                    .filter(#child_fk_column, self.__rustango_pk_value())
1708            }
1709
1710            #[doc = #fetch_doc]
1711            pub async fn #fetch_ident(
1712                &self,
1713                pool: &#root::sql::Pool,
1714            ) -> ::core::result::Result<
1715                ::std::vec::Vec<#child>,
1716                #root::sql::ExecError,
1717            > {
1718                use #root::sql::FetcherPool as _;
1719                self.#accessor_ident().fetch(pool).await
1720            }
1721
1722            /// Eloquent `$model->relation->first()` / `hasOne`
1723            /// semantics — bare-name shortcut over
1724            /// `self.<name>().first(&pool)`. Returns `None` when no
1725            /// child rows match. Useful when the relation is
1726            /// nominally many-to-one in shape but at most one row is
1727            /// expected (latest comment, primary tag, etc.).
1728            pub async fn #first_ident(
1729                &self,
1730                pool: &#root::sql::Pool,
1731            ) -> ::core::result::Result<
1732                ::core::option::Option<#child>,
1733                #root::sql::ExecError,
1734            > {
1735                self.#accessor_ident().first(pool).await
1736            }
1737
1738            /// Eloquent `$model->relation->pluck($col)` — project a
1739            /// single column from the child rows into a `Vec<U>`.
1740            /// Skips the typed `Child` decode, which is cheaper when
1741            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1742            pub async fn #pluck_ident<U>(
1743                &self,
1744                col: &'static str,
1745                pool: &#root::sql::Pool,
1746            ) -> ::core::result::Result<
1747                ::std::vec::Vec<U>,
1748                #root::sql::ExecError,
1749            >
1750            where
1751                U: #root::sql::MaybePgScalar
1752                    + #root::sql::MaybeMyScalar
1753                    + #root::sql::MaybeSqliteScalar
1754                    + ::core::marker::Send
1755                    + ::core::marker::Unpin,
1756            {
1757                self.#accessor_ident()
1758                    .values_list_flat(col)
1759                    .fetch::<U>(pool)
1760                    .await
1761            }
1762
1763            #[doc = #exists_doc]
1764            pub fn #exists_ident() -> #root::core::WhereExpr {
1765                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1766                let child_schema =
1767                    <#child as #root::core::Model>::SCHEMA;
1768                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1769                    lhs: Expr::Column(#child_fk_column),
1770                    op: Op::Eq,
1771                    rhs: Expr::OuterRef(#self_pk_column),
1772                });
1773                WhereExpr::Exists(::std::boxed::Box::new(inner))
1774            }
1775
1776            #[doc = #not_exists_doc]
1777            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1778                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1779                let child_schema =
1780                    <#child as #root::core::Model>::SCHEMA;
1781                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1782                    lhs: Expr::Column(#child_fk_column),
1783                    op: Op::Eq,
1784                    rhs: Expr::OuterRef(#self_pk_column),
1785                });
1786                WhereExpr::NotExists(::std::boxed::Box::new(inner))
1787            }
1788
1789            #[doc = #count_doc]
1790            pub async fn #count_ident(
1791                &self,
1792                pool: &#root::sql::Pool,
1793            ) -> ::core::result::Result<
1794                i64,
1795                #root::sql::ExecError,
1796            > {
1797                use #root::sql::CounterPool as _;
1798                #root::query::QuerySet::<#child>::new()
1799                    .filter(#child_fk_column, self.__rustango_pk_value())
1800                    .count(pool)
1801                    .await
1802            }
1803        }
1804    });
1805    quote! {
1806        impl #struct_name {
1807            #( #methods )*
1808        }
1809    }
1810}
1811
1812/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1813/// `#[rustango(through(...))]` attribute. Issue #817.
1814///
1815/// Each accessor builds a correlated subquery via
1816/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1817/// intermediate table, filters on the FK column pointing at this
1818/// model, and projects the intermediate PK. The outer queryset
1819/// filters the far model by its FK-to-intermediate column being in
1820/// that set.
1821///
1822/// The returned `QuerySet<Far>` is **chainable** — the subquery
1823/// lives inside a `where_raw` clause so the user's later
1824/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1825/// disturb it.
1826///
1827/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1828/// SQLite — no LATERAL or backend-specific syntax involved.
1829fn through_accessor_tokens(
1830    struct_name: &syn::Ident,
1831    through_relations: &[ThroughAttr],
1832) -> TokenStream2 {
1833    let root = rustango_root();
1834    if through_relations.is_empty() {
1835        return TokenStream2::new();
1836    }
1837    let methods = through_relations.iter().map(|rel| {
1838        let method_name = format!("{}_through", rel.name);
1839        let count_name = format!("{}_through_count", rel.name);
1840        let fetch_name = format!("{}_through_fetch", rel.name);
1841        let first_name = format!("{}_through_first", rel.name);
1842        let pluck_name = format!("{}_through_pluck", rel.name);
1843        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1844        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1845        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1846        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1847        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1848        let far = &rel.far;
1849        let intermediate = &rel.intermediate;
1850        let far_fk_column = rel.far_fk_column.as_str();
1851        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1852        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1853        let doc = format!(
1854            "Eloquent `hasManyThrough` accessor — returns a \
1855             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1856             instance through the intermediate `{intermediate}` table. \
1857             Generated SQL shape: \
1858             `… WHERE {far_fk_column} IN (SELECT \
1859             {intermediate_pk_column} FROM <{intermediate}> WHERE \
1860             {intermediate_fk_column} = self.pk)`. Chainable like any \
1861             other QuerySet — compose `.filter()` / `.order_by()` / \
1862             `.limit()` etc. on top.",
1863        );
1864        let count_doc = format!(
1865            "Eloquent `$model->{name}->count()` analog for the \
1866             through-relation — returns the number of `{far}` rows \
1867             reachable through `{intermediate}`. Equivalent to \
1868             `self.{name}_through().count(pool)` but spelled \
1869             as a bare instance method for parity with the \
1870             `reverse_has` `<name>_count` shape.",
1871            name = rel.name,
1872        );
1873        quote! {
1874            #[doc = #doc]
1875            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1876                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1877                let intermediate_schema =
1878                    <#intermediate as #root::core::Model>::SCHEMA;
1879                let sub = SelectQuery::new(intermediate_schema)
1880                    .where_clause(WhereExpr::Predicate(Filter::new(
1881                        #intermediate_fk_column,
1882                        Op::Eq,
1883                        self.__rustango_pk_value(),
1884                    )))
1885                    .projection(::std::vec![#intermediate_pk_column]);
1886                #root::query::QuerySet::<#far>::new().where_raw(
1887                    WhereExpr::InSubquery {
1888                        column: #far_fk_column,
1889                        negated: false,
1890                        subquery: ::std::boxed::Box::new(sub),
1891                    },
1892                )
1893            }
1894
1895            #[doc = #count_doc]
1896            pub async fn #count_ident(
1897                &self,
1898                pool: &#root::sql::Pool,
1899            ) -> ::core::result::Result<
1900                i64,
1901                #root::sql::ExecError,
1902            > {
1903                use #root::sql::CounterPool as _;
1904                self.#method_ident().count(pool).await
1905            }
1906
1907            /// Eloquent `$model->relation->get()` for the
1908            /// through-relation — bare-name hot-path over
1909            /// `self.<name>_through().fetch(&pool)`. Use this
1910            /// when you don't need further `.filter()` /
1911            /// `.order_by()` composition; falls back to the
1912            /// chainable accessor when you do. Avoids the `_pool`
1913            /// suffix on the most common call-site shape.
1914            pub async fn #fetch_ident(
1915                &self,
1916                pool: &#root::sql::Pool,
1917            ) -> ::core::result::Result<
1918                ::std::vec::Vec<#far>,
1919                #root::sql::ExecError,
1920            > {
1921                use #root::sql::FetcherPool as _;
1922                self.#method_ident().fetch(pool).await
1923            }
1924
1925            /// Eloquent `hasOneThrough` analog — bare-name shortcut
1926            /// over `self.<name>_through().first(&pool)`. Returns
1927            /// `None` when no far rows are reachable through the
1928            /// intermediate. Useful when at most one row is
1929            /// expected (latest comment by country, primary tag,
1930            /// etc.).
1931            pub async fn #first_ident(
1932                &self,
1933                pool: &#root::sql::Pool,
1934            ) -> ::core::result::Result<
1935                ::core::option::Option<#far>,
1936                #root::sql::ExecError,
1937            > {
1938                self.#method_ident().first(pool).await
1939            }
1940
1941            /// Pluck a single column from the far rows into `Vec<U>`
1942            /// — cheaper than the typed `<Far>` decode when only one
1943            /// scalar column is needed.
1944            pub async fn #pluck_ident<U>(
1945                &self,
1946                col: &'static str,
1947                pool: &#root::sql::Pool,
1948            ) -> ::core::result::Result<
1949                ::std::vec::Vec<U>,
1950                #root::sql::ExecError,
1951            >
1952            where
1953                U: #root::sql::MaybePgScalar
1954                    + #root::sql::MaybeMyScalar
1955                    + #root::sql::MaybeSqliteScalar
1956                    + ::core::marker::Send
1957                    + ::core::marker::Unpin,
1958            {
1959                self.#method_ident()
1960                    .values_list_flat(col)
1961                    .fetch::<U>(pool)
1962                    .await
1963            }
1964        }
1965    });
1966    quote! {
1967        impl #struct_name {
1968            #( #methods )*
1969        }
1970    }
1971}
1972
1973struct ColumnEntry {
1974    /// The struct field ident, used both for the inherent const name on
1975    /// the model and for the inner column type's name.
1976    ident: syn::Ident,
1977    /// The struct's field type, used as `Column::Value`.
1978    value_ty: Type,
1979    /// Rust-side field name (e.g. `"id"`).
1980    name: String,
1981    /// SQL-side column name (e.g. `"user_id"`).
1982    column: String,
1983    /// `#root::core::FieldType::I64` etc.
1984    field_type_tokens: TokenStream2,
1985}
1986
1987struct CollectedFields {
1988    field_schemas: Vec<TokenStream2>,
1989    from_row_inits: Vec<TokenStream2>,
1990    /// Aliased counterparts of `from_row_inits` — read columns via
1991    /// `format!("{prefix}__{col}")` aliases so a Model can be
1992    /// decoded from a JOINed row's projected target columns.
1993    from_aliased_row_inits: Vec<TokenStream2>,
1994    /// Static column-name list — used by the simple insert path
1995    /// (no `Auto<T>` fields). Aligned with `insert_values`.
1996    insert_columns: Vec<TokenStream2>,
1997    /// Static `Into<SqlValue>` expressions, one per field. Aligned
1998    /// with `insert_columns`. Used by the simple insert path only.
1999    insert_values: Vec<TokenStream2>,
2000    /// Per-field push expressions for the dynamic (Auto-aware)
2001    /// insert path. Each statement either unconditionally pushes
2002    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2003    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2004    insert_pushes: Vec<TokenStream2>,
2005    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2006    /// when `has_auto == false`.
2007    returning_cols: Vec<TokenStream2>,
2008    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2009    /// field. Run after `insert_returning` to populate the model.
2010    auto_assigns: Vec<TokenStream2>,
2011    /// `(ident, column_literal)` pairs for every Auto field. Used by
2012    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2013    /// instead of `self`.
2014    auto_field_idents: Vec<(syn::Ident, String)>,
2015    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2016    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2017    /// back into the struct after insert (MySQL has no RETURNING → the
2018    /// field stays at its placeholder until the next read).
2019    generated_field_idents: Vec<(syn::Ident, String)>,
2020    /// Inner `T` of the first `Auto<T>` field, for the MySQL
2021    /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
2022    first_auto_value_ty: Option<Type>,
2023    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2024    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2025    /// `columns`).
2026    bulk_pushes_no_auto: Vec<TokenStream2>,
2027    /// Bulk-insert per-row pushes for **all fields including Auto**.
2028    /// Used by the all-Auto-Set bulk path (Auto col included with the
2029    /// caller-supplied value).
2030    bulk_pushes_all: Vec<TokenStream2>,
2031    /// Column-name literals for non-Auto fields only (paired with
2032    /// `bulk_pushes_no_auto`).
2033    bulk_columns_no_auto: Vec<TokenStream2>,
2034    /// Column-name literals for every field including Auto (paired
2035    /// with `bulk_pushes_all`).
2036    bulk_columns_all: Vec<TokenStream2>,
2037    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2038    /// + the loop that asserts every row matches. One pair per Auto
2039    /// field. Empty when `has_auto == false`.
2040    bulk_auto_uniformity: Vec<TokenStream2>,
2041    /// Identifier of the first Auto field, used as the witness for
2042    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2043    first_auto_ident: Option<syn::Ident>,
2044    /// `true` if any field on the struct is `Auto<T>`.
2045    has_auto: bool,
2046    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2047    /// Gates `save()` codegen — only Auto PKs let us infer
2048    /// insert-vs-update from the in-memory value.
2049    pk_is_auto: bool,
2050    /// `Assignment` constructors for every non-PK column. Drives the
2051    /// UPDATE branch of `save()`.
2052    update_assignments: Vec<TokenStream2>,
2053    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2054    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2055    upsert_update_columns: Vec<TokenStream2>,
2056    primary_key: Option<(syn::Ident, String)>,
2057    column_entries: Vec<ColumnEntry>,
2058    /// Rust-side field names, in declaration order. Used to validate
2059    /// container attributes like `display = "…"`.
2060    field_names: Vec<String>,
2061    /// FK fields on this child model. Drives the reverse-relation
2062    /// helper emit — for each FK, the macro adds an inherent
2063    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2064    /// method on the parent type.
2065    fk_relations: Vec<FkRelation>,
2066    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2067    /// the model has one. Drives emission of the `soft_delete_on` /
2068    /// `restore_on` inherent methods. At most one such column per
2069    /// model is allowed; collect_fields rejects duplicates.
2070    soft_delete_column: Option<String>,
2071    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2072    /// companion to `soft_delete_column` for emitting predicates
2073    /// that need to read the field off `&self` (e.g. `trashed()`).
2074    soft_delete_field_ident: Option<syn::Ident>,
2075}
2076
2077#[derive(Clone)]
2078struct FkRelation {
2079    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2080    /// helper is emitted as `impl <ParentType> { … }`.
2081    parent_type: Type,
2082    /// SQL column name on the child table for this FK (e.g. `"author"`).
2083    /// Used in the generated `WHERE <fk_column> = $1` clause.
2084    fk_column: String,
2085    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2086    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2087    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2088    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2089    pk_kind: DetectedKind,
2090    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2091    /// FK column). Drives the `Some(...)` wrapping in load_related
2092    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2093    /// the codegen matches the field's declared shape.
2094    nullable: bool,
2095    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2096    /// override. When set, the reverse helper picks this name instead
2097    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2098    /// #816 (issue's "Related" note re: per-FK override).
2099    related_name: Option<String>,
2100}
2101
2102fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2103    let root = rustango_root();
2104    let cap = named.named.len();
2105    let mut out = CollectedFields {
2106        field_schemas: Vec::with_capacity(cap),
2107        from_row_inits: Vec::with_capacity(cap),
2108        from_aliased_row_inits: Vec::with_capacity(cap),
2109        insert_columns: Vec::with_capacity(cap),
2110        insert_values: Vec::with_capacity(cap),
2111        insert_pushes: Vec::with_capacity(cap),
2112        returning_cols: Vec::new(),
2113        auto_assigns: Vec::new(),
2114        auto_field_idents: Vec::new(),
2115        generated_field_idents: Vec::new(),
2116        first_auto_value_ty: None,
2117        bulk_pushes_no_auto: Vec::with_capacity(cap),
2118        bulk_pushes_all: Vec::with_capacity(cap),
2119        bulk_columns_no_auto: Vec::with_capacity(cap),
2120        bulk_columns_all: Vec::with_capacity(cap),
2121        bulk_auto_uniformity: Vec::new(),
2122        first_auto_ident: None,
2123        has_auto: false,
2124        pk_is_auto: false,
2125        update_assignments: Vec::with_capacity(cap),
2126        upsert_update_columns: Vec::with_capacity(cap),
2127        primary_key: None,
2128        column_entries: Vec::with_capacity(cap),
2129        field_names: Vec::with_capacity(cap),
2130        fk_relations: Vec::new(),
2131        soft_delete_column: None,
2132        soft_delete_field_ident: None,
2133    };
2134
2135    for field in &named.named {
2136        let info = process_field(field, table)?;
2137        out.field_names.push(info.ident.to_string());
2138        out.field_schemas.push(info.schema);
2139        out.from_row_inits.push(info.from_row_init);
2140        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2141        if let Some(parent_ty) = info.fk_inner.clone() {
2142            out.fk_relations.push(FkRelation {
2143                parent_type: parent_ty,
2144                fk_column: info.column.clone(),
2145                pk_kind: info.fk_pk_kind,
2146                nullable: info.nullable,
2147                related_name: info.related_name.clone(),
2148            });
2149        }
2150        if info.soft_delete {
2151            if out.soft_delete_column.is_some() {
2152                return Err(syn::Error::new_spanned(
2153                    field,
2154                    "only one field may be marked `#[rustango(soft_delete)]`",
2155                ));
2156            }
2157            out.soft_delete_column = Some(info.column.clone());
2158            out.soft_delete_field_ident = Some(info.ident.clone());
2159        }
2160        let column = info.column.as_str();
2161        let ident = info.ident;
2162        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2163        // skip every write path — Postgres recomputes the value
2164        // from EXPR. Push only the column-entry record (so typed
2165        // column constants still exist for filtering / projection)
2166        // and the schema literal (already pushed above) and move
2167        // on. No insert_columns/values, no insert_pushes, no
2168        // bulk_*, no update_assignments, no upsert_update_columns,
2169        // no returning_cols.
2170        if info.generated_as.is_some() {
2171            out.column_entries.push(ColumnEntry {
2172                ident: ident.clone(),
2173                value_ty: info.value_ty.clone(),
2174                name: ident.to_string(),
2175                column: info.column.clone(),
2176                field_type_tokens: info.field_type_tokens,
2177            });
2178            // #1028 — refresh the DB-computed value after insert on
2179            // RETURNING-capable backends. The column joins `returning_cols`
2180            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2181            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2182            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2183            // it keeps the placeholder until the next read.
2184            out.returning_cols.push(quote!(#column));
2185            out.generated_field_idents
2186                .push((ident.clone(), info.column.clone()));
2187            continue;
2188        }
2189        out.insert_columns.push(quote!(#column));
2190        out.insert_values.push(quote! {
2191            ::core::convert::Into::<#root::core::SqlValue>::into(
2192                ::core::clone::Clone::clone(&self.#ident)
2193            )
2194        });
2195        if info.auto {
2196            out.has_auto = true;
2197            if out.first_auto_ident.is_none() {
2198                out.first_auto_ident = Some(ident.clone());
2199                out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2200            }
2201            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2202            // before binding, so the value is already in
2203            // `self.#ident` after the insert_push — RETURNING is
2204            // redundant. Skip adding this column to returning_cols /
2205            // auto_assigns / auto_field_idents to avoid (a) an
2206            // unnecessary RETURNING column on every dialect, and (b)
2207            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2208            // integer PK.
2209            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2210            // for the same reason: they are filled Rust-side below and
2211            // bound, so the value is already in `self` and RETURNING
2212            // would be a redundant column on every dialect.
2213            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2214                out.returning_cols.push(quote!(#column));
2215                out.auto_field_idents
2216                    .push((ident.clone(), info.column.clone()));
2217                out.auto_assigns.push(quote! {
2218                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2219                });
2220            }
2221            if info.default_uuid_v7 {
2222                // Rust-side UUIDv7 generation (issue #823, Eloquent
2223                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2224                // and bind; Auto::Set → bind the user's value. The
2225                // column is ALWAYS present in the INSERT statement —
2226                // no RETURNING / no DB DEFAULT needed.
2227                out.insert_pushes.push(quote! {
2228                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2229                        self.#ident = #root::sql::Auto::Set(
2230                            #root::__uuid::Uuid::now_v7(),
2231                        );
2232                    }
2233                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2234                        _columns.push(#column);
2235                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2236                            ::core::clone::Clone::clone(_v)
2237                        ));
2238                    }
2239                });
2240            } else if info.auto_now_add || info.auto_now {
2241                // Rust-side write timestamp (#1464), mirroring
2242                // `default_uuid_v7` above. `auto_now` has always bound
2243                // `Utc::now()` on UPDATE but not on INSERT, so the
2244                // first write of an `updated_at` took the DB default
2245                // while every later one took the clock — the same
2246                // column, filled two different ways.
2247                //
2248                // The default is the problem. `CREATE TABLE IF NOT
2249                // EXISTS` leaves an upgraded table's
2250                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2251                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2252                // statement that changes it — so every insert relying
2253                // on that default wrote the legacy shape forever, while
2254                // the migrate sweep converted the rows around it. The
2255                // column went permanently mixed and `ORDER BY` inverted
2256                // the admin audit log (#1616 rework review,
2257                // correctness-001 / dialects-005 / security-002).
2258                //
2259                // Binding it here makes the stale default unreachable
2260                // rather than trying to migrate it: the timestamp comes
2261                // from the application, not the database.
2262                out.insert_pushes.push(quote! {
2263                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2264                        self.#ident = #root::sql::Auto::Set(
2265                            #root::__chrono::Utc::now(),
2266                        );
2267                    }
2268                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2269                        _columns.push(#column);
2270                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2271                            ::core::clone::Clone::clone(_v)
2272                        ));
2273                    }
2274                });
2275            } else {
2276                out.insert_pushes.push(quote! {
2277                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2278                        _columns.push(#column);
2279                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2280                            ::core::clone::Clone::clone(_v)
2281                        ));
2282                    }
2283                });
2284            }
2285            // Bulk: Auto fields appear only in the all-Set path,
2286            // never in the Unset path (we drop them from `columns`).
2287            out.bulk_columns_all.push(quote!(#column));
2288            out.bulk_pushes_all.push(quote! {
2289                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2290                    ::core::clone::Clone::clone(&_row.#ident)
2291                ));
2292            });
2293            // …except the timestamp columns, which must appear in BOTH
2294            // paths (#1464). The path is chosen by the *first* Auto
2295            // field — in practice the PK — so a bulk insert of rows
2296            // with an unset PK took the no-auto branch and dropped the
2297            // timestamp column, falling back to the DB default that
2298            // this change exists to stop depending on.
2299            //
2300            // `rows` is borrowed immutably here, so this fills per row
2301            // at push time rather than writing back into the struct. An
2302            // explicitly-Set value is still honoured; only `Unset`
2303            // takes the clock.
2304            if info.auto_now_add || info.auto_now {
2305                out.bulk_columns_no_auto.push(quote!(#column));
2306                out.bulk_pushes_no_auto.push(quote! {
2307                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2308                        match &_row.#ident {
2309                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2310                            #root::sql::Auto::Unset => #root::__chrono::Utc::now(),
2311                        }
2312                    ));
2313                });
2314            }
2315            // Uniformity check: every row's Auto state must match the
2316            // first row's. Mixed Set/Unset within one bulk_insert is
2317            // rejected here so the column list stays consistent.
2318            let ident_clone = ident.clone();
2319            out.bulk_auto_uniformity.push(quote! {
2320                for _r in rows.iter().skip(1) {
2321                    if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2322                        return ::core::result::Result::Err(
2323                            #root::sql::ExecError::Sql(
2324                                #root::sql::SqlError::BulkAutoMixed
2325                            )
2326                        );
2327                    }
2328                }
2329            });
2330        } else {
2331            out.insert_pushes.push(quote! {
2332                _columns.push(#column);
2333                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2334                    ::core::clone::Clone::clone(&self.#ident)
2335                ));
2336            });
2337            // Bulk: non-Auto fields appear in BOTH paths.
2338            out.bulk_columns_no_auto.push(quote!(#column));
2339            out.bulk_columns_all.push(quote!(#column));
2340            let push_expr = quote! {
2341                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2342                    ::core::clone::Clone::clone(&_row.#ident)
2343                ));
2344            };
2345            out.bulk_pushes_no_auto.push(push_expr.clone());
2346            out.bulk_pushes_all.push(push_expr);
2347        }
2348        if info.primary_key {
2349            if out.primary_key.is_some() {
2350                return Err(syn::Error::new_spanned(
2351                    field,
2352                    "only one field may be marked `#[rustango(primary_key)]`",
2353                ));
2354            }
2355            out.primary_key = Some((ident.clone(), info.column.clone()));
2356            if info.auto {
2357                out.pk_is_auto = true;
2358            }
2359        } else if info.auto_now_add {
2360            // Immutable post-insert: skip from UPDATE entirely.
2361        } else if info.auto_now {
2362            // `auto_now` columns: bind `chrono::Utc::now()` on every
2363            // UPDATE so the column is always overridden with the
2364            // wall-clock at write time, regardless of what value the
2365            // user left in the struct field.
2366            out.update_assignments.push(quote! {
2367                #root::core::Assignment::new(
2368                    #column,
2369                    ::core::convert::Into::<#root::core::SqlValue>::into(
2370                        #root::__chrono::Utc::now()
2371                    ),
2372                )
2373            });
2374            out.upsert_update_columns.push(quote!(#column));
2375        } else {
2376            out.update_assignments.push(quote! {
2377                #root::core::Assignment::new(
2378                    #column,
2379                    ::core::convert::Into::<#root::core::SqlValue>::into(
2380                        ::core::clone::Clone::clone(&self.#ident)
2381                    ),
2382                )
2383            });
2384            out.upsert_update_columns.push(quote!(#column));
2385        }
2386        out.column_entries.push(ColumnEntry {
2387            ident: ident.clone(),
2388            value_ty: info.value_ty.clone(),
2389            name: ident.to_string(),
2390            column: info.column.clone(),
2391            field_type_tokens: info.field_type_tokens,
2392        });
2393    }
2394    Ok(out)
2395}
2396
2397fn model_impl_tokens(
2398    struct_name: &syn::Ident,
2399    model_name: &str,
2400    table: &str,
2401    display: Option<&str>,
2402    app_label: Option<&str>,
2403    admin: Option<&AdminAttrs>,
2404    default_order: &[(String, bool, proc_macro2::Span)],
2405    field_schemas: &[TokenStream2],
2406    soft_delete_column: Option<&str>,
2407    permissions: bool,
2408    audit_track: Option<&[String]>,
2409    m2m_relations: &[M2MAttr],
2410    indexes: &[IndexAttr],
2411    checks: &[CheckAttr],
2412    excludes: &[ExcludeAttr],
2413    composite_fks: &[CompositeFkAttr],
2414    generic_fks: &[GenericFkAttr],
2415    scope: Option<&str>,
2416    is_view: bool,
2417    verbose_name: Option<&str>,
2418    verbose_name_plural: Option<&str>,
2419    managed: bool,
2420    base_manager_name: Option<&str>,
2421    order_with_respect_to: Option<&str>,
2422    proxy: bool,
2423    required_db_features: &[String],
2424    required_db_vendor: Option<&str>,
2425    default_related_name: Option<&str>,
2426    db_table_comment: Option<&str>,
2427    get_latest_by: Option<(&str, bool)>,
2428    extra_permissions: &[(String, String)],
2429    default_permissions: &[String],
2430    global_scopes: &[GlobalScopeAttr],
2431    reverse_has_relations: &[ReverseHasAttr],
2432    generic_has_relations: &[GenericHasAttr],
2433) -> TokenStream2 {
2434    let root = rustango_root();
2435    let display_tokens = if let Some(name) = display {
2436        quote!(::core::option::Option::Some(#name))
2437    } else {
2438        quote!(::core::option::Option::None)
2439    };
2440    let app_label_tokens = if let Some(name) = app_label {
2441        quote!(::core::option::Option::Some(#name))
2442    } else {
2443        quote!(::core::option::Option::None)
2444    };
2445    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2446        quote!(::core::option::Option::Some(#col))
2447    } else {
2448        quote!(::core::option::Option::None)
2449    };
2450    let audit_track_tokens = match audit_track {
2451        None => quote!(::core::option::Option::None),
2452        Some(names) => {
2453            let lits = names.iter().map(|n| n.as_str());
2454            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2455        }
2456    };
2457    let admin_tokens = admin_config_tokens(admin);
2458    // Default `tenant` so single-tenant projects (no `scope` attr
2459    // anywhere) keep the v0.24.x behavior. Container-attr parser
2460    // already validated the value is "registry" or "tenant".
2461    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2462        Some("registry") => quote!(#root::core::ModelScope::Registry),
2463        _ => quote!(#root::core::ModelScope::Tenant),
2464    };
2465    let verbose_name_tokens = optional_str(verbose_name);
2466    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2467    let base_manager_name_tokens = optional_str(base_manager_name);
2468    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2469    let required_db_features_lits: Vec<&str> =
2470        required_db_features.iter().map(String::as_str).collect();
2471    let required_db_vendor_tokens = optional_str(required_db_vendor);
2472    let default_related_name_tokens = optional_str(default_related_name);
2473    let db_table_comment_tokens = optional_str(db_table_comment);
2474    let get_latest_by_tokens = match get_latest_by {
2475        Some((col, desc)) => {
2476            quote!(::core::option::Option::Some((#col, #desc)))
2477        }
2478        None => quote!(::core::option::Option::None),
2479    };
2480    let extra_permission_tokens: Vec<_> = extra_permissions
2481        .iter()
2482        .map(|(c, l)| quote!((#c, #l)))
2483        .collect();
2484    let default_permission_tokens: Vec<_> = default_permissions
2485        .iter()
2486        .map(|action| quote!(#action))
2487        .collect();
2488    let indexes_tokens = indexes.iter().map(|idx| {
2489        // When no explicit `name = "..."` was given, derive a stable,
2490        // collision-free name from the table + columns
2491        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2492        // would clash the moment a model declares two unnamed indexes.
2493        // Capped at Postgres's 63-char identifier limit.
2494        let derived_name = idx.name.clone().unwrap_or_else(|| {
2495            let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2496            if n.len() > 63 {
2497                n.truncate(63);
2498            }
2499            n
2500        });
2501        let name = derived_name.as_str();
2502        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2503        let unique = idx.unique;
2504        // Map the parsed method string onto the IndexMethod enum
2505        // variant — kept at the codegen layer so the IR doesn't
2506        // carry the string form.
2507        let method_variant = match idx.method.as_str() {
2508            "gin" => quote!(#root::core::IndexMethod::Gin),
2509            "gist" => quote!(#root::core::IndexMethod::Gist),
2510            "brin" => quote!(#root::core::IndexMethod::Brin),
2511            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2512            "hash" => quote!(#root::core::IndexMethod::Hash),
2513            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2514            _ => quote!(#root::core::IndexMethod::BTree),
2515        };
2516        let where_clause = match &idx.where_clause {
2517            Some(s) => quote!(::core::option::Option::Some(#s)),
2518            None => quote!(::core::option::Option::None),
2519        };
2520        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2521        quote! {{
2522            let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2523            i.unique = #unique;
2524            i.method = #method_variant;
2525            i.where_clause = #where_clause;
2526            i.include = &[ #(#include_lits),* ];
2527            i
2528        }}
2529    });
2530    let checks_tokens = checks.iter().map(|c| {
2531        let name = c.name.as_str();
2532        let expr = c.expr.as_str();
2533        quote!(#root::core::CheckConstraint::new(#name, #expr))
2534    });
2535    let excludes_tokens = excludes.iter().map(|e| {
2536        let name = e.name.as_str();
2537        let using = e.using.as_str();
2538        let element_tokens = e.elements.iter().map(|(col, op)| {
2539            let col_s = col.as_str();
2540            let op_s = op.as_str();
2541            quote!((#col_s, #op_s))
2542        });
2543        let where_tokens = match e.where_clause.as_deref() {
2544            Some(w) => quote!(::core::option::Option::Some(#w)),
2545            None => quote!(::core::option::Option::None),
2546        };
2547        quote! {{
2548            let mut e = #root::core::ExclusionConstraint::new(
2549                #name,
2550                #using,
2551                &[ #(#element_tokens),* ],
2552            );
2553            e.where_clause = #where_tokens;
2554            e
2555        }}
2556    });
2557    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2558        let name = rel.name.as_str();
2559        let to = rel.to.as_str();
2560        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2561        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2562        quote! {
2563            #root::core::CompositeFkRelation::new(
2564                #name,
2565                #to,
2566                &[ #(#from_cols),* ],
2567                &[ #(#on_cols),* ],
2568            )
2569        }
2570    });
2571    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2572        let name = rel.name.as_str();
2573        let ct_col = rel.ct_column.as_str();
2574        let pk_col = rel.pk_column.as_str();
2575        quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2576    });
2577    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2578    // no `#[rustango(default_order = "...")]` attribute was supplied.
2579    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2580        let col_lit = col.as_str();
2581        quote! { (#col_lit, #desc) }
2582    });
2583
2584    // Issue #820 — `global_scopes` slice literal. Empty when no
2585    // `#[rustango(global_scope(...))]` attribute was supplied. The
2586    // `apply` path is re-emitted verbatim so it resolves in the
2587    // consumer's scope; the name is stored as a string literal.
2588    let global_scope_tokens = global_scopes.iter().map(|s| {
2589        let name = s.name.as_str();
2590        // The slice sits in a nested `const`, where `Self` does not resolve.
2591        let mut apply = s.apply.clone();
2592        if let Some(first) = apply.segments.first_mut() {
2593            if first.ident == "Self" {
2594                first.ident = struct_name.clone();
2595            }
2596        }
2597        quote!(#root::core::GlobalScope::new(#name, #apply))
2598    });
2599
2600    let m2m_tokens = m2m_relations.iter().map(|rel| {
2601        let name = rel.name.as_str();
2602        let to = rel.to.as_str();
2603        let through = rel.through.as_str();
2604        let src = rel.src.as_str();
2605        let dst = rel.dst.as_str();
2606        let auto_create = rel.auto_create;
2607        quote! {{
2608            let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2609            m.auto_create = #auto_create;
2610            m
2611        }}
2612    });
2613    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2614    // when the model declares `#[rustango(reverse_has(...))]`. Each
2615    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2616    // a const expression. Models without reverse_has fall through to
2617    // the trait's empty default — no override emitted.
2618    let reverse_relations_override = if reverse_has_relations.is_empty() {
2619        quote!()
2620    } else {
2621        let entries = reverse_has_relations.iter().map(|rel| {
2622            let name = rel.name.as_str();
2623            let child = &rel.child;
2624            let child_fk_column = rel.child_fk_column.as_str();
2625            let self_pk_column = rel.self_pk_column.as_str();
2626            quote! {
2627                #root::core::ReverseRelation::new(
2628                    #name,
2629                    <#child as #root::core::Model>::SCHEMA,
2630                    #child_fk_column,
2631                    #self_pk_column,
2632                )
2633            }
2634        });
2635        quote! {
2636            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2637                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2638                RELS
2639            }
2640        }
2641    };
2642    // Issue #830 — `Model::generic_reverse_relations()` override for
2643    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2644    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2645        quote!()
2646    } else {
2647        let entries = generic_has_relations.iter().map(|rel| {
2648            let name = rel.name.as_str();
2649            let child = &rel.child;
2650            let ct_column = rel.ct_column.as_str();
2651            let pk_column = rel.pk_column.as_str();
2652            let self_pk_column = rel.self_pk_column.as_str();
2653            quote! {
2654                #root::core::GenericReverseRelation::new(
2655                    #name,
2656                    <#child as #root::core::Model>::SCHEMA,
2657                    #ct_column,
2658                    #pk_column,
2659                    #self_pk_column,
2660                )
2661            }
2662        });
2663        quote! {
2664            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2665                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2666                RELS
2667            }
2668        }
2669    };
2670    let fields_slice = const_slice(
2671        &quote!(#root::core::FieldSchema),
2672        field_schemas.iter().cloned(),
2673    );
2674    let m2m_slice = const_slice(&quote!(#root::core::M2MRelation), m2m_tokens);
2675    let indexes_slice = const_slice(&quote!(#root::core::IndexSchema), indexes_tokens);
2676    let checks_slice = const_slice(&quote!(#root::core::CheckConstraint), checks_tokens);
2677    let excludes_slice = const_slice(&quote!(#root::core::ExclusionConstraint), excludes_tokens);
2678    let composite_fk_slice = const_slice(
2679        &quote!(#root::core::CompositeFkRelation),
2680        composite_fk_tokens,
2681    );
2682    let generic_fk_slice = const_slice(&quote!(#root::core::GenericRelation), generic_fk_tokens);
2683    let global_scope_slice = const_slice(&quote!(#root::core::GlobalScope), global_scope_tokens);
2684    quote! {
2685        impl #root::core::Model for #struct_name {
2686            const SCHEMA: &'static #root::core::ModelSchema = &{
2687                let mut s = #root::core::ModelSchema::new(#model_name, #table);
2688                s.fields = #fields_slice;
2689                s.display = #display_tokens;
2690                s.app_label = #app_label_tokens;
2691                s.admin = #admin_tokens;
2692                s.soft_delete_column = #soft_delete_tokens;
2693                s.permissions = #permissions;
2694                s.audit_track = #audit_track_tokens;
2695                s.m2m = #m2m_slice;
2696                s.indexes = #indexes_slice;
2697                s.check_constraints = #checks_slice;
2698                s.exclusion_constraints = #excludes_slice;
2699                s.composite_relations = #composite_fk_slice;
2700                s.generic_relations = #generic_fk_slice;
2701                s.scope = #scope_tokens;
2702                s.default_order = &[ #(#default_order_tokens),* ];
2703                s.is_view = #is_view;
2704                s.verbose_name = #verbose_name_tokens;
2705                s.verbose_name_plural = #verbose_name_plural_tokens;
2706                s.managed = #managed;
2707                s.base_manager_name = #base_manager_name_tokens;
2708                s.order_with_respect_to = #order_with_respect_to_tokens;
2709                s.proxy = #proxy;
2710                s.required_db_features = &[ #(#required_db_features_lits),* ];
2711                s.required_db_vendor = #required_db_vendor_tokens;
2712                s.default_related_name = #default_related_name_tokens;
2713                s.db_table_comment = #db_table_comment_tokens;
2714                s.get_latest_by = #get_latest_by_tokens;
2715                s.extra_permissions = &[ #(#extra_permission_tokens),* ];
2716                s.default_permissions = &[ #(#default_permission_tokens),* ];
2717                s.global_scopes = #global_scope_slice;
2718                s
2719            };
2720
2721            #reverse_relations_override
2722            #generic_reverse_relations_override
2723        }
2724    }
2725}
2726
2727/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2728/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2729/// otherwise a static reference to a populated `AdminConfig`.
2730fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2731    let root = rustango_root();
2732    let Some(admin) = admin else {
2733        return quote!(::core::option::Option::None);
2734    };
2735
2736    let list_display = admin
2737        .list_display
2738        .as_ref()
2739        .map(|(v, _)| v.as_slice())
2740        .unwrap_or(&[]);
2741    let list_display_lits = list_display.iter().map(|s| s.as_str());
2742
2743    let search_fields = admin
2744        .search_fields
2745        .as_ref()
2746        .map(|(v, _)| v.as_slice())
2747        .unwrap_or(&[]);
2748    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2749
2750    let readonly_fields = admin
2751        .readonly_fields
2752        .as_ref()
2753        .map(|(v, _)| v.as_slice())
2754        .unwrap_or(&[]);
2755    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2756
2757    let list_filter = admin
2758        .list_filter
2759        .as_ref()
2760        .map(|(v, _)| v.as_slice())
2761        .unwrap_or(&[]);
2762    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2763
2764    let actions = admin
2765        .actions
2766        .as_ref()
2767        .map(|(v, _)| v.as_slice())
2768        .unwrap_or(&[]);
2769    let actions_lits = actions.iter().map(|s| s.as_str());
2770
2771    let fieldsets = admin
2772        .fieldsets
2773        .as_ref()
2774        .map(|(v, _)| v.as_slice())
2775        .unwrap_or(&[]);
2776    let fieldset_tokens = const_slice(
2777        &quote!(#root::core::Fieldset),
2778        fieldsets.iter().map(|(title, fields)| {
2779            let title = title.as_str();
2780            let field_lits = fields.iter().map(String::as_str);
2781            quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
2782        }),
2783    );
2784
2785    let list_display_links = admin
2786        .list_display_links
2787        .as_ref()
2788        .map(|(v, _)| v.as_slice())
2789        .unwrap_or(&[]);
2790    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2791
2792    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2793    let actions_on_top = admin.actions_on_top.unwrap_or(true);
2794    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2795    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2796
2797    let prepopulated = admin
2798        .prepopulated_fields
2799        .as_ref()
2800        .map(|(v, _)| v.as_slice())
2801        .unwrap_or(&[]);
2802    let prepopulated_tokens = const_slice(
2803        &quote!(#root::core::PrepopulatedField),
2804        prepopulated.iter().map(|(target, sources)| {
2805            let target = target.as_str();
2806            let source_lits = sources.iter().map(String::as_str);
2807            quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
2808        }),
2809    );
2810
2811    let raw_id_fields = admin
2812        .raw_id_fields
2813        .as_ref()
2814        .map(|(v, _)| v.as_slice())
2815        .unwrap_or(&[]);
2816    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2817
2818    let autocomplete_fields = admin
2819        .autocomplete_fields
2820        .as_ref()
2821        .map(|(v, _)| v.as_slice())
2822        .unwrap_or(&[]);
2823    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2824
2825    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2826    let list_select_related_tokens = match admin.list_select_related.as_deref() {
2827        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2828        Some("none") => quote!(#root::core::ListSelectRelated::None),
2829        Some(raw) => {
2830            let names: Vec<&str> = raw
2831                .split(',')
2832                .map(str::trim)
2833                .filter(|s| !s.is_empty())
2834                .collect();
2835            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2836        }
2837    };
2838
2839    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2840    // a Vec<(String, String)>. Empty / unset → no overrides.
2841    let formfield_pairs: Vec<(&str, &str)> = admin
2842        .formfield_overrides
2843        .as_ref()
2844        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2845        .unwrap_or_default();
2846    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2847        let field = *field;
2848        let widget = *widget;
2849        quote!((#field, #widget))
2850    });
2851
2852    let list_per_page = admin.list_per_page.unwrap_or(0);
2853
2854    let ordering_pairs = admin
2855        .ordering
2856        .as_ref()
2857        .map(|(v, _)| v.as_slice())
2858        .unwrap_or(&[]);
2859    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2860        let name = name.as_str();
2861        let desc = *desc;
2862        quote!((#name, #desc))
2863    });
2864
2865    quote! {
2866        ::core::option::Option::Some({
2867            const ADMIN: &#root::core::AdminConfig = &{
2868                let mut a = #root::core::AdminConfig::DEFAULT;
2869                a.list_display = &[ #( #list_display_lits ),* ];
2870                a.search_fields = &[ #( #search_fields_lits ),* ];
2871                a.list_per_page = #list_per_page;
2872                a.ordering = &[ #( #ordering_tokens ),* ];
2873                a.readonly_fields = &[ #( #readonly_fields_lits ),* ];
2874                a.list_filter = &[ #( #list_filter_lits ),* ];
2875                a.actions = &[ #( #actions_lits ),* ];
2876                a.fieldsets = #fieldset_tokens;
2877                a.list_display_links = &[ #( #list_display_links_lits ),* ];
2878                a.search_help_text = #search_help_text;
2879                a.actions_on_top = #actions_on_top;
2880                a.actions_on_bottom = #actions_on_bottom;
2881                a.date_hierarchy = #date_hierarchy;
2882                a.prepopulated_fields = #prepopulated_tokens;
2883                a.raw_id_fields = &[ #( #raw_id_fields_lits ),* ];
2884                a.autocomplete_fields = &[ #( #autocomplete_fields_lits ),* ];
2885                a.list_select_related = #list_select_related_tokens;
2886                a.formfield_overrides = &[ #( #formfield_tokens ),* ];
2887                a
2888            };
2889            ADMIN
2890        })
2891    }
2892}
2893
2894fn inherent_impl_tokens(
2895    struct_name: &syn::Ident,
2896    fields: &CollectedFields,
2897    primary_key: Option<&(syn::Ident, String)>,
2898    column_consts: &TokenStream2,
2899    audited_fields: Option<&[&ColumnEntry]>,
2900    indexes: &[IndexAttr],
2901    manager_fns: &[syn::Ident],
2902) -> TokenStream2 {
2903    let root = rustango_root();
2904    // Audit-emit fragments threaded into write paths. Non-empty only
2905    // when the model carries `#[rustango(audit(...))]`. They reborrow
2906    // `_executor` (a `&mut PgConnection` for audited models — the
2907    // macro switches the signature below) so the data write and the
2908    // audit INSERT both run on the same caller-supplied connection.
2909    let executor_passes_to_data_write = if audited_fields.is_some() {
2910        quote!(&mut *_executor)
2911    } else {
2912        quote!(_executor)
2913    };
2914    let executor_param = if audited_fields.is_some() {
2915        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2916    } else {
2917        quote!(_executor: _E)
2918    };
2919    let executor_generics = if audited_fields.is_some() {
2920        quote!()
2921    } else {
2922        quote!(<'_c, _E>)
2923    };
2924    let executor_where = if audited_fields.is_some() {
2925        quote!()
2926    } else {
2927        quote! {
2928            where
2929                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2930        }
2931    };
2932    // For audited models the `_on` methods take `&mut PgConnection`, so
2933    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2934    // must acquire a connection first. Non-audited models keep the
2935    // direct delegation since `&PgPool` IS an Executor.
2936    let pool_to_save_on = if audited_fields.is_some() {
2937        quote! {
2938            let mut _conn = pool.acquire().await?;
2939            self.save_on(&mut *_conn).await
2940        }
2941    } else {
2942        quote!(self.save_on(pool).await)
2943    };
2944    let pool_to_insert_on = if audited_fields.is_some() {
2945        quote! {
2946            let mut _conn = pool.acquire().await?;
2947            self.insert_on(&mut *_conn).await
2948        }
2949    } else {
2950        quote!(self.insert_on(pool).await)
2951    };
2952    let pool_to_delete_on = if audited_fields.is_some() {
2953        quote! {
2954            let mut _conn = pool.acquire().await?;
2955            self.delete_on(&mut *_conn).await
2956        }
2957    } else {
2958        quote!(self.delete_on(pool).await)
2959    };
2960    let pool_to_bulk_insert_on = if audited_fields.is_some() {
2961        quote! {
2962            let mut _conn = pool.acquire().await?;
2963            Self::bulk_insert_on(rows, &mut *_conn).await
2964        }
2965    } else {
2966        quote!(Self::bulk_insert_on(rows, pool).await)
2967    };
2968    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2969    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2970    // directly, but `upsert_on` for audited models takes
2971    // `&mut PgConnection` (the audit emit needs a real connection).
2972    // Add the missing acquire shim to keep audited Auto-PK upsert
2973    // compiling.
2974    let pool_to_upsert_on = if audited_fields.is_some() {
2975        quote! {
2976            let mut _conn = pool.acquire().await?;
2977            self.upsert_on(&mut *_conn).await
2978        }
2979    } else {
2980        quote!(self.upsert_on(pool).await)
2981    };
2982
2983    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2984    // (audit-on-connection over &Pool needs a bi-dialect transaction
2985    // helper, deferred). Two body shapes:
2986    // - has_auto: build InsertQuery skipping Auto::Unset columns,
2987    //   request Auto cols in `returning`, dispatch via
2988    //   `insert_returning_pool`, then on the returned `PgRow` /
2989    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
2990    //   row OR drop the single i64 into the first Auto field on MySQL
2991    //   (multi-Auto models on MySQL error at runtime since
2992    //   `LAST_INSERT_ID()` only reports one)
2993    // - non-Auto: build InsertQuery with explicit columns/values and
2994    //   call `insert_pool` (no returning needed)
2995    // pool_insert_method body for the audited Auto-PK case is moved
2996    // to after audit_pair_tokens / audit_pk_to_string (they live
2997    // ~150 lines below). This block keeps the non-audited and
2998    // non-Auto branches in place — the audited Auto-PK arm is
2999    // computed below and merged via the dispatch helper variable.
3000    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3001        // Audited models with explicit (non-Auto) PKs go through
3002        // the non-Auto insert path below — the audit emit is one
3003        // round-trip after the INSERT inside the same tx via
3004        // audit::save_one_with_audit? No, INSERT semantics
3005        // differ. For non-Auto PK + audited, route through a
3006        // dedicated insert + audit emit on the same tx, but defer
3007        // the macro emission to the audit-bundle-aware block below
3008        // — this `quote!()` placeholder gets overwritten there.
3009        quote!()
3010    } else if audited_fields.is_some() && fields.has_auto {
3011        // Audited Auto-PK insert_pool — assembled after the audit
3012        // bundles. Placeholder; real emission below.
3013        quote!()
3014    } else if fields.has_auto {
3015        let pushes = &fields.insert_pushes;
3016        let returning_cols = &fields.returning_cols;
3017        // When every `Auto<T>` field is filled Rust-side
3018        // (`default_uuid_v7`, issue #823), there is no column to read
3019        // back from the database — `returning_cols` is empty. Route
3020        // through plain `insert_pool` instead of
3021        // `insert_returning_pool` to skip the redundant RETURNING /
3022        // LAST_INSERT_ID round-trip.
3023        if fields.returning_cols.is_empty() {
3024            quote! {
3025                /// Insert this row against either backend. Every
3026                /// `Auto<T>` PK on this model is filled Rust-side
3027                /// (e.g. `default_uuid_v7`) before binding, so no
3028                /// RETURNING round-trip is needed.
3029                ///
3030                /// # Errors
3031                /// As [`Self::insert`].
3032                pub async fn insert_pool(
3033                    &mut self,
3034                    pool: &#root::sql::Pool,
3035                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3036                    let mut _columns: ::std::vec::Vec<&'static str> =
3037                        ::std::vec::Vec::new();
3038                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3039                        ::std::vec::Vec::new();
3040                    #( #pushes )*
3041                    let _query = #root::core::InsertQuery::new(
3042                        <Self as #root::core::Model>::SCHEMA,
3043                        _columns,
3044                        _values,
3045                    );
3046                    #root::sql::insert_pool(pool, &_query).await
3047                }
3048
3049                /// Eloquent `Model::insertOrIgnore()` — same shape
3050                /// as the auto-PK branch above. Returns `Ok(true)`
3051                /// when inserted, `Ok(false)` when a conflict caused
3052                /// the INSERT to silently skip.
3053                ///
3054                /// # Errors
3055                /// As [`Self::insert`].
3056                pub async fn insert_or_ignore(
3057                    &mut self,
3058                    pool: &#root::sql::Pool,
3059                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3060                    let mut _columns: ::std::vec::Vec<&'static str> =
3061                        ::std::vec::Vec::new();
3062                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3063                        ::std::vec::Vec::new();
3064                    #( #pushes )*
3065                    let _query = #root::core::InsertQuery::new(
3066                        <Self as #root::core::Model>::SCHEMA,
3067                        _columns,
3068                        _values,
3069                    )
3070                    .on_conflict(#root::core::ConflictClause::DoNothing);
3071                    let dialect = pool.dialect();
3072                    let stmt = dialect.compile_insert(&_query)?;
3073                    let rows = #root::sql::raw_execute_pool(
3074                        pool, &stmt.sql, stmt.params,
3075                    ).await?;
3076                    ::core::result::Result::Ok(rows > 0)
3077                }
3078            }
3079        } else {
3080            quote! {
3081                /// Insert this row against either backend, populating any
3082                /// `Auto<T>` PK from the auto-assigned value.
3083                ///
3084                /// # Errors
3085                /// As [`Self::insert`].
3086                pub async fn insert_pool(
3087                    &mut self,
3088                    pool: &#root::sql::Pool,
3089                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3090                    let mut _columns: ::std::vec::Vec<&'static str> =
3091                        ::std::vec::Vec::new();
3092                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3093                        ::std::vec::Vec::new();
3094                    #( #pushes )*
3095                    let _query = #root::core::InsertQuery::new(
3096                        <Self as #root::core::Model>::SCHEMA,
3097                        _columns,
3098                        _values,
3099                    )
3100                    .returning(::std::vec![ #( #returning_cols ),* ]);
3101                    let _result = #root::sql::insert_returning_pool(
3102                        pool, &_query,
3103                    ).await?;
3104                    #root::sql::apply_auto_pk(_result, self)
3105                }
3106
3107                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3108                /// row or silently skip on unique-constraint
3109                /// violation. Returns `Ok(true)` when a row was
3110                /// inserted, `Ok(false)` when a conflict caused the
3111                /// INSERT to silently skip.
3112                ///
3113                /// **Caveat on auto-PK models**: when the row is
3114                /// skipped (conflict), this instance's `Auto<T>`
3115                /// fields stay `Unset` — no PK is back-populated
3116                /// because the server didn't auto-assign one. For
3117                /// "insert then read back the PK or the existing
3118                /// row's PK", use the `upsert` family or
3119                /// `get_or_create`.
3120                ///
3121                /// # Errors
3122                /// As [`Self::insert`].
3123                pub async fn insert_or_ignore(
3124                    &mut self,
3125                    pool: &#root::sql::Pool,
3126                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3127                    let mut _columns: ::std::vec::Vec<&'static str> =
3128                        ::std::vec::Vec::new();
3129                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3130                        ::std::vec::Vec::new();
3131                    #( #pushes )*
3132                    let _query = #root::core::InsertQuery::new(
3133                        <Self as #root::core::Model>::SCHEMA,
3134                        _columns,
3135                        _values,
3136                    )
3137                    .on_conflict(#root::core::ConflictClause::DoNothing);
3138                    let dialect = pool.dialect();
3139                    let stmt = dialect.compile_insert(&_query)?;
3140                    let rows = #root::sql::raw_execute_pool(
3141                        pool, &stmt.sql, stmt.params,
3142                    ).await?;
3143                    ::core::result::Result::Ok(rows > 0)
3144                }
3145            }
3146        }
3147    } else {
3148        let insert_columns = &fields.insert_columns;
3149        let insert_values = &fields.insert_values;
3150        quote! {
3151            /// Insert this row into its table against either backend.
3152            /// Equivalent to [`Self::insert`] but takes
3153            /// [`#root::sql::Pool`].
3154            ///
3155            /// # Errors
3156            /// As [`Self::insert`].
3157            pub async fn insert_pool(
3158                &self,
3159                pool: &#root::sql::Pool,
3160            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3161                let _query = #root::core::InsertQuery::new(
3162                    <Self as #root::core::Model>::SCHEMA,
3163                    ::std::vec![ #( #insert_columns ),* ],
3164                    ::std::vec![ #( #insert_values ),* ],
3165                );
3166                #root::sql::insert_pool(pool, &_query).await
3167            }
3168
3169            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3170            /// or silently skip on unique-constraint violation. Maps
3171            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3172            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3173            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3174            /// `INSERT IGNORE INTO …`.
3175            ///
3176            /// Returns `Ok(true)` when a row was inserted,
3177            /// `Ok(false)` when a conflict caused the INSERT to
3178            /// silently skip.
3179            ///
3180            /// Use for "create-if-absent" patterns where you don't
3181            /// need the row back. For "find-or-create with the row
3182            /// returned", use the queryset-level
3183            /// `crate::sql::get_or_create` free function.
3184            ///
3185            /// # Errors
3186            /// As [`Self::insert`], plus any dialect-specific
3187            /// translation error from the ConflictClause writer.
3188            pub async fn insert_or_ignore(
3189                &self,
3190                pool: &#root::sql::Pool,
3191            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3192                let _query = #root::core::InsertQuery::new(
3193                    <Self as #root::core::Model>::SCHEMA,
3194                    ::std::vec![ #( #insert_columns ),* ],
3195                    ::std::vec![ #( #insert_values ),* ],
3196                )
3197                .on_conflict(#root::core::ConflictClause::DoNothing);
3198                let dialect = pool.dialect();
3199                let stmt = dialect.compile_insert(&_query)?;
3200                let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3201                ::core::result::Result::Ok(rows > 0)
3202            }
3203        }
3204    };
3205
3206    // pool_save_method moved to after audit_pair_tokens /
3207    // audit_pk_to_string (they live ~70 lines below) — needed for
3208    // the audited branch which builds an UpdateQuery + PendingEntry
3209    // and dispatches via audit::save_one_with_audit.
3210
3211    // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3212    // are computed (they live ~80 lines below).
3213
3214    // Build the (column, JSON value) pair list used by every
3215    // snapshot-style audit emission. Reused across delete_on,
3216    // soft_delete_on, restore_on, and (later) bulk paths. Empty
3217    // when the model isn't audited.
3218    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3219        .map(|tracked| {
3220            tracked
3221                .iter()
3222                .map(|c| {
3223                    let column_lit = c.column.as_str();
3224                    let ident = &c.ident;
3225                    quote! {
3226                        (
3227                            #column_lit,
3228                            #root::__serde_json::to_value(&self.#ident)
3229                                .unwrap_or(#root::__serde_json::Value::Null),
3230                        )
3231                    }
3232                })
3233                .collect()
3234        })
3235        .unwrap_or_default();
3236    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3237        if fields.pk_is_auto {
3238            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3239        } else {
3240            quote!(::std::format!("{}", &self.#pk_ident))
3241        }
3242    } else {
3243        quote!(::std::string::String::new())
3244    };
3245    // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3246    // is built, so every audited write path records the same shape.
3247    let audit_entry_method = if audited_fields.is_some() {
3248        let pairs = audit_pair_tokens.iter();
3249        let pk_str = audit_pk_to_string.clone();
3250        quote! {
3251            /// Snapshot audit entry for `operation` on this row.
3252            #[doc(hidden)]
3253            pub fn __rustango_audit_entry(
3254                &self,
3255                operation: #root::audit::AuditOp,
3256            ) -> #root::audit::PendingEntry {
3257                #root::audit::PendingEntry {
3258                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3259                    entity_pk: #pk_str,
3260                    operation,
3261                    source: #root::audit::current_source(),
3262                    changes: #root::audit::snapshot_changes(&[
3263                        #( #pairs ),*
3264                    ]),
3265                }
3266            }
3267        }
3268    } else {
3269        quote!()
3270    };
3271    // Builds `_audit_entry`; `written` is the soft-delete column's new
3272    // value, recorded in place of the stale `self` field.
3273    let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3274        if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3275            quote! {
3276                let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3277                _audit_entry.set_tracked(#col, #v);
3278            }
3279        } else {
3280            quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3281        }
3282    };
3283    let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3284        .unwrap_or(#root::__serde_json::Value::Null));
3285    let restored_json = quote!(#root::__serde_json::Value::Null);
3286    // `_on` emit; `affected` paths skip the row when nothing changed.
3287    let make_op_emit =
3288        |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3289            if audited_fields.is_none() {
3290                return quote!();
3291            }
3292            let entry = make_entry(op_path, written);
3293            let emit = quote! {
3294                #entry
3295                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3296            };
3297            if affected {
3298                quote!(if _affected > 0 { #emit })
3299            } else {
3300                emit
3301            }
3302        };
3303    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3304    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3305    let audit_softdelete_emit = make_op_emit(
3306        quote!(#root::audit::AuditOp::SoftDelete),
3307        Some(soft_deleted_json.clone()),
3308        true,
3309    );
3310    let audit_restore_emit = make_op_emit(
3311        quote!(#root::audit::AuditOp::Restore),
3312        Some(restored_json.clone()),
3313        true,
3314    );
3315    // `&Pool` update runner for soft_delete / restore: one tx with the
3316    // audit row when audited, a plain UPDATE otherwise.
3317    let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3318        if audited_fields.is_some() {
3319            let entry = make_entry(op_path, Some(written));
3320            quote! {
3321                #entry
3322                #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3323            }
3324        } else {
3325            quote!(#root::sql::update_pool(pool, &_query).await)
3326        }
3327    };
3328    let pool_softdelete_run =
3329        make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3330    let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3331
3332    // `save_pool(&Pool)` — emitted for every model with a PK.
3333    // Audited Auto-PK models are deferred (the Auto::Unset →
3334    // insert_pool path needs the audited-insert flow from a future
3335    // batch). Three body shapes:
3336    // - non-audited, plain PK: build UpdateQuery + dispatch through
3337    //   sql::update_pool
3338    // - non-audited, Auto-PK: same, but Auto::Unset routes to
3339    //   self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3340    // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3341    //   through audit::save_one_with_audit (per-backend tx wraps
3342    //   UPDATE + audit emit atomically). Snapshot-style audit (post-
3343    //   write field values) — diff-style audit (with pre-UPDATE
3344    //   SELECT for `before` values) needs per-tracked-column codegen
3345    //   that doesn't fit the runtime-helper pattern; legacy &PgPool
3346    //   `save` keeps the diff for now.
3347    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3348        let pk_column_lit = pk_col.as_str();
3349        let assignments = &fields.update_assignments;
3350        if audited_fields.is_some() {
3351            if fields.pk_is_auto {
3352                // Auto-PK + audited: defer. The Auto::Unset insert
3353                // path needs a transactional INSERT + LAST_INSERT_ID
3354                // + audit emit flow — that's a follow-up batch.
3355                quote!()
3356            } else {
3357                let pairs = audit_pair_tokens.iter();
3358                let pairs2 = audit_pair_tokens.iter();
3359                let pk_str = audit_pk_to_string.clone();
3360                let pk_str2 = audit_pk_to_string.clone();
3361                quote! {
3362                    /// Save (UPDATE) this row against either backend
3363                    /// with audit emission inside the same transaction.
3364                    /// Bi-dialect counterpart of [`Self::save`] for
3365                    /// audited models with non-`Auto<T>` PKs.
3366                    ///
3367                    /// Captures **post-write** field state (snapshot
3368                    /// audit). The legacy &PgPool [`Self::save`]
3369                    /// captures BEFORE+AFTER for true diff audit;
3370                    /// porting that to the &Pool path needs runtime
3371                    /// per-tracked-column decoding and is deferred.
3372                    ///
3373                    /// # Errors
3374                    /// As [`Self::save`].
3375                    pub async fn save_pool(
3376                        &mut self,
3377                        pool: &#root::sql::Pool,
3378                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3379                        let _query = #root::core::UpdateQuery::new(
3380                            <Self as #root::core::Model>::SCHEMA,
3381                            ::std::vec![ #( #assignments ),* ],
3382                            #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3383                                #pk_column_lit,
3384                                #root::core::Op::Eq,
3385                                ::core::convert::Into::<#root::core::SqlValue>::into(
3386                                    ::core::clone::Clone::clone(&self.#pk_ident)
3387                                ),
3388                            )),
3389                        );
3390                        let _audit_entry = #root::audit::PendingEntry {
3391                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3392                            entity_pk: #pk_str,
3393                            operation: #root::audit::AuditOp::Update,
3394                            source: #root::audit::current_source(),
3395                            changes: #root::audit::snapshot_changes(&[
3396                                #( #pairs ),*
3397                            ]),
3398                        };
3399                        let _affected = #root::audit::save_one_with_audit(
3400                            pool, &_query, &_audit_entry,
3401                        ).await?;
3402                        ::core::result::Result::Ok(_affected)
3403                    }
3404
3405                    /// `save_pool` narrowed to a Rust-field allowlist — issue #66.
3406                    /// Audit emission shrinks to the same column set so
3407                    /// the audit log reflects exactly what was written.
3408                    ///
3409                    /// # Errors
3410                    /// As [`Self::save_pool`], plus
3411                    /// [`#root::core::QueryError::UnknownField`] wrapped
3412                    /// in `ExecError::Query` for unknown field names.
3413                    pub async fn save_partial(
3414                        &mut self,
3415                        fields: &[&str],
3416                        pool: &#root::sql::Pool,
3417                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3418                        if fields.is_empty() {
3419                            #root::__tracing::warn!(
3420                                target: "rustango::save_partial",
3421                                model = <Self as #root::core::Model>::SCHEMA.name,
3422                                "save_partial called with empty field list — no-op"
3423                            );
3424                            return ::core::result::Result::Ok(0);
3425                        }
3426                        let _schema = <Self as #root::core::Model>::SCHEMA;
3427                        let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3428                            ::std::collections::HashSet::with_capacity(fields.len());
3429                        for f in fields {
3430                            match _schema.field(f) {
3431                                ::core::option::Option::Some(fs) => {
3432                                    _wanted_cols.insert(fs.column);
3433                                }
3434                                ::core::option::Option::None => {
3435                                    return ::core::result::Result::Err(
3436                                        #root::sql::ExecError::Query(
3437                                            #root::core::QueryError::UnknownField {
3438                                                model: _schema.name,
3439                                                field: (*f).to_owned(),
3440                                            }
3441                                        )
3442                                    );
3443                                }
3444                            }
3445                        }
3446                        let _full: ::std::vec::Vec<#root::core::Assignment> =
3447                            ::std::vec![ #( #assignments ),* ];
3448                        let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3449                            .into_iter()
3450                            .filter(|a| _wanted_cols.contains(a.column))
3451                            .collect();
3452                        if _filtered.is_empty() {
3453                            #root::__tracing::warn!(
3454                                target: "rustango::save_partial",
3455                                model = _schema.name,
3456                                "save_partial: every named field maps to a non-assignable column — no-op"
3457                            );
3458                            return ::core::result::Result::Ok(0);
3459                        }
3460                        let _query = #root::core::UpdateQuery::new(
3461                            _schema,
3462                            _filtered,
3463                            #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3464                                #pk_column_lit,
3465                                #root::core::Op::Eq,
3466                                ::core::convert::Into::<#root::core::SqlValue>::into(
3467                                    ::core::clone::Clone::clone(&self.#pk_ident)
3468                                ),
3469                            )),
3470                        );
3471                        // Narrow the audit snapshot to the same column set.
3472                        let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3473                            ::std::vec![ #( #pairs2 ),* ];
3474                        let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3475                            _all_pairs
3476                                .into_iter()
3477                                .filter(|(col, _)| _wanted_cols.contains(col))
3478                                .collect();
3479                        let _audit_entry = #root::audit::PendingEntry {
3480                            entity_table: _schema.table,
3481                            entity_pk: #pk_str2,
3482                            operation: #root::audit::AuditOp::Update,
3483                            source: #root::audit::current_source(),
3484                            changes: #root::audit::snapshot_changes(&_narrowed),
3485                        };
3486                        let _affected = #root::audit::save_one_with_audit(
3487                            pool, &_query, &_audit_entry,
3488                        ).await?;
3489                        ::core::result::Result::Ok(_affected)
3490                    }
3491
3492                    /// Typed-column counterpart of [`Self::save_partial`] —
3493                    /// issue #67. `fields` is a tuple of [`Column`]
3494                    /// constants whose `Model` matches `Self`; typos and
3495                    /// model mismatches surface at *compile time*
3496                    /// (`Author::name` inside a `Post::save_partial_typed`
3497                    /// call is a type error, no runtime check).
3498                    ///
3499                    /// ```ignore
3500                    /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3501                    /// ```
3502                    ///
3503                    /// Lowers to [`Self::save_partial`] under the hood;
3504                    /// audit narrowing + every other semantic is identical.
3505                    ///
3506                    /// [`Column`]: #root::core::Column
3507                    ///
3508                    /// # Errors
3509                    /// As [`Self::save_partial`].
3510                    pub async fn save_partial_typed<
3511                        L: #root::core::TypedFieldList<Self>,
3512                    >(
3513                        &mut self,
3514                        fields: L,
3515                        pool: &#root::sql::Pool,
3516                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3517                        let _names = fields.rust_field_names();
3518                        let _refs: ::std::vec::Vec<&str> =
3519                            _names.iter().copied().collect();
3520                        self.save_partial(&_refs, pool).await
3521                    }
3522                }
3523            }
3524        } else {
3525            let dispatch_unset = if fields.pk_is_auto {
3526                quote! {
3527                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3528                        return self.insert_pool(pool).await.map(|()| 1u64);
3529                    }
3530                }
3531            } else {
3532                quote!()
3533            };
3534            quote! {
3535                /// Save this row to its table against either backend.
3536                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3537                /// `UPDATE` keyed on the PK.
3538                ///
3539                /// # Errors
3540                /// As [`Self::save`].
3541                pub async fn save_pool(
3542                    &mut self,
3543                    pool: &#root::sql::Pool,
3544                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3545                    #dispatch_unset
3546                    let _query = #root::core::UpdateQuery::new(
3547                        <Self as #root::core::Model>::SCHEMA,
3548                        ::std::vec![ #( #assignments ),* ],
3549                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3550                            #pk_column_lit,
3551                            #root::core::Op::Eq,
3552                            ::core::convert::Into::<#root::core::SqlValue>::into(
3553                                ::core::clone::Clone::clone(&self.#pk_ident)
3554                            ),
3555                        )),
3556                    );
3557                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3558                    ::core::result::Result::Ok(_affected)
3559                }
3560
3561                /// Save (UPDATE) only the listed Rust-side fields,
3562                /// leaving every other column untouched. Issue #66.
3563                ///
3564                /// `fields` are Rust-side struct field names; the macro
3565                /// resolves each to its SQL column. Unknown field
3566                /// names return [`#root::core::QueryError::UnknownField`]
3567                /// wrapped in `ExecError::Query`. An empty list is a
3568                /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3569                ///
3570                /// Use this when:
3571                /// * you only mutated a couple of fields on a wide row
3572                ///   (avoid re-writing every column on every save), or
3573                /// * two writers diverged after their initial read and
3574                ///   you want to preserve the other writer's changes to
3575                ///   columns you didn't touch.
3576                ///
3577                /// Auto-PK models with an unset PK return
3578                /// [`#root::core::QueryError::UnknownField`] with
3579                /// field name `<pk>` — `save_partial` is an
3580                /// UPDATE-only path. Call [`Self::insert_pool`]
3581                /// (or [`Self::save_pool`] which dispatches based on
3582                /// PK state) for the INSERT case.
3583                ///
3584                /// # Errors
3585                /// As [`Self::save_pool`], plus `UnknownField` for
3586                /// unknown / empty / Auto-Unset cases.
3587                pub async fn save_partial(
3588                    &mut self,
3589                    fields: &[&str],
3590                    pool: &#root::sql::Pool,
3591                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3592                    if fields.is_empty() {
3593                        #root::__tracing::warn!(
3594                            target: "rustango::save_partial",
3595                            model = <Self as #root::core::Model>::SCHEMA.name,
3596                            "save_partial called with empty field list — no-op"
3597                        );
3598                        return ::core::result::Result::Ok(0);
3599                    }
3600                    let _schema = <Self as #root::core::Model>::SCHEMA;
3601                    // Validate field names against the schema.
3602                    let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3603                        ::std::collections::HashSet::with_capacity(fields.len());
3604                    for f in fields {
3605                        match _schema.field(f) {
3606                            ::core::option::Option::Some(fs) => {
3607                                _wanted_cols.insert(fs.column);
3608                            }
3609                            ::core::option::Option::None => {
3610                                return ::core::result::Result::Err(
3611                                    #root::sql::ExecError::Query(
3612                                        #root::core::QueryError::UnknownField {
3613                                            model: _schema.name,
3614                                            field: (*f).to_owned(),
3615                                        }
3616                                    )
3617                                );
3618                            }
3619                        }
3620                    }
3621                    // Build the full assignment vec, then keep only the
3622                    // assignments whose column is in `_wanted_cols`.
3623                    let _full: ::std::vec::Vec<#root::core::Assignment> =
3624                        ::std::vec![ #( #assignments ),* ];
3625                    let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3626                        .into_iter()
3627                        .filter(|a| _wanted_cols.contains(a.column))
3628                        .collect();
3629                    if _filtered.is_empty() {
3630                        // All field names valid, but they all map to
3631                        // non-assignable slots (PK column, computed/
3632                        // virtual fields, relations without an
3633                        // assignment). Same no-op as an empty list.
3634                        #root::__tracing::warn!(
3635                            target: "rustango::save_partial",
3636                            model = _schema.name,
3637                            "save_partial: every named field maps to a non-assignable column — no-op"
3638                        );
3639                        return ::core::result::Result::Ok(0);
3640                    }
3641                    let _query = #root::core::UpdateQuery::new(
3642                        _schema,
3643                        _filtered,
3644                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3645                            #pk_column_lit,
3646                            #root::core::Op::Eq,
3647                            ::core::convert::Into::<#root::core::SqlValue>::into(
3648                                ::core::clone::Clone::clone(&self.#pk_ident)
3649                            ),
3650                        )),
3651                    );
3652                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3653                    ::core::result::Result::Ok(_affected)
3654                }
3655
3656                /// Typed-column counterpart of [`Self::save_partial`] —
3657                /// issue #67. `fields` is a tuple of [`Column`]
3658                /// constants whose `Model` matches `Self`; typos and
3659                /// model mismatches surface at *compile time*
3660                /// (`Author::name` inside a `Post::save_partial_typed`
3661                /// call is a type error, no runtime check).
3662                ///
3663                /// ```ignore
3664                /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3665                /// ```
3666                ///
3667                /// Lowers to [`Self::save_partial`] under the hood — the
3668                /// tuple is reduced to a `&[&str]` slice of Rust-side
3669                /// field names and forwarded.
3670                ///
3671                /// [`Column`]: #root::core::Column
3672                ///
3673                /// # Errors
3674                /// As [`Self::save_partial`].
3675                pub async fn save_partial_typed<
3676                    L: #root::core::TypedFieldList<Self>,
3677                >(
3678                    &mut self,
3679                    fields: L,
3680                    pool: &#root::sql::Pool,
3681                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3682                    let _names = fields.rust_field_names();
3683                    let _refs: ::std::vec::Vec<&str> =
3684                        _names.iter().copied().collect();
3685                    self.save_partial(&_refs, pool).await
3686                }
3687            }
3688        }
3689    } else {
3690        quote!()
3691    };
3692
3693    // Audited `insert_pool` (overrides the placeholder set higher up
3694    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3695    // audited models get insert_pool routing through
3696    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3697    // + auto-PK readback + audit emit). Snapshot-style audit (the
3698    // PendingEntry's `changes` carries post-write field values).
3699    let pool_insert_method = if audited_fields.is_some() {
3700        if let Some(_) = primary_key {
3701            let pushes = if fields.has_auto {
3702                fields.insert_pushes.clone()
3703            } else {
3704                // For non-Auto-PK models, the macro normally builds
3705                // {columns, values} from fields.insert_columns +
3706                // fields.insert_values rather than insert_pushes.
3707                // Map those into the pushes shape.
3708                fields
3709                    .insert_columns
3710                    .iter()
3711                    .zip(&fields.insert_values)
3712                    .map(|(col, val)| {
3713                        quote! {
3714                            _columns.push(#col);
3715                            _values.push(#val);
3716                        }
3717                    })
3718                    .collect()
3719            };
3720            let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3721                fields.returning_cols.clone()
3722            } else {
3723                // Non-Auto-PK: still need RETURNING something for the
3724                // audit helper's contract (it errors on empty
3725                // returning). Return the PK column so the audit row
3726                // can carry the assigned PK back. Some non-Auto PKs
3727                // are server-side-default (e.g. UUIDv4 default), so
3728                // RETURNING is genuinely useful.
3729                primary_key
3730                    .map(|(_, col)| {
3731                        let lit = col.as_str();
3732                        vec![quote!(#lit)]
3733                    })
3734                    .unwrap_or_default()
3735            };
3736            quote! {
3737                /// Insert this row against either backend with audit
3738                /// emission inside the same transaction. Bi-dialect
3739                /// counterpart of [`Self::insert`] for audited models.
3740                ///
3741                /// Snapshot-style audit (post-write field values).
3742                ///
3743                /// # Errors
3744                /// As [`Self::insert`].
3745                pub async fn insert_pool(
3746                    &mut self,
3747                    pool: &#root::sql::Pool,
3748                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3749                    let mut _columns: ::std::vec::Vec<&'static str> =
3750                        ::std::vec::Vec::new();
3751                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3752                        ::std::vec::Vec::new();
3753                    #( #pushes )*
3754                    let _query = #root::core::InsertQuery::new(
3755                        <Self as #root::core::Model>::SCHEMA,
3756                        _columns,
3757                        _values,
3758                    )
3759                    .returning(::std::vec![ #( #returning_cols ),* ]);
3760                    #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
3761                        _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
3762                    })
3763                    .await
3764                }
3765            }
3766        } else {
3767            quote!()
3768        }
3769    } else {
3770        // Keep the non-audited pool_insert_method we built earlier.
3771        pool_insert_method
3772    };
3773
3774    // Update audited save_pool: now that insert_pool is wired for
3775    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3776    // insert_pool. Non-audited save_pool already does this.
3777    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3778    // Replaces the snapshot-only emission with a per-backend transaction
3779    // body that:
3780    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3781    //     column), capturing BEFORE values
3782    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3783    //  3. builds AFTER pairs from &self
3784    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3785    //     AuditOp::Update + diff_changes(...) on the same tx connection
3786    //  5. commits
3787    //
3788    // Per-backend arms inline the SQL string + placeholder shape, then
3789    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3790    // is polymorphic over Row type — the same tokens work against
3791    // PgRow and MySqlRow as long as the field's Rust type implements
3792    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3793    // primitives + chrono/uuid/serde_json::Value all do).
3794    let pool_save_method = if let Some(tracked) = audited_fields {
3795        if let Some((pk_ident, pk_col)) = primary_key {
3796            let pk_column_lit = pk_col.as_str();
3797            // Two iterators — quote!'s `#(#var)*` consumes the
3798            // iterator, and we need to splice the same after-pairs
3799            // sequence into both per-backend arms.
3800            let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3801            let pk_str = audit_pk_to_string.clone();
3802            // Per-tracked-column BEFORE-pair token list. Each entry
3803            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3804            // The Row alias resolves to PgRow / MySqlRow per call site,
3805            // so the same template generates both the PG and MySQL bodies.
3806            let mk_before_pairs =
3807                |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3808                    tracked
3809                        .iter()
3810                        .map(|c| {
3811                            let column_lit = c.column.as_str();
3812                            let value_ty = &c.value_ty;
3813                            quote! {
3814                                (
3815                                    #column_lit,
3816                                    match #getter::<#value_ty>(
3817                                        _audit_before_row, #column_lit,
3818                                    ) {
3819                                        ::core::result::Result::Ok(v) => {
3820                                            #root::__serde_json::to_value(&v)
3821                                                .unwrap_or(#root::__serde_json::Value::Null)
3822                                        }
3823                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3824                                    },
3825                                )
3826                            }
3827                        })
3828                        .collect()
3829                };
3830            let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3831                mk_before_pairs(quote!(#root::sql::try_get_returning));
3832            let before_pairs_my: Vec<proc_macro2::TokenStream> =
3833                mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3834            let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3835                mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3836            let pg_select_cols: String = tracked
3837                .iter()
3838                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3839                .collect::<Vec<_>>()
3840                .join(", ");
3841            let my_select_cols: String = tracked
3842                .iter()
3843                .map(|c| format!("`{}`", c.column.replace('`', "``")))
3844                .collect::<Vec<_>>()
3845                .join(", ");
3846            // SQLite uses double-quote identifier quoting (same as
3847            // Postgres in default config), so the column-list shape
3848            // matches PG.
3849            let sqlite_select_cols: String = pg_select_cols.clone();
3850            let pk_value_for_bind = if fields.pk_is_auto {
3851                quote!(self.#pk_ident.get().copied().unwrap_or_default())
3852            } else {
3853                quote!(::core::clone::Clone::clone(&self.#pk_ident))
3854            };
3855            let assignments = &fields.update_assignments;
3856            let unset_dispatch = if fields.has_auto {
3857                quote! {
3858                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3859                        return self.insert_pool(pool).await.map(|()| 1u64);
3860                    }
3861                }
3862            } else {
3863                quote!()
3864            };
3865            quote! {
3866                /// Save this row against either backend with audit
3867                /// emission (diff-style: BEFORE+AFTER) inside the
3868                /// same transaction. Auto::Unset PK routes to
3869                /// insert_pool. Bi-dialect counterpart of
3870                /// [`Self::save`] for audited models.
3871                ///
3872                /// The audit row's `changes` JSON contains one
3873                /// `{ "field": { "before": …, "after": … } }` entry
3874                /// per tracked column whose value actually changed
3875                /// — same shape as the existing &PgPool save() emits.
3876                ///
3877                /// # Errors
3878                /// As [`Self::save`].
3879                pub async fn save_pool(
3880                    &mut self,
3881                    pool: &#root::sql::Pool,
3882                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3883                    #unset_dispatch
3884                    let _query = #root::core::UpdateQuery::new(
3885                        <Self as #root::core::Model>::SCHEMA,
3886                        ::std::vec![ #( #assignments ),* ],
3887                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3888                            #pk_column_lit,
3889                            #root::core::Op::Eq,
3890                            ::core::convert::Into::<#root::core::SqlValue>::into(
3891                                ::core::clone::Clone::clone(&self.#pk_ident)
3892                            ),
3893                        )),
3894                    );
3895                    let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3896                        ::std::vec![ #( #after_pairs_pg ),* ];
3897                    #root::audit::save_one_with_diff(
3898                        pool,
3899                        &_query,
3900                        #pk_column_lit,
3901                        ::core::convert::Into::<#root::core::SqlValue>::into(
3902                            #pk_value_for_bind,
3903                        ),
3904                        <Self as #root::core::Model>::SCHEMA.table,
3905                        #pk_str,
3906                        _after_pairs,
3907                        #pg_select_cols,
3908                        #my_select_cols,
3909                        #sqlite_select_cols,
3910                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3911                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3912                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3913                    ).await
3914                }
3915            }
3916        } else {
3917            quote!()
3918        }
3919    } else {
3920        pool_save_method
3921    };
3922
3923    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3924    // body shapes:
3925    // - non-audited: simple dispatch through `sql::delete_pool`
3926    // - audited: routes through `audit::delete_one_with_audit`,
3927    //   which opens a per-backend transaction wrapping DELETE +
3928    //   audit emit so the data write and audit row commit atomically.
3929    let pool_delete_method = {
3930        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3931        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3932        if let Some(pk_ident) = pk_ident_for_pool {
3933            if audited_fields.is_some() {
3934                quote! {
3935                    /// Delete this row against either backend with audit
3936                    /// emission inside the same transaction. Bi-dialect
3937                    /// counterpart of [`Self::delete`] for audited models.
3938                    ///
3939                    /// # Errors
3940                    /// As [`Self::delete`].
3941                    pub async fn delete_pool(
3942                        &self,
3943                        pool: &#root::sql::Pool,
3944                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3945                        let _query = #root::core::DeleteQuery::by_pk(
3946                            <Self as #root::core::Model>::SCHEMA,
3947                            #pk_column_lit,
3948                            ::core::convert::Into::<#root::core::SqlValue>::into(
3949                                ::core::clone::Clone::clone(&self.#pk_ident)
3950                            ),
3951                        );
3952                        let _audit_entry =
3953                            self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
3954                        #root::audit::delete_one_with_audit(
3955                            pool, &_query, &_audit_entry,
3956                        ).await
3957                    }
3958                }
3959            } else {
3960                quote! {
3961                    /// Delete the row identified by this instance's primary key
3962                    /// against either backend. Equivalent to [`Self::delete`] but
3963                    /// takes [`#root::sql::Pool`] and dispatches per backend.
3964                    ///
3965                    /// # Errors
3966                    /// As [`Self::delete`].
3967                    pub async fn delete_pool(
3968                        &self,
3969                        pool: &#root::sql::Pool,
3970                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3971                        let _query = #root::core::DeleteQuery::by_pk(
3972                            <Self as #root::core::Model>::SCHEMA,
3973                            #pk_column_lit,
3974                            ::core::convert::Into::<#root::core::SqlValue>::into(
3975                                ::core::clone::Clone::clone(&self.#pk_ident)
3976                            ),
3977                        );
3978                        #root::sql::delete_pool(pool, &_query).await
3979                    }
3980                }
3981            }
3982        } else {
3983            quote!()
3984        }
3985    };
3986
3987    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3988    // matching this instance's PK and overwrite the in-memory state
3989    // with the freshly-fetched columns.
3990    // Issue #825. Only emitted when the model declares a PK; non-PK
3991    // models can't address a specific row.
3992    //
3993    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3994    // every field from `self`; resets the PK to `Auto::Unset` when
3995    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3996    // a fresh autoincrement value. Non-Auto PKs preserve the source
3997    // PK — the caller must overwrite before insert. Pure-Rust, no
3998    // I/O, no dialect surface.
3999    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4000        let other_field_clones: Vec<TokenStream2> = fields
4001            .column_entries
4002            .iter()
4003            .filter(|c| &c.ident != pk_ident)
4004            .map(|c| {
4005                let ident = &c.ident;
4006                quote! {
4007                    #ident: ::core::clone::Clone::clone(&self.#ident)
4008                }
4009            })
4010            .collect();
4011        let pk_clone_token = if fields.pk_is_auto {
4012            quote! { #pk_ident: #root::sql::Auto::Unset }
4013        } else {
4014            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4015        };
4016        let replicate_doc = if fields.pk_is_auto {
4017            quote! {
4018                /// Eloquent-style `replicate()` — return a clone of this
4019                /// row with the primary key reset to [`Auto::Unset`] so
4020                /// the copy is ready for `insert_pool` / `save_pool` to
4021                /// allocate a fresh autoincrement value. Every other
4022                /// field is `Clone`d verbatim. Issue #825.
4023                ///
4024                /// `auto_now_add` / `auto_now` timestamp fields are
4025                /// **not** reset (Eloquent's `replicate` doesn't reset
4026                /// them either) — pass them through the normal insert
4027                /// path if you want fresh values, or assign them
4028                /// explicitly after the call.
4029            }
4030        } else {
4031            quote! {
4032                /// Eloquent-style `replicate()` — clone this row
4033                /// verbatim. Because the primary key is **not** an
4034                /// `Auto<T>`, the clone keeps the source PK; the
4035                /// caller must overwrite `copy.<pk>` before inserting
4036                /// to avoid a unique-key violation. Issue #825.
4037            }
4038        };
4039        // 2026-06-07 — field-name / shortcut collision guard.
4040        //
4041        // The macro emits both `pub const <field>: <field>_col = ...`
4042        // (per-field typed-column const, used by the typed-builder
4043        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4044        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4045        // model has a field named e.g. `count`, both items would land
4046        // in the same inherent impl with the same name, and the
4047        // compiler rejects the derive with "duplicate definitions".
4048        //
4049        // Drop the conflicting shortcut for that model. Callers can
4050        // still reach the same behavior via
4051        // `QuerySet::<Model>::default().count(&pool)`.
4052        let column_names: ::std::collections::HashSet<String> = fields
4053            .column_entries
4054            .iter()
4055            .map(|c| c.ident.to_string())
4056            .collect();
4057        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4058            if column_names.contains(name) {
4059                quote! {}
4060            } else {
4061                tokens
4062            }
4063        };
4064        let count_method = emit_if_no_field_collision(
4065            "count",
4066            quote! {
4067                /// Count rows of this model — `SELECT COUNT(*) FROM
4068                /// <table>`. Eloquent `Model::count()` parity.
4069                ///
4070                /// Skipped on models that already declare a field named
4071                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4072                /// in that case.
4073                ///
4074                /// # Errors
4075                /// As [`CounterPool::count`].
4076                ///
4077                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4078                pub async fn count(
4079                    pool: &#root::sql::Pool,
4080                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4081                    use #root::sql::CounterPool as _;
4082                    #root::query::QuerySet::<Self>::default()
4083                        .count(pool)
4084                        .await
4085                }
4086            },
4087        );
4088        let value_method = emit_if_no_field_collision(
4089            "value",
4090            quote! {
4091                /// Pluck a single scalar from the first row.
4092                /// Eloquent `Model::query()->value($col)` parity.
4093                ///
4094                /// Skipped on models that already declare a field named
4095                /// `value`. Drop into
4096                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4097                ///
4098                /// # Errors
4099                /// As `ValuesFlatQuerySet::first`.
4100                pub async fn value<U>(
4101                    col: &str,
4102                    pool: &#root::sql::Pool,
4103                ) -> ::core::result::Result<
4104                    ::core::option::Option<U>,
4105                    #root::sql::ExecError,
4106                >
4107                where
4108                    U: #root::sql::MaybePgScalar
4109                        + #root::sql::MaybeMyScalar
4110                        + #root::sql::MaybeSqliteScalar
4111                        + ::core::marker::Send
4112                        + ::core::marker::Unpin,
4113                {
4114                    let _col_static: &'static str = Self::__resolve_col(col)?;
4115                    #root::query::QuerySet::<Self>::default()
4116                        .values_list_flat(_col_static)
4117                        .first::<U>(pool)
4118                        .await
4119                }
4120            },
4121        );
4122        let sum_method = emit_if_no_field_collision(
4123            "sum",
4124            quote! {
4125                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4126                /// Skipped on models that already declare a field named
4127                /// `sum`.
4128                ///
4129                /// # Errors
4130                /// As [`#root::sql::fetch_aggregate_pool`].
4131                pub async fn sum<U>(
4132                    col: &str,
4133                    pool: &#root::sql::Pool,
4134                ) -> ::core::result::Result<
4135                    ::core::option::Option<U>,
4136                    #root::sql::ExecError,
4137                >
4138                where
4139                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4140                        + #root::sql::MaybeMyFromRow
4141                        + #root::sql::MaybeSqliteFromRow
4142                        + ::core::marker::Send
4143                        + ::core::marker::Unpin,
4144                {
4145                    #root::query::QuerySet::<Self>::default()
4146                        .sum::<U>(col, pool)
4147                        .await
4148                }
4149            },
4150        );
4151        let avg_method = emit_if_no_field_collision(
4152            "avg",
4153            quote! {
4154                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4155                /// models that already declare a field named `avg`.
4156                ///
4157                /// # Errors
4158                /// As [`#root::sql::fetch_aggregate_pool`].
4159                pub async fn avg<U>(
4160                    col: &str,
4161                    pool: &#root::sql::Pool,
4162                ) -> ::core::result::Result<
4163                    ::core::option::Option<U>,
4164                    #root::sql::ExecError,
4165                >
4166                where
4167                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4168                        + #root::sql::MaybeMyFromRow
4169                        + #root::sql::MaybeSqliteFromRow
4170                        + ::core::marker::Send
4171                        + ::core::marker::Unpin,
4172                {
4173                    #root::query::QuerySet::<Self>::default()
4174                        .avg::<U>(col, pool)
4175                        .await
4176                }
4177            },
4178        );
4179        let min_method = emit_if_no_field_collision(
4180            "min",
4181            quote! {
4182                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4183                /// models that already declare a field named `min`.
4184                ///
4185                /// # Errors
4186                /// As [`#root::sql::fetch_aggregate_pool`].
4187                pub async fn min<U>(
4188                    col: &str,
4189                    pool: &#root::sql::Pool,
4190                ) -> ::core::result::Result<
4191                    ::core::option::Option<U>,
4192                    #root::sql::ExecError,
4193                >
4194                where
4195                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4196                        + #root::sql::MaybeMyFromRow
4197                        + #root::sql::MaybeSqliteFromRow
4198                        + ::core::marker::Send
4199                        + ::core::marker::Unpin,
4200                {
4201                    #root::query::QuerySet::<Self>::default()
4202                        .min::<U>(col, pool)
4203                        .await
4204                }
4205            },
4206        );
4207        let max_method = emit_if_no_field_collision(
4208            "max",
4209            quote! {
4210                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4211                /// models that already declare a field named `max`.
4212                ///
4213                /// # Errors
4214                /// As [`#root::sql::fetch_aggregate_pool`].
4215                pub async fn max<U>(
4216                    col: &str,
4217                    pool: &#root::sql::Pool,
4218                ) -> ::core::result::Result<
4219                    ::core::option::Option<U>,
4220                    #root::sql::ExecError,
4221                >
4222                where
4223                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4224                        + #root::sql::MaybeMyFromRow
4225                        + #root::sql::MaybeSqliteFromRow
4226                        + ::core::marker::Send
4227                        + ::core::marker::Unpin,
4228                {
4229                    #root::query::QuerySet::<Self>::default()
4230                        .max::<U>(col, pool)
4231                        .await
4232                }
4233            },
4234        );
4235        let first_method = emit_if_no_field_collision(
4236            "first",
4237            quote! {
4238                /// First row of this model. Eloquent `Model::first()`.
4239                /// Skipped on models that already declare a field named
4240                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4241                ///
4242                /// # Errors
4243                /// As `QuerySet::first`.
4244                pub async fn first(
4245                    pool: &#root::sql::Pool,
4246                ) -> ::core::result::Result<
4247                    ::core::option::Option<Self>,
4248                    #root::sql::ExecError,
4249                > {
4250                    #root::query::QuerySet::<Self>::default()
4251                        .first(pool)
4252                        .await
4253                }
4254            },
4255        );
4256        let last_method = emit_if_no_field_collision(
4257            "last",
4258            quote! {
4259                /// Last row of this model by primary-key DESC.
4260                /// Eloquent `Model::query()->latest('id')->first()`
4261                /// parity — fetches the highest-PK row without
4262                /// requiring the caller to spell the PK column.
4263                /// Returns `None` on an empty table.
4264                ///
4265                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4266                /// Skipped on models that already declare a field
4267                /// named `last`.
4268                ///
4269                /// # Errors
4270                /// As `QuerySet::last`.
4271                pub async fn last(
4272                    pool: &#root::sql::Pool,
4273                ) -> ::core::result::Result<
4274                    ::core::option::Option<Self>,
4275                    #root::sql::ExecError,
4276                > {
4277                    #root::query::QuerySet::<Self>::default()
4278                        .last(pool)
4279                        .await
4280                }
4281            },
4282        );
4283        quote! {
4284            /// Re-SELECT this row by its primary key and overwrite
4285            /// every in-memory field with the freshly-fetched value.
4286            /// Issue #825.
4287            ///
4288            /// Use this when the row may have been modified by another
4289            /// process / connection / job since you read it — e.g. after
4290            /// a queued task callback, or to re-sync stale UI state
4291            /// before re-saving.
4292            ///
4293            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4294            /// when the primary key no longer matches any row (e.g.
4295            /// the row was deleted concurrently).
4296            ///
4297            /// # Errors
4298            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4299            /// the PK no longer exists.
4300            ///
4301            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4302            pub async fn refresh_from_db(
4303                &mut self,
4304                pool: &#root::sql::Pool,
4305            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4306                use #root::sql::FetcherPool as _;
4307                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4308                    ::core::clone::Clone::clone(&self.#pk_ident),
4309                );
4310                let mut _rows: ::std::vec::Vec<Self> =
4311                    #root::query::QuerySet::<Self>::default()
4312                        .filter(::core::stringify!(#pk_ident), _pk_val)
4313                        .limit(1)
4314                        .fetch(pool)
4315                        .await?;
4316                match _rows.into_iter().next() {
4317                    ::core::option::Option::Some(_fresh) => {
4318                        *self = _fresh;
4319                        ::core::result::Result::Ok(())
4320                    }
4321                    ::core::option::Option::None => ::core::result::Result::Err(
4322                        #root::sql::ExecError::Driver(
4323                            #root::sql::sqlx::Error::RowNotFound,
4324                        ),
4325                    ),
4326                }
4327            }
4328
4329            /// Atomically increment the integer column `col` by
4330            /// `by` for this row. Equivalent to
4331            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4332            ///
4333            /// **Doesn't mutate `self`** — the in-memory copy is now
4334            /// stale; call [`Self::refresh_from_db`] /
4335            /// [`Self::fresh`] to re-sync. Returns the rows-
4336            /// affected count (0 when the PK doesn't match any row,
4337            /// 1 on success).
4338            ///
4339            /// `col` is the Rust field name as a string; unknown
4340            /// fields surface as `UnknownField` at runtime. Negative
4341            /// `by` values atomically decrement (see also
4342            /// [`Self::decrement`]).
4343            ///
4344            /// # Errors
4345            /// As [`UpdaterPool::execute_pool`].
4346            ///
4347            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4348            pub async fn increment(
4349                &self,
4350                col: &str,
4351                by: i64,
4352                pool: &#root::sql::Pool,
4353            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4354                Self::__increment_one(self, col, by, pool).await
4355            }
4356
4357            /// Sibling of [`Self::increment`] — atomically
4358            /// decrement this row's `col` by `by`. Eloquent
4359            /// `$model->decrement($col, $by)` parity. Equivalent to
4360            /// `self.increment(col, -by, &pool)`; the separate name
4361            /// keeps call sites readable.
4362            ///
4363            /// # Errors
4364            /// As [`Self::increment`].
4365            pub async fn decrement(
4366                &self,
4367                col: &str,
4368                by: i64,
4369                pool: &#root::sql::Pool,
4370            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4371                Self::__increment_one(self, col, -by, pool).await
4372            }
4373
4374            /// Bulk-increment: add `by` to `col` on every row of the
4375            /// table. Eloquent `Model::query()->increment($col, $by)`
4376            /// parity. Use for counters, score adjustments, view
4377            /// rollups.
4378            ///
4379            /// # Errors
4380            /// As [`UpdaterPool::execute_pool`].
4381            ///
4382            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4383            pub async fn increment_each(
4384                col: &str,
4385                by: i64,
4386                pool: &#root::sql::Pool,
4387            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4388                Self::__increment_all(col, by, pool).await
4389            }
4390
4391            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4392            ///
4393            /// # Errors
4394            /// As [`Self::increment_each`].
4395            pub async fn decrement_each(
4396                col: &str,
4397                by: i64,
4398                pool: &#root::sql::Pool,
4399            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4400                Self::__increment_all(col, -by, pool).await
4401            }
4402
4403            /// Internal: forward to
4404            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4405            /// One-line wrapper kept as a per-Model method so the
4406            /// macro's emitted `increment` / `decrement` instance
4407            /// calls don't have to thread `Self` through manually.
4408            #[doc(hidden)]
4409            pub async fn __increment_one(
4410                this: &Self,
4411                col: &str,
4412                by: i64,
4413                pool: &#root::sql::Pool,
4414            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4415                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4416                    ::core::clone::Clone::clone(&this.#pk_ident),
4417                );
4418                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4419                    ::core::stringify!(#pk_ident),
4420                    _pk_val,
4421                    col,
4422                    by,
4423                    pool,
4424                )
4425                .await
4426            }
4427
4428            /// Internal: forward to
4429            /// [`#root::sql::model_shortcuts::increment_all_pool`].
4430            #[doc(hidden)]
4431            pub async fn __increment_all(
4432                col: &str,
4433                by: i64,
4434                pool: &#root::sql::Pool,
4435            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4436                #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4437            }
4438
4439            /// Internal: forward to
4440            /// [`#root::sql::model_shortcuts::resolve_col`].
4441            #[doc(hidden)]
4442            pub fn __resolve_col(
4443                col: &str,
4444            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4445                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4446            }
4447
4448            /// Internal: forward to
4449            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4450            #[doc(hidden)]
4451            #[must_use]
4452            pub fn __add_signed_expr(
4453                col_static: &'static str,
4454                signed_by: i64,
4455            ) -> #root::core::Expr {
4456                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4457            }
4458
4459            /// Re-SELECT this row by its primary key and return a
4460            /// **new** instance with the freshly-fetched fields.
4461            /// Eloquent `Model::fresh()` parity — non-mutating
4462            /// counterpart of [`Self::refresh_from_db`].
4463            ///
4464            /// Returns `Ok(None)` when the row was deleted
4465            /// concurrently — vs [`Self::refresh_from_db`]
4466            /// which surfaces that as `RowNotFound` because
4467            /// in-place mutation has nothing to write to.
4468            ///
4469            /// Useful when you want to compare the in-memory
4470            /// instance against the persisted state (audit-style
4471            /// diffs, conflict detection) without mutating the
4472            /// reference you already hold.
4473            ///
4474            /// # Errors
4475            /// As [`FetcherPool::fetch`].
4476            ///
4477            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4478            pub async fn fresh(
4479                &self,
4480                pool: &#root::sql::Pool,
4481            ) -> ::core::result::Result<
4482                ::core::option::Option<Self>,
4483                #root::sql::ExecError,
4484            > {
4485                use #root::sql::FetcherPool as _;
4486                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4487                    ::core::clone::Clone::clone(&self.#pk_ident),
4488                );
4489                let _rows: ::std::vec::Vec<Self> =
4490                    #root::query::QuerySet::<Self>::default()
4491                        .filter(::core::stringify!(#pk_ident), _pk_val)
4492                        .limit(1)
4493                        .fetch(pool)
4494                        .await?;
4495                ::core::result::Result::Ok(_rows.into_iter().next())
4496            }
4497
4498            #replicate_doc
4499            #[must_use]
4500            pub fn replicate(&self) -> Self {
4501                Self {
4502                    #pk_clone_token,
4503                    #( #other_field_clones, )*
4504                }
4505            }
4506
4507            #first_method
4508
4509            #last_method
4510
4511            /// Throwing counterpart of [`Self::first`] —
4512            /// errors with `RowNotFound` when the table is empty.
4513            /// Eloquent `Model::firstOrFail()` parity.
4514            ///
4515            /// # Errors
4516            /// As [`Self::first`]; additionally
4517            /// [`sqlx::Error::RowNotFound`] on empty tables.
4518            ///
4519            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4520            pub async fn first_or_fail(
4521                pool: &#root::sql::Pool,
4522            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4523                // Route through the queryset rather than `Self::first`,
4524                // which is suppressed on models with a field named
4525                // `first` (field/shortcut collision guard).
4526                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4527                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4528                    ::core::option::Option::None => ::core::result::Result::Err(
4529                        #root::sql::ExecError::Driver(
4530                            #root::sql::sqlx::Error::RowNotFound,
4531                        ),
4532                    ),
4533                }
4534            }
4535
4536            /// Single-column projection — `SELECT <col> FROM
4537            /// <table>`. Returns `Vec<U>` where each element is the
4538            /// decoded value of the column. Eloquent
4539            /// `Model::pluck($column)` parity.
4540            ///
4541            /// Thin wrapper over `QuerySet::<Self>::default()
4542            /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4543            /// be decodable from the column's SQL type on every
4544            /// dialect the binary targets (common picks: `i64` /
4545            /// `i32` / `String` / `bool` / `f64`).
4546            ///
4547            /// # Errors
4548            /// As `ValuesFlatQuerySet::fetch`.
4549            pub async fn pluck<U>(
4550                col: &'static str,
4551                pool: &#root::sql::Pool,
4552            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4553            where
4554                U: #root::sql::MaybePgScalar
4555                    + #root::sql::MaybeMyScalar
4556                    + #root::sql::MaybeSqliteScalar
4557                    + ::core::marker::Send
4558                    + ::core::marker::Unpin,
4559            {
4560                #root::query::QuerySet::<Self>::default()
4561                    .values_list_flat(col)
4562                    .fetch::<U>(pool)
4563                    .await
4564            }
4565
4566            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4567            /// stream every row of this model in batches of `n`,
4568            /// invoking the callback once per batch. Stable PK-ASC
4569            /// ordering so the LIMIT/OFFSET pagination is
4570            /// deterministic across drivers.
4571            ///
4572            /// The callback is async — it can do further DB work
4573            /// (writes, related-row lookups, queue dispatch) per
4574            /// batch. Return `Err(...)` from the callback to abort
4575            /// the iteration early; the error bubbles up.
4576            ///
4577            /// **When to use**: bulk processing flows that can't
4578            /// fit the whole table in memory — sending newsletters,
4579            /// running data migrations, computing summary
4580            /// statistics. For small / known-bounded tables, plain
4581            /// `Self::all(&pool)` is simpler.
4582            ///
4583            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4584            /// large tables. For multi-million-row scans prefer
4585            /// keyset-by-PK (the standard "WHERE id > last_seen"
4586            /// shape) over `chunk(...)`.
4587            ///
4588            /// Skipped on models without a primary key — chunking
4589            /// needs a stable order to avoid skipping / repeating
4590            /// rows across batches.
4591            pub async fn chunk<F, Fut>(
4592                n: i64,
4593                pool: &#root::sql::Pool,
4594                mut cb: F,
4595            ) -> ::core::result::Result<(), #root::sql::ExecError>
4596            where
4597                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4598                Fut: ::core::future::Future<
4599                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4600                >,
4601            {
4602                use #root::sql::FetcherPool as _;
4603                let pk_col = match Self::primary_key_column() {
4604                    ::core::option::Option::Some(c) => c,
4605                    ::core::option::Option::None => {
4606                        return ::core::result::Result::Ok(());
4607                    }
4608                };
4609                let mut offset: i64 = 0;
4610                loop {
4611                    let rows: ::std::vec::Vec<Self> =
4612                        #root::query::QuerySet::<Self>::default()
4613                            .order_by(&[(pk_col, false)])
4614                            .limit(n)
4615                            .offset(offset)
4616                            .fetch(pool)
4617                            .await?;
4618                    if rows.is_empty() {
4619                        return ::core::result::Result::Ok(());
4620                    }
4621                    let len = rows.len() as i64;
4622                    cb(rows).await?;
4623                    if len < n {
4624                        return ::core::result::Result::Ok(());
4625                    }
4626                    offset += n;
4627                }
4628            }
4629
4630            /// Eloquent `Model::chunkById($n, fn (...))` — same
4631            /// per-batch callback shape as [`Self::chunk`], but uses
4632            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4633            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4634            /// — the right choice for multi-million-row sweeps.
4635            ///
4636            /// Requires the primary key to be a signed integer type
4637            /// (`i64` / `i32`); the keyset comparison rides on
4638            /// `__rustango_pk_value()` lowering through
4639            /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4640            /// non-integer PKs (UUID, String) — those should use
4641            /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4642            /// keyset loop.
4643            ///
4644            /// Callback errors abort iteration; the error bubbles up
4645            /// unchanged. Empty table → callback invoked zero times.
4646            pub async fn chunk_by_id<F, Fut>(
4647                n: i64,
4648                pool: &#root::sql::Pool,
4649                mut cb: F,
4650            ) -> ::core::result::Result<(), #root::sql::ExecError>
4651            where
4652                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4653                Fut: ::core::future::Future<
4654                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4655                >,
4656            {
4657                use #root::sql::FetcherPool as _;
4658                let pk_col = match Self::primary_key_column() {
4659                    ::core::option::Option::Some(c) => c,
4660                    ::core::option::Option::None => {
4661                        return ::core::result::Result::Ok(());
4662                    }
4663                };
4664                // Track the largest PK seen so the next batch picks
4665                // up from there. `i64::MIN` as the sentinel — the
4666                // very first iteration's `> MIN` matches every row,
4667                // so the loop entry is uniform with subsequent
4668                // iterations.
4669                let mut last_seen: i64 = i64::MIN;
4670                loop {
4671                    let key = ::std::format!("{}__gt", pk_col);
4672                    let rows: ::std::vec::Vec<Self> =
4673                        #root::query::QuerySet::<Self>::default()
4674                            .filter(key.as_str(), last_seen)
4675                            .order_by(&[(pk_col, false)])
4676                            .limit(n)
4677                            .fetch(pool)
4678                            .await?;
4679                    if rows.is_empty() {
4680                        return ::core::result::Result::Ok(());
4681                    }
4682                    let len = rows.len() as i64;
4683                    // Capture the last row's PK BEFORE moving rows
4684                    // into the callback.
4685                    let max_pk = match rows
4686                        .last()
4687                        .map(|r| r.__rustango_pk_value())
4688                    {
4689                        ::core::option::Option::Some(
4690                            #root::core::SqlValue::I64(v),
4691                        ) => v,
4692                        ::core::option::Option::Some(
4693                            #root::core::SqlValue::I32(v),
4694                        ) => i64::from(v),
4695                        _ => return ::core::result::Result::Ok(()),
4696                    };
4697                    cb(rows).await?;
4698                    if len < n {
4699                        return ::core::result::Result::Ok(());
4700                    }
4701                    last_seen = max_pk;
4702                }
4703            }
4704
4705            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4706            /// per-row callback companion to [`Self::chunk`].
4707            /// Streams every row in keyset-paginated batches of
4708            /// `batch` size, calling `cb` once per row.
4709            ///
4710            /// Inherits the keyset-paginated scan of
4711            /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4712            /// non-integer PKs are silently a no-op).
4713            ///
4714            /// ```ignore
4715            /// Post::each(500, &pool, |p| async move {
4716            ///     reindex(p).await?;
4717            ///     Ok(())
4718            /// }).await?;
4719            /// ```
4720            ///
4721            /// Return `Err(...)` from the callback to abort
4722            /// iteration; the error bubbles up unchanged.
4723            pub async fn each<F, Fut>(
4724                batch: i64,
4725                pool: &#root::sql::Pool,
4726                mut cb: F,
4727            ) -> ::core::result::Result<(), #root::sql::ExecError>
4728            where
4729                F: ::core::ops::FnMut(Self) -> Fut,
4730                Fut: ::core::future::Future<
4731                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4732                >,
4733            {
4734                use #root::sql::FetcherPool as _;
4735                let pk_col = match Self::primary_key_column() {
4736                    ::core::option::Option::Some(c) => c,
4737                    ::core::option::Option::None => {
4738                        return ::core::result::Result::Ok(());
4739                    }
4740                };
4741                let mut last_seen: i64 = i64::MIN;
4742                loop {
4743                    let key = ::std::format!("{}__gt", pk_col);
4744                    let rows: ::std::vec::Vec<Self> =
4745                        #root::query::QuerySet::<Self>::default()
4746                            .filter(key.as_str(), last_seen)
4747                            .order_by(&[(pk_col, false)])
4748                            .limit(batch)
4749                            .fetch(pool)
4750                            .await?;
4751                    if rows.is_empty() {
4752                        return ::core::result::Result::Ok(());
4753                    }
4754                    let len = rows.len() as i64;
4755                    let max_pk = match rows
4756                        .last()
4757                        .map(|r| r.__rustango_pk_value())
4758                    {
4759                        ::core::option::Option::Some(
4760                            #root::core::SqlValue::I64(v),
4761                        ) => v,
4762                        ::core::option::Option::Some(
4763                            #root::core::SqlValue::I32(v),
4764                        ) => i64::from(v),
4765                        _ => return ::core::result::Result::Ok(()),
4766                    };
4767                    for row in rows {
4768                        cb(row).await?;
4769                    }
4770                    if len < batch {
4771                        return ::core::result::Result::Ok(());
4772                    }
4773                    last_seen = max_pk;
4774                }
4775            }
4776
4777            /// Delete every row of this model — `TRUNCATE TABLE
4778            /// <table> RESTART IDENTITY CASCADE` on Postgres,
4779            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4780            /// support `TRUNCATE` inside foreign-key constraints
4781            /// or — for SQLite — at all). Eloquent `Model::truncate()`
4782            /// parity.
4783            ///
4784            /// **Use only in tests / fixture-reset flows.** Production
4785            /// writes through this would silently bypass the
4786            /// `pre_delete` / `post_delete` signals (no per-row hooks
4787            /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4788            /// row's audit-log entry.
4789            ///
4790            /// # Errors
4791            /// As [`raw_execute_pool`].
4792            ///
4793            /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4794            pub async fn truncate(
4795                pool: &#root::sql::Pool,
4796            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4797                let _table = <Self as #root::core::Model>::SCHEMA.table;
4798                let _dialect = pool.dialect();
4799                let _quoted = _dialect.quote_ident(_table);
4800                let _sql = if _dialect.name() == "postgres" {
4801                    ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4802                } else {
4803                    ::std::format!("DELETE FROM {}", _quoted)
4804                };
4805                #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4806            }
4807
4808            /// Bulk-delete every row whose primary key is in
4809            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4810            /// Returns the affected row count.
4811            ///
4812            /// Eloquent `Model::destroy([1, 2, 3])` parity.
4813            /// Empty `pks` is a no-op (returns 0).
4814            ///
4815            /// Accepts any iterable whose elements are
4816            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4817            /// `[i64; N]`, etc.
4818            ///
4819            /// # Errors
4820            /// As `delete_pool`.
4821            pub async fn destroy<V>(
4822                pks: impl ::core::iter::IntoIterator<Item = V>,
4823                pool: &#root::sql::Pool,
4824            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4825            where
4826                V: ::core::convert::Into<#root::core::SqlValue>,
4827            {
4828                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4829                    pks.into_iter().map(::core::convert::Into::into).collect();
4830                if _values.is_empty() {
4831                    return ::core::result::Result::Ok(0);
4832                }
4833                let _pk = <Self as #root::core::Model>::SCHEMA
4834                    .primary_key()
4835                    .ok_or_else(|| {
4836                        #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
4837                    })?;
4838                let _query = #root::query::QuerySet::<Self>::default()
4839                    .filter_op(
4840                        _pk.name,
4841                        #root::core::Op::In,
4842                        #root::core::SqlValue::List(_values),
4843                    )
4844                    .compile_delete()?;
4845                #root::sql::delete_pool(pool, &_query).await
4846            }
4847
4848            /// Fetch every row where `<col> = <val>`. Eloquent
4849            /// `Model::where($col, $val)->get()` parity.
4850            ///
4851            /// Thin wrapper over `QuerySet::<Self>::default()
4852            /// .filter(col, val).fetch(pool)`. For one row,
4853            /// use [`Self::first_where`]; for a chain that
4854            /// needs further `.filter()` / `.order_by()` /
4855            /// `.limit()`, drop down to `Self::query().filter(...)`
4856            /// directly.
4857            ///
4858            /// `val` accepts any value `Into<SqlValue>` so plain
4859            /// strings, ints, UUIDs, etc. all work.
4860            ///
4861            /// # Errors
4862            /// As [`FetcherPool::fetch`].
4863            ///
4864            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4865            pub async fn where_(
4866                col: &str,
4867                val: impl ::core::convert::Into<#root::core::SqlValue>,
4868                pool: &#root::sql::Pool,
4869            ) -> ::core::result::Result<
4870                ::std::vec::Vec<Self>,
4871                #root::sql::ExecError,
4872            > {
4873                use #root::sql::FetcherPool as _;
4874                #root::query::QuerySet::<Self>::default()
4875                    .filter(col, val)
4876                    .fetch(pool)
4877                    .await
4878            }
4879
4880            /// Fetch every row where `<col> IN (vals)`. Eloquent
4881            /// `Model::whereIn($col, $vals)->get()` parity. Empty
4882            /// `vals` returns no rows (matches SQL's empty-IN
4883            /// semantics).
4884            ///
4885            /// `vals` accepts any iterable whose items are
4886            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4887            /// etc.
4888            ///
4889            /// # Errors
4890            /// As [`FetcherPool::fetch`].
4891            ///
4892            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4893            pub async fn where_in<V>(
4894                col: &str,
4895                vals: impl ::core::iter::IntoIterator<Item = V>,
4896                pool: &#root::sql::Pool,
4897            ) -> ::core::result::Result<
4898                ::std::vec::Vec<Self>,
4899                #root::sql::ExecError,
4900            >
4901            where
4902                V: ::core::convert::Into<#root::core::SqlValue>,
4903            {
4904                use #root::sql::FetcherPool as _;
4905                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4906                    vals.into_iter().map(::core::convert::Into::into).collect();
4907                if _values.is_empty() {
4908                    return ::core::result::Result::Ok(::std::vec::Vec::new());
4909                }
4910                let _key = ::std::format!("{}__in", col);
4911                #root::query::QuerySet::<Self>::default()
4912                    .filter(&_key, #root::core::SqlValue::List(_values))
4913                    .fetch(pool)
4914                    .await
4915            }
4916
4917            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4918            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4919            /// `vals` returns every row (matches SQL's empty-NOT-IN
4920            /// semantics — vacuously true for every row).
4921            ///
4922            /// # Errors
4923            /// As [`FetcherPool::fetch`].
4924            ///
4925            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4926            pub async fn where_not_in<V>(
4927                col: &str,
4928                vals: impl ::core::iter::IntoIterator<Item = V>,
4929                pool: &#root::sql::Pool,
4930            ) -> ::core::result::Result<
4931                ::std::vec::Vec<Self>,
4932                #root::sql::ExecError,
4933            >
4934            where
4935                V: ::core::convert::Into<#root::core::SqlValue>,
4936            {
4937                use #root::sql::FetcherPool as _;
4938                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4939                    vals.into_iter().map(::core::convert::Into::into).collect();
4940                if _values.is_empty() {
4941                    return #root::query::QuerySet::<Self>::default()
4942                        .fetch(pool)
4943                        .await;
4944                }
4945                let _key = ::std::format!("{}__not_in", col);
4946                #root::query::QuerySet::<Self>::default()
4947                    .filter(&_key, #root::core::SqlValue::List(_values))
4948                    .fetch(pool)
4949                    .await
4950            }
4951
4952            /// Fetch every row where `<col> IS NULL`. Eloquent
4953            /// `Model::whereNull($col)->get()` parity.
4954            ///
4955            /// # Errors
4956            /// As [`FetcherPool::fetch`].
4957            ///
4958            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4959            pub async fn where_null(
4960                col: &str,
4961                pool: &#root::sql::Pool,
4962            ) -> ::core::result::Result<
4963                ::std::vec::Vec<Self>,
4964                #root::sql::ExecError,
4965            > {
4966                use #root::sql::FetcherPool as _;
4967                let _key = ::std::format!("{}__isnull", col);
4968                #root::query::QuerySet::<Self>::default()
4969                    .filter(&_key, #root::core::SqlValue::Bool(true))
4970                    .fetch(pool)
4971                    .await
4972            }
4973
4974            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4975            /// `Model::whereNotNull($col)->get()` parity.
4976            ///
4977            /// # Errors
4978            /// As [`FetcherPool::fetch`].
4979            ///
4980            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4981            pub async fn where_not_null(
4982                col: &str,
4983                pool: &#root::sql::Pool,
4984            ) -> ::core::result::Result<
4985                ::std::vec::Vec<Self>,
4986                #root::sql::ExecError,
4987            > {
4988                use #root::sql::FetcherPool as _;
4989                let _key = ::std::format!("{}__isnull", col);
4990                #root::query::QuerySet::<Self>::default()
4991                    .filter(&_key, #root::core::SqlValue::Bool(false))
4992                    .fetch(pool)
4993                    .await
4994            }
4995
4996            /// Fetch up to `n` rows in random order. Eloquent
4997            /// `Model::inRandomOrder()->limit($n)->get()` /
4998            /// `Model::query()->inRandomOrder()->get()->take($n)`
4999            /// parity. **Performance caveat**: random ordering
5000            /// forces a full table scan + per-row random key sort;
5001            /// the optimizer cannot use an index. Prefer a
5002            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5003            ///
5004            /// # Errors
5005            /// As [`FetcherPool::fetch`].
5006            ///
5007            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5008            pub async fn random_n(
5009                n: i64,
5010                pool: &#root::sql::Pool,
5011            ) -> ::core::result::Result<
5012                ::std::vec::Vec<Self>,
5013                #root::sql::ExecError,
5014            > {
5015                use #root::sql::FetcherPool as _;
5016                #root::query::QuerySet::<Self>::default()
5017                    .order_random()
5018                    .limit(n)
5019                    .fetch(pool)
5020                    .await
5021            }
5022
5023            /// Fetch one row in random order. Eloquent
5024            /// `Model::inRandomOrder()->first()` parity. Same
5025            /// performance caveat as [`Self::random_n`].
5026            ///
5027            /// # Errors
5028            /// As [`FetcherPool::fetch`].
5029            ///
5030            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5031            pub async fn random(
5032                pool: &#root::sql::Pool,
5033            ) -> ::core::result::Result<
5034                ::core::option::Option<Self>,
5035                #root::sql::ExecError,
5036            > {
5037                ::core::result::Result::Ok(
5038                    Self::random_n(1, pool).await?.into_iter().next(),
5039                )
5040            }
5041
5042            /// Fetch every row ordered ASC by `field`. Eloquent
5043            /// `Model::oldest($field)->get()` parity — the multi-row
5044            /// counterpart of [`Self::earliest`].
5045            ///
5046            /// # Errors
5047            /// As [`FetcherPool::fetch`].
5048            ///
5049            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5050            pub async fn oldest(
5051                field: &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                #root::query::QuerySet::<Self>::default()
5059                    .order_by(&[(field, false)])
5060                    .fetch(pool)
5061                    .await
5062            }
5063
5064            /// Fetch every row ordered DESC by `field`. Eloquent
5065            /// `Model::latest($field)->get()` parity — the multi-row
5066            /// counterpart of [`Self::latest`].
5067            ///
5068            /// # Errors
5069            /// As [`FetcherPool::fetch`].
5070            ///
5071            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5072            pub async fn newest(
5073                field: &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                #root::query::QuerySet::<Self>::default()
5081                    .order_by(&[(field, true)])
5082                    .fetch(pool)
5083                    .await
5084            }
5085
5086            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5087            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5088            /// Routes through the existing `__year` lookup suffix
5089            /// (issue #829).
5090            ///
5091            /// # Errors
5092            /// As [`FetcherPool::fetch`].
5093            ///
5094            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5095            pub async fn where_year(
5096                col: &str,
5097                year: i64,
5098                pool: &#root::sql::Pool,
5099            ) -> ::core::result::Result<
5100                ::std::vec::Vec<Self>,
5101                #root::sql::ExecError,
5102            > {
5103                use #root::sql::FetcherPool as _;
5104                let _key = ::std::format!("{}__year", col);
5105                #root::query::QuerySet::<Self>::default()
5106                    .filter(&_key, #root::core::SqlValue::I64(year))
5107                    .fetch(pool)
5108                    .await
5109            }
5110
5111            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5112            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5113            /// `month` is 1–12.
5114            ///
5115            /// # Errors
5116            /// As [`FetcherPool::fetch`].
5117            ///
5118            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5119            pub async fn where_month(
5120                col: &str,
5121                month: i64,
5122                pool: &#root::sql::Pool,
5123            ) -> ::core::result::Result<
5124                ::std::vec::Vec<Self>,
5125                #root::sql::ExecError,
5126            > {
5127                use #root::sql::FetcherPool as _;
5128                let _key = ::std::format!("{}__month", col);
5129                #root::query::QuerySet::<Self>::default()
5130                    .filter(&_key, #root::core::SqlValue::I64(month))
5131                    .fetch(pool)
5132                    .await
5133            }
5134
5135            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5136            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5137            /// `day` is 1–31.
5138            ///
5139            /// # Errors
5140            /// As [`FetcherPool::fetch`].
5141            ///
5142            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5143            pub async fn where_day(
5144                col: &str,
5145                day: i64,
5146                pool: &#root::sql::Pool,
5147            ) -> ::core::result::Result<
5148                ::std::vec::Vec<Self>,
5149                #root::sql::ExecError,
5150            > {
5151                use #root::sql::FetcherPool as _;
5152                let _key = ::std::format!("{}__day", col);
5153                #root::query::QuerySet::<Self>::default()
5154                    .filter(&_key, #root::core::SqlValue::I64(day))
5155                    .fetch(pool)
5156                    .await
5157            }
5158
5159            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5160            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5161            /// `hour` is 0–23.
5162            ///
5163            /// # Errors
5164            /// As [`FetcherPool::fetch`].
5165            ///
5166            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5167            pub async fn where_hour(
5168                col: &str,
5169                hour: i64,
5170                pool: &#root::sql::Pool,
5171            ) -> ::core::result::Result<
5172                ::std::vec::Vec<Self>,
5173                #root::sql::ExecError,
5174            > {
5175                use #root::sql::FetcherPool as _;
5176                let _key = ::std::format!("{}__hour", col);
5177                #root::query::QuerySet::<Self>::default()
5178                    .filter(&_key, #root::core::SqlValue::I64(hour))
5179                    .fetch(pool)
5180                    .await
5181            }
5182
5183            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5184            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5185            /// `minute` is 0–59.
5186            ///
5187            /// # Errors
5188            /// As [`FetcherPool::fetch`].
5189            ///
5190            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5191            pub async fn where_minute(
5192                col: &str,
5193                minute: i64,
5194                pool: &#root::sql::Pool,
5195            ) -> ::core::result::Result<
5196                ::std::vec::Vec<Self>,
5197                #root::sql::ExecError,
5198            > {
5199                use #root::sql::FetcherPool as _;
5200                let _key = ::std::format!("{}__minute", col);
5201                #root::query::QuerySet::<Self>::default()
5202                    .filter(&_key, #root::core::SqlValue::I64(minute))
5203                    .fetch(pool)
5204                    .await
5205            }
5206
5207            /// Fetch every row where `<col> LIKE <pattern>` —
5208            /// caller-supplied pattern (must include `%` / `_`
5209            /// wildcards manually). Eloquent `Model::whereLike`
5210            /// parity.
5211            ///
5212            /// # Errors
5213            /// As [`FetcherPool::fetch`].
5214            ///
5215            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5216            pub async fn where_like(
5217                col: &str,
5218                pattern: impl ::core::convert::Into<::std::string::String>,
5219                pool: &#root::sql::Pool,
5220            ) -> ::core::result::Result<
5221                ::std::vec::Vec<Self>,
5222                #root::sql::ExecError,
5223            > {
5224                use #root::sql::FetcherPool as _;
5225                let _key = ::std::format!("{}__like", col);
5226                #root::query::QuerySet::<Self>::default()
5227                    .filter(
5228                        &_key,
5229                        #root::core::SqlValue::String(pattern.into()),
5230                    )
5231                    .fetch(pool)
5232                    .await
5233            }
5234
5235            /// Fetch every row where `<col> ILIKE <pattern>` —
5236            /// case-insensitive LIKE (PG native, MySQL/SQLite
5237            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5238            ///
5239            /// # Errors
5240            /// As [`FetcherPool::fetch`].
5241            ///
5242            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5243            pub async fn where_ilike(
5244                col: &str,
5245                pattern: impl ::core::convert::Into<::std::string::String>,
5246                pool: &#root::sql::Pool,
5247            ) -> ::core::result::Result<
5248                ::std::vec::Vec<Self>,
5249                #root::sql::ExecError,
5250            > {
5251                use #root::sql::FetcherPool as _;
5252                let _key = ::std::format!("{}__ilike", col);
5253                #root::query::QuerySet::<Self>::default()
5254                    .filter(
5255                        &_key,
5256                        #root::core::SqlValue::String(pattern.into()),
5257                    )
5258                    .fetch(pool)
5259                    .await
5260            }
5261
5262            /// Fetch every row where `<col>` starts with `prefix`
5263            /// (auto-appends `%`). Eloquent
5264            /// `whereLike("col", "$prefix%")` parity.
5265            ///
5266            /// # Errors
5267            /// As [`FetcherPool::fetch`].
5268            ///
5269            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5270            pub async fn where_starts_with(
5271                col: &str,
5272                prefix: impl ::core::convert::Into<::std::string::String>,
5273                pool: &#root::sql::Pool,
5274            ) -> ::core::result::Result<
5275                ::std::vec::Vec<Self>,
5276                #root::sql::ExecError,
5277            > {
5278                use #root::sql::FetcherPool as _;
5279                let _key = ::std::format!("{}__startswith", col);
5280                #root::query::QuerySet::<Self>::default()
5281                    .filter(
5282                        &_key,
5283                        #root::core::SqlValue::String(prefix.into()),
5284                    )
5285                    .fetch(pool)
5286                    .await
5287            }
5288
5289            /// Fetch every row where `<col>` ends with `suffix`
5290            /// (auto-prepends `%`). Eloquent
5291            /// `whereLike("col", "%$suffix")` parity.
5292            ///
5293            /// # Errors
5294            /// As [`FetcherPool::fetch`].
5295            ///
5296            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5297            pub async fn where_ends_with(
5298                col: &str,
5299                suffix: impl ::core::convert::Into<::std::string::String>,
5300                pool: &#root::sql::Pool,
5301            ) -> ::core::result::Result<
5302                ::std::vec::Vec<Self>,
5303                #root::sql::ExecError,
5304            > {
5305                use #root::sql::FetcherPool as _;
5306                let _key = ::std::format!("{}__endswith", col);
5307                #root::query::QuerySet::<Self>::default()
5308                    .filter(
5309                        &_key,
5310                        #root::core::SqlValue::String(suffix.into()),
5311                    )
5312                    .fetch(pool)
5313                    .await
5314            }
5315
5316            /// Fetch every row where `<col>` contains `substr`
5317            /// (auto-wraps with `%`).
5318            /// Eloquent `whereLike("col", "%$substr%")` parity.
5319            ///
5320            /// # Errors
5321            /// As [`FetcherPool::fetch`].
5322            ///
5323            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5324            pub async fn where_contains(
5325                col: &str,
5326                substr: impl ::core::convert::Into<::std::string::String>,
5327                pool: &#root::sql::Pool,
5328            ) -> ::core::result::Result<
5329                ::std::vec::Vec<Self>,
5330                #root::sql::ExecError,
5331            > {
5332                use #root::sql::FetcherPool as _;
5333                let _key = ::std::format!("{}__contains", col);
5334                #root::query::QuerySet::<Self>::default()
5335                    .filter(
5336                        &_key,
5337                        #root::core::SqlValue::String(substr.into()),
5338                    )
5339                    .fetch(pool)
5340                    .await
5341            }
5342
5343            /// Fetch every row where `<col> > val`. Eloquent
5344            /// `Model::where($col, ">", $val)->get()` parity.
5345            ///
5346            /// # Errors
5347            /// As [`FetcherPool::fetch`].
5348            ///
5349            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5350            pub async fn where_gt(
5351                col: &str,
5352                val: impl ::core::convert::Into<#root::core::SqlValue>,
5353                pool: &#root::sql::Pool,
5354            ) -> ::core::result::Result<
5355                ::std::vec::Vec<Self>,
5356                #root::sql::ExecError,
5357            > {
5358                use #root::sql::FetcherPool as _;
5359                let _key = ::std::format!("{}__gt", col);
5360                #root::query::QuerySet::<Self>::default()
5361                    .filter(&_key, val)
5362                    .fetch(pool)
5363                    .await
5364            }
5365
5366            /// Fetch every row where `<col> >= val`. Eloquent
5367            /// `Model::where($col, ">=", $val)->get()` parity.
5368            ///
5369            /// # Errors
5370            /// As [`FetcherPool::fetch`].
5371            ///
5372            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5373            pub async fn where_gte(
5374                col: &str,
5375                val: impl ::core::convert::Into<#root::core::SqlValue>,
5376                pool: &#root::sql::Pool,
5377            ) -> ::core::result::Result<
5378                ::std::vec::Vec<Self>,
5379                #root::sql::ExecError,
5380            > {
5381                use #root::sql::FetcherPool as _;
5382                let _key = ::std::format!("{}__gte", col);
5383                #root::query::QuerySet::<Self>::default()
5384                    .filter(&_key, val)
5385                    .fetch(pool)
5386                    .await
5387            }
5388
5389            /// Fetch every row where `<col> < val`. Eloquent
5390            /// `Model::where($col, "<", $val)->get()` parity.
5391            ///
5392            /// # Errors
5393            /// As [`FetcherPool::fetch`].
5394            ///
5395            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5396            pub async fn where_lt(
5397                col: &str,
5398                val: impl ::core::convert::Into<#root::core::SqlValue>,
5399                pool: &#root::sql::Pool,
5400            ) -> ::core::result::Result<
5401                ::std::vec::Vec<Self>,
5402                #root::sql::ExecError,
5403            > {
5404                use #root::sql::FetcherPool as _;
5405                let _key = ::std::format!("{}__lt", col);
5406                #root::query::QuerySet::<Self>::default()
5407                    .filter(&_key, val)
5408                    .fetch(pool)
5409                    .await
5410            }
5411
5412            /// Fetch every row where `<col> <= val`. Eloquent
5413            /// `Model::where($col, "<=", $val)->get()` parity.
5414            ///
5415            /// # Errors
5416            /// As [`FetcherPool::fetch`].
5417            ///
5418            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5419            pub async fn where_lte(
5420                col: &str,
5421                val: impl ::core::convert::Into<#root::core::SqlValue>,
5422                pool: &#root::sql::Pool,
5423            ) -> ::core::result::Result<
5424                ::std::vec::Vec<Self>,
5425                #root::sql::ExecError,
5426            > {
5427                use #root::sql::FetcherPool as _;
5428                let _key = ::std::format!("{}__lte", col);
5429                #root::query::QuerySet::<Self>::default()
5430                    .filter(&_key, val)
5431                    .fetch(pool)
5432                    .await
5433            }
5434
5435            /// Fetch every row where `<col> <> val`. Eloquent
5436            /// `Model::where($col, "!=", $val)->get()` parity.
5437            ///
5438            /// # Errors
5439            /// As [`FetcherPool::fetch`].
5440            ///
5441            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5442            pub async fn where_ne(
5443                col: &str,
5444                val: impl ::core::convert::Into<#root::core::SqlValue>,
5445                pool: &#root::sql::Pool,
5446            ) -> ::core::result::Result<
5447                ::std::vec::Vec<Self>,
5448                #root::sql::ExecError,
5449            > {
5450                use #root::sql::FetcherPool as _;
5451                let _key = ::std::format!("{}__ne", col);
5452                #root::query::QuerySet::<Self>::default()
5453                    .filter(&_key, val)
5454                    .fetch(pool)
5455                    .await
5456            }
5457
5458            /// Fetch every row matching `<col> = val` for ANY of the
5459            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5460            /// parity. Empty `cols` returns no rows.
5461            ///
5462            /// Resolves each `&str` column to its SCHEMA-registered
5463            /// `&'static str` once and builds a single OR-composed
5464            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5465            ///
5466            /// # Errors
5467            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5468            /// when any column is not declared on the model.
5469            ///
5470            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5471            pub async fn where_any(
5472                cols: &[&str],
5473                val: impl ::core::convert::Into<#root::core::SqlValue>,
5474                pool: &#root::sql::Pool,
5475            ) -> ::core::result::Result<
5476                ::std::vec::Vec<Self>,
5477                #root::sql::ExecError,
5478            > {
5479                Self::__where_multi(cols, val, false, pool).await
5480            }
5481
5482            /// Fetch every row matching `<col> = val` for ALL listed
5483            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5484            /// Empty `cols` returns every row (vacuous AND).
5485            ///
5486            /// # Errors
5487            /// As [`Self::where_any`].
5488            pub async fn where_all(
5489                cols: &[&str],
5490                val: impl ::core::convert::Into<#root::core::SqlValue>,
5491                pool: &#root::sql::Pool,
5492            ) -> ::core::result::Result<
5493                ::std::vec::Vec<Self>,
5494                #root::sql::ExecError,
5495            > {
5496                Self::__where_multi(cols, val, true, pool).await
5497            }
5498
5499            /// Internal: build a Q expression composing `cols` via
5500            /// AND (`all`) or OR (`!all`), then fetch. Backs
5501            /// `where_any` / `where_all`.
5502            #[doc(hidden)]
5503            pub async fn __where_multi(
5504                cols: &[&str],
5505                val: impl ::core::convert::Into<#root::core::SqlValue>,
5506                all: bool,
5507                pool: &#root::sql::Pool,
5508            ) -> ::core::result::Result<
5509                ::std::vec::Vec<Self>,
5510                #root::sql::ExecError,
5511            > {
5512                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5513                    .await
5514            }
5515
5516            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5517            /// parity. PK-ordered
5518            /// is NOT guaranteed without an explicit `order_by` —
5519            /// drop into `Self::query()` for that.
5520            ///
5521            /// # Errors
5522            /// As [`FetcherPool::fetch`].
5523            ///
5524            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5525            pub async fn take(
5526                n: i64,
5527                pool: &#root::sql::Pool,
5528            ) -> ::core::result::Result<
5529                ::std::vec::Vec<Self>,
5530                #root::sql::ExecError,
5531            > {
5532                use #root::sql::FetcherPool as _;
5533                #root::query::QuerySet::<Self>::default()
5534                    .limit(n)
5535                    .fetch(pool)
5536                    .await
5537            }
5538
5539            /// Fetch the page-th window of `per_page` rows
5540            /// (1-indexed). Eloquent
5541            /// `Model::query()->forPage($page, $perPage)->get()`
5542            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5543            /// LIMIT per_page`; for large offsets this is O(N) —
5544            /// prefer keyset pagination via PK on hot paths.
5545            ///
5546            /// # Errors
5547            /// As [`FetcherPool::fetch`].
5548            ///
5549            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5550            pub async fn for_page(
5551                page: i64,
5552                per_page: i64,
5553                pool: &#root::sql::Pool,
5554            ) -> ::core::result::Result<
5555                ::std::vec::Vec<Self>,
5556                #root::sql::ExecError,
5557            > {
5558                use #root::sql::FetcherPool as _;
5559                let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5560                #root::query::QuerySet::<Self>::default()
5561                    .limit(per_page)
5562                    .offset(_offset)
5563                    .fetch(pool)
5564                    .await
5565            }
5566
5567            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5568            /// one page of rows AND the total row count in one
5569            /// call. Returns `(rows, total)`. Two queries: a
5570            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5571            /// for the total.
5572            ///
5573            /// Useful for paginated UIs that need both the visible
5574            /// rows AND a "Page X of Y" / total-count widget. For
5575            /// large tables prefer keyset pagination + cached count;
5576            /// every call to `paginate` re-counts the full table.
5577            ///
5578            /// Same 1-indexed `page` convention as [`Self::for_page`].
5579            ///
5580            /// # Errors
5581            /// As [`Self::for_page`] and [`Self::count`].
5582            pub async fn paginate(
5583                page: i64,
5584                per_page: i64,
5585                pool: &#root::sql::Pool,
5586            ) -> ::core::result::Result<
5587                (::std::vec::Vec<Self>, i64),
5588                #root::sql::ExecError,
5589            > {
5590                // Route through the queryset rather than `Self::count`:
5591                // the `count` inherent method is suppressed on models
5592                // that declare a field named `count` (field/shortcut
5593                // collision guard), and `paginate` must still compile
5594                // for those models.
5595                let total = {
5596                    use #root::sql::CounterPool as _;
5597                    #root::query::QuerySet::<Self>::default()
5598                        .count(pool)
5599                        .await?
5600                };
5601                let rows = Self::for_page(page, per_page, pool).await?;
5602                ::core::result::Result::Ok((rows, total))
5603            }
5604
5605            /// Bulk-update — set `set_col = set_val` on every row
5606            /// matching `where_col = where_val`. Returns affected row
5607            /// count. Eloquent
5608            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5609            /// parity.
5610            ///
5611            /// For multi-column updates drop into the queryset
5612            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5613            ///
5614            /// # Errors
5615            /// As [`UpdaterPool::execute_pool`].
5616            ///
5617            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5618            pub async fn update_where(
5619                where_col: &str,
5620                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5621                set_col: &str,
5622                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5623                pool: &#root::sql::Pool,
5624            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5625                use #root::sql::UpdaterPool as _;
5626                #root::query::QuerySet::<Self>::default()
5627                    .filter(where_col, where_val)
5628                    .update()
5629                    .set(set_col, set_val)
5630                    .execute_pool(pool)
5631                    .await
5632            }
5633
5634            /// Bulk-delete — remove every row matching
5635            /// `where_col = where_val`. Returns affected row count.
5636            /// Eloquent
5637            /// `Model::where($where_col, $where_val)->delete()` parity.
5638            ///
5639            /// # Errors
5640            /// As [`#root::sql::delete_pool`].
5641            pub async fn delete_where(
5642                where_col: &str,
5643                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5644                pool: &#root::sql::Pool,
5645            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5646                let _query = #root::query::QuerySet::<Self>::default()
5647                    .filter_op(where_col, #root::core::Op::Eq, where_val)
5648                    .compile_delete()?;
5649                #root::sql::delete_pool(pool, &_query).await
5650            }
5651
5652            /// Bulk-update — set `set_col = set_val` on EVERY row of
5653            /// this model's table. **No WHERE clause** — use with
5654            /// care. Eloquent `Model::query()->update([$col => $val])`
5655            /// parity.
5656            ///
5657            /// Use for backfills, one-shot reset flows, etc. The
5658            /// returned count is rows affected.
5659            ///
5660            /// # Errors
5661            /// As [`UpdaterPool::execute_pool`].
5662            ///
5663            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5664            pub async fn update_all(
5665                set_col: &str,
5666                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5667                pool: &#root::sql::Pool,
5668            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5669                use #root::sql::UpdaterPool as _;
5670                #root::query::QuerySet::<Self>::default()
5671                    .update()
5672                    .set(set_col, set_val)
5673                    .execute_pool(pool)
5674                    .await
5675            }
5676
5677            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5678            /// Eloquent `Model::whereNotLike` parity. Pattern is
5679            /// passed verbatim — caller controls `%` / `_`.
5680            ///
5681            /// # Errors
5682            /// As [`FetcherPool::fetch`].
5683            ///
5684            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5685            pub async fn where_not_like(
5686                col: &str,
5687                pattern: impl ::core::convert::Into<::std::string::String>,
5688                pool: &#root::sql::Pool,
5689            ) -> ::core::result::Result<
5690                ::std::vec::Vec<Self>,
5691                #root::sql::ExecError,
5692            > {
5693                use #root::sql::FetcherPool as _;
5694                let _key = ::std::format!("{}__not_like", col);
5695                #root::query::QuerySet::<Self>::default()
5696                    .filter(
5697                        &_key,
5698                        #root::core::SqlValue::String(pattern.into()),
5699                    )
5700                    .fetch(pool)
5701                    .await
5702            }
5703
5704            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5705            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5706            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5707            ///
5708            /// # Errors
5709            /// As [`FetcherPool::fetch`].
5710            ///
5711            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5712            pub async fn where_not_ilike(
5713                col: &str,
5714                pattern: impl ::core::convert::Into<::std::string::String>,
5715                pool: &#root::sql::Pool,
5716            ) -> ::core::result::Result<
5717                ::std::vec::Vec<Self>,
5718                #root::sql::ExecError,
5719            > {
5720                use #root::sql::FetcherPool as _;
5721                let _key = ::std::format!("{}__not_ilike", col);
5722                #root::query::QuerySet::<Self>::default()
5723                    .filter(
5724                        &_key,
5725                        #root::core::SqlValue::String(pattern.into()),
5726                    )
5727                    .fetch(pool)
5728                    .await
5729            }
5730
5731            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5732            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5733            /// parity.
5734            ///
5735            /// # Errors
5736            /// As [`FetcherPool::fetch`].
5737            ///
5738            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5739            pub async fn where_not_between(
5740                col: &str,
5741                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5742                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5743                pool: &#root::sql::Pool,
5744            ) -> ::core::result::Result<
5745                ::std::vec::Vec<Self>,
5746                #root::sql::ExecError,
5747            > {
5748                use #root::sql::FetcherPool as _;
5749                let _key = ::std::format!("{}__not_between", col);
5750                let _vals = #root::core::SqlValue::List(::std::vec![
5751                    ::core::convert::Into::into(lo),
5752                    ::core::convert::Into::into(hi),
5753                ]);
5754                #root::query::QuerySet::<Self>::default()
5755                    .filter(&_key, _vals)
5756                    .fetch(pool)
5757                    .await
5758            }
5759
5760            /// Returns the SQL table name for this model. Eloquent
5761            /// `$model->getTable()` parity.
5762            #[must_use]
5763            pub fn table_name() -> &'static str {
5764                <Self as #root::core::Model>::SCHEMA.table
5765            }
5766
5767            /// Returns the SQL column name of this model's primary
5768            /// key, or `None` when the model has no
5769            /// `#[rustango(primary_key)]`. Eloquent
5770            /// `$model->getKeyName()` parity.
5771            #[must_use]
5772            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5773                <Self as #root::core::Model>::SCHEMA
5774                    .primary_key()
5775                    .map(|f| f.column)
5776            }
5777
5778            /// Fetch every row where `<col> BETWEEN lo AND hi`
5779            /// (inclusive on both ends — same as SQL). Eloquent
5780            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5781            ///
5782            /// # Errors
5783            /// As [`FetcherPool::fetch`].
5784            ///
5785            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5786            pub async fn where_between(
5787                col: &str,
5788                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5789                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5790                pool: &#root::sql::Pool,
5791            ) -> ::core::result::Result<
5792                ::std::vec::Vec<Self>,
5793                #root::sql::ExecError,
5794            > {
5795                use #root::sql::FetcherPool as _;
5796                let _key = ::std::format!("{}__between", col);
5797                let _vals = #root::core::SqlValue::List(::std::vec![
5798                    ::core::convert::Into::into(lo),
5799                    ::core::convert::Into::into(hi),
5800                ]);
5801                #root::query::QuerySet::<Self>::default()
5802                    .filter(&_key, _vals)
5803                    .fetch(pool)
5804                    .await
5805            }
5806
5807            /// Fetch the first row where `<col> = <val>`. Returns
5808            /// `Ok(None)` when no row matches. Eloquent
5809            /// `Model::firstWhere($col, $val)` parity.
5810            ///
5811            /// Thin wrapper over `QuerySet::<Self>::default()
5812            /// .filter(col, val).first(pool)`. Use this when you
5813            /// want one row identified by a non-PK column (e.g.
5814            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5815            ///
5816            /// `val` accepts any value `Into<SqlValue>` so plain
5817            /// strings, ints, UUIDs, etc. all work.
5818            ///
5819            /// # Errors
5820            /// As `QuerySet::first`.
5821            pub async fn first_where(
5822                col: &str,
5823                val: impl ::core::convert::Into<#root::core::SqlValue>,
5824                pool: &#root::sql::Pool,
5825            ) -> ::core::result::Result<
5826                ::core::option::Option<Self>,
5827                #root::sql::ExecError,
5828            > {
5829                #root::query::QuerySet::<Self>::default()
5830                    .filter(col, val)
5831                    .first(pool)
5832                    .await
5833            }
5834
5835            #value_method
5836
5837            /// Fetch the row with the largest `field` value —
5838            /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5839            /// `Ok(None)` for an empty table. Eloquent
5840            /// `Model::latest($field)->first()` parity.
5841            /// Thin wrapper over `QuerySet::<Self>::default()
5842            /// .latest(field, pool)`.
5843            ///
5844            /// **Field name** is the Rust field ident as a string
5845            /// (not the SQL column). It is a runtime `&str`, so an
5846            /// unknown one compiles and surfaces as
5847            /// `ExecError::Query(QueryError::UnknownField)` when the
5848            /// query runs.
5849            ///
5850            /// # Errors
5851            /// As `QuerySet::latest`.
5852            pub async fn latest(
5853                field: &str,
5854                pool: &#root::sql::Pool,
5855            ) -> ::core::result::Result<
5856                ::core::option::Option<Self>,
5857                #root::sql::ExecError,
5858            > {
5859                #root::query::QuerySet::<Self>::default()
5860                    .latest(field, pool)
5861                    .await
5862            }
5863
5864            /// Sibling of [`Self::latest`] — fetches the row
5865            /// with the smallest `field` value (`ORDER BY <field>
5866            /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5867            /// ->first()` parity.
5868            ///
5869            /// # Errors
5870            /// As [`Self::latest`].
5871            pub async fn earliest(
5872                field: &str,
5873                pool: &#root::sql::Pool,
5874            ) -> ::core::result::Result<
5875                ::core::option::Option<Self>,
5876                #root::sql::ExecError,
5877            > {
5878                #root::query::QuerySet::<Self>::default()
5879                    .earliest(field, pool)
5880                    .await
5881            }
5882
5883            #count_method
5884
5885            /// `true` when the table contains at least one row.
5886            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
5887            /// over `QuerySet::<Self>::default().exists(pool)`.
5888            ///
5889            /// # Errors
5890            /// As [`ExistsPool::exists`].
5891            ///
5892            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5893            pub async fn exists(
5894                pool: &#root::sql::Pool,
5895            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5896                use #root::sql::ExistsPool as _;
5897                #root::query::QuerySet::<Self>::default()
5898                    .exists(pool)
5899                    .await
5900            }
5901
5902            /// Inverse of [`Self::exists`] — returns `true` when
5903            /// the table has zero rows. Eloquent
5904            /// `Model::doesntExist()` parity.
5905            ///
5906            /// # Errors
5907            /// As [`Self::exists`].
5908            pub async fn doesnt_exist(
5909                pool: &#root::sql::Pool,
5910            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5911                Self::exists(pool).await.map(|e| !e)
5912            }
5913
5914            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5915            /// `true` when a row with primary key `pk` exists in
5916            /// the table. Sugar over
5917            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5918            ///
5919            /// Differs from [`Self::exists`] (which checks "any row
5920            /// in the table") by checking a specific PK existence.
5921            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5922            /// because the row is never materialized — the SQL is
5923            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5924            ///
5925            /// # Errors
5926            /// As [`#root::sql::ExistsPool::contains_pk`].
5927            pub async fn contains_pk(
5928                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5929                pool: &#root::sql::Pool,
5930            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5931                use #root::sql::ExistsPool as _;
5932                #root::query::QuerySet::<Self>::default()
5933                    .contains_pk(pool, pk)
5934                    .await
5935            }
5936
5937            #sum_method
5938            #avg_method
5939            #min_method
5940            #max_method
5941
5942            /// Fetch every row of this model from `pool`. Eloquent
5943            /// `Model::all()` parity — a thin wrapper over
5944            /// `QuerySet::<Self>::default().fetch(pool)`.
5945            ///
5946            /// **Use with care on large tables** — there's no
5947            /// pagination or limit; the entire table is materialized
5948            /// into memory. For anything beyond fixture / lookup
5949            /// tables, page through `QuerySet::<Self>::default()
5950            /// .order_by(...).limit(N).offset(M).fetch(pool)`
5951            /// or stream via `.iterator(chunk_size)`.
5952            ///
5953            /// # Errors
5954            /// As [`FetcherPool::fetch`].
5955            ///
5956            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5957            pub async fn all(
5958                pool: &#root::sql::Pool,
5959            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5960            {
5961                use #root::sql::FetcherPool as _;
5962                #root::query::QuerySet::<Self>::default()
5963                    .fetch(pool)
5964                    .await
5965            }
5966
5967            /// Look up every row whose primary key is in `pks`.
5968            /// Returns the matching rows in **inventory** order — NOT
5969            /// the order of `pks`. Empty `pks` returns an empty
5970            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5971            /// with a list) parity.
5972            ///
5973            /// Thin wrapper over `QuerySet::<Self>::default()
5974            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
5975            /// Caller-supplied PKs that don't match a row are
5976            /// silently skipped (the returned vec is shorter than
5977            /// the input list). For an order-preserving / "fail
5978            /// when any missing" variant, build the queryset
5979            /// explicitly with `in_bulk` instead.
5980            ///
5981            /// Accepts any iterable whose elements are
5982            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5983            /// `Vec<Uuid>`, etc.
5984            ///
5985            /// # Errors
5986            /// As [`FetcherPool::fetch`].
5987            ///
5988            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5989            pub async fn find_many<V>(
5990                pks: impl ::core::iter::IntoIterator<Item = V>,
5991                pool: &#root::sql::Pool,
5992            ) -> ::core::result::Result<
5993                ::std::vec::Vec<Self>,
5994                #root::sql::ExecError,
5995            >
5996            where
5997                V: ::core::convert::Into<#root::core::SqlValue>,
5998            {
5999                use #root::sql::FetcherPool as _;
6000                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6001                    pks.into_iter().map(::core::convert::Into::into).collect();
6002                if _values.is_empty() {
6003                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6004                }
6005                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6006                #root::query::QuerySet::<Self>::default()
6007                    .filter(&_key, #root::core::SqlValue::List(_values))
6008                    .fetch(pool)
6009                    .await
6010            }
6011
6012            /// Look up the row whose primary key equals `pk`. Returns
6013            /// `Ok(None)` when no row matches, rather than erroring.
6014            /// Eloquent `Model::find`
6015            /// shape — accepts any value `Into<SqlValue>`.
6016            ///
6017            /// One-liner shortcut for the common
6018            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6019            /// .limit(1).fetch(pool).await?.into_iter().next()`
6020            /// dance.
6021            ///
6022            /// # Errors
6023            /// As [`FetcherPool::fetch`].
6024            ///
6025            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6026            pub async fn find(
6027                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6028                pool: &#root::sql::Pool,
6029            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6030            {
6031                use #root::sql::FetcherPool as _;
6032                let _pk_val: #root::core::SqlValue = pk.into();
6033                let mut _rows: ::std::vec::Vec<Self> =
6034                    #root::query::QuerySet::<Self>::default()
6035                        .filter(::core::stringify!(#pk_ident), _pk_val)
6036                        .limit(1)
6037                        .fetch(pool)
6038                        .await?;
6039                ::core::result::Result::Ok(_rows.into_iter().next())
6040            }
6041
6042            /// Look up the row whose primary key equals `pk`. Errors
6043            /// when no row matches — the throwing counterpart of
6044            /// [`Self::find`]. Eloquent `Model::findOrFail`
6045            /// parity.
6046            ///
6047            /// Translates the miss into
6048            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6049            /// so callers can `?`-bubble straight through the typical
6050            /// `ExecError` error chain.
6051            ///
6052            /// # Errors
6053            /// As [`Self::find`]; additionally
6054            /// [`sqlx::Error::RowNotFound`] when no row matches.
6055            ///
6056            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6057            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6058            pub async fn find_or_fail(
6059                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6060                pool: &#root::sql::Pool,
6061            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6062                match Self::find(pk, pool).await? {
6063                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6064                    ::core::option::Option::None => ::core::result::Result::Err(
6065                        #root::sql::ExecError::Driver(
6066                            #root::sql::sqlx::Error::RowNotFound,
6067                        ),
6068                    ),
6069                }
6070            }
6071
6072            /// Look up multiple rows by primary key, **failing** if
6073            /// any requested PK is missing. Eloquent
6074            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6075            /// inventory order (NOT request order — same as
6076            /// [`Self::find_many`]).
6077            ///
6078            /// Empty `pks` returns an empty `Vec` (no rows requested
6079            /// → nothing to fail on).
6080            ///
6081            /// Differs from [`Self::find_many`] in that this method
6082            /// requires every PK to resolve. If even one is missing,
6083            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6084            /// in `ExecError::Driver`). Rows are deduped at the SQL
6085            /// layer, so passing the same PK twice in `pks` counts as
6086            /// one expected row.
6087            ///
6088            /// # Errors
6089            /// As [`Self::find_many`], plus `RowNotFound` when the
6090            /// returned row count is less than the count of distinct
6091            /// requested PKs.
6092            pub async fn find_many_or_fail<V>(
6093                pks: impl ::core::iter::IntoIterator<Item = V>,
6094                pool: &#root::sql::Pool,
6095            ) -> ::core::result::Result<
6096                ::std::vec::Vec<Self>,
6097                #root::sql::ExecError,
6098            >
6099            where
6100                V: ::core::convert::Into<#root::core::SqlValue>,
6101            {
6102                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6103                    pks.into_iter().map(::core::convert::Into::into).collect();
6104                if _values.is_empty() {
6105                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6106                }
6107                // Dedup the requested PK list before counting so
6108                // duplicate-PK requests don't false-fail (the SQL
6109                // `IN (...)` clause naturally dedups too).
6110                let mut _seen: ::std::collections::HashSet<
6111                    ::std::string::String,
6112                > = ::std::collections::HashSet::new();
6113                for v in &_values {
6114                    _seen.insert(v.to_display_string());
6115                }
6116                let _expected = _seen.len();
6117                let _rows = Self::find_many(_values, pool).await?;
6118                if _rows.len() < _expected {
6119                    return ::core::result::Result::Err(
6120                        #root::sql::ExecError::Driver(
6121                            #root::sql::sqlx::Error::RowNotFound,
6122                        ),
6123                    );
6124                }
6125                ::core::result::Result::Ok(_rows)
6126            }
6127
6128            /// Find by primary key or run `fallback` to produce a
6129            /// default row to return. Eloquent
6130            /// `Model::findOr($pk, fn() => …)` parity.
6131            ///
6132            /// Unlike [`Self::find_or_fail`] (which raises on
6133            /// miss), this is the "give me something sensible"
6134            /// branch: typical use is "fetch the user's row, else
6135            /// fall back to an anonymous/guest stub".
6136            ///
6137            /// `fallback` runs only when no row matches — the DB
6138            /// round-trip happens unconditionally.
6139            ///
6140            /// # Errors
6141            /// As [`Self::find`].
6142            pub async fn find_or<F>(
6143                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6144                pool: &#root::sql::Pool,
6145                fallback: F,
6146            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6147            where
6148                F: ::core::ops::FnOnce() -> Self,
6149            {
6150                ::core::result::Result::Ok(
6151                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6152                )
6153            }
6154
6155            /// Same as [`Self::find_or`] but also returns a `bool`
6156            /// indicating whether the row was found in the DB
6157            /// (`true`) or freshly built from `fallback` (`false`).
6158            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6159            /// in PHP the returned model also exposes `->exists`;
6160            /// here that's surfaced as the second tuple element.
6161            ///
6162            /// Useful for edit-or-create form handlers where the
6163            /// caller needs to know whether to PATCH or POST when
6164            /// the user submits the form.
6165            ///
6166            /// # Errors
6167            /// As [`Self::find`].
6168            pub async fn find_or_new<F>(
6169                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6170                pool: &#root::sql::Pool,
6171                fallback: F,
6172            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6173            where
6174                F: ::core::ops::FnOnce() -> Self,
6175            {
6176                match Self::find(pk, pool).await? {
6177                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6178                    ::core::option::Option::None => {
6179                        ::core::result::Result::Ok((fallback(), false))
6180                    }
6181                }
6182            }
6183
6184            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6185            /// [`Self::find_or_new`] but **persists** the new row
6186            /// when the PK isn't found. Returns `(row, exists: bool)`
6187            /// — `exists=true` when the PK was found,
6188            /// `false` when a fresh row was inserted.
6189            ///
6190            /// ```ignore
6191            /// let (post, found) = Post::find_or_insert(
6192            ///     pk,
6193            ///     &pool,
6194            ///     || Post { id: Auto::default(), title: "new".into() },
6195            /// ).await?;
6196            /// // `found` true → returned existing row;
6197            /// // `found` false → fallback was inserted, `post.id` populated.
6198            /// ```
6199            ///
6200            /// **Caveat**: two concurrent calls that both miss the
6201            /// find can both INSERT, violating uniqueness. Wrap in a
6202            /// transaction or rely on a UNIQUE constraint + handle
6203            /// the conflict error if you need race-free semantics.
6204            ///
6205            /// # Errors
6206            /// As [`Self::find`] and [`Self::save_pool`].
6207            pub async fn find_or_insert<F>(
6208                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6209                pool: &#root::sql::Pool,
6210                fallback: F,
6211            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6212            where
6213                F: ::core::ops::FnOnce() -> Self,
6214            {
6215                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6216                    return ::core::result::Result::Ok((_row, true));
6217                }
6218                let mut _new = fallback();
6219                _new.save_pool(pool).await?;
6220                ::core::result::Result::Ok((_new, false))
6221            }
6222
6223            /// Fetch the first row of the table, or run `fallback`
6224            /// when the table is empty. Eloquent
6225            /// `Model::firstOr(fn() => …)` parity.
6226            ///
6227            /// # Errors
6228            /// As [`Self::first`].
6229            pub async fn first_or<F>(
6230                pool: &#root::sql::Pool,
6231                fallback: F,
6232            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6233            where
6234                F: ::core::ops::FnOnce() -> Self,
6235            {
6236                // Route through the queryset rather than `Self::first`,
6237                // which is suppressed on models with a field named
6238                // `first` (field/shortcut collision guard).
6239                ::core::result::Result::Ok(
6240                    #root::query::QuerySet::<Self>::default()
6241                        .first(pool)
6242                        .await?
6243                        .unwrap_or_else(fallback),
6244                )
6245            }
6246
6247            /// Fetch exactly one row matching `<col> = <val>`. Errors
6248            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6249            /// or more than one matches
6250            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6251            /// Eloquent `Model::sole($col, $val)` parity.
6252            ///
6253            /// # Errors
6254            /// As [`Self::where_`] plus the explicit
6255            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6256            pub async fn sole(
6257                col: &str,
6258                val: impl ::core::convert::Into<#root::core::SqlValue>,
6259                pool: &#root::sql::Pool,
6260            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6261                let mut _rows = Self::where_(col, val, pool).await?;
6262                match _rows.len() {
6263                    0 => ::core::result::Result::Err(
6264                        #root::sql::ExecError::Driver(
6265                            #root::sql::sqlx::Error::RowNotFound,
6266                        ),
6267                    ),
6268                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6269                    n => ::core::result::Result::Err(
6270                        #root::sql::ExecError::MultipleRowsReturned {
6271                            op: "sole",
6272                            table: <Self as #root::core::Model>::SCHEMA.name,
6273                            count: n,
6274                        },
6275                    ),
6276                }
6277            }
6278        }
6279    } else {
6280        quote!()
6281    };
6282
6283    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6284    // the non-audited `_pool` methods but execute against an open
6285    // `PoolTx` so the writes participate in the caller's transaction.
6286    // Auditing inside TX is deferred; these always use the plain
6287    // executor primitives regardless of whether the model is audited.
6288    let tx_insert_method = if fields.has_auto {
6289        let pushes = &fields.insert_pushes;
6290        let returning_cols = &fields.returning_cols;
6291        quote! {
6292            /// Insert this row inside an open transaction, populating
6293            /// any `Auto<T>` PK from the auto-assigned value. Works
6294            /// against any backend that `tx` wraps.
6295            ///
6296            /// # Errors
6297            /// As [`Self::insert_pool`].
6298            pub async fn insert_tx(
6299                &mut self,
6300                tx: &mut #root::sql::PoolTx<'_>,
6301            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6302                let mut _columns: ::std::vec::Vec<&'static str> =
6303                    ::std::vec::Vec::new();
6304                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6305                    ::std::vec::Vec::new();
6306                #( #pushes )*
6307                let _query = #root::core::InsertQuery::new(
6308                    <Self as #root::core::Model>::SCHEMA,
6309                    _columns,
6310                    _values,
6311                )
6312                .returning(::std::vec![ #( #returning_cols ),* ]);
6313                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6314                #root::sql::apply_auto_pk(_result, self)
6315            }
6316        }
6317    } else {
6318        let insert_columns = &fields.insert_columns;
6319        let insert_values = &fields.insert_values;
6320        quote! {
6321            /// Insert this row inside an open transaction.
6322            ///
6323            /// # Errors
6324            /// As [`Self::insert_pool`].
6325            pub async fn insert_tx(
6326                &self,
6327                tx: &mut #root::sql::PoolTx<'_>,
6328            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6329                let _query = #root::core::InsertQuery::new(
6330                    <Self as #root::core::Model>::SCHEMA,
6331                    ::std::vec![ #( #insert_columns ),* ],
6332                    ::std::vec![ #( #insert_values ),* ],
6333                );
6334                #root::sql::insert_tx(tx, &_query).await
6335            }
6336        }
6337    };
6338
6339    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6340        let pk_column_lit = pk_col.as_str();
6341        let assignments = &fields.update_assignments;
6342        let dispatch_unset = if fields.pk_is_auto {
6343            quote! {
6344                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6345                    return self.insert_tx(tx).await.map(|()| 1u64);
6346                }
6347            }
6348        } else {
6349            quote!()
6350        };
6351        quote! {
6352            /// Save this row inside an open transaction. `INSERT` when
6353            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6354            /// PK. Works against any backend that `tx` wraps.
6355            ///
6356            /// # Errors
6357            /// As [`Self::save_pool`].
6358            pub async fn save_tx(
6359                &mut self,
6360                tx: &mut #root::sql::PoolTx<'_>,
6361            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6362                #dispatch_unset
6363                let _query = #root::core::UpdateQuery::new(
6364                    <Self as #root::core::Model>::SCHEMA,
6365                    ::std::vec![ #( #assignments ),* ],
6366                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6367                        #pk_column_lit,
6368                        #root::core::Op::Eq,
6369                        ::core::convert::Into::<#root::core::SqlValue>::into(
6370                            ::core::clone::Clone::clone(&self.#pk_ident)
6371                        ),
6372                    )),
6373                );
6374                let _affected = #root::sql::update_tx(tx, &_query).await?;
6375                ::core::result::Result::Ok(_affected)
6376            }
6377        }
6378    } else {
6379        quote!()
6380    };
6381
6382    let tx_delete_method = {
6383        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6384        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6385        if let Some(pk_ident) = pk_ident_for_tx {
6386            quote! {
6387                /// Delete the row identified by this instance's PK
6388                /// inside an open transaction. Works against any backend
6389                /// that `tx` wraps.
6390                ///
6391                /// # Errors
6392                /// As [`Self::delete_pool`].
6393                pub async fn delete_tx(
6394                    &self,
6395                    tx: &mut #root::sql::PoolTx<'_>,
6396                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6397                    let _query = #root::core::DeleteQuery::by_pk(
6398                        <Self as #root::core::Model>::SCHEMA,
6399                        #pk_column_lit,
6400                        ::core::convert::Into::<#root::core::SqlValue>::into(
6401                            ::core::clone::Clone::clone(&self.#pk_ident)
6402                        ),
6403                    );
6404                    #root::sql::delete_tx(tx, &_query).await
6405                }
6406            }
6407        } else {
6408            quote!()
6409        }
6410    };
6411
6412    // Update emission captures both BEFORE and AFTER state — runs an
6413    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6414    // each tracked field's prior value, then after the UPDATE diffs
6415    // against the in-memory `&self`. `diff_changes` drops unchanged
6416    // columns so the JSON only contains the actual delta.
6417    //
6418    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6419    // and binds `_audit_before_pairs`; `audit_update_post` runs
6420    // after the UPDATE and emits the PendingEntry.
6421    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6422        audited_fields
6423    {
6424        if tracked.is_empty() {
6425            (quote!(), quote!())
6426        } else {
6427            let select_cols: String = tracked
6428                .iter()
6429                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6430                .collect::<Vec<_>>()
6431                .join(", ");
6432            let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6433            let select_cols_lit = select_cols;
6434            let pk_column_lit_for_select = pk_column_for_select;
6435            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6436                if fields.pk_is_auto {
6437                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6438                } else {
6439                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6440                }
6441            } else {
6442                quote!(0_i64)
6443            };
6444            let before_pairs = tracked.iter().map(|c| {
6445                let column_lit = c.column.as_str();
6446                let value_ty = &c.value_ty;
6447                quote! {
6448                    (
6449                        #column_lit,
6450                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6451                            &_audit_before_row, #column_lit,
6452                        ) {
6453                            ::core::result::Result::Ok(v) => {
6454                                #root::__serde_json::to_value(&v)
6455                                    .unwrap_or(#root::__serde_json::Value::Null)
6456                            }
6457                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6458                        },
6459                    )
6460                }
6461            });
6462            let after_pairs = tracked.iter().map(|c| {
6463                let column_lit = c.column.as_str();
6464                let ident = &c.ident;
6465                quote! {
6466                    (
6467                        #column_lit,
6468                        #root::__serde_json::to_value(&self.#ident)
6469                            .unwrap_or(#root::__serde_json::Value::Null),
6470                    )
6471                }
6472            });
6473            let pk_str = audit_pk_to_string.clone();
6474            let pre = quote! {
6475                let _audit_select_sql = ::std::format!(
6476                    r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6477                    #select_cols_lit,
6478                    <Self as #root::core::Model>::SCHEMA.table,
6479                    #pk_column_lit_for_select,
6480                );
6481                let _audit_before_pairs:
6482                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6483                    match #root::sql::sqlx::query(&_audit_select_sql)
6484                        .bind(#pk_value_for_bind)
6485                        .fetch_optional(&mut *_executor)
6486                        .await
6487                    {
6488                        ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6489                            ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6490                        }
6491                        _ => ::core::option::Option::None,
6492                    };
6493            };
6494            let post = quote! {
6495                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6496                    let _audit_after:
6497                        ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6498                        ::std::vec![ #( #after_pairs ),* ];
6499                    let _audit_entry = #root::audit::PendingEntry {
6500                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
6501                        entity_pk: #pk_str,
6502                        operation: #root::audit::AuditOp::Update,
6503                        source: #root::audit::current_source(),
6504                        changes: #root::audit::diff_changes(
6505                            &_audit_before,
6506                            &_audit_after,
6507                        ),
6508                    };
6509                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6510                }
6511            };
6512            (pre, post)
6513        }
6514    } else {
6515        (quote!(), quote!())
6516    };
6517
6518    // Bulk-insert audit: capture every row's tracked fields after the
6519    // RETURNING populates each PK, then push one batched INSERT INTO
6520    // audit_log via `emit_many`. One round-trip regardless of N rows.
6521    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6522        let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6523            if fields.pk_is_auto {
6524                quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6525            } else {
6526                quote!(::std::format!("{}", &_row.#pk_ident))
6527            }
6528        } else {
6529            quote!(::std::string::String::new())
6530        };
6531        let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6532            let column_lit = c.column.as_str();
6533            let ident = &c.ident;
6534            quote! {
6535                (
6536                    #column_lit,
6537                    #root::__serde_json::to_value(&_row.#ident)
6538                        .unwrap_or(#root::__serde_json::Value::Null),
6539                )
6540            }
6541        });
6542        quote! {
6543            let _audit_source = #root::audit::current_source();
6544            let mut _audit_entries:
6545                ::std::vec::Vec<#root::audit::PendingEntry> =
6546                    ::std::vec::Vec::with_capacity(rows.len());
6547            for _row in rows.iter() {
6548                _audit_entries.push(#root::audit::PendingEntry {
6549                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
6550                    entity_pk: #row_pk_str,
6551                    operation: #root::audit::AuditOp::Create,
6552                    source: _audit_source.clone(),
6553                    changes: #root::audit::snapshot_changes(&[
6554                        #( #row_pairs ),*
6555                    ]),
6556                });
6557            }
6558            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6559        }
6560    } else {
6561        quote!()
6562    };
6563
6564    let save_method = if fields.pk_is_auto {
6565        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6566        let pk_column_lit = pk_column.as_str();
6567        let assignments = &fields.update_assignments;
6568        let upsert_cols = &fields.upsert_update_columns;
6569        let upsert_pushes = &fields.insert_pushes;
6570        let upsert_returning = &fields.returning_cols;
6571        let upsert_auto_assigns = &fields.auto_assigns;
6572        // Conflict target: prefer the first declared `unique_together`
6573        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6574        // `BIGSERIAL` and never collide on insert, so a PK-only target
6575        // would silently turn `upsert()` into "always-insert" for
6576        // surrogate-PK models with composite UNIQUE constraints — see
6577        // `RolePermission` / `UserRole` / `UserPermission` in the
6578        // tenancy permission engine. When no `unique_together` is
6579        // declared we keep the PK target (the original behaviour).
6580        let upsert_target_columns: Vec<String> = indexes
6581            .iter()
6582            .find(|i| i.unique && !i.columns.is_empty())
6583            .map(|i| i.columns.clone())
6584            .unwrap_or_else(|| vec![pk_column.clone()]);
6585        let upsert_target_lits = upsert_target_columns
6586            .iter()
6587            .map(String::as_str)
6588            .collect::<Vec<_>>();
6589        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6590            quote!(#root::core::ConflictClause::DoNothing)
6591        } else {
6592            quote!(#root::core::ConflictClause::DoUpdate {
6593                target: ::std::vec![ #( #upsert_target_lits ),* ],
6594                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6595            })
6596        };
6597        Some(quote! {
6598            /// Insert this row if its `Auto<T>` primary key is
6599            /// `Unset`, otherwise update the existing row matching the
6600            /// PK. The caller doesn't need to pick `insert` vs the
6601            /// bulk-update path manually.
6602            ///
6603            /// On the insert branch, populates the PK from `RETURNING`
6604            /// (same behavior as `insert`). On the update branch,
6605            /// writes every non-PK column back; if no row matches the
6606            /// PK, returns `Ok(())` silently.
6607            ///
6608            /// Only generated when the primary key is declared as
6609            /// `Auto<T>`. Models with a manually-managed PK must use
6610            /// `insert` or the QuerySet update builder.
6611            ///
6612            /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6613            /// driver-specific `PgPool`, so it does not exist at all on a
6614            /// build without the `postgres` feature: selecting `sqlite`
6615            /// makes the method vanish rather than fail with anything that
6616            /// names the cause.
6617            ///
6618            /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6619            /// all three backends. The naming is inverted — the short name
6620            /// is the narrow one — and that is tracked in #1293.
6621            ///
6622            /// # Errors
6623            /// Returns [`#root::sql::ExecError`] for SQL-writing
6624            /// or driver failures.
6625            #[cfg(feature = "postgres")]
6626            pub async fn save(
6627                &mut self,
6628                pool: &#root::sql::sqlx::PgPool,
6629            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6630                #pool_to_save_on
6631            }
6632
6633            /// Like [`Self::save`] but accepts any sqlx executor —
6634            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6635            /// escape hatch for tenant-scoped writes: schema-mode
6636            /// tenants share the registry pool but rely on a per-
6637            /// checkout `SET search_path`, so passing `&PgPool` would
6638            /// silently hit the wrong schema. Acquire a connection
6639            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6640            ///
6641            /// # Errors
6642            /// As [`Self::save`].
6643            #[cfg(feature = "postgres")]
6644            pub async fn save_on #executor_generics (
6645                &mut self,
6646                #executor_param,
6647            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6648            #executor_where
6649            {
6650                // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6651                // the rows-affected count (0 when the PK no longer exists).
6652                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6653                    return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6654                }
6655                #audit_update_pre
6656                let _query = #root::core::UpdateQuery::new(
6657                    <Self as #root::core::Model>::SCHEMA,
6658                    ::std::vec![ #( #assignments ),* ],
6659                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6660                        #pk_column_lit,
6661                        #root::core::Op::Eq,
6662                        ::core::convert::Into::<#root::core::SqlValue>::into(
6663                            ::core::clone::Clone::clone(&self.#pk_ident)
6664                        ),
6665                    )),
6666                );
6667                let _affected = #root::sql::__macro_internals::update_on(
6668                    #executor_passes_to_data_write,
6669                    &_query,
6670                ).await?;
6671                #audit_update_post
6672                ::core::result::Result::Ok(_affected)
6673            }
6674
6675            /// Per-call override for the audit source. Runs
6676            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6677            /// scope so the resulting audit entry records `source`
6678            /// instead of the task-local default. Useful for seed
6679            /// scripts and one-off CLI tools that don't sit inside an
6680            /// admin handler. The override applies only to this call;
6681            /// no global state changes.
6682            ///
6683            /// # Errors
6684            /// As [`Self::save_on`].
6685            #[cfg(feature = "postgres")]
6686            pub async fn save_on_with #executor_generics (
6687                &mut self,
6688                #executor_param,
6689                source: #root::audit::AuditSource,
6690            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6691            #executor_where
6692            {
6693                #root::audit::with_source(source, self.save_on(_executor)).await
6694            }
6695
6696            /// Insert this row or update it in-place if the primary key already
6697            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6698            ///
6699            /// With `Auto::Unset` PK the server assigns a new key and no conflict
6700            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6701            /// inserted if absent or all non-PK columns are overwritten if present.
6702            ///
6703            /// # Errors
6704            /// As [`Self::insert_on`].
6705            #[cfg(feature = "postgres")]
6706            pub async fn upsert(
6707                &mut self,
6708                pool: &#root::sql::sqlx::PgPool,
6709            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6710                #pool_to_upsert_on
6711            }
6712
6713            /// Like [`Self::upsert`] but accepts any sqlx executor.
6714            /// See [`Self::save_on`] for tenancy-scoped rationale.
6715            ///
6716            /// # Errors
6717            /// As [`Self::upsert`].
6718            #[cfg(feature = "postgres")]
6719            pub async fn upsert_on #executor_generics (
6720                &mut self,
6721                #executor_param,
6722            ) -> ::core::result::Result<(), #root::sql::ExecError>
6723            #executor_where
6724            {
6725                let mut _columns: ::std::vec::Vec<&'static str> =
6726                    ::std::vec::Vec::new();
6727                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6728                    ::std::vec::Vec::new();
6729                #( #upsert_pushes )*
6730                let query = #root::core::InsertQuery::new(
6731                    <Self as #root::core::Model>::SCHEMA,
6732                    _columns,
6733                    _values,
6734                )
6735                .returning(::std::vec![ #( #upsert_returning ),* ])
6736                .on_conflict(#conflict_clause);
6737                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6738                    #executor_passes_to_data_write,
6739                    &query,
6740                ).await?;
6741                let _returning_row = &_returning_row_v;
6742                #( #upsert_auto_assigns )*
6743                ::core::result::Result::Ok(())
6744            }
6745        })
6746    } else {
6747        None
6748    };
6749
6750    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6751        let pk_column_lit = pk_column.as_str();
6752        // Optional `soft_delete_on` / `restore_on` companions when the
6753        // model has a `#[rustango(soft_delete)]` column. They land
6754        // alongside the regular `delete_on` so callers have both
6755        // options — a hard delete (audit-tracked as a real DELETE) and
6756        // a logical delete (audit-tracked as an UPDATE setting the
6757        // deleted_at column to NOW()).
6758        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6759            let col_lit = col;
6760            let sd_field_ident = fields
6761                .soft_delete_field_ident
6762                .clone()
6763                .expect("soft_delete_column without ident");
6764            quote! {
6765                /// Soft-delete this row by setting its
6766                /// `#[rustango(soft_delete)]` column to `NOW()`.
6767                /// The row stays in the table; query helpers can
6768                /// filter it out by checking the column for `IS NOT
6769                /// NULL`.
6770                ///
6771                /// # Errors
6772                /// As [`Self::delete`].
6773                #[cfg(feature = "postgres")]
6774                pub async fn soft_delete_on #executor_generics (
6775                    &self,
6776                    #executor_param,
6777                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6778                #executor_where
6779                {
6780                    let _now = #root::__chrono::Utc::now();
6781                    let _query = #root::core::UpdateQuery::new(
6782                        <Self as #root::core::Model>::SCHEMA,
6783                        ::std::vec![#root::core::Assignment::new(
6784                            #col_lit,
6785                            ::core::convert::Into::<#root::core::SqlValue>::into(_now),
6786                        )],
6787                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6788                            #pk_column_lit,
6789                            #root::core::Op::Eq,
6790                            ::core::convert::Into::<#root::core::SqlValue>::into(
6791                                ::core::clone::Clone::clone(&self.#pk_ident)
6792                            ),
6793                        )),
6794                    );
6795                    let _affected = #root::sql::__macro_internals::update_on(
6796                        #executor_passes_to_data_write,
6797                        &_query,
6798                    ).await?;
6799                    #audit_softdelete_emit
6800                    ::core::result::Result::Ok(_affected)
6801                }
6802
6803                /// Inverse of [`Self::soft_delete_on`] — clears the
6804                /// soft-delete column back to NULL so the row is
6805                /// considered live again.
6806                ///
6807                /// # Errors
6808                /// As [`Self::delete`].
6809                #[cfg(feature = "postgres")]
6810                pub async fn restore_on #executor_generics (
6811                    &self,
6812                    #executor_param,
6813                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6814                #executor_where
6815                {
6816                    let _query = #root::core::UpdateQuery::new(
6817                        <Self as #root::core::Model>::SCHEMA,
6818                        ::std::vec![#root::core::Assignment::new(
6819                            #col_lit,
6820                            #root::core::SqlValue::Null,
6821                        )],
6822                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6823                            #pk_column_lit,
6824                            #root::core::Op::Eq,
6825                            ::core::convert::Into::<#root::core::SqlValue>::into(
6826                                ::core::clone::Clone::clone(&self.#pk_ident)
6827                            ),
6828                        )),
6829                    );
6830                    let _affected = #root::sql::__macro_internals::update_on(
6831                        #executor_passes_to_data_write,
6832                        &_query,
6833                    ).await?;
6834                    #audit_restore_emit
6835                    ::core::result::Result::Ok(_affected)
6836                }
6837
6838                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6839                /// — takes [`#root::sql::Pool`] and dispatches per
6840                /// backend. Eloquent `Model::delete()` semantics on
6841                /// soft-delete-enabled models (closes #821 partial).
6842                ///
6843                /// Sets the `#[rustango(soft_delete)]` column to
6844                /// `NOW()` on every backend. Query helpers
6845                /// (`QuerySet::active()` / `only_trashed()`,
6846                /// `soft_delete::active_filter` /
6847                /// `compose_with_active`) filter trashed rows out by
6848                /// reading `IS NULL` on the same column.
6849                ///
6850                /// # Errors
6851                /// As [`#root::sql::update_pool`].
6852                pub async fn soft_delete(
6853                    &self,
6854                    pool: &#root::sql::Pool,
6855                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6856                    let _now = #root::__chrono::Utc::now();
6857                    let _query = #root::core::UpdateQuery::new(
6858                        <Self as #root::core::Model>::SCHEMA,
6859                        ::std::vec![#root::core::Assignment::new(
6860                            #col_lit,
6861                            ::core::convert::Into::<#root::core::SqlValue>::into(_now),
6862                        )],
6863                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6864                            #pk_column_lit,
6865                            #root::core::Op::Eq,
6866                            ::core::convert::Into::<#root::core::SqlValue>::into(
6867                                ::core::clone::Clone::clone(&self.#pk_ident)
6868                            ),
6869                        )),
6870                    );
6871                    #pool_softdelete_run
6872                }
6873
6874                /// Tri-dialect counterpart of [`Self::restore_on`].
6875                /// Clears the `#[rustango(soft_delete)]` column back
6876                /// to `NULL`, marking the row live again. Eloquent
6877                /// `Model::restore()` parity.
6878                ///
6879                /// # Errors
6880                /// As [`#root::sql::update_pool`].
6881                pub async fn restore(
6882                    &self,
6883                    pool: &#root::sql::Pool,
6884                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6885                    let _query = #root::core::UpdateQuery::new(
6886                        <Self as #root::core::Model>::SCHEMA,
6887                        ::std::vec![#root::core::Assignment::new(
6888                            #col_lit,
6889                            #root::core::SqlValue::Null,
6890                        )],
6891                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6892                            #pk_column_lit,
6893                            #root::core::Op::Eq,
6894                            ::core::convert::Into::<#root::core::SqlValue>::into(
6895                                ::core::clone::Clone::clone(&self.#pk_ident)
6896                            ),
6897                        )),
6898                    );
6899                    #pool_restore_run
6900                }
6901
6902                /// Hard-delete this row, ignoring the soft-delete
6903                /// column. Eloquent `Model::forceDelete()` parity —
6904                /// the escape hatch when you need to actually purge
6905                /// data (GDPR, fixture cleanup, etc.).
6906                ///
6907                /// Equivalent to [`Self::delete_pool`] (the framework's
6908                /// non-soft delete) — exposed under the Eloquent name
6909                /// for muscle-memory + so soft-delete-enabled models
6910                /// have all three operations (soft / restore / force)
6911                /// in one place.
6912                ///
6913                /// # Errors
6914                /// As [`Self::delete_pool`].
6915                pub async fn force_delete(
6916                    &self,
6917                    pool: &#root::sql::Pool,
6918                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6919                    Self::delete_pool(self, pool).await
6920                }
6921
6922                /// Returns `true` when this row is soft-deleted (its
6923                /// `#[rustango(soft_delete)]` column is currently
6924                /// set — Eloquent `$model->trashed()` parity).
6925                ///
6926                /// Pure in-memory predicate over `&self`; does not
6927                /// hit the database. Useful in admin/template code
6928                /// like `{% if post.trashed() %}…{% endif %}` and in
6929                /// guard clauses on restore/force-delete flows.
6930                pub fn trashed(&self) -> bool {
6931                    ::core::option::Option::is_some(&self.#sd_field_ident)
6932                }
6933
6934                /// Fetch every row whose `#[rustango(soft_delete)]`
6935                /// column is `NULL` (a.k.a. the live, non-trashed
6936                /// rows). Eloquent's default `Model::all()` behavior on
6937                /// a soft-delete model (Eloquent auto-scopes trashed
6938                /// rows out; rustango doesn't have auto-scoping yet —
6939                /// see issue #820 — so this is the explicit shortcut).
6940                ///
6941                /// One-liner over `QuerySet::<Self>::default()
6942                /// .active().fetch(pool)`. Closes #821 partial.
6943                ///
6944                /// # Errors
6945                /// As [`#root::sql::FetcherPool::fetch`].
6946                pub async fn active(
6947                    pool: &#root::sql::Pool,
6948                ) -> ::core::result::Result<
6949                    ::std::vec::Vec<Self>,
6950                    #root::sql::ExecError,
6951                > {
6952                    use #root::sql::FetcherPool as _;
6953                    #root::query::QuerySet::<Self>::default()
6954                        .active()
6955                        .fetch(pool)
6956                        .await
6957                }
6958
6959                /// Fetch ONLY soft-deleted rows. Eloquent
6960                /// `Model::onlyTrashed()->get()` parity — drives the
6961                /// admin "Trash" page, restore flows, GDPR purge
6962                /// scans, etc.
6963                ///
6964                /// One-liner over `QuerySet::<Self>::default()
6965                /// .only_trashed().fetch(pool)`. Closes #821
6966                /// partial.
6967                ///
6968                /// # Errors
6969                /// As [`#root::sql::FetcherPool::fetch`].
6970                pub async fn only_trashed(
6971                    pool: &#root::sql::Pool,
6972                ) -> ::core::result::Result<
6973                    ::std::vec::Vec<Self>,
6974                    #root::sql::ExecError,
6975                > {
6976                    use #root::sql::FetcherPool as _;
6977                    #root::query::QuerySet::<Self>::default()
6978                        .only_trashed()
6979                        .fetch(pool)
6980                        .await
6981                }
6982
6983                /// Fetch every row, both live and soft-deleted.
6984                /// Eloquent `Model::withTrashed()->get()` parity.
6985                ///
6986                /// Today every queryset already includes trashed rows
6987                /// (rustango has no global-scope tracking yet — issue
6988                /// #820), so this is functionally equivalent to
6989                /// [`Self::all`]. Exposed as a named shortcut so
6990                /// soft-delete-aware code reads `Model::with_trashed_pool`
6991                /// rather than `Model::all_pool` — keeps intent visible
6992                /// in callers and stays correct when auto-scoping lands.
6993                ///
6994                /// Closes #821 partial.
6995                ///
6996                /// # Errors
6997                /// As [`#root::sql::FetcherPool::fetch`].
6998                pub async fn with_trashed(
6999                    pool: &#root::sql::Pool,
7000                ) -> ::core::result::Result<
7001                    ::std::vec::Vec<Self>,
7002                    #root::sql::ExecError,
7003                > {
7004                    use #root::sql::FetcherPool as _;
7005                    #root::query::QuerySet::<Self>::default()
7006                        .with_trashed()
7007                        .fetch(pool)
7008                        .await
7009                }
7010
7011            }
7012        } else {
7013            quote!()
7014        };
7015        quote! {
7016            /// Delete the row identified by this instance's primary key.
7017            ///
7018            /// Returns the number of rows affected (0 or 1).
7019            ///
7020            /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7021            /// a driver-specific `PgPool`, so it does not exist on a build
7022            /// without the `postgres` feature. [`Self::delete_pool`] takes
7023            /// `rustango::sql::Pool` and works on all three. The naming is
7024            /// inverted; tracked in #1293.
7025            ///
7026            /// # Errors
7027            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7028            /// driver failures.
7029            #[cfg(feature = "postgres")]
7030            pub async fn delete(
7031                &self,
7032                pool: &#root::sql::sqlx::PgPool,
7033            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7034                #pool_to_delete_on
7035            }
7036
7037            /// Like [`Self::delete`] but accepts any sqlx executor —
7038            /// for tenant-scoped deletes against an explicitly-acquired
7039            /// connection. See [`Self::save_on`] for the rationale.
7040            ///
7041            /// # Errors
7042            /// As [`Self::delete`].
7043            #[cfg(feature = "postgres")]
7044            pub async fn delete_on #executor_generics (
7045                &self,
7046                #executor_param,
7047            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7048            #executor_where
7049            {
7050                let query = #root::core::DeleteQuery::by_pk(
7051                    <Self as #root::core::Model>::SCHEMA,
7052                    #pk_column_lit,
7053                    ::core::convert::Into::<#root::core::SqlValue>::into(
7054                        ::core::clone::Clone::clone(&self.#pk_ident)
7055                    ),
7056                );
7057                let _affected = #root::sql::__macro_internals::delete_on(
7058                    #executor_passes_to_data_write,
7059                    &query,
7060                ).await?;
7061                #audit_delete_emit
7062                ::core::result::Result::Ok(_affected)
7063            }
7064
7065            /// Per-call audit-source override for [`Self::delete_on`].
7066            /// See [`Self::save_on_with`] for shape rationale.
7067            ///
7068            /// # Errors
7069            /// As [`Self::delete_on`].
7070            #[cfg(feature = "postgres")]
7071            pub async fn delete_on_with #executor_generics (
7072                &self,
7073                #executor_param,
7074                source: #root::audit::AuditSource,
7075            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7076            #executor_where
7077            {
7078                #root::audit::with_source(source, self.delete_on(_executor)).await
7079            }
7080            #pool_delete_method
7081            #pool_insert_method
7082            #audit_entry_method
7083            #pool_save_method
7084            #refresh_replicate_methods
7085            #tx_delete_method
7086            #tx_insert_method
7087            #tx_save_method
7088            #soft_delete_methods
7089
7090            /// Returns `true` when `other` represents the same DB
7091            /// row as `self` — i.e. their primary keys compare
7092            /// equal. Eloquent `$model->is($other)` parity.
7093            ///
7094            /// Because both arguments are typed `&Self`, the
7095            /// model/table check is automatic — `Post::is` cannot
7096            /// be invoked against a `Comment` at compile time. Only
7097            /// the PK has to be compared at runtime.
7098            pub fn is(&self, other: &Self) -> bool {
7099                self.#pk_ident == other.#pk_ident
7100            }
7101
7102            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7103            /// parity.
7104            pub fn is_not(&self, other: &Self) -> bool {
7105                self.#pk_ident != other.#pk_ident
7106            }
7107
7108            /// Returns this row's primary-key value as an
7109            /// [`#root::core::SqlValue`]. Eloquent
7110            /// `$model->getKey()` parity.
7111            ///
7112            /// Useful when you need to thread the PK through a
7113            /// generic `Into<SqlValue>`-bound API without knowing the
7114            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7115            #[must_use]
7116            pub fn get_key(&self) -> #root::core::SqlValue {
7117                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7118            }
7119
7120        }
7121    });
7122
7123    let insert_method = if fields.has_auto {
7124        let pushes = &fields.insert_pushes;
7125        let returning_cols = &fields.returning_cols;
7126        let auto_assigns = &fields.auto_assigns;
7127        quote! {
7128            /// Insert this row into its table. Skips columns whose
7129            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7130            /// sequence fills them in, then reads each `Auto` column
7131            /// back via `RETURNING` and stores it on `self`.
7132            ///
7133            /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7134            /// a driver-specific `PgPool`, so it does not exist on a build
7135            /// without the `postgres` feature. [`Self::insert_pool`] takes
7136            /// `rustango::sql::Pool` and works on all three. The naming is
7137            /// inverted; tracked in #1293.
7138            ///
7139            /// # Errors
7140            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7141            /// driver failures.
7142            #[cfg(feature = "postgres")]
7143            pub async fn insert(
7144                &mut self,
7145                pool: &#root::sql::sqlx::PgPool,
7146            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7147                #pool_to_insert_on
7148            }
7149
7150            /// Like [`Self::insert`] but accepts any sqlx executor.
7151            /// See [`Self::save_on`] for tenancy-scoped rationale.
7152            ///
7153            /// # Errors
7154            /// As [`Self::insert`].
7155            #[cfg(feature = "postgres")]
7156            pub async fn insert_on #executor_generics (
7157                &mut self,
7158                #executor_param,
7159            ) -> ::core::result::Result<(), #root::sql::ExecError>
7160            #executor_where
7161            {
7162                let mut _columns: ::std::vec::Vec<&'static str> =
7163                    ::std::vec::Vec::new();
7164                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7165                    ::std::vec::Vec::new();
7166                #( #pushes )*
7167                let query = #root::core::InsertQuery::new(
7168                    <Self as #root::core::Model>::SCHEMA,
7169                    _columns,
7170                    _values,
7171                )
7172                .returning(::std::vec![ #( #returning_cols ),* ]);
7173                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7174                    #executor_passes_to_data_write,
7175                    &query,
7176                ).await?;
7177                let _returning_row = &_returning_row_v;
7178                #( #auto_assigns )*
7179                #audit_insert_emit
7180                ::core::result::Result::Ok(())
7181            }
7182
7183            /// Per-call audit-source override for [`Self::insert_on`].
7184            /// See [`Self::save_on_with`] for shape rationale.
7185            ///
7186            /// # Errors
7187            /// As [`Self::insert_on`].
7188            #[cfg(feature = "postgres")]
7189            pub async fn insert_on_with #executor_generics (
7190                &mut self,
7191                #executor_param,
7192                source: #root::audit::AuditSource,
7193            ) -> ::core::result::Result<(), #root::sql::ExecError>
7194            #executor_where
7195            {
7196                #root::audit::with_source(source, self.insert_on(_executor)).await
7197            }
7198        }
7199    } else {
7200        let insert_columns = &fields.insert_columns;
7201        let insert_values = &fields.insert_values;
7202        quote! {
7203            /// Insert this row into its table.
7204            ///
7205            /// # Errors
7206            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7207            /// driver failures.
7208            #[cfg(feature = "postgres")]
7209            pub async fn insert(
7210                &self,
7211                pool: &#root::sql::sqlx::PgPool,
7212            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7213                self.insert_on(pool).await
7214            }
7215
7216            /// Like [`Self::insert`] but accepts any sqlx executor.
7217            /// See [`Self::save_on`] for tenancy-scoped rationale.
7218            ///
7219            /// # Errors
7220            /// As [`Self::insert`].
7221            #[cfg(feature = "postgres")]
7222            pub async fn insert_on<'_c, _E>(
7223                &self,
7224                _executor: _E,
7225            ) -> ::core::result::Result<(), #root::sql::ExecError>
7226            where
7227                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7228            {
7229                let query = #root::core::InsertQuery::new(
7230                    <Self as #root::core::Model>::SCHEMA,
7231                    ::std::vec![ #( #insert_columns ),* ],
7232                    ::std::vec![ #( #insert_values ),* ],
7233                );
7234                #root::sql::__macro_internals::insert_on(_executor, &query).await
7235            }
7236        }
7237    };
7238
7239    let bulk_insert_method = if fields.has_auto {
7240        let cols_no_auto = &fields.bulk_columns_no_auto;
7241        let cols_all = &fields.bulk_columns_all;
7242        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7243        let pushes_all = &fields.bulk_pushes_all;
7244        let returning_cols = &fields.returning_cols;
7245        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7246        let uniformity = &fields.bulk_auto_uniformity;
7247        let first_auto_ident = fields
7248            .first_auto_ident
7249            .as_ref()
7250            .expect("has_auto implies first_auto_ident is Some");
7251        quote! {
7252            /// Bulk-insert `rows` in a single round-trip. Every row's
7253            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7254            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7255            /// (caller-supplied values). Mixed Set/Unset is rejected
7256            /// — call `insert` per row for that case.
7257            ///
7258            /// Empty slice is a no-op. Each row's `Auto` fields are
7259            /// populated from the `RETURNING` clause in input order
7260            /// before this returns.
7261            ///
7262            /// # Errors
7263            /// Returns [`#root::sql::ExecError`] for validation,
7264            /// SQL-writing, mixed-Auto rejection, or driver failures.
7265            #[cfg(feature = "postgres")]
7266            pub async fn bulk_insert(
7267                rows: &mut [Self],
7268                pool: &#root::sql::sqlx::PgPool,
7269            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7270                #pool_to_bulk_insert_on
7271            }
7272
7273            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7274            /// See [`Self::save_on`] for tenancy-scoped rationale.
7275            ///
7276            /// # Errors
7277            /// As [`Self::bulk_insert`].
7278            #[cfg(feature = "postgres")]
7279            pub async fn bulk_insert_on #executor_generics (
7280                rows: &mut [Self],
7281                #executor_param,
7282            ) -> ::core::result::Result<(), #root::sql::ExecError>
7283            #executor_where
7284            {
7285                if rows.is_empty() {
7286                    return ::core::result::Result::Ok(());
7287                }
7288                let _first_unset = matches!(
7289                    rows[0].#first_auto_ident,
7290                    #root::sql::Auto::Unset
7291                );
7292                #( #uniformity )*
7293
7294                let mut _all_rows: ::std::vec::Vec<
7295                    ::std::vec::Vec<#root::core::SqlValue>,
7296                > = ::std::vec::Vec::with_capacity(rows.len());
7297                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7298                    for _row in rows.iter() {
7299                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7300                            ::std::vec::Vec::new();
7301                        #( #pushes_no_auto )*
7302                        _all_rows.push(_row_vals);
7303                    }
7304                    ::std::vec![ #( #cols_no_auto ),* ]
7305                } else {
7306                    for _row in rows.iter() {
7307                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7308                            ::std::vec::Vec::new();
7309                        #( #pushes_all )*
7310                        _all_rows.push(_row_vals);
7311                    }
7312                    ::std::vec![ #( #cols_all ),* ]
7313                };
7314
7315                let _query = #root::core::BulkInsertQuery::new(
7316                    <Self as #root::core::Model>::SCHEMA,
7317                    _columns,
7318                    _all_rows,
7319                )
7320                .returning(::std::vec![ #( #returning_cols ),* ]);
7321                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7322                    #executor_passes_to_data_write,
7323                    &_query,
7324                ).await?;
7325                if _returned.len() != rows.len() {
7326                    return ::core::result::Result::Err(
7327                        #root::sql::ExecError::Sql(
7328                            #root::sql::SqlError::BulkInsertReturningMismatch {
7329                                expected: rows.len(),
7330                                actual: _returned.len(),
7331                            }
7332                        )
7333                    );
7334                }
7335                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7336                    #auto_assigns_for_row
7337                }
7338                #audit_bulk_insert_emit
7339                ::core::result::Result::Ok(())
7340            }
7341        }
7342    } else {
7343        let cols_all = &fields.bulk_columns_all;
7344        let pushes_all = &fields.bulk_pushes_all;
7345        quote! {
7346            /// Bulk-insert `rows` in a single round-trip. Every row's
7347            /// fields are written verbatim — there are no `Auto<T>`
7348            /// fields on this model.
7349            ///
7350            /// Empty slice is a no-op.
7351            ///
7352            /// # Errors
7353            /// Returns [`#root::sql::ExecError`] for validation,
7354            /// SQL-writing, or driver failures.
7355            #[cfg(feature = "postgres")]
7356            pub async fn bulk_insert(
7357                rows: &[Self],
7358                pool: &#root::sql::sqlx::PgPool,
7359            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7360                Self::bulk_insert_on(rows, pool).await
7361            }
7362
7363            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7364            /// See [`Self::save_on`] for tenancy-scoped rationale.
7365            ///
7366            /// # Errors
7367            /// As [`Self::bulk_insert`].
7368            #[cfg(feature = "postgres")]
7369            pub async fn bulk_insert_on<'_c, _E>(
7370                rows: &[Self],
7371                _executor: _E,
7372            ) -> ::core::result::Result<(), #root::sql::ExecError>
7373            where
7374                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7375            {
7376                if rows.is_empty() {
7377                    return ::core::result::Result::Ok(());
7378                }
7379                let mut _all_rows: ::std::vec::Vec<
7380                    ::std::vec::Vec<#root::core::SqlValue>,
7381                > = ::std::vec::Vec::with_capacity(rows.len());
7382                for _row in rows.iter() {
7383                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7384                        ::std::vec::Vec::new();
7385                    #( #pushes_all )*
7386                    _all_rows.push(_row_vals);
7387                }
7388                let _query = #root::core::BulkInsertQuery::new(
7389                    <Self as #root::core::Model>::SCHEMA,
7390                    ::std::vec![ #( #cols_all ),* ],
7391                    _all_rows,
7392                );
7393                let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7394                ::core::result::Result::Ok(())
7395            }
7396        }
7397    };
7398
7399    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7400    // (no postgres-feature gate); routes through the existing
7401    // `bulk_insert_pool` + per-dialect conflict writer.
7402    //
7403    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7404    // sequence picks the PK for fresh inserts; the UPDATE branch never
7405    // touches the Auto column.
7406    let bulk_upsert_pool_method = {
7407        // Pick the "no Auto" columns when the model has Auto fields,
7408        // else every column.
7409        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7410            (
7411                fields.bulk_columns_no_auto.clone(),
7412                fields.bulk_pushes_no_auto.clone(),
7413            )
7414        } else {
7415            (
7416                fields.bulk_columns_all.clone(),
7417                fields.bulk_pushes_all.clone(),
7418            )
7419        };
7420        quote! {
7421            /// Tri-dialect bulk insert that updates on conflict — the
7422            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7423            ///
7424            /// Per-row values are extracted and lowered into a
7425            /// [`#root::core::BulkInsertQuery`] with
7426            /// `on_conflict = DoUpdate { target, update_columns }`. The
7427            /// writer dispatches per-dialect:
7428            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7429            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7430            ///
7431            /// `target` names the column(s) whose unique constraint
7432            /// defines the conflict (typically a `unique` or
7433            /// `unique_together` natural-key column, NOT the `Auto<T>`
7434            /// PK). `update_cols` names the columns to overwrite on
7435            /// conflict — every other column is left untouched on the
7436            /// existing row.
7437            ///
7438            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7439            /// picks the PK on insert; the update path never touches
7440            /// the Auto column). Auto-set rows trigger a hard error.
7441            /// Empty slice is a no-op.
7442            ///
7443            /// # Errors
7444            /// Returns [`#root::sql::ExecError`] for validation,
7445            /// SQL-writing, or driver failures.
7446            pub async fn bulk_upsert_pool(
7447                rows: &[Self],
7448                target: &[&'static str],
7449                update_cols: &[&'static str],
7450                pool: &#root::sql::Pool,
7451            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7452                if rows.is_empty() {
7453                    return ::core::result::Result::Ok(());
7454                }
7455                let mut _all_rows: ::std::vec::Vec<
7456                    ::std::vec::Vec<#root::core::SqlValue>,
7457                > = ::std::vec::Vec::with_capacity(rows.len());
7458                for _row in rows.iter() {
7459                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7460                        ::std::vec::Vec::new();
7461                    #( #upsert_pushes )*
7462                    _all_rows.push(_row_vals);
7463                }
7464                let _query = #root::core::BulkInsertQuery::new(
7465                    <Self as #root::core::Model>::SCHEMA,
7466                    ::std::vec![ #( #upsert_cols ),* ],
7467                    _all_rows,
7468                )
7469                .on_conflict_do_update(target, update_cols);
7470                #root::sql::bulk_insert_pool(pool, &_query).await
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                #root::sql::bulk_insert_pool(pool, &_query).await
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_method = match &fields.primary_key {
7518        None => quote! {},
7519        Some((pk_ident, pk_col)) => {
7520            // One pair of match arms per non-PK column: a validation arm
7521            // resolving the runtime name to its `&'static str` column,
7522            // and a value arm pushing that field off the row.
7523            let mut col_arms: Vec<TokenStream2> = Vec::new();
7524            let mut val_arms: Vec<TokenStream2> = Vec::new();
7525            for entry in &fields.column_entries {
7526                if &entry.column == pk_col {
7527                    continue;
7528                }
7529                let col = &entry.column;
7530                let ident = &entry.ident;
7531                col_arms.push(quote! { #col => #col, });
7532                val_arms.push(quote! {
7533                    #col => _row_vals.push(
7534                        ::core::convert::Into::<#root::core::SqlValue>::into(
7535                            ::core::clone::Clone::clone(&_o.#ident)
7536                        )
7537                    ),
7538                });
7539            }
7540            quote! {
7541                /// Write per-row-different values for the named `fields`
7542                /// across every object in `objs` in a single statement,
7543                /// matched by primary key.
7544                ///
7545                /// `fields` names the **columns** to update. The primary
7546                /// key identifies each row and cannot itself be updated
7547                /// (pass it and you get
7548                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7549                /// Empty `objs` or `fields` is a no-op returning `0`.
7550                /// Objects whose PK matches no row are simply not updated.
7551                /// Returns the number of rows affected.
7552                ///
7553                /// Tri-dialect: lowers to one
7554                /// [`#root::core::BulkUpdateQuery`] and dispatches
7555                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7556                /// a CTE + correlated subquery on SQLite, an inner
7557                /// `JOIN (VALUES …)` on MySQL.
7558                ///
7559                /// # Errors
7560                /// [`#root::core::QueryError::UnknownField`] for a name
7561                /// that isn't a column on this model,
7562                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7563                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7564                /// SQL-writing / driver failures.
7565                pub async fn bulk_update(
7566                    objs: &[Self],
7567                    fields: &[&str],
7568                    pool: &#root::sql::Pool,
7569                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7570                    if objs.is_empty() || fields.is_empty() {
7571                        return ::core::result::Result::Ok(0);
7572                    }
7573                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7574                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7575                        ::std::vec::Vec::with_capacity(fields.len());
7576                    for &_f in fields {
7577                        let _col: &'static str = match _f {
7578                            #pk_col => {
7579                                return ::core::result::Result::Err(
7580                                    ::core::convert::Into::into(
7581                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7582                                            model: _model_name,
7583                                            field: ::std::string::ToString::to_string(_f),
7584                                        }
7585                                    )
7586                                );
7587                            }
7588                            #( #col_arms )*
7589                            _ => {
7590                                return ::core::result::Result::Err(
7591                                    ::core::convert::Into::into(
7592                                        #root::core::QueryError::UnknownField {
7593                                            model: _model_name,
7594                                            field: ::std::string::ToString::to_string(_f),
7595                                        }
7596                                    )
7597                                );
7598                            }
7599                        };
7600                        // A model whose only field is the primary key emits no
7601                        // non-PK arms, so every arm above diverges and this push
7602                        // is genuinely unreachable — correct, since such a model
7603                        // has nothing to bulk-update. Scoped to the statement
7604                        // rather than the fn so a real unreachability elsewhere
7605                        // in this body still warns. Without it every PK-only
7606                        // model warns in the *user's* crate, which they cannot
7607                        // act on (#1290).
7608                        #[allow(unreachable_code)]
7609                        _update_columns.push(_col);
7610                    }
7611                    let mut _rows: ::std::vec::Vec<
7612                        ::std::vec::Vec<#root::core::SqlValue>,
7613                    > = ::std::vec::Vec::with_capacity(objs.len());
7614                    for _o in objs.iter() {
7615                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7616                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7617                        // PK first — the writers expect `[pk, …update cols]`.
7618                        _row_vals.push(
7619                            ::core::convert::Into::<#root::core::SqlValue>::into(
7620                                ::core::clone::Clone::clone(&_o.#pk_ident)
7621                            )
7622                        );
7623                        for &_f in fields {
7624                            match _f {
7625                                #( #val_arms )*
7626                                // Unreachable: every name was validated
7627                                // against the same arm set above.
7628                                _ => {}
7629                            }
7630                        }
7631                        _rows.push(_row_vals);
7632                    }
7633                    let _query = #root::core::BulkUpdateQuery::new(
7634                        <Self as #root::core::Model>::SCHEMA,
7635                        _update_columns,
7636                        _rows,
7637                    );
7638                    #root::sql::bulk_update_pool(pool, &_query).await
7639                }
7640            }
7641        }
7642    };
7643
7644    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7645        quote! {
7646            /// Hidden runtime accessor for the primary-key value as a
7647            /// [`SqlValue`]. Used by reverse-relation helpers
7648            /// (`<parent>::<child>_set`) emitted from sibling models'
7649            /// FK fields. Not part of the public API.
7650            #[doc(hidden)]
7651            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7652                ::core::convert::Into::<#root::core::SqlValue>::into(
7653                    ::core::clone::Clone::clone(&self.#pk_ident)
7654                )
7655            }
7656        }
7657    });
7658
7659    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7660        quote! {
7661            impl #root::sql::HasPkValue for #struct_name {
7662                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7663                    ::core::convert::Into::<#root::core::SqlValue>::into(
7664                        ::core::clone::Clone::clone(&self.#pk_ident)
7665                    )
7666                }
7667            }
7668        }
7669    });
7670
7671    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7672
7673    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7674    // dispatch to the right per-backend body without the macro emitting
7675    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7676    // so audited models with non-Auto PKs (which still go through
7677    // `insert_one_with_audit` → `apply_auto_pk`) link.
7678    let assign_auto_pk_pool_impl = {
7679        let auto_assigns = &fields.auto_assigns;
7680        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7681        // so the body is structurally identical to `auto_assigns` —
7682        // only the helper name swaps from `try_get_returning` to
7683        // `try_get_returning_sqlite` so the closure typechecks against
7684        // a `SqliteRow` instead of a `PgRow`.
7685        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7686            .auto_field_idents
7687            .iter()
7688            .map(|(ident, column)| {
7689                quote! {
7690                    self.#ident = #root::sql::try_get_returning_sqlite(
7691                        _returning_row, #column
7692                    )?;
7693                }
7694            })
7695            .collect();
7696        // #1028 — decode each `generated_as` column from the same
7697        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7698        // own type drives the decode (no `Auto<T>` wrapper).
7699        let generated_assigns: Vec<TokenStream2> = fields
7700            .generated_field_idents
7701            .iter()
7702            .map(|(ident, column)| {
7703                quote! {
7704                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7705                }
7706            })
7707            .collect();
7708        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7709            .generated_field_idents
7710            .iter()
7711            .map(|(ident, column)| {
7712                quote! {
7713                    self.#ident = #root::sql::try_get_returning_sqlite(
7714                        _returning_row, #column
7715                    )?;
7716                }
7717            })
7718            .collect();
7719        let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7720            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7721            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7722            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7723            // those models can't use AUTO_INCREMENT). The trait is only
7724            // touched on the MySQL arm at runtime, so PG-only consumers
7725            // never see the bound failure.
7726            //
7727            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7728            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7729            // runtime with "multi-column RETURNING". MySQL has no
7730            // multi-column RETURNING semantic and a follow-up SELECT
7731            // would need cross-trait plumbing. Pragmatic shape: succeed
7732            // with the FIRST Auto field populated from LAST_INSERT_ID();
7733            // any other Auto fields stay `Auto::Unset`. Callers that
7734            // need the DB-defaulted timestamp / UUID can re-fetch the
7735            // row by PK after `save_pool`. Fixes the cookbook chapter
7736            // 12 dialect divergence.
7737            let value_ty = fields
7738                .first_auto_value_ty
7739                .as_ref()
7740                .expect("first_auto_value_ty set whenever first_auto_ident is");
7741            quote! {
7742                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7743                    ::rustango_from_mysql_auto_id(_id)?;
7744                self.#first = #root::sql::Auto::Set(_converted);
7745                ::core::result::Result::Ok(())
7746            }
7747        } else {
7748            quote! {
7749                let _ = _id;
7750                ::core::result::Result::Ok(())
7751            }
7752        };
7753        quote! {
7754            impl #root::sql::AssignAutoPkPool for #struct_name {
7755                fn __rustango_assign_from_pg_row(
7756                    &mut self,
7757                    _returning_row: &#root::sql::PgReturningRow,
7758                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7759                    #( #auto_assigns )*
7760                    #( #generated_assigns )*
7761                    ::core::result::Result::Ok(())
7762                }
7763                fn __rustango_assign_from_mysql_id(
7764                    &mut self,
7765                    _id: i64,
7766                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7767                    #mysql_body
7768                }
7769                fn __rustango_assign_from_sqlite_row(
7770                    &mut self,
7771                    _returning_row: &#root::sql::SqliteReturningRow,
7772                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7773                    #( #auto_assigns_sqlite )*
7774                    #( #generated_assigns_sqlite )*
7775                    ::core::result::Result::Ok(())
7776                }
7777            }
7778        }
7779    };
7780
7781    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7782    let aliased_row_helper = quote! {
7783        /// Decode a row's aliased target columns (produced by
7784        /// `select_related`'s LEFT JOIN) into a fresh instance of
7785        /// this model. Reads each column via
7786        /// `format!("{prefix}__{col}")`, matching the alias the
7787        /// SELECT writer emitted. Slice 9.0d.
7788        #[doc(hidden)]
7789        #[cfg(feature = "postgres")]
7790        pub fn __rustango_from_aliased_row(
7791            row: &#root::sql::sqlx::postgres::PgRow,
7792            prefix: &str,
7793        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7794            ::core::result::Result::Ok(Self {
7795                #( #from_aliased_row_inits ),*
7796            })
7797        }
7798    };
7799    // v0.23.0-batch8 — MySQL counterpart, gated through the
7800    // cfg-aware macro_rules so PG-only builds expand to nothing.
7801    let aliased_row_helper_my = quote! {
7802        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7803            #( #from_aliased_row_inits ),*
7804        });
7805    };
7806
7807    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7808    // pattern + cfg gate on the `sqlite` feature.
7809    let aliased_row_helper_sqlite = quote! {
7810        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7811            #( #from_aliased_row_inits ),*
7812        });
7813    };
7814
7815    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7816    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7817    let load_related_impl_sqlite =
7818        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7819
7820    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7821    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7822    // default `Self::objects()`. Each accessor returns a fresh
7823    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7824    // methods the user defined.
7825    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7826        .iter()
7827        .map(|fn_ident| {
7828            let model_name_str = struct_name.to_string();
7829            let fn_name_str = fn_ident.to_string();
7830            let doc = format!(
7831                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7832                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7833                 equivalent to `Self::objects()`. Chains with any \
7834                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7835                 extension methods."
7836            );
7837            quote! {
7838                #[doc = #doc]
7839                #[must_use]
7840                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7841                    #root::query::QuerySet::new()
7842                }
7843            }
7844        })
7845        .collect();
7846
7847    quote! {
7848        impl #struct_name {
7849            /// Start a new `QuerySet` over this model.
7850            #[must_use]
7851            pub fn objects() -> #root::query::QuerySet<#struct_name> {
7852                #root::query::QuerySet::new()
7853            }
7854
7855            /// Eloquent-shape alias of [`Self::objects`]. Returns
7856            /// a fresh `QuerySet<Self>` ready for `.filter()` /
7857            /// `.where_()` / etc. Matches Laravel muscle-memory:
7858            ///
7859            /// ```ignore
7860            /// // Eloquent:    Post::query()->where('published', true)
7861            /// // rustango:    Post::query().filter("published", true)
7862            /// //         or:  Post::objects().filter("published", true)
7863            /// ```
7864            ///
7865            /// Both names point at the same underlying constructor;
7866            /// neither is preferred.
7867            #[must_use]
7868            pub fn query() -> #root::query::QuerySet<#struct_name> {
7869                #root::query::QuerySet::new()
7870            }
7871
7872            #( #extra_manager_fns )*
7873
7874            #insert_method
7875
7876            #bulk_insert_method
7877
7878            #bulk_upsert_pool_method
7879
7880            #bulk_update_method
7881
7882            #save_method
7883
7884            #pk_methods
7885
7886            #pk_value_helper
7887
7888            #aliased_row_helper
7889
7890            #column_consts
7891        }
7892
7893        #aliased_row_helper_my
7894
7895        #aliased_row_helper_sqlite
7896
7897        #load_related_impl
7898
7899        #load_related_impl_my
7900
7901        #load_related_impl_sqlite
7902
7903        #has_pk_value_impl
7904
7905        #fk_pk_access_impl
7906
7907        #assign_auto_pk_pool_impl
7908    }
7909}
7910
7911/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7912/// `auto_assigns` but reading from `_returning_row` and writing to
7913/// `_row_mut` instead of `self`.
7914fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7915    let root = rustango_root();
7916    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7917        let col_lit = column.as_str();
7918        quote! {
7919            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7920                _returning_row,
7921                #col_lit,
7922            )?;
7923        }
7924    });
7925    quote! { #( #lines )* }
7926}
7927
7928/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7929fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7930    let lines = entries.iter().map(|e| {
7931        let ident = &e.ident;
7932        let col_ty = column_type_ident(ident);
7933        quote! {
7934            #[allow(non_upper_case_globals)]
7935            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7936        }
7937    });
7938    quote! { #(#lines)* }
7939}
7940
7941/// Emit a hidden per-model module carrying one zero-sized type per field,
7942/// each with a `Column` impl pointing back at the model.
7943fn column_module_tokens(
7944    module_ident: &syn::Ident,
7945    struct_name: &syn::Ident,
7946    entries: &[ColumnEntry],
7947) -> TokenStream2 {
7948    let root = rustango_root();
7949    let items = entries.iter().map(|e| {
7950        let col_ty = column_type_ident(&e.ident);
7951        let value_ty = &e.value_ty;
7952        let name = &e.name;
7953        let column = &e.column;
7954        let field_type_tokens = &e.field_type_tokens;
7955        quote! {
7956            #[derive(::core::clone::Clone, ::core::marker::Copy)]
7957            pub struct #col_ty;
7958
7959            impl #root::core::Column for #col_ty {
7960                type Model = super::#struct_name;
7961                type Value = #value_ty;
7962                const NAME: &'static str = #name;
7963                const COLUMN: &'static str = #column;
7964                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7965            }
7966        }
7967    });
7968    quote! {
7969        #[doc(hidden)]
7970        #[allow(non_camel_case_types, non_snake_case)]
7971        pub mod #module_ident {
7972            // Re-import the parent scope so field types referencing
7973            // sibling models (e.g. `ForeignKey<Author>`) resolve
7974            // inside this submodule. Without this we'd hit
7975            // `proc_macro_derive_resolution_fallback` warnings.
7976            #[allow(unused_imports)]
7977            use super::*;
7978            #(#items)*
7979        }
7980    }
7981}
7982
7983fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
7984    syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
7985}
7986
7987fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
7988    syn::Ident::new(
7989        &format!("__rustango_cols_{struct_name}"),
7990        struct_name.span(),
7991    )
7992}
7993
7994fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
7995    let root = rustango_root();
7996    // The Postgres impl is always emitted — every rustango build pulls in
7997    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
7998    // routed through `#root::__impl_my_from_row!`, a cfg-gated
7999    // macro_rules whose body collapses to nothing when rustango is built
8000    // without the `mysql` feature. No user-facing feature shim required.
8001    //
8002    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8003    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8004    // back into a comma-separated list inside square brackets. Since each
8005    // entry is already in `field: expr` shape, the existing tokens slot in.
8006    quote! {
8007        #[cfg(feature = "postgres")]
8008        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8009            for #struct_name
8010        {
8011            fn from_row(
8012                row: &'r #root::sql::sqlx::postgres::PgRow,
8013            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8014                ::core::result::Result::Ok(Self {
8015                    #( #from_row_inits ),*
8016                })
8017            }
8018        }
8019
8020        #root::__impl_my_from_row!(#struct_name, |row| {
8021            #( #from_row_inits ),*
8022        });
8023
8024        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8025            #( #from_row_inits ),*
8026        });
8027    }
8028}
8029
8030struct ContainerAttrs {
8031    table: Option<String>,
8032    display: Option<(String, proc_macro2::Span)>,
8033    /// Explicit app label from `#[rustango(app = "blog")]`.
8034    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8035    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8036    /// at runtime — this attribute is the override for cases where
8037    /// the inference is wrong (e.g. a model that conceptually belongs
8038    /// to one app but is physically in another module).
8039    app: Option<String>,
8040    /// Per-model admin knobs from
8041    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8042    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8043    /// admin code falls back to `AdminConfig::DEFAULT`.
8044    admin: Option<AdminAttrs>,
8045    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8046    /// `None` when the model isn't audited — write paths emit no
8047    /// audit entries. When present, single-row writes capture
8048    /// before/after for the listed fields and bulk writes batch
8049    /// snapshots into one INSERT into `rustango_audit_log`.
8050    audit: Option<AuditAttrs>,
8051    /// `true` when `#[rustango(permissions)]` is present. Signals that
8052    /// `auto_create_permissions` should seed the four CRUD codenames for
8053    /// this model.
8054    permissions: bool,
8055    /// Many-to-many relations declared via
8056    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8057    ///                 src = "post_id", dst = "tag_id"))]`.
8058    m2m: Vec<M2MAttr>,
8059    /// Polymorphic M2M relations declared via
8060    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8061    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8062    ///   related_column = "tag_id"))]` (issue #818).
8063    generic_m2m: Vec<GenericM2MAttr>,
8064    /// Composite indexes declared via
8065    /// `#[rustango(index("col1, col2"))]` or
8066    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8067    /// Single-column indexes from `#[rustango(index)]` on fields are
8068    /// accumulated here during field collection.
8069    indexes: Vec<IndexAttr>,
8070    /// Table-level CHECK constraints declared via
8071    /// `#[rustango(check(name = "…", expr = "…"))]`.
8072    checks: Vec<CheckAttr>,
8073    /// Table-level PG `EXCLUDE` constraints declared via
8074    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8075    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8076    /// migration writer renders them on Postgres and skips with a
8077    /// warning on MySQL/SQLite. Issue #319.
8078    excludes: Vec<ExcludeAttr>,
8079    /// Composite (multi-column) FKs declared via
8080    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8081    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8082    composite_fks: Vec<CompositeFkAttr>,
8083    /// Generic ("any model") FKs declared via
8084    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8085    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8086    generic_fks: Vec<GenericFkAttr>,
8087    /// Where this model lives in a tenancy deployment, declared via
8088    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8089    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8090    /// to partition diff output between registry-scoped and
8091    /// tenant-scoped migration files.
8092    scope: Option<String>,
8093    /// Custom-Manager extension-trait name from
8094    /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8095    /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8096    /// adjacent to the model so users can write
8097    /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8098    /// and discover the convention from the model definition.
8099    manager_ext: Option<syn::Ident>,
8100    /// Extra QuerySet accessor names from
8101    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8102    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8103    /// the default `Self::objects()`. Multiple attributes allowed.
8104    manager_fns: Vec<syn::Ident>,
8105    /// Default ordering declared via `#[rustango(default_order =
8106    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8107    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8108    /// `-` prefix means descending; the `+` prefix or no prefix means
8109    /// ascending.
8110    default_order: Vec<(String, bool, proc_macro2::Span)>,
8111    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8112    /// Routes the emitted schema's `is_view = true` so the migration
8113    /// snapshot skips this model (its underlying SQL view is operator-
8114    /// managed, not rustango-managed).
8115    is_view: bool,
8116    /// `#[rustango(managed = false)]` — who owns the table.
8117    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8118    /// the migration snapshot skips this model so `makemigrations` /
8119    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8120    /// TABLE` against it (operator-managed schema).
8121    managed: bool,
8122    /// Name of the model's base manager, from
8123    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8124    /// `ModelSchema::base_manager_name`. Declarative-only today.
8125    base_manager_name: Option<String>,
8126    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8127    /// field this model's instances are ordered relative to.
8128    /// Declarative-only today; threaded onto
8129    /// `ModelSchema::order_with_respect_to`.
8130    order_with_respect_to: Option<String>,
8131    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8132    /// Marks the model as a proxy that
8133    /// shares its DB table with another struct. Threaded into
8134    /// `ModelSchema::proxy` so future codegen can skip table-owning
8135    /// behavior for proxies.
8136    proxy: bool,
8137    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8138    /// Each comma-separated capability token surfaces on
8139    /// `ModelSchema::required_db_features` so `manage check --deploy`
8140    /// can warn when the active dialect lacks one.
8141    required_db_features: Vec<String>,
8142    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8143    /// Normalized to the dialect name `manage check --deploy`
8144    /// compares against `Settings.database.backend`. Aliases
8145    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8146    /// stored under the canonical name.
8147    required_db_vendor: Option<String>,
8148    /// `#[rustango(default_related_name = "...")]`. Threaded into
8149    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8150    /// name to use when an FK / M2M field doesn't override it.
8151    /// Today rustango doesn't auto-emit reverse managers; the
8152    /// metadata is the foundation for that work.
8153    default_related_name: Option<String>,
8154    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8155    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8156    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8157    /// inline `COMMENT='...'`, SQLite: no-op).
8158    db_table_comment: Option<String>,
8159    /// `#[rustango(get_latest_by = "created_at")]` /
8160    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8161    /// `(column, descending)` where `descending = true` when the
8162    /// attribute value starts with `-`. Threaded into
8163    /// `ModelSchema::get_latest_by`.
8164    get_latest_by: Option<(String, bool)>,
8165    /// Extra `(codename, label)` permissions
8166    /// from `#[rustango(extra_permissions = "approve:Can approve,
8167    /// archive:Can archive")]`. Comma-separated `codename:label`
8168    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8169    extra_permissions: Vec<(String, String)>,
8170    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8171    /// `"view"`) the framework
8172    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8173    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8174    /// Validated at parse time; unknown actions fail with a span-pointing
8175    /// error.
8176    default_permissions: Vec<String>,
8177    /// `#[rustango(verbose_name = "blog post")]` — the
8178    /// human-readable singular label for the model. Threaded into
8179    /// `ModelSchema::verbose_name` so admin section headers /
8180    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8181    /// over the Rust struct identifier.
8182    verbose_name: Option<String>,
8183    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8184    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8185    /// When unset, `display_label_plural()` auto-suffixes `s`.
8186    verbose_name_plural: Option<String>,
8187    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8188    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8189    /// a name (used by `QuerySet::without_global_scope`) with a
8190    /// `fn() -> WhereExpr` path that the macro emits into
8191    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8192    global_scopes: Vec<GlobalScopeAttr>,
8193    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8194    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8195    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8196    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8197    /// entry emits an inherent `<name>_through(&self)` accessor that
8198    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8199    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8200    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8201    through_relations: Vec<ThroughAttr>,
8202    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8203    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8204    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8205    /// Each entry emits two associated functions on the parent —
8206    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8207    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8208    /// over a correlated subquery against the child table. Users
8209    /// drop the result into `QuerySet::where_raw(...)`.
8210    reverse_has_relations: Vec<ReverseHasAttr>,
8211    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8212    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8213    /// entry so the relation-existence family resolves polymorphic
8214    /// children by name.
8215    generic_has_relations: Vec<GenericHasAttr>,
8216}
8217
8218/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8219/// declaration. Each entry becomes one `core::GlobalScope` in the
8220/// emitted schema literal; `apply` resolves at macro-expand time
8221/// against the consumer's scope so the function must be in scope at
8222/// the use site. Issue #820.
8223struct GlobalScopeAttr {
8224    name: String,
8225    apply: syn::Path,
8226}
8227
8228/// Parsed `#[rustango(through(...))]` declaration. Issue
8229/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8230/// `hasManyThrough` / `hasOneThrough` parity.
8231///
8232/// `Country hasManyThrough Post via User` declares as:
8233///
8234/// ```ignore
8235/// #[rustango(through(
8236///     name                   = "posts",
8237///     far                    = "Post",
8238///     far_fk_column          = "author_id",
8239///     intermediate           = "User",
8240///     intermediate_fk_column = "country_id",
8241/// ))]
8242/// struct Country { ... }
8243/// ```
8244///
8245/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8246/// which returns a queryset filtered via a correlated subquery —
8247/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8248/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8249/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8250/// because the subquery lives inside a `WhereExpr::InSubquery` node
8251/// and the outer queryset's pending list stays empty.
8252///
8253/// All four required arguments use **SQL column / table names**
8254/// (not Rust field names) to sidestep the multi-hop-filter substrate
8255/// gap. Once that substrate lands, a higher-level Rust-field-name
8256/// shorthand can be added without breaking this surface.
8257struct ThroughAttr {
8258    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8259    name: String,
8260    /// Far model type identifier. `far = "Post"` → returns
8261    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8262    /// the derive expands.
8263    far: syn::Ident,
8264    /// SQL column on the far model's table that references the
8265    /// intermediate model's primary key. For `Post`'s
8266    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8267    /// default `<field>_id` convention) or whatever the user
8268    /// declared via `#[rustango(db_column = "...")]`.
8269    far_fk_column: String,
8270    /// Intermediate model type identifier. Needed to look up its
8271    /// `SCHEMA` so the subquery's `FROM` clause points at the
8272    /// intermediate table. `intermediate = "User"`.
8273    intermediate: syn::Ident,
8274    /// SQL column on the intermediate's table that references the
8275    /// source (this) model's primary key. For `User`'s
8276    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8277    intermediate_fk_column: String,
8278    /// SQL primary-key column on the intermediate's table — the
8279    /// column the subquery projects. Optional; defaults to `"id"`
8280    /// (rustango's default PK column name). Override when the
8281    /// intermediate declares a custom PK column.
8282    intermediate_pk_column: String,
8283}
8284
8285/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8286/// child_fk_column = "..."))]` declaration. Issue
8287/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8288/// `whereHas` / `whereDoesntHave` parity.
8289///
8290/// `Post hasMany Comment` declares as:
8291///
8292/// ```ignore
8293/// #[rustango(reverse_has(
8294///     name             = "comments",
8295///     child            = "Comment",
8296///     child_fk_column  = "post_id",
8297/// ))]
8298/// struct Post { ... }
8299/// ```
8300///
8301/// The macro emits two associated functions on `Post`:
8302///
8303/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8304///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8305///   <outer>.<self_pk_column>)`.
8306/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8307///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8308///
8309/// User code:
8310///
8311/// ```ignore
8312/// // Posts with at least one comment:
8313/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8314/// // Posts with no comments:
8315/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8316/// ```
8317///
8318/// As with #817, all column / table identifiers are **SQL names**
8319/// (not Rust field names) so the substrate is independent of the
8320/// outstanding multi-hop filter resolver gap. The emitted
8321/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8322/// SQL-emit time via the writer's scope stack.
8323struct ReverseHasAttr {
8324    /// Accessor name. `name = "comments"` → emits
8325    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8326    name: String,
8327    /// Child model type identifier. `child = "Comment"` — needed to
8328    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8329    /// points at the child table.
8330    child: syn::Ident,
8331    /// SQL column on the child's table that references this model's
8332    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8333    /// column is `"post_id"`.
8334    child_fk_column: String,
8335    /// SQL primary-key column on **this** model's table — the column
8336    /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8337    /// (rustango's default PK column name).
8338    self_pk_column: String,
8339}
8340
8341/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8342/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8343/// relation-existence family (issue #830). The child is a polymorphic,
8344/// content-type-discriminated model; emits a `Model::
8345/// generic_reverse_relations()` entry the queryset resolves by name.
8346struct GenericHasAttr {
8347    /// Accessor name, e.g. `name = "tags"`.
8348    name: String,
8349    /// Child model type identifier (`child = "Tag"`) — looked up for its
8350    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8351    child: syn::Ident,
8352    /// Column on the child table holding the parent's content-type id.
8353    /// Optional; defaults to `"content_type_id"`.
8354    ct_column: String,
8355    /// Column on the child table holding the parent's PK value.
8356    /// Optional; defaults to `"object_pk"`.
8357    pk_column: String,
8358    /// SQL primary-key column on **this** (parent) model's table.
8359    /// Optional; defaults to `"id"`.
8360    self_pk_column: String,
8361}
8362
8363/// Parsed form of one index declaration (field-level or container-level).
8364struct IndexAttr {
8365    /// Index name; auto-derived when `None` at parse time.
8366    name: Option<String>,
8367    /// Column names in the index.
8368    columns: Vec<String>,
8369    /// `true` for `CREATE UNIQUE INDEX`.
8370    unique: bool,
8371    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8372    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8373    /// `"btree"` when the attribute is absent — the DDL writer omits
8374    /// the `USING` clause and the backend uses its own default
8375    /// (btree on every supported dialect).
8376    method: String,
8377    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8378    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8379    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8380    where_clause: Option<String>,
8381    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8382    /// Empty `Vec` (the default) means "no covering columns".
8383    include: Vec<String>,
8384}
8385
8386/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8387struct CheckAttr {
8388    name: String,
8389    expr: String,
8390}
8391
8392/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8393/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8394/// PG-only — surfaced on every backend in the macro emit; the migration
8395/// writer skips the op on MySQL/SQLite. Issue #319.
8396struct ExcludeAttr {
8397    /// Constraint name (free-form Rust identifier).
8398    name: String,
8399    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8400    using: String,
8401    /// Comma-separated `(column, operator)` pairs, in declaration
8402    /// order. Parsed from `"col WITH op, col WITH op"`.
8403    elements: Vec<(String, String)>,
8404    /// Optional `WHERE` predicate (raw SQL).
8405    where_clause: Option<String>,
8406}
8407
8408/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8409/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8410/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8411/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8412/// the model, not the field.
8413struct CompositeFkAttr {
8414    /// Logical relation name (free-form Rust identifier).
8415    name: String,
8416    /// SQL table name of the target.
8417    to: String,
8418    /// Source-side column names, in declaration order.
8419    from: Vec<String>,
8420    /// Target-side column names, same length / order as `from`.
8421    on: Vec<String>,
8422}
8423
8424/// Parsed form of one `#[rustango(generic_fk(name = "target",
8425/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8426/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8427/// generic ("any model") FKs live on the model, not the field.
8428struct GenericFkAttr {
8429    /// Logical relation name (free-form Rust identifier).
8430    name: String,
8431    /// Source-side column carrying the `content_type_id` value.
8432    ct_column: String,
8433    /// Source-side column carrying the target row's primary key.
8434    pk_column: String,
8435}
8436
8437/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8438struct M2MAttr {
8439    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8440    name: String,
8441    /// Target table (e.g. `"app_tags"`).
8442    to: String,
8443    /// Junction table (e.g. `"post_tags"`).
8444    through: String,
8445    /// Source FK column in the junction table (e.g. `"post_id"`).
8446    src: String,
8447    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8448    dst: String,
8449    /// Whether the migration writer should auto-create the junction
8450    /// table. Default `true`. Set `auto_create = false` (#324) when
8451    /// the operator declares the through table as its own
8452    /// `#[derive(Model)]` struct with extra columns.
8453    auto_create: bool,
8454}
8455
8456/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8457/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8458/// junction carries a ContentType discriminator so unrelated models
8459/// share one pivot + related set.
8460struct GenericM2MAttr {
8461    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8462    name: String,
8463    /// Polymorphic junction table (e.g. `"taggables"`).
8464    through: String,
8465    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8466    pk_column: String,
8467    /// Junction column holding the owning model's `content_type_id`
8468    /// discriminator (e.g. `"taggable_type"`).
8469    ct_column: String,
8470    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8471    related_column: String,
8472}
8473
8474/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8475/// "user"))]`. `track` is a comma-separated list of field names whose
8476/// before/after values land in the JSONB `changes` column. `source`
8477/// is informational only — it pins a default source when the model
8478/// is written outside any `audit::with_source(...)` scope (rare).
8479#[derive(Default)]
8480struct AuditAttrs {
8481    /// Field names to capture in the `changes` JSONB. Validated
8482    /// against declared scalar fields at compile time. Empty means
8483    /// "track every scalar field".
8484    track: Option<(Vec<String>, proc_macro2::Span)>,
8485}
8486
8487/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8488/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8489/// comma-separated strings; we validate each ident against the model's
8490/// declared fields at compile time.
8491#[derive(Default)]
8492struct AdminAttrs {
8493    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8494    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8495    list_per_page: Option<usize>,
8496    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8497    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8498    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8499    /// Bulk action names. No field-validation against model fields —
8500    /// these are action handlers, not column references.
8501    actions: Option<(Vec<String>, proc_macro2::Span)>,
8502    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8503    /// sections, comma-separated fields per section, optional
8504    /// `Title:` prefix. Empty title omits the `<legend>`.
8505    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8506    /// `admin(list_display_links = "title")`. Names
8507    /// from `list_display` whose cells should link to detail/edit.
8508    /// Issue #350.
8509    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8510    /// `admin(search_help_text = "...")`. Short
8511    /// caption rendered beside the admin list view's search box.
8512    /// Issue #353.
8513    search_help_text: Option<String>,
8514    /// `admin(actions_on_top = false)`. Hides the
8515    /// action-bar above the table. Default `true`. Issue #354.
8516    actions_on_top: Option<bool>,
8517    /// `admin(actions_on_bottom = true)`. Renders an
8518    /// additional action-bar below the table. Default `false`.
8519    /// Issue #354.
8520    actions_on_bottom: Option<bool>,
8521    /// `admin(date_hierarchy = "created_at")`. Name of
8522    /// a date / datetime field whose values render as a clickable
8523    /// year / month / day drill-down strip above the list table.
8524    /// Empty / unset disables the strip. Issue #355.
8525    date_hierarchy: Option<String>,
8526    /// `admin(prepopulated_fields = "slug:title")`.
8527    /// Each entry is `target:source[+source2]`; multiple entries are
8528    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8529    /// The admin change-form emits JS that slugifies the source values
8530    /// into the target field on every keystroke. Issue #356.
8531    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8532    /// `admin(raw_id_fields = "parent, owner")`. Names
8533    /// of FK fields whose change-form widget renders a lookup link
8534    /// next to the input. Issue #357.
8535    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8536    /// `admin(autocomplete_fields = "author_id")`.
8537    /// Names of FK fields whose change-form widget renders an
8538    /// Ajax-driven typeahead populated from a `__autocomplete`
8539    /// endpoint on the target model. Issue #358.
8540    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8541    /// `admin(list_select_related = "all" | "none" | "author, …")`
8542    /// — tunes the admin list view's FK auto-JOIN
8543    /// policy. Default `"all"` matches rustango's join-everything
8544    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8545    list_select_related: Option<String>,
8546    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8547    /// Each entry is `field_name:widget_name`; multiple
8548    /// entries comma-separated. Empty / unset → no overrides. The
8549    /// list of widget names supported is documented on
8550    /// `AdminConfig::formfield_overrides`. Issue #359.
8551    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8552}
8553
8554fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8555    let mut out = ContainerAttrs {
8556        table: None,
8557        display: None,
8558        app: None,
8559        admin: None,
8560        audit: None,
8561        // Default `permissions = true` so every `#[derive(Model)]`
8562        // gets the four CRUD codenames seeded by `auto_create_permissions`
8563        // and is visible to non-superusers in the tenant admin without
8564        // manual per-model annotation. Models that intentionally don't
8565        // want permission rows (registry-internal types, framework
8566        // tables operators shouldn't manage directly) opt out via
8567        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8568        // out-of-the-box admin invisibility regression (#62).
8569        permissions: true,
8570        m2m: Vec::new(),
8571        generic_m2m: Vec::new(),
8572        indexes: Vec::new(),
8573        checks: Vec::new(),
8574        excludes: Vec::new(),
8575        composite_fks: Vec::new(),
8576        generic_fks: Vec::new(),
8577        scope: None,
8578        manager_ext: None,
8579        manager_fns: Vec::new(),
8580        default_order: Vec::new(),
8581        is_view: false,
8582        managed: true,
8583        verbose_name: None,
8584        verbose_name_plural: None,
8585        base_manager_name: None,
8586        order_with_respect_to: None,
8587        proxy: false,
8588        required_db_features: Vec::new(),
8589        required_db_vendor: None,
8590        default_related_name: None,
8591        db_table_comment: None,
8592        get_latest_by: None,
8593        extra_permissions: Vec::new(),
8594        default_permissions: Vec::new(),
8595        global_scopes: Vec::new(),
8596        through_relations: Vec::new(),
8597        reverse_has_relations: Vec::new(),
8598        generic_has_relations: Vec::new(),
8599    };
8600    for attr in &input.attrs {
8601        if !attr.path().is_ident("rustango") {
8602            continue;
8603        }
8604        attr.parse_nested_meta(|meta| {
8605            if meta.path.is_ident("table") {
8606                let s: LitStr = meta.value()?.parse()?;
8607                let name = s.value();
8608                // v0.27.3 (#65) — macro-time guard against table names
8609                // that compile but break SQL downstream. Hyphens are
8610                // the common footgun: PostgreSQL accepts a quoted
8611                // `"intermediate-region"` in CREATE TABLE, but the
8612                // FK / index name derivation in `migrate::ddl`
8613                // emits `intermediate-region_field_fkey` unquoted,
8614                // which then fails the SQL parser. Same shape rule
8615                // as Postgres regular identifiers so the safe path
8616                // is the only path.
8617                validate_table_name(&name, s.span())?;
8618                out.table = Some(name);
8619                return Ok(());
8620            }
8621            if meta.path.is_ident("display") {
8622                let s: LitStr = meta.value()?.parse()?;
8623                out.display = Some((s.value(), s.span()));
8624                return Ok(());
8625            }
8626            if meta.path.is_ident("app") {
8627                let s: LitStr = meta.value()?.parse()?;
8628                out.app = Some(s.value());
8629                return Ok(());
8630            }
8631            if meta.path.is_ident("scope") {
8632                let s: LitStr = meta.value()?.parse()?;
8633                let val = s.value();
8634                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8635                    return Err(meta.error(format!(
8636                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8637                    )));
8638                }
8639                out.scope = Some(val);
8640                return Ok(());
8641            }
8642            if meta.path.is_ident("admin") {
8643                let mut admin = AdminAttrs::default();
8644                meta.parse_nested_meta(|inner| {
8645                    if inner.path.is_ident("list_display") {
8646                        let s: LitStr = inner.value()?.parse()?;
8647                        admin.list_display =
8648                            Some((split_field_list(&s.value()), s.span()));
8649                        return Ok(());
8650                    }
8651                    if inner.path.is_ident("search_fields") {
8652                        let s: LitStr = inner.value()?.parse()?;
8653                        admin.search_fields =
8654                            Some((split_field_list(&s.value()), s.span()));
8655                        return Ok(());
8656                    }
8657                    if inner.path.is_ident("readonly_fields") {
8658                        let s: LitStr = inner.value()?.parse()?;
8659                        admin.readonly_fields =
8660                            Some((split_field_list(&s.value()), s.span()));
8661                        return Ok(());
8662                    }
8663                    if inner.path.is_ident("list_per_page") {
8664                        let lit: syn::LitInt = inner.value()?.parse()?;
8665                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8666                        return Ok(());
8667                    }
8668                    if inner.path.is_ident("ordering") {
8669                        let s: LitStr = inner.value()?.parse()?;
8670                        admin.ordering = Some((
8671                            parse_ordering_list(&s.value()),
8672                            s.span(),
8673                        ));
8674                        return Ok(());
8675                    }
8676                    if inner.path.is_ident("list_filter") {
8677                        let s: LitStr = inner.value()?.parse()?;
8678                        admin.list_filter =
8679                            Some((split_field_list(&s.value()), s.span()));
8680                        return Ok(());
8681                    }
8682                    if inner.path.is_ident("actions") {
8683                        let s: LitStr = inner.value()?.parse()?;
8684                        admin.actions =
8685                            Some((split_field_list(&s.value()), s.span()));
8686                        return Ok(());
8687                    }
8688                    if inner.path.is_ident("fieldsets") {
8689                        let s: LitStr = inner.value()?.parse()?;
8690                        admin.fieldsets =
8691                            Some((parse_fieldset_list(&s.value()), s.span()));
8692                        return Ok(());
8693                    }
8694                    if inner.path.is_ident("list_display_links") {
8695                        let s: LitStr = inner.value()?.parse()?;
8696                        admin.list_display_links =
8697                            Some((split_field_list(&s.value()), s.span()));
8698                        return Ok(());
8699                    }
8700                    if inner.path.is_ident("search_help_text") {
8701                        let s: LitStr = inner.value()?.parse()?;
8702                        admin.search_help_text = Some(s.value());
8703                        return Ok(());
8704                    }
8705                    if inner.path.is_ident("actions_on_top") {
8706                        let lit: syn::LitBool = inner.value()?.parse()?;
8707                        admin.actions_on_top = Some(lit.value);
8708                        return Ok(());
8709                    }
8710                    if inner.path.is_ident("actions_on_bottom") {
8711                        let lit: syn::LitBool = inner.value()?.parse()?;
8712                        admin.actions_on_bottom = Some(lit.value);
8713                        return Ok(());
8714                    }
8715                    if inner.path.is_ident("date_hierarchy") {
8716                        let s: LitStr = inner.value()?.parse()?;
8717                        admin.date_hierarchy = Some(s.value());
8718                        return Ok(());
8719                    }
8720                    if inner.path.is_ident("prepopulated_fields") {
8721                        let s: LitStr = inner.value()?.parse()?;
8722                        admin.prepopulated_fields =
8723                            Some((parse_prepopulated_list(&s.value()), s.span()));
8724                        return Ok(());
8725                    }
8726                    if inner.path.is_ident("raw_id_fields") {
8727                        let s: LitStr = inner.value()?.parse()?;
8728                        admin.raw_id_fields =
8729                            Some((split_field_list(&s.value()), s.span()));
8730                        return Ok(());
8731                    }
8732                    if inner.path.is_ident("autocomplete_fields") {
8733                        let s: LitStr = inner.value()?.parse()?;
8734                        admin.autocomplete_fields =
8735                            Some((split_field_list(&s.value()), s.span()));
8736                        return Ok(());
8737                    }
8738                    if inner.path.is_ident("list_select_related") {
8739                        let s: LitStr = inner.value()?.parse()?;
8740                        admin.list_select_related = Some(s.value());
8741                        return Ok(());
8742                    }
8743                    if inner.path.is_ident("formfield_overrides") {
8744                        let s: LitStr = inner.value()?.parse()?;
8745                        admin.formfield_overrides =
8746                            Some((parse_formfield_overrides(&s.value()), s.span()));
8747                        return Ok(());
8748                    }
8749                    Err(inner.error(
8750                        "unknown admin attribute (supported: \
8751                         `list_display`, `list_display_links`, \
8752                         `search_fields`, `search_help_text`, \
8753                         `readonly_fields`, \
8754                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8755                         `actions_on_top`, `actions_on_bottom`, \
8756                         `date_hierarchy`, \
8757                         `prepopulated_fields`, \
8758                         `raw_id_fields`, \
8759                         `autocomplete_fields`, \
8760                         `list_select_related`, \
8761                         `formfield_overrides`, \
8762                         `fieldsets`)",
8763                    ))
8764                })?;
8765                out.admin = Some(admin);
8766                return Ok(());
8767            }
8768            if meta.path.is_ident("manager") {
8769                // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8770                // Stretch `from_queryset = "..."` is left as a
8771                // follow-up — the issue's primary acceptance is the
8772                // `ext = ...` trait emission.
8773                meta.parse_nested_meta(|inner| {
8774                    if inner.path.is_ident("ext") {
8775                        let s: LitStr = inner.value()?.parse()?;
8776                        let name = s.value();
8777                        if name.is_empty() {
8778                            return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8779                        }
8780                        out.manager_ext =
8781                            Some(syn::Ident::new(&name, s.span()));
8782                        return Ok(());
8783                    }
8784                    Err(inner.error(
8785                        "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8786                    ))
8787                })?;
8788                return Ok(());
8789            }
8790            if meta.path.is_ident("manager_fn") {
8791                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8792                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8793                // next to the default `Self::objects()`. Multiple
8794                // attributes accumulate.
8795                let s: LitStr = meta.value()?.parse()?;
8796                let name = s.value();
8797                if name.is_empty() {
8798                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8799                }
8800                if name == "objects" {
8801                    return Err(meta.error(
8802                        "`manager_fn = \"objects\"` collides with the default \
8803                         accessor — pick a different name",
8804                    ));
8805                }
8806                let ident = syn::Ident::new(&name, s.span());
8807                if out.manager_fns.iter().any(|prev| *prev == ident) {
8808                    return Err(meta.error(format!(
8809                        "duplicate `manager_fn = \"{name}\"`"
8810                    )));
8811                }
8812                out.manager_fns.push(ident);
8813                return Ok(());
8814            }
8815            if meta.path.is_ident("default_order") {
8816                // `#[rustango(default_order = "-created_at, status")]`
8817                // — issue #291 / T2.5. Comma-separated list; `-prefix`
8818                // means descending, `+prefix` or bare name means ascending.
8819                // Per-query opt-in via `QuerySet::with_default_order()`.
8820                let s: LitStr = meta.value()?.parse()?;
8821                let raw = s.value();
8822                let span = s.span();
8823                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8824                    Vec::new();
8825                for entry in raw.split(',') {
8826                    let trimmed = entry.trim();
8827                    if trimmed.is_empty() {
8828                        return Err(syn::Error::new(
8829                            span,
8830                            "`default_order = \"...\"` has an empty entry — \
8831                             check for a stray comma",
8832                        ));
8833                    }
8834                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8835                        (true, rest.trim().to_owned())
8836                    } else if let Some(rest) = trimmed.strip_prefix('+') {
8837                        (false, rest.trim().to_owned())
8838                    } else {
8839                        (false, trimmed.to_owned())
8840                    };
8841                    if name.is_empty() {
8842                        return Err(syn::Error::new(
8843                            span,
8844                            "`default_order` entry has no column name after the prefix",
8845                        ));
8846                    }
8847                    if parsed.iter().any(|(n, _, _)| *n == name) {
8848                        return Err(syn::Error::new(
8849                            span,
8850                            format!("duplicate column `{name}` in `default_order`"),
8851                        ));
8852                    }
8853                    parsed.push((name, desc, span));
8854                }
8855                if parsed.is_empty() {
8856                    return Err(syn::Error::new(
8857                        span,
8858                        "`default_order = \"...\"` cannot be empty",
8859                    ));
8860                }
8861                out.default_order = parsed;
8862                return Ok(());
8863            }
8864            if meta.path.is_ident("global_scope") {
8865                // `#[rustango(global_scope(name = "active", apply =
8866                //  path::to::fn))]` — issue #820. The apply function
8867                // path resolves at macro-expand time in the consumer's
8868                // scope; the macro re-emits it verbatim into the
8869                // `ModelSchema::global_scopes` slice literal.
8870                let span = meta.path.span();
8871                let mut scope_name: Option<String> = None;
8872                let mut apply_path: Option<syn::Path> = None;
8873                meta.parse_nested_meta(|inner| {
8874                    if inner.path.is_ident("name") {
8875                        let s: LitStr = inner.value()?.parse()?;
8876                        let raw = s.value();
8877                        if raw.trim().is_empty() {
8878                            return Err(syn::Error::new(
8879                                s.span(),
8880                                "`global_scope(name = \"...\")` must not be empty",
8881                            ));
8882                        }
8883                        scope_name = Some(raw);
8884                        return Ok(());
8885                    }
8886                    if inner.path.is_ident("apply") {
8887                        let p: syn::Path = inner.value()?.parse()?;
8888                        apply_path = Some(p);
8889                        return Ok(());
8890                    }
8891                    Err(inner.error(
8892                        "unknown `global_scope` attribute (supported: \
8893                         `name`, `apply`)",
8894                    ))
8895                })?;
8896                let Some(name) = scope_name else {
8897                    return Err(syn::Error::new(
8898                        span,
8899                        "`global_scope` requires `name = \"...\"`",
8900                    ));
8901                };
8902                let Some(apply) = apply_path else {
8903                    return Err(syn::Error::new(
8904                        span,
8905                        "`global_scope` requires `apply = fn_path`",
8906                    ));
8907                };
8908                if out.global_scopes.iter().any(|s| s.name == name) {
8909                    return Err(syn::Error::new(
8910                        span,
8911                        format!(
8912                            "duplicate global scope name `{name}` — \
8913                             pick a unique identifier so \
8914                             `QuerySet::without_global_scope(\"{name}\")` \
8915                             is unambiguous"
8916                        ),
8917                    ));
8918                }
8919                out.global_scopes.push(GlobalScopeAttr { name, apply });
8920                return Ok(());
8921            }
8922            if meta.path.is_ident("through") {
8923                // `#[rustango(through(name = "posts", far = "Post",
8924                //  far_fk_column = "author_id", intermediate = "User",
8925                //  intermediate_fk_column = "country_id"
8926                //  [, intermediate_pk_column = "id"]))]` — issue #817.
8927                let span = meta.path.span();
8928                let mut accessor_name: Option<String> = None;
8929                let mut far_ident: Option<syn::Ident> = None;
8930                let mut far_fk_column: Option<String> = None;
8931                let mut intermediate_ident: Option<syn::Ident> = None;
8932                let mut intermediate_fk_column: Option<String> = None;
8933                let mut intermediate_pk_column: Option<String> = None;
8934                fn parse_nonempty_string(
8935                    inner: &syn::meta::ParseNestedMeta<'_>,
8936                    field: &str,
8937                ) -> syn::Result<String> {
8938                    let s: LitStr = inner.value()?.parse()?;
8939                    let raw = s.value();
8940                    let trimmed = raw.trim();
8941                    if trimmed.is_empty() {
8942                        return Err(syn::Error::new(
8943                            s.span(),
8944                            format!("`through({field} = \"...\")` must not be empty"),
8945                        ));
8946                    }
8947                    Ok(trimmed.to_owned())
8948                }
8949                meta.parse_nested_meta(|inner| {
8950                    if inner.path.is_ident("name") {
8951                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
8952                        return Ok(());
8953                    }
8954                    if inner.path.is_ident("far") {
8955                        // Far model name accepted as a string literal
8956                        // so the attribute fits inside the existing
8957                        // `parse_nested_meta` shape; parsed back into a
8958                        // `syn::Ident` so the emitted accessor can
8959                        // reference the type directly.
8960                        let s: LitStr = inner.value()?.parse()?;
8961                        let raw = s.value();
8962                        let trimmed = raw.trim();
8963                        if trimmed.is_empty() {
8964                            return Err(syn::Error::new(
8965                                s.span(),
8966                                "`through(far = \"...\")` must not be empty",
8967                            ));
8968                        }
8969                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
8970                        return Ok(());
8971                    }
8972                    if inner.path.is_ident("far_fk_column") {
8973                        far_fk_column =
8974                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
8975                        return Ok(());
8976                    }
8977                    if inner.path.is_ident("intermediate") {
8978                        let s: LitStr = inner.value()?.parse()?;
8979                        let raw = s.value();
8980                        let trimmed = raw.trim();
8981                        if trimmed.is_empty() {
8982                            return Err(syn::Error::new(
8983                                s.span(),
8984                                "`through(intermediate = \"...\")` must not be empty",
8985                            ));
8986                        }
8987                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
8988                        return Ok(());
8989                    }
8990                    if inner.path.is_ident("intermediate_fk_column") {
8991                        intermediate_fk_column =
8992                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
8993                        return Ok(());
8994                    }
8995                    if inner.path.is_ident("intermediate_pk_column") {
8996                        intermediate_pk_column =
8997                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
8998                        return Ok(());
8999                    }
9000                    Err(inner.error(
9001                        "unknown `through` attribute (supported: \
9002                         `name`, `far`, `far_fk_column`, \
9003                         `intermediate`, `intermediate_fk_column`, \
9004                         `intermediate_pk_column`)",
9005                    ))
9006                })?;
9007                let Some(name) = accessor_name else {
9008                    return Err(syn::Error::new(
9009                        span,
9010                        "`through` requires `name = \"...\"`",
9011                    ));
9012                };
9013                let Some(far) = far_ident else {
9014                    return Err(syn::Error::new(
9015                        span,
9016                        "`through` requires `far = \"FarModelType\"`",
9017                    ));
9018                };
9019                let Some(far_fk_column) = far_fk_column else {
9020                    return Err(syn::Error::new(
9021                        span,
9022                        "`through` requires `far_fk_column = \"<column>\"`",
9023                    ));
9024                };
9025                let Some(intermediate) = intermediate_ident else {
9026                    return Err(syn::Error::new(
9027                        span,
9028                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9029                    ));
9030                };
9031                let Some(intermediate_fk_column) = intermediate_fk_column else {
9032                    return Err(syn::Error::new(
9033                        span,
9034                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9035                    ));
9036                };
9037                let intermediate_pk_column =
9038                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9039                if out.through_relations.iter().any(|t| t.name == name) {
9040                    return Err(syn::Error::new(
9041                        span,
9042                        format!(
9043                            "duplicate `through(name = \"{name}\")` — \
9044                             pick a unique accessor name"
9045                        ),
9046                    ));
9047                }
9048                out.through_relations.push(ThroughAttr {
9049                    name,
9050                    far,
9051                    far_fk_column,
9052                    intermediate,
9053                    intermediate_fk_column,
9054                    intermediate_pk_column,
9055                });
9056                return Ok(());
9057            }
9058            if meta.path.is_ident("reverse_has") {
9059                // `#[rustango(reverse_has(name = "comments",
9060                //  child = "Comment", child_fk_column = "post_id"
9061                //  [, self_pk_column = "id"]))]` — issue #830.
9062                let span = meta.path.span();
9063                let mut accessor_name: Option<String> = None;
9064                let mut child_ident: Option<syn::Ident> = None;
9065                let mut child_fk_column: Option<String> = None;
9066                let mut self_pk_column: Option<String> = None;
9067                meta.parse_nested_meta(|inner| {
9068                    if inner.path.is_ident("name") {
9069                        let s: LitStr = inner.value()?.parse()?;
9070                        let raw = s.value();
9071                        if raw.trim().is_empty() {
9072                            return Err(syn::Error::new(
9073                                s.span(),
9074                                "`reverse_has(name = \"...\")` must not be empty",
9075                            ));
9076                        }
9077                        accessor_name = Some(raw);
9078                        return Ok(());
9079                    }
9080                    if inner.path.is_ident("child") {
9081                        let s: LitStr = inner.value()?.parse()?;
9082                        let raw = s.value();
9083                        let trimmed = raw.trim();
9084                        if trimmed.is_empty() {
9085                            return Err(syn::Error::new(
9086                                s.span(),
9087                                "`reverse_has(child = \"...\")` must not be empty",
9088                            ));
9089                        }
9090                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9091                        return Ok(());
9092                    }
9093                    if inner.path.is_ident("child_fk_column") {
9094                        let s: LitStr = inner.value()?.parse()?;
9095                        let raw = s.value();
9096                        let trimmed = raw.trim();
9097                        if trimmed.is_empty() {
9098                            return Err(syn::Error::new(
9099                                s.span(),
9100                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9101                            ));
9102                        }
9103                        child_fk_column = Some(trimmed.to_owned());
9104                        return Ok(());
9105                    }
9106                    if inner.path.is_ident("self_pk_column") {
9107                        let s: LitStr = inner.value()?.parse()?;
9108                        let raw = s.value();
9109                        let trimmed = raw.trim();
9110                        if trimmed.is_empty() {
9111                            return Err(syn::Error::new(
9112                                s.span(),
9113                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9114                            ));
9115                        }
9116                        self_pk_column = Some(trimmed.to_owned());
9117                        return Ok(());
9118                    }
9119                    Err(inner.error(
9120                        "unknown `reverse_has` attribute (supported: \
9121                         `name`, `child`, `child_fk_column`, \
9122                         `self_pk_column`)",
9123                    ))
9124                })?;
9125                let Some(name) = accessor_name else {
9126                    return Err(syn::Error::new(
9127                        span,
9128                        "`reverse_has` requires `name = \"...\"`",
9129                    ));
9130                };
9131                let Some(child) = child_ident else {
9132                    return Err(syn::Error::new(
9133                        span,
9134                        "`reverse_has` requires `child = \"ChildModelType\"`",
9135                    ));
9136                };
9137                let Some(child_fk_column) = child_fk_column else {
9138                    return Err(syn::Error::new(
9139                        span,
9140                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9141                    ));
9142                };
9143                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9144                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9145                    return Err(syn::Error::new(
9146                        span,
9147                        format!(
9148                            "duplicate `reverse_has(name = \"{name}\")` — \
9149                             pick a unique accessor name"
9150                        ),
9151                    ));
9152                }
9153                out.reverse_has_relations.push(ReverseHasAttr {
9154                    name,
9155                    child,
9156                    child_fk_column,
9157                    self_pk_column,
9158                });
9159                return Ok(());
9160            }
9161            if meta.path.is_ident("generic_has") {
9162                // `#[rustango(generic_has(name = "tags",
9163                //  child = "Tag", ct_column = "content_type_id",
9164                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9165                //  — issue #830, the reverse generic-FK (GFK) arm of the
9166                //  relation-existence family.
9167                let span = meta.path.span();
9168                let mut accessor_name: Option<String> = None;
9169                let mut child_ident: Option<syn::Ident> = None;
9170                let mut ct_column: Option<String> = None;
9171                let mut pk_column: Option<String> = None;
9172                let mut self_pk_column: Option<String> = None;
9173                meta.parse_nested_meta(|inner| {
9174                    if inner.path.is_ident("name") {
9175                        let s: LitStr = inner.value()?.parse()?;
9176                        let raw = s.value();
9177                        if raw.trim().is_empty() {
9178                            return Err(syn::Error::new(
9179                                s.span(),
9180                                "`generic_has(name = \"...\")` must not be empty",
9181                            ));
9182                        }
9183                        accessor_name = Some(raw);
9184                        return Ok(());
9185                    }
9186                    if inner.path.is_ident("child") {
9187                        let s: LitStr = inner.value()?.parse()?;
9188                        let trimmed = s.value().trim().to_owned();
9189                        if trimmed.is_empty() {
9190                            return Err(syn::Error::new(
9191                                s.span(),
9192                                "`generic_has(child = \"...\")` must not be empty",
9193                            ));
9194                        }
9195                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9196                        return Ok(());
9197                    }
9198                    if inner.path.is_ident("ct_column") {
9199                        let s: LitStr = inner.value()?.parse()?;
9200                        let trimmed = s.value().trim().to_owned();
9201                        if trimmed.is_empty() {
9202                            return Err(syn::Error::new(
9203                                s.span(),
9204                                "`generic_has(ct_column = \"...\")` must not be empty",
9205                            ));
9206                        }
9207                        ct_column = Some(trimmed);
9208                        return Ok(());
9209                    }
9210                    if inner.path.is_ident("pk_column") {
9211                        let s: LitStr = inner.value()?.parse()?;
9212                        let trimmed = s.value().trim().to_owned();
9213                        if trimmed.is_empty() {
9214                            return Err(syn::Error::new(
9215                                s.span(),
9216                                "`generic_has(pk_column = \"...\")` must not be empty",
9217                            ));
9218                        }
9219                        pk_column = Some(trimmed);
9220                        return Ok(());
9221                    }
9222                    if inner.path.is_ident("self_pk_column") {
9223                        let s: LitStr = inner.value()?.parse()?;
9224                        let trimmed = s.value().trim().to_owned();
9225                        if trimmed.is_empty() {
9226                            return Err(syn::Error::new(
9227                                s.span(),
9228                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9229                            ));
9230                        }
9231                        self_pk_column = Some(trimmed);
9232                        return Ok(());
9233                    }
9234                    Err(inner.error(
9235                        "unknown `generic_has` attribute (supported: \
9236                         `name`, `child`, `ct_column`, `pk_column`, \
9237                         `self_pk_column`)",
9238                    ))
9239                })?;
9240                let Some(name) = accessor_name else {
9241                    return Err(syn::Error::new(
9242                        span,
9243                        "`generic_has` requires `name = \"...\"`",
9244                    ));
9245                };
9246                let Some(child) = child_ident else {
9247                    return Err(syn::Error::new(
9248                        span,
9249                        "`generic_has` requires `child = \"ChildModelType\"`",
9250                    ));
9251                };
9252                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9253                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9254                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9255                if out.generic_has_relations.iter().any(|r| r.name == name) {
9256                    return Err(syn::Error::new(
9257                        span,
9258                        format!(
9259                            "duplicate `generic_has(name = \"{name}\")` — \
9260                             pick a unique accessor name"
9261                        ),
9262                    ));
9263                }
9264                out.generic_has_relations.push(GenericHasAttr {
9265                    name,
9266                    child,
9267                    ct_column,
9268                    pk_column,
9269                    self_pk_column,
9270                });
9271                return Ok(());
9272            }
9273            if meta.path.is_ident("audit") {
9274                let mut audit = AuditAttrs::default();
9275                meta.parse_nested_meta(|inner| {
9276                    if inner.path.is_ident("track") {
9277                        let s: LitStr = inner.value()?.parse()?;
9278                        audit.track =
9279                            Some((split_field_list(&s.value()), s.span()));
9280                        return Ok(());
9281                    }
9282                    Err(inner.error(
9283                        "unknown audit attribute (supported: `track`)",
9284                    ))
9285                })?;
9286                out.audit = Some(audit);
9287                return Ok(());
9288            }
9289            if meta.path.is_ident("permissions") {
9290                // Two forms accepted:
9291                //   #[rustango(permissions)]          — flag form, true
9292                //   #[rustango(permissions = false)]  — explicit opt-out
9293                //   #[rustango(permissions = true)]   — explicit opt-in
9294                if let Ok(v) = meta.value() {
9295                    let lit: syn::LitBool = v.parse()?;
9296                    out.permissions = lit.value;
9297                } else {
9298                    out.permissions = true;
9299                }
9300                return Ok(());
9301            }
9302            if meta.path.is_ident("view") {
9303                // Issue #293 / T2.10. Two forms accepted, matching
9304                // the `permissions` flag pattern:
9305                //   #[rustango(view)]          — flag form, true
9306                //   #[rustango(view = false)]  — explicit opt-out
9307                //   #[rustango(view = true)]   — explicit opt-in
9308                if let Ok(v) = meta.value() {
9309                    let lit: syn::LitBool = v.parse()?;
9310                    out.is_view = lit.value;
9311                } else {
9312                    out.is_view = true;
9313                }
9314                return Ok(());
9315            }
9316            if meta.path.is_ident("managed") {
9317                // Who owns the table. Issue #321.
9318                //   #[rustango(managed = false)]  — operator-managed table
9319                //   #[rustango(managed = true)]   — rustango-managed (the default)
9320                // Bare-flag form is intentionally not accepted: writing
9321                // `#[rustango(managed)]` reads as "yes please manage it"
9322                // which is already the default. The opt-out is the only
9323                // useful state, so it must be explicit.
9324                let v = meta.value()?;
9325                let lit: syn::LitBool = v.parse()?;
9326                out.managed = lit.value;
9327                return Ok(());
9328            }
9329            if meta.path.is_ident("verbose_name") {
9330                let s: LitStr = meta.value()?.parse()?;
9331                out.verbose_name = Some(s.value());
9332                return Ok(());
9333            }
9334            if meta.path.is_ident("verbose_name_plural") {
9335                let s: LitStr = meta.value()?.parse()?;
9336                out.verbose_name_plural = Some(s.value());
9337                return Ok(());
9338            }
9339            if meta.path.is_ident("db_table_comment") {
9340                // Free-form table-level comment attached to the DB
9341                // catalog.
9342                let s: LitStr = meta.value()?.parse()?;
9343                out.db_table_comment = Some(s.value());
9344                return Ok(());
9345            }
9346            if meta.path.is_ident("proxy") {
9347                // Declarative flag
9348                // marking the struct as a proxy of another model that
9349                // shares its DB table. Stored on `ModelSchema::proxy`
9350                // so future codegen can skip `CreateTable` emission
9351                // for proxies (parent owns the table) and route
9352                // per-instance method resolution to the proxy class.
9353                //
9354                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9355                let value = if meta.input.peek(syn::Token![=]) {
9356                    meta.value()?.parse::<syn::LitBool>()?.value
9357                } else {
9358                    true
9359                };
9360                out.proxy = value;
9361                return Ok(());
9362            }
9363            if meta.path.is_ident("order_with_respect_to") {
9364                // `order_with_respect_to = "parent_fk"` — the model's
9365                // instances are intrinsically ordered relative to
9366                // their parent FK.
9367                //
9368                // rustango stores the FK field name on
9369                // `ModelSchema::order_with_respect_to`. Declarative-only
9370                // today: the migration writer + admin surfaces still
9371                // treat every model identically. Future codegen will
9372                // key off the metadata to auto-emit the `_order`
9373                // column and reorder helpers.
9374                //
9375                // Validated as a Rust-shape identifier so the macro
9376                // can reject typos at derive time.
9377                let s: LitStr = meta.value()?.parse()?;
9378                let raw = s.value();
9379                if raw.is_empty() {
9380                    return Err(syn::Error::new(
9381                        s.span(),
9382                        "`order_with_respect_to` must be a non-empty FK field name",
9383                    ));
9384                }
9385                let valid = raw
9386                    .chars()
9387                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9388                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9389                if !valid {
9390                    return Err(syn::Error::new(
9391                        s.span(),
9392                        format!(
9393                            "`order_with_respect_to` must be a valid Rust \
9394                             identifier (letters / digits / underscores, \
9395                             not starting with a digit); got `{raw}`"
9396                        ),
9397                    ));
9398                }
9399                out.order_with_respect_to = Some(raw);
9400                return Ok(());
9401            }
9402            if meta.path.is_ident("required_db_features") {
9403                // Capability tokens the model needs (e.g.
9404                // `"json_extract"`, `"window_functions"`,
9405                // `"row_security"`). Comma-separated.
9406                // `manage check --deploy` walks every model and warns
9407                // when the active backend doesn't advertise the
9408                // capability.
9409                //
9410                // rustango ships a small registry of capability tokens
9411                // each dialect supports — see `Dialect::supports`.
9412                // Unknown tokens still parse (they end up on the
9413                // schema and show up in the warning) so projects can
9414                // declare aspirational capabilities and the check
9415                // verb will keep nagging until the dialect implements
9416                // them.
9417                let s: LitStr = meta.value()?.parse()?;
9418                let raw = s.value();
9419                let features: Vec<String> = raw
9420                    .split(',')
9421                    .map(str::trim)
9422                    .filter(|s| !s.is_empty())
9423                    .map(str::to_owned)
9424                    .collect();
9425                if features.is_empty() {
9426                    return Err(syn::Error::new(
9427                        s.span(),
9428                        "`required_db_features` must list at least one \
9429                         comma-separated capability token",
9430                    ));
9431                }
9432                out.required_db_features = features;
9433                return Ok(());
9434            }
9435            if meta.path.is_ident("required_db_vendor") {
9436                // The model is only meant to run against the named DB
9437                // backend. `manage check --deploy` flags a mismatch so
9438                // ops catches "I forgot to switch DATABASE_URL" at
9439                // deploy time rather than runtime.
9440                //
9441                // Restricted to the three backends rustango ships
9442                // dialects for so the check verb can compare reliably.
9443                let s: LitStr = meta.value()?.parse()?;
9444                let raw = s.value().to_ascii_lowercase();
9445                match raw.as_str() {
9446                    "postgresql" | "postgres" | "pg" => {
9447                        out.required_db_vendor = Some("postgres".to_owned());
9448                    }
9449                    "mysql" | "mariadb" => {
9450                        out.required_db_vendor = Some("mysql".to_owned());
9451                    }
9452                    "sqlite" | "sqlite3" => {
9453                        out.required_db_vendor = Some("sqlite".to_owned());
9454                    }
9455                    _ => {
9456                        return Err(syn::Error::new(
9457                            s.span(),
9458                            format!(
9459                                "unknown required_db_vendor `{raw}` — \
9460                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9461                                 `mysql` (alias: `mariadb`), or `sqlite` \
9462                                 (alias: `sqlite3`)"
9463                            ),
9464                        ));
9465                    }
9466                }
9467                return Ok(());
9468            }
9469            if meta.path.is_ident("base_manager_name") {
9470                // Name of the manager that `<instance>.<relation>_set`
9471                // uses when resolving reverse-relation managers.
9472                // Distinct from `default_manager_name` (what
9473                // `Self::objects()` returns). Stored on
9474                // `ModelSchema::base_manager_name`.
9475                //
9476                // Validated as a Rust identifier so it stays safe to
9477                // re-emit as code in future reverse-manager codegen.
9478                let s: LitStr = meta.value()?.parse()?;
9479                let raw = s.value();
9480                if raw.is_empty() {
9481                    return Err(syn::Error::new(
9482                        s.span(),
9483                        "`base_manager_name` must be a non-empty string",
9484                    ));
9485                }
9486                let valid = raw
9487                    .chars()
9488                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9489                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9490                if !valid {
9491                    return Err(syn::Error::new(
9492                        s.span(),
9493                        format!(
9494                            "`base_manager_name` must be a valid Rust \
9495                             identifier (letters / digits / underscores, \
9496                             not starting with a digit); got `{raw}`"
9497                        ),
9498                    ));
9499                }
9500                out.base_manager_name = Some(raw);
9501                return Ok(());
9502            }
9503            if meta.path.is_ident("default_related_name") {
9504                // The name reverse-relation accessors use when callers
9505                // don't override `related_name = "..."` on the FK / M2M
9506                // field. Stored on `ModelSchema::default_related_name`
9507                // so future reverse-manager codegen / schema emit /
9508                // admin templates can pick the right accessor name
9509                // (today rustango doesn't auto-emit reverse managers;
9510                // the metadata is the foundation for that work).
9511                //
9512                // Enforced non-empty + ASCII identifier-shape so the
9513                // string is safe to use as a Rust ident later.
9514                let s: LitStr = meta.value()?.parse()?;
9515                let raw = s.value();
9516                if raw.is_empty() {
9517                    return Err(syn::Error::new(
9518                        s.span(),
9519                        "`default_related_name` must be a non-empty string",
9520                    ));
9521                }
9522                let valid = raw
9523                    .chars()
9524                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9525                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9526                if !valid {
9527                    return Err(syn::Error::new(
9528                        s.span(),
9529                        format!(
9530                            "`default_related_name` must be snake_case ASCII \
9531                             (lowercase letters / digits / underscores, not \
9532                             starting with a digit); got `{raw}`"
9533                        ),
9534                    ));
9535                }
9536                out.default_related_name = Some(raw);
9537                return Ok(());
9538            }
9539            if meta.path.is_ident("extra_permissions") {
9540                // Comma-separated `codename:label` pairs.
9541                let s: LitStr = meta.value()?.parse()?;
9542                let raw = s.value();
9543                let mut pairs = Vec::new();
9544                for entry in raw.split(',') {
9545                    let entry = entry.trim();
9546                    if entry.is_empty() {
9547                        continue;
9548                    }
9549                    let (codename, label) = match entry.split_once(':') {
9550                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9551                        None => (entry.to_owned(), entry.to_owned()),
9552                    };
9553                    if codename.is_empty() {
9554                        return Err(meta.error(
9555                            "`extra_permissions` entries must be `codename:label` pairs",
9556                        ));
9557                    }
9558                    pairs.push((codename, label));
9559                }
9560                if pairs.is_empty() {
9561                    return Err(meta
9562                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9563                }
9564                out.extra_permissions = pairs;
9565                return Ok(());
9566            }
9567            if meta.path.is_ident("default_permissions") {
9568                // Comma-separated subset of the CRUD action set.
9569                // Empty means all four (the framework default).
9570                let s: LitStr = meta.value()?.parse()?;
9571                let raw = s.value();
9572                let mut actions: Vec<String> = Vec::new();
9573                for entry in raw.split(',') {
9574                    let action = entry.trim().to_ascii_lowercase();
9575                    if action.is_empty() {
9576                        continue;
9577                    }
9578                    match action.as_str() {
9579                        "add" | "change" | "delete" | "view" => {}
9580                        other => {
9581                            return Err(syn::Error::new(
9582                                s.span(),
9583                                format!(
9584                                    "unknown default_permissions action `{other}` — \
9585                                     expected one of `add`, `change`, `delete`, `view`"
9586                                ),
9587                            ));
9588                        }
9589                    }
9590                    if !actions.contains(&action) {
9591                        actions.push(action);
9592                    }
9593                }
9594                if actions.is_empty() {
9595                    return Err(syn::Error::new(
9596                        s.span(),
9597                        "`default_permissions = \"…\"` must list at least one action; \
9598                         use `permissions = false` on the container if you want NO \
9599                         permissions seeded for this model.",
9600                    ));
9601                }
9602                out.default_permissions = actions;
9603                return Ok(());
9604            }
9605            if meta.path.is_ident("get_latest_by") {
9606                // The `-` prefix selects descending order.
9607                let s: LitStr = meta.value()?.parse()?;
9608                let raw = s.value();
9609                let trimmed = raw.trim();
9610                if trimmed.is_empty() {
9611                    return Err(meta.error("`get_latest_by` must name a column"));
9612                }
9613                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9614                    (stripped.to_owned(), true)
9615                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9616                    (stripped.to_owned(), false)
9617                } else {
9618                    (trimmed.to_owned(), false)
9619                };
9620                if col.is_empty() {
9621                    return Err(meta.error("`get_latest_by` must name a column"));
9622                }
9623                out.get_latest_by = Some((col, desc));
9624                return Ok(());
9625            }
9626            if meta.path.is_ident("unique_together") {
9627                // Composite UNIQUE index. Two syntaxes:
9628                //
9629                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9630                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9631                //
9632                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9633                // (col1, col2)`, where <name> defaults to
9634                // `<table>_<col1>_<col2>_uq` when not supplied.
9635                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9636                out.indexes.push(IndexAttr {
9637                    name,
9638                    columns,
9639                    unique: true,
9640                    method: "btree".to_owned(),
9641                    where_clause: None,
9642                include: Vec::new(),
9643                });
9644                return Ok(());
9645            }
9646            if meta.path.is_ident("index_together") {
9647                // Composite (non-unique) index. Two syntaxes
9648                // mirroring `unique_together`.
9649                //
9650                //   #[rustango(index_together = "created_at, status")]
9651                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9652                let (columns, name) = parse_together_attr(&meta, "index_together")?;
9653                out.indexes.push(IndexAttr {
9654                    name,
9655                    columns,
9656                    unique: false,
9657                    method: "btree".to_owned(),
9658                    where_clause: None,
9659                include: Vec::new(),
9660                });
9661                return Ok(());
9662            }
9663            if meta.path.is_ident("unique_when") {
9664                // Partial unique index. Issue #265 / T1.3.
9665                //
9666                //   #[rustango(unique_when(
9667                //       columns   = "email",
9668                //       condition = "deleted_at IS NULL",
9669                //       name      = "unique_active_email"
9670                //   ))]
9671                //
9672                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9673                // on PG / SQLite (both ship partial indexes natively).
9674                // MySQL falls back to a plain UNIQUE index — the
9675                // condition is lost; document the limitation in the
9676                // generated migration.
9677                let mut columns: Option<Vec<String>> = None;
9678                let mut condition: Option<String> = None;
9679                let mut name: Option<String> = None;
9680                let mut include: Vec<String> = Vec::new();
9681                meta.parse_nested_meta(|inner| {
9682                    if inner.path.is_ident("columns") {
9683                        let s: LitStr = inner.value()?.parse()?;
9684                        columns = Some(split_field_list(&s.value()));
9685                        return Ok(());
9686                    }
9687                    if inner.path.is_ident("condition") {
9688                        let s: LitStr = inner.value()?.parse()?;
9689                        condition = Some(s.value());
9690                        return Ok(());
9691                    }
9692                    if inner.path.is_ident("name") {
9693                        let s: LitStr = inner.value()?.parse()?;
9694                        name = Some(s.value());
9695                        return Ok(());
9696                    }
9697                    if inner.path.is_ident("include") {
9698                        // PG 11+ covering-index columns. Non-key columns
9699                        // travel with the index leaf for index-only
9700                        // scans. Dropped on MySQL/SQLite by the writer.
9701                        let s: LitStr = inner.value()?.parse()?;
9702                        include = split_field_list(&s.value());
9703                        return Ok(());
9704                    }
9705                    Err(inner.error(
9706                        "unknown unique_when attribute (supported: \
9707                         `columns = \"...\"`, `condition = \"...\"`, \
9708                         `name = \"...\"`, `include = \"...\"`)",
9709                    ))
9710                })?;
9711                let columns = columns.ok_or_else(|| {
9712                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9713                })?;
9714                let condition = condition.ok_or_else(|| {
9715                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9716                })?;
9717                if columns.is_empty() {
9718                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9719                }
9720                out.indexes.push(IndexAttr {
9721                    name,
9722                    columns,
9723                    unique: true,
9724                    method: "btree".to_owned(),
9725                    where_clause: Some(condition),
9726                    include,
9727                });
9728                return Ok(());
9729            }
9730            if meta.path.is_ident("index_when") {
9731                // Non-unique partial index. Sibling of `unique_when`
9732                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9733                //
9734                //   #[rustango(index_when(
9735                //       columns   = "status, created_at",
9736                //       condition = "deleted_at IS NULL",
9737                //       name      = "active_status_created_idx"
9738                //   ))]
9739                //
9740                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9741                // on PG / SQLite (both ship partial indexes natively).
9742                // MySQL has no native partial-index support — the writer
9743                // emits a plain CREATE INDEX and the condition is lost;
9744                // operators wanting that selectivity on MySQL should
9745                // declare a covering index plus an application-level
9746                // filter.
9747                let mut columns: Option<Vec<String>> = None;
9748                let mut condition: Option<String> = None;
9749                let mut name: Option<String> = None;
9750                let mut method: String = "btree".to_owned();
9751                let mut include: Vec<String> = Vec::new();
9752                meta.parse_nested_meta(|inner| {
9753                    if inner.path.is_ident("columns") {
9754                        let s: LitStr = inner.value()?.parse()?;
9755                        columns = Some(split_field_list(&s.value()));
9756                        return Ok(());
9757                    }
9758                    if inner.path.is_ident("condition") {
9759                        let s: LitStr = inner.value()?.parse()?;
9760                        condition = Some(s.value());
9761                        return Ok(());
9762                    }
9763                    if inner.path.is_ident("name") {
9764                        let s: LitStr = inner.value()?.parse()?;
9765                        name = Some(s.value());
9766                        return Ok(());
9767                    }
9768                    if inner.path.is_ident("method") {
9769                        let s: LitStr = inner.value()?.parse()?;
9770                        method = s.value();
9771                        return Ok(());
9772                    }
9773                    if inner.path.is_ident("include") {
9774                        // PG 11+ covering-index columns; non-key
9775                        // columns travel with the index leaf. Dropped
9776                        // on MySQL/SQLite.
9777                        let s: LitStr = inner.value()?.parse()?;
9778                        include = split_field_list(&s.value());
9779                        return Ok(());
9780                    }
9781                    Err(inner.error(
9782                        "unknown index_when attribute (supported: \
9783                         `columns = \"...\"`, `condition = \"...\"`, \
9784                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9785                         `include = \"...\"`)",
9786                    ))
9787                })?;
9788                let columns = columns
9789                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9790                let condition = condition.ok_or_else(|| {
9791                    meta.error("`index_when(...)` requires `condition = \"...\"`")
9792                })?;
9793                if columns.is_empty() {
9794                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
9795                }
9796                out.indexes.push(IndexAttr {
9797                    name,
9798                    columns,
9799                    unique: false,
9800                    method,
9801                    where_clause: Some(condition),
9802                    include,
9803                });
9804                return Ok(());
9805            }
9806            if meta.path.is_ident("index") {
9807                // Container-level composite index. Two syntaxes:
9808                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
9809                //   #[rustango(index("col1, col2"))]    — call form (same result)
9810                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9811                // The call form takes the column list as a leading string
9812                // literal, then optional `unique` / `name = "..."` /
9813                // `method = "..."` flags (a leading literal can't compose
9814                // under `parse_nested_meta`, so the paren body is parsed by
9815                // hand). `unique_together` / `index_together` remain
9816                // aliases for the same feature.
9817                let cols_lit: LitStr;
9818                let mut unique = false;
9819                let mut name: Option<String> = None;
9820                let mut method = "btree".to_owned();
9821                if meta.input.peek(syn::token::Paren) {
9822                    let content;
9823                    syn::parenthesized!(content in meta.input);
9824                    cols_lit = content.parse()?;
9825                    while content.peek(syn::Token![,]) {
9826                        content.parse::<syn::Token![,]>()?;
9827                        if content.is_empty() {
9828                            break;
9829                        }
9830                        let flag: syn::Ident = content.parse()?;
9831                        if flag == "unique" {
9832                            unique = true;
9833                        } else if flag == "name" {
9834                            content.parse::<syn::Token![=]>()?;
9835                            let s: LitStr = content.parse()?;
9836                            name = Some(s.value());
9837                        } else if flag == "method" {
9838                            content.parse::<syn::Token![=]>()?;
9839                            let s: LitStr = content.parse()?;
9840                            let v = s.value();
9841                            match v.as_str() {
9842                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9843                                | "bloom" => method = v,
9844                                other => {
9845                                    return Err(syn::Error::new(
9846                                        s.span(),
9847                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9848                                    ));
9849                                }
9850                            }
9851                        } else {
9852                            return Err(syn::Error::new(
9853                                flag.span(),
9854                                "unknown index flag (supported: `unique`, `name`, `method`)",
9855                            ));
9856                        }
9857                    }
9858                } else {
9859                    cols_lit = meta.value()?.parse()?;
9860                }
9861                let columns = split_field_list(&cols_lit.value());
9862                out.indexes.push(IndexAttr {
9863                    name,
9864                    columns,
9865                    unique,
9866                    method,
9867                    where_clause: None,
9868                    include: Vec::new(),
9869                });
9870                return Ok(());
9871            }
9872            if meta.path.is_ident("check") {
9873                // #[rustango(check(name = "…", expr = "…"))]
9874                let mut name: Option<String> = None;
9875                let mut expr: Option<String> = None;
9876                meta.parse_nested_meta(|inner| {
9877                    if inner.path.is_ident("name") {
9878                        let s: LitStr = inner.value()?.parse()?;
9879                        name = Some(s.value());
9880                        return Ok(());
9881                    }
9882                    if inner.path.is_ident("expr") {
9883                        let s: LitStr = inner.value()?.parse()?;
9884                        expr = Some(s.value());
9885                        return Ok(());
9886                    }
9887                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9888                })?;
9889                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9890                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9891                out.checks.push(CheckAttr { name, expr });
9892                return Ok(());
9893            }
9894            if meta.path.is_ident("exclude") {
9895                // #[rustango(exclude(name = "…", using = "gist",
9896                //                    elements = "col WITH op, col WITH op",
9897                //                    where = "…"))]
9898                let mut name: Option<String> = None;
9899                let mut using: Option<String> = None;
9900                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9901                let mut where_clause: Option<String> = None;
9902                meta.parse_nested_meta(|inner| {
9903                    if inner.path.is_ident("name") {
9904                        let s: LitStr = inner.value()?.parse()?;
9905                        name = Some(s.value());
9906                        return Ok(());
9907                    }
9908                    if inner.path.is_ident("using") {
9909                        let s: LitStr = inner.value()?.parse()?;
9910                        using = Some(s.value());
9911                        return Ok(());
9912                    }
9913                    if inner.path.is_ident("elements") {
9914                        let s: LitStr = inner.value()?.parse()?;
9915                        elements_raw = Some((s.value(), s.span()));
9916                        return Ok(());
9917                    }
9918                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9919                        let s: LitStr = inner.value()?.parse()?;
9920                        where_clause = Some(s.value());
9921                        return Ok(());
9922                    }
9923                    Err(inner.error(
9924                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9925                    ))
9926                })?;
9927                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9928                let using = using.unwrap_or_else(|| "gist".to_owned());
9929                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
9930                    meta.error(
9931                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
9932                    )
9933                })?;
9934                // Parse `col WITH op` pairs separated by commas.
9935                let mut elements: Vec<(String, String)> = Vec::new();
9936                for pair in elements_str.split(',') {
9937                    let pair = pair.trim();
9938                    if pair.is_empty() {
9939                        continue;
9940                    }
9941                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
9942                    let col = split.next().unwrap_or("").trim();
9943                    let rest = split.next().unwrap_or("").trim();
9944                    // `WITH op` — case-insensitive on `WITH`, then op.
9945                    let rest_lc = rest.to_ascii_lowercase();
9946                    let op = rest_lc
9947                        .strip_prefix("with")
9948                        .map(|r| r.trim_start())
9949                        .filter(|r| !r.is_empty())
9950                        .map(|_| {
9951                            // Pull the original-case op from `rest` after the
9952                            // `WITH ` token (5 chars).
9953                            rest[4..].trim_start().to_owned()
9954                        });
9955                    let Some(op) = op else {
9956                        return Err(syn::Error::new(
9957                            elements_span,
9958                            format!(
9959                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
9960                                 (e.g. `room_id WITH =` or `during WITH &&`)"
9961                            ),
9962                        ));
9963                    };
9964                    if col.is_empty() || op.is_empty() {
9965                        return Err(syn::Error::new(
9966                            elements_span,
9967                            format!(
9968                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
9969                                 (both sides non-empty)"
9970                            ),
9971                        ));
9972                    }
9973                    elements.push((col.to_owned(), op));
9974                }
9975                if elements.is_empty() {
9976                    return Err(syn::Error::new(
9977                        elements_span,
9978                        "exclude requires at least one `col WITH op` element",
9979                    ));
9980                }
9981                out.excludes.push(ExcludeAttr {
9982                    name,
9983                    using,
9984                    elements,
9985                    where_clause,
9986                });
9987                return Ok(());
9988            }
9989            if meta.path.is_ident("generic_fk") {
9990                let mut gfk = GenericFkAttr {
9991                    name: String::new(),
9992                    ct_column: String::new(),
9993                    pk_column: String::new(),
9994                };
9995                meta.parse_nested_meta(|inner| {
9996                    if inner.path.is_ident("name") {
9997                        let s: LitStr = inner.value()?.parse()?;
9998                        gfk.name = s.value();
9999                        return Ok(());
10000                    }
10001                    if inner.path.is_ident("ct_column") {
10002                        let s: LitStr = inner.value()?.parse()?;
10003                        gfk.ct_column = s.value();
10004                        return Ok(());
10005                    }
10006                    if inner.path.is_ident("pk_column") {
10007                        let s: LitStr = inner.value()?.parse()?;
10008                        gfk.pk_column = s.value();
10009                        return Ok(());
10010                    }
10011                    Err(inner.error(
10012                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10013                    ))
10014                })?;
10015                if gfk.name.is_empty() {
10016                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10017                }
10018                if gfk.ct_column.is_empty() {
10019                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10020                }
10021                if gfk.pk_column.is_empty() {
10022                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10023                }
10024                out.generic_fks.push(gfk);
10025                return Ok(());
10026            }
10027            if meta.path.is_ident("fk_composite") {
10028                let mut fk = CompositeFkAttr {
10029                    name: String::new(),
10030                    to: String::new(),
10031                    from: Vec::new(),
10032                    on: Vec::new(),
10033                };
10034                meta.parse_nested_meta(|inner| {
10035                    if inner.path.is_ident("name") {
10036                        let s: LitStr = inner.value()?.parse()?;
10037                        fk.name = s.value();
10038                        return Ok(());
10039                    }
10040                    if inner.path.is_ident("to") {
10041                        let s: LitStr = inner.value()?.parse()?;
10042                        fk.to = s.value();
10043                        return Ok(());
10044                    }
10045                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10046                    // comma-list of string literals.
10047                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10048                        let value = inner.value()?;
10049                        let content;
10050                        syn::parenthesized!(content in value);
10051                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10052                            content.parse_terminated(
10053                                |p| p.parse::<syn::LitStr>(),
10054                                syn::Token![,],
10055                            )?;
10056                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10057                        if inner.path.is_ident("on") {
10058                            fk.on = cols;
10059                        } else {
10060                            fk.from = cols;
10061                        }
10062                        return Ok(());
10063                    }
10064                    Err(inner.error(
10065                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10066                    ))
10067                })?;
10068                if fk.name.is_empty() {
10069                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10070                }
10071                if fk.to.is_empty() {
10072                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10073                }
10074                if fk.from.is_empty() || fk.on.is_empty() {
10075                    return Err(meta.error(
10076                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10077                    ));
10078                }
10079                if fk.from.len() != fk.on.len() {
10080                    return Err(meta.error(format!(
10081                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10082                        fk.from.len(),
10083                        fk.on.len(),
10084                    )));
10085                }
10086                out.composite_fks.push(fk);
10087                return Ok(());
10088            }
10089            if meta.path.is_ident("m2m") {
10090                let mut m2m = M2MAttr {
10091                    name: String::new(),
10092                    to: String::new(),
10093                    through: String::new(),
10094                    src: String::new(),
10095                    dst: String::new(),
10096                    auto_create: true,
10097                };
10098                meta.parse_nested_meta(|inner| {
10099                    if inner.path.is_ident("name") {
10100                        let s: LitStr = inner.value()?.parse()?;
10101                        m2m.name = s.value();
10102                        return Ok(());
10103                    }
10104                    if inner.path.is_ident("to") {
10105                        let s: LitStr = inner.value()?.parse()?;
10106                        m2m.to = s.value();
10107                        return Ok(());
10108                    }
10109                    if inner.path.is_ident("through") {
10110                        let s: LitStr = inner.value()?.parse()?;
10111                        m2m.through = s.value();
10112                        return Ok(());
10113                    }
10114                    if inner.path.is_ident("src") {
10115                        let s: LitStr = inner.value()?.parse()?;
10116                        m2m.src = s.value();
10117                        return Ok(());
10118                    }
10119                    if inner.path.is_ident("dst") {
10120                        let s: LitStr = inner.value()?.parse()?;
10121                        m2m.dst = s.value();
10122                        return Ok(());
10123                    }
10124                    if inner.path.is_ident("auto_create") {
10125                        let lit: syn::LitBool = inner.value()?.parse()?;
10126                        m2m.auto_create = lit.value;
10127                        return Ok(());
10128                    }
10129                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10130                })?;
10131                if m2m.name.is_empty() {
10132                    return Err(meta.error("m2m requires `name = \"...\"`"));
10133                }
10134                if m2m.to.is_empty() {
10135                    return Err(meta.error("m2m requires `to = \"...\"`"));
10136                }
10137                if m2m.through.is_empty() {
10138                    return Err(meta.error("m2m requires `through = \"...\"`"));
10139                }
10140                if m2m.src.is_empty() {
10141                    return Err(meta.error("m2m requires `src = \"...\"`"));
10142                }
10143                if m2m.dst.is_empty() {
10144                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10145                }
10146                out.m2m.push(m2m);
10147                return Ok(());
10148            }
10149            if meta.path.is_ident("generic_m2m") {
10150                let mut gm = GenericM2MAttr {
10151                    name: String::new(),
10152                    through: String::new(),
10153                    pk_column: String::new(),
10154                    ct_column: String::new(),
10155                    related_column: String::new(),
10156                };
10157                meta.parse_nested_meta(|inner| {
10158                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10159                        let s: LitStr = inner.value()?.parse()?;
10160                        Ok(s.value())
10161                    };
10162                    if inner.path.is_ident("name") {
10163                        gm.name = field(&inner)?;
10164                        return Ok(());
10165                    }
10166                    if inner.path.is_ident("through") {
10167                        gm.through = field(&inner)?;
10168                        return Ok(());
10169                    }
10170                    if inner.path.is_ident("pk_column") {
10171                        gm.pk_column = field(&inner)?;
10172                        return Ok(());
10173                    }
10174                    if inner.path.is_ident("ct_column") {
10175                        gm.ct_column = field(&inner)?;
10176                        return Ok(());
10177                    }
10178                    if inner.path.is_ident("related_column") {
10179                        gm.related_column = field(&inner)?;
10180                        return Ok(());
10181                    }
10182                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10183                })?;
10184                for (val, label) in [
10185                    (&gm.name, "name"),
10186                    (&gm.through, "through"),
10187                    (&gm.pk_column, "pk_column"),
10188                    (&gm.ct_column, "ct_column"),
10189                    (&gm.related_column, "related_column"),
10190                ] {
10191                    if val.is_empty() {
10192                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10193                    }
10194                }
10195                out.generic_m2m.push(gm);
10196                return Ok(());
10197            }
10198            Err(meta.error("unknown rustango container attribute"))
10199        })?;
10200    }
10201    Ok(out)
10202}
10203
10204/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10205/// owned field names, trimming whitespace and skipping empty entries.
10206/// Field-name validation against the model is done by the caller.
10207fn split_field_list(raw: &str) -> Vec<String> {
10208    raw.split(',')
10209        .map(str::trim)
10210        .filter(|s| !s.is_empty())
10211        .map(str::to_owned)
10212        .collect()
10213}
10214
10215/// Shared parser for `unique_together` and `index_together` container
10216/// attrs. Accepts both shapes:
10217///
10218///   * `attr = "col1, col2"`              — auto-derived index name.
10219///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10220///
10221/// Returns `(columns, name)`.
10222fn parse_together_attr(
10223    meta: &syn::meta::ParseNestedMeta<'_>,
10224    attr: &str,
10225) -> syn::Result<(Vec<String>, Option<String>)> {
10226    // Disambiguate by whether the next token is `=` (key-value) or
10227    // `(` (parenthesized).
10228    if meta.input.peek(syn::Token![=]) {
10229        let cols_lit: LitStr = meta.value()?.parse()?;
10230        let columns = split_field_list(&cols_lit.value());
10231        check_together_columns(meta, attr, &columns)?;
10232        return Ok((columns, None));
10233    }
10234    let mut columns: Option<Vec<String>> = None;
10235    let mut name: Option<String> = None;
10236    meta.parse_nested_meta(|inner| {
10237        if inner.path.is_ident("columns") {
10238            let s: LitStr = inner.value()?.parse()?;
10239            columns = Some(split_field_list(&s.value()));
10240            return Ok(());
10241        }
10242        if inner.path.is_ident("name") {
10243            let s: LitStr = inner.value()?.parse()?;
10244            name = Some(s.value());
10245            return Ok(());
10246        }
10247        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10248    })?;
10249    let columns = columns.ok_or_else(|| {
10250        meta.error(format!(
10251            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10252        ))
10253    })?;
10254    check_together_columns(meta, attr, &columns)?;
10255    Ok((columns, name))
10256}
10257
10258fn check_together_columns(
10259    meta: &syn::meta::ParseNestedMeta<'_>,
10260    attr: &str,
10261    columns: &[String],
10262) -> syn::Result<()> {
10263    if columns.len() < 2 {
10264        let single = if attr == "unique_together" {
10265            "#[rustango(unique)] on the field"
10266        } else {
10267            "#[rustango(index)] on the field"
10268        };
10269        return Err(meta.error(format!(
10270            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10271        )));
10272    }
10273    Ok(())
10274}
10275
10276/// Parse the fieldsets DSL: pipe-separated sections, optional
10277/// `"Title:"` prefix on each, comma-separated field names after.
10278/// Examples:
10279/// * `"name, office"` → one untitled section with two fields
10280/// * `"Identity: name, office | Metadata: created_at"` → two titled
10281///   sections
10282///
10283/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10284fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10285    raw.split('|')
10286        .map(str::trim)
10287        .filter(|s| !s.is_empty())
10288        .map(|section| {
10289            // Split off an optional `Title:` prefix (first colon).
10290            let (title, rest) = match section.split_once(':') {
10291                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10292                _ => (String::new(), section),
10293            };
10294            let fields = split_field_list(rest);
10295            (title, fields)
10296        })
10297        .collect()
10298}
10299
10300/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10301/// comma-separated entry maps a target field to one or more source
10302/// fields joined with `+`. Whitespace around tokens is trimmed.
10303/// Entries missing `:` or with empty target/source lists are dropped.
10304fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10305    raw.split(',')
10306        .map(str::trim)
10307        .filter(|s| !s.is_empty())
10308        .filter_map(|entry| {
10309            let (target, sources_raw) = entry.split_once(':')?;
10310            let target = target.trim().to_owned();
10311            if target.is_empty() {
10312                return None;
10313            }
10314            let sources: Vec<String> = sources_raw
10315                .split('+')
10316                .map(|s| s.trim().to_owned())
10317                .filter(|s| !s.is_empty())
10318                .collect();
10319            if sources.is_empty() {
10320                return None;
10321            }
10322            Some((target, sources))
10323        })
10324        .collect()
10325}
10326
10327/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10328/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10329/// and empty halves drop silently — the macro layer only enforces shape,
10330/// not field-name vs. widget-name validity (those checks happen at
10331/// `AdminConfig` consumption time). Issue #359.
10332fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10333    raw.split(',')
10334        .map(str::trim)
10335        .filter(|s| !s.is_empty())
10336        .filter_map(|entry| {
10337            let (field, widget) = entry.split_once(':')?;
10338            let field = field.trim().to_owned();
10339            let widget = widget.trim().to_owned();
10340            if field.is_empty() || widget.is_empty() {
10341                return None;
10342            }
10343            Some((field, widget))
10344        })
10345        .collect()
10346}
10347
10348/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10349/// Returns `(field_name, desc)` pairs in the same order as the input.
10350fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10351    raw.split(',')
10352        .map(str::trim)
10353        .filter(|s| !s.is_empty())
10354        .map(|spec| {
10355            spec.strip_prefix('-')
10356                .map_or((spec.to_owned(), false), |rest| {
10357                    (rest.trim().to_owned(), true)
10358                })
10359        })
10360        .collect()
10361}
10362
10363struct FieldAttrs {
10364    column: Option<String>,
10365    primary_key: bool,
10366    fk: Option<String>,
10367    o2o: Option<String>,
10368    on: Option<String>,
10369    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10370    /// "set_default" | "no_action")]` — what happens to this row when
10371    /// the row it points at is deleted.
10372    /// Only meaningful when `fk` / `o2o` is
10373    /// also set; the macro errors at compile time if applied to a
10374    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10375    /// DDL writer renders `ON DELETE <action>` after the constraint
10376    /// clause when this is `Some`; `None` falls back to the database
10377    /// default (NO ACTION on every backend rustango supports).
10378    on_delete: Option<String>,
10379    /// `#[rustango(related_name = "...")]` — per-FK
10380    /// reverse-accessor override. When set, the derive emits
10381    /// `Parent::<related_name>[_pool]` instead of the container-level
10382    /// `default_related_name` or the `<child_snake>_set[_pool]`
10383    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10384    /// silently ignored on non-FK fields. Follow-up to #816.
10385    related_name: Option<String>,
10386    max_length: Option<u32>,
10387    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10388    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10389    /// unconstrained `vector` column.
10390    vector_dims: Option<u32>,
10391    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10392    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10393    /// unconstrained `geometry(Point)` column (SRID 0).
10394    geometry_srid: Option<u32>,
10395    min: Option<i64>,
10396    max: Option<i64>,
10397    default: Option<String>,
10398    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10399    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10400    /// "gen_random_uuid()"`. The Rust field type must be
10401    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10402    /// INSERTs so the DB DEFAULT fires.
10403    auto_uuid: bool,
10404    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10405    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10406    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10407    /// the field is `Auto::Unset`, then bound as a normal parameter
10408    /// rather than relying on a per-dialect DB function. Issue #823
10409    /// (Eloquent `HasUuids`).
10410    ///
10411    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10412    /// Composes with every backend (PG / MySQL / SQLite) — no
10413    /// `gen_random_uuid()` requirement on the database.
10414    default_uuid_v7: bool,
10415    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10416    /// Server-set on insert, immutable from app code afterwards.
10417    /// Implies `auto + default = "now()"`. Field type must be
10418    /// `DateTime<Utc>`.
10419    auto_now_add: bool,
10420    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10421    /// every insert AND every update. Implies `auto + default =
10422    /// "now()"`; the macro additionally rewrites `update_on` /
10423    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10424    /// field value.
10425    auto_now: bool,
10426    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10427    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10428    /// `soft_delete_on(executor)` and `restore_on(executor)`
10429    /// methods on the model.
10430    soft_delete: bool,
10431    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10432    /// the column in the generated DDL.
10433    unique: bool,
10434    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10435    /// generates a `CREATE INDEX` for this column. `unique` here means
10436    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10437    index: bool,
10438    index_unique: bool,
10439    index_name: Option<String>,
10440    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10441    /// `"btree"`. Issue #34.
10442    index_method: String,
10443    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10444    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10445    /// the macro skips this column from every INSERT and UPDATE
10446    /// path, so the database always recomputes the value from
10447    /// `EXPR`. Backlog item #35.
10448    generated_as: Option<String>,
10449    /// `#[rustango(help_text = "…")]` — help text
10450    /// rendered below the admin form's input. Threaded into
10451    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10452    /// layers can read it.
10453    help_text: Option<String>,
10454    /// `#[rustango(choices = "value:Label, value:Label")]` — the
10455    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10456    /// as a `&'static [(&'static str, &'static str)]` slice. When
10457    /// present, the admin form renders a `<select>` instead of `<input>`
10458    /// and the validator rejects values not in the list. Only meaningful
10459    /// for `FieldType::String`; the macro errors at compile time if
10460    /// applied to a non-string field.
10461    choices: Option<Vec<(String, String)>>,
10462    /// `#[rustango(db_comment = "…")]` — DB-side column
10463    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10464    /// the comment in CREATE TABLE; Postgres emits a separate
10465    /// `COMMENT ON COLUMN` statement after the table is created;
10466    /// SQLite silently drops the value (no native column comments).
10467    db_comment: Option<String>,
10468    /// `#[rustango(verbose_name = "…")]` — human-readable
10469    /// label for the field. Threaded into `FieldSchema::verbose_name`
10470    /// so admin column headers, form labels, and other display
10471    /// surfaces can prefer the friendly caption over the Rust
10472    /// identifier. `None` means renderers fall back to the field name.
10473    verbose_name: Option<String>,
10474    /// `#[rustango(editable = false)]` — opt out of
10475    /// auto-generated form rendering. Defaults to `true` so existing
10476    /// fields keep their current admin / form behavior; setting
10477    /// `false` removes the field from the admin change-form entirely
10478    /// (the value is still visible on detail / list views, just not
10479    /// editable).
10480    editable: bool,
10481    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10482    /// "form may submit empty even when DB is NOT NULL". Threaded into
10483    /// `FieldSchema::blank`. Defaults to `false`.
10484    blank: bool,
10485    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10486    /// case-insensitive text column. Threaded into
10487    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10488    /// fields; the macro errors at derive time if applied elsewhere.
10489    case_insensitive: bool,
10490    /// `#[rustango(validators = "email,url")]` — the
10491    /// model-level validator chain. Comma-separated names that
10492    /// dispatch to the `validators::*` family in `validate_value`.
10493    /// Empty by default; fires on every typed INSERT/UPDATE.
10494    validators: Vec<String>,
10495}
10496
10497fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10498    let mut out = FieldAttrs {
10499        column: None,
10500        primary_key: false,
10501        fk: None,
10502        o2o: None,
10503        on: None,
10504        on_delete: None,
10505        related_name: None,
10506        max_length: None,
10507        vector_dims: None,
10508        geometry_srid: None,
10509        min: None,
10510        max: None,
10511        default: None,
10512        auto_uuid: false,
10513        default_uuid_v7: false,
10514        auto_now_add: false,
10515        auto_now: false,
10516        soft_delete: false,
10517        unique: false,
10518        index: false,
10519        index_unique: false,
10520        index_name: None,
10521        index_method: "btree".to_owned(),
10522        generated_as: None,
10523        help_text: None,
10524        choices: None,
10525        db_comment: None,
10526        verbose_name: None,
10527        editable: true,
10528        blank: false,
10529        case_insensitive: false,
10530        validators: Vec::new(),
10531    };
10532    for attr in &field.attrs {
10533        if !attr.path().is_ident("rustango") {
10534            continue;
10535        }
10536        attr.parse_nested_meta(|meta| {
10537            if meta.path.is_ident("column") {
10538                let s: LitStr = meta.value()?.parse()?;
10539                let name = s.value();
10540                validate_sql_identifier(&name, "column", s.span())?;
10541                out.column = Some(name);
10542                return Ok(());
10543            }
10544            if meta.path.is_ident("primary_key") {
10545                out.primary_key = true;
10546                return Ok(());
10547            }
10548            if meta.path.is_ident("fk") {
10549                let s: LitStr = meta.value()?.parse()?;
10550                out.fk = Some(s.value());
10551                return Ok(());
10552            }
10553            if meta.path.is_ident("o2o") {
10554                let s: LitStr = meta.value()?.parse()?;
10555                out.o2o = Some(s.value());
10556                return Ok(());
10557            }
10558            if meta.path.is_ident("on") {
10559                let s: LitStr = meta.value()?.parse()?;
10560                out.on = Some(s.value());
10561                return Ok(());
10562            }
10563            if meta.path.is_ident("on_delete") {
10564                let s: LitStr = meta.value()?.parse()?;
10565                let raw = s.value();
10566                let normalized = raw.trim().to_ascii_lowercase();
10567                // Validate at parse time so the user gets a clear span
10568                // rather than a downstream compile error in the emit.
10569                match normalized.as_str() {
10570                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10571                    _ => {
10572                        return Err(syn::Error::new(
10573                            s.span(),
10574                            format!(
10575                                "unknown on_delete action `{raw}`; expected one of \
10576                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10577                            ),
10578                        ));
10579                    }
10580                }
10581                out.on_delete = Some(normalized);
10582                return Ok(());
10583            }
10584            if meta.path.is_ident("related_name") {
10585                let s: LitStr = meta.value()?.parse()?;
10586                let raw = s.value();
10587                if raw.trim().is_empty() {
10588                    return Err(syn::Error::new(
10589                        s.span(),
10590                        "`related_name` must be a non-empty identifier",
10591                    ));
10592                }
10593                // Validate as a Rust-ident shape — the value becomes
10594                // a method name on the parent type. Same rule as
10595                // `default_related_name` so the two surfaces match.
10596                if !raw
10597                    .chars()
10598                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10599                    || raw.starts_with(char::is_numeric)
10600                {
10601                    return Err(syn::Error::new(
10602                        s.span(),
10603                        "`related_name` must be snake_case ASCII (lowercase letters, \
10604                         digits, underscores; no leading digit)",
10605                    ));
10606                }
10607                out.related_name = Some(raw);
10608                return Ok(());
10609            }
10610            if meta.path.is_ident("max_length") {
10611                let lit: syn::LitInt = meta.value()?.parse()?;
10612                out.max_length = Some(lit.base10_parse::<u32>()?);
10613                return Ok(());
10614            }
10615            // `#[rustango(vector(dims = N))]` — pgvector column
10616            // dimension (#824). Nested-meta form so it reads like the
10617            // other typed-column attrs.
10618            if meta.path.is_ident("vector") {
10619                meta.parse_nested_meta(|inner| {
10620                    if inner.path.is_ident("dims") {
10621                        let lit: syn::LitInt = inner.value()?.parse()?;
10622                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10623                        return Ok(());
10624                    }
10625                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10626                })?;
10627                return Ok(());
10628            }
10629            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10630            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10631            if meta.path.is_ident("geometry") {
10632                meta.parse_nested_meta(|inner| {
10633                    if inner.path.is_ident("srid") {
10634                        let lit: syn::LitInt = inner.value()?.parse()?;
10635                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10636                        return Ok(());
10637                    }
10638                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10639                })?;
10640                return Ok(());
10641            }
10642            if meta.path.is_ident("min") {
10643                out.min = Some(parse_signed_i64(&meta)?);
10644                return Ok(());
10645            }
10646            if meta.path.is_ident("max") {
10647                out.max = Some(parse_signed_i64(&meta)?);
10648                return Ok(());
10649            }
10650            if meta.path.is_ident("default") {
10651                let s: LitStr = meta.value()?.parse()?;
10652                out.default = Some(s.value());
10653                return Ok(());
10654            }
10655            if meta.path.is_ident("generated_as") {
10656                let s: LitStr = meta.value()?.parse()?;
10657                out.generated_as = Some(s.value());
10658                return Ok(());
10659            }
10660            if meta.path.is_ident("help_text") {
10661                let s: LitStr = meta.value()?.parse()?;
10662                out.help_text = Some(s.value());
10663                return Ok(());
10664            }
10665            if meta.path.is_ident("choices") {
10666                let s: LitStr = meta.value()?.parse()?;
10667                let raw = s.value();
10668                let mut pairs: Vec<(String, String)> = Vec::new();
10669                for chunk in raw.split(',') {
10670                    let chunk = chunk.trim();
10671                    if chunk.is_empty() {
10672                        continue;
10673                    }
10674                    let (value, label) = match chunk.split_once(':') {
10675                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10676                        None => (chunk.to_owned(), chunk.to_owned()),
10677                    };
10678                    if value.is_empty() {
10679                        return Err(syn::Error::new(
10680                            s.span(),
10681                            "`choices` entry has empty value before `:`",
10682                        ));
10683                    }
10684                    pairs.push((value, label));
10685                }
10686                if pairs.is_empty() {
10687                    return Err(syn::Error::new(
10688                        s.span(),
10689                        "`choices = \"…\"` must contain at least one value",
10690                    ));
10691                }
10692                out.choices = Some(pairs);
10693                return Ok(());
10694            }
10695            if meta.path.is_ident("db_comment") {
10696                let s: LitStr = meta.value()?.parse()?;
10697                out.db_comment = Some(s.value());
10698                return Ok(());
10699            }
10700            if meta.path.is_ident("verbose_name") {
10701                let s: LitStr = meta.value()?.parse()?;
10702                out.verbose_name = Some(s.value());
10703                return Ok(());
10704            }
10705            if meta.path.is_ident("editable") {
10706                // Two forms accepted:
10707                //   #[rustango(editable = false)] / true — explicit
10708                //   #[rustango(editable)] — flag form (= true, the
10709                //   default, so harmless; included for symmetry)
10710                if let Ok(v) = meta.value() {
10711                    let lit: syn::LitBool = v.parse()?;
10712                    out.editable = lit.value;
10713                } else {
10714                    out.editable = true;
10715                }
10716                return Ok(());
10717            }
10718            if meta.path.is_ident("blank") {
10719                // Two forms accepted:
10720                //   #[rustango(blank)] — flag form, true
10721                //   #[rustango(blank = false)] / true — explicit
10722                if let Ok(v) = meta.value() {
10723                    let lit: syn::LitBool = v.parse()?;
10724                    out.blank = lit.value;
10725                } else {
10726                    out.blank = true;
10727                }
10728                return Ok(());
10729            }
10730            if meta.path.is_ident("citext") {
10731                // Case-insensitive text column (#344). Two forms:
10732                //   #[rustango(citext)]          — flag form, true
10733                //   #[rustango(citext = true)]   — explicit
10734                //   #[rustango(citext = false)]  — explicit opt-out
10735                // String-only validation lives in the field-type
10736                // emitter (the FieldType discriminant is computed in
10737                // `detect_type`); the macro records the flag and the
10738                // DDL writer emits dialect-specific COLLATE / CITEXT.
10739                if let Ok(v) = meta.value() {
10740                    let lit: syn::LitBool = v.parse()?;
10741                    out.case_insensitive = lit.value;
10742                } else {
10743                    out.case_insensitive = true;
10744                }
10745                return Ok(());
10746            }
10747            if meta.path.is_ident("validators") {
10748                let s: LitStr = meta.value()?.parse()?;
10749                let raw = s.value();
10750                out.validators = raw
10751                    .split(',')
10752                    .map(str::trim)
10753                    .filter(|s| !s.is_empty())
10754                    .map(str::to_owned)
10755                    .collect();
10756                if out.validators.is_empty() {
10757                    return Err(syn::Error::new(
10758                        s.span(),
10759                        "`validators = \"…\"` must list at least one name",
10760                    ));
10761                }
10762                return Ok(());
10763            }
10764            if meta.path.is_ident("auto_uuid") {
10765                out.auto_uuid = true;
10766                // Implied: PK + auto + DEFAULT gen_random_uuid().
10767                // Each is also explicitly settable; the explicit
10768                // value wins if conflicting.
10769                out.primary_key = true;
10770                if out.default.is_none() {
10771                    out.default = Some("gen_random_uuid()".into());
10772                }
10773                return Ok(());
10774            }
10775            if meta.path.is_ident("default_uuid_v7") {
10776                // Backend-neutral counterpart of `auto_uuid` — issue #823.
10777                // No SQL DEFAULT (the macro fills the value Rust-side
10778                // before binding); just mark the field as PK + Auto
10779                // so the insert path is the `Auto::Unset → generate`
10780                // branch.
10781                out.default_uuid_v7 = true;
10782                out.primary_key = true;
10783                return Ok(());
10784            }
10785            if meta.path.is_ident("auto_now_add") {
10786                out.auto_now_add = true;
10787                if out.default.is_none() {
10788                    out.default = Some("now()".into());
10789                }
10790                return Ok(());
10791            }
10792            if meta.path.is_ident("auto_now") {
10793                out.auto_now = true;
10794                if out.default.is_none() {
10795                    out.default = Some("now()".into());
10796                }
10797                return Ok(());
10798            }
10799            if meta.path.is_ident("soft_delete") {
10800                out.soft_delete = true;
10801                return Ok(());
10802            }
10803            if meta.path.is_ident("unique") {
10804                out.unique = true;
10805                return Ok(());
10806            }
10807            if meta.path.is_ident("index") {
10808                out.index = true;
10809                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10810                if meta.input.peek(syn::token::Paren) {
10811                    meta.parse_nested_meta(|inner| {
10812                        if inner.path.is_ident("unique") {
10813                            out.index_unique = true;
10814                            return Ok(());
10815                        }
10816                        if inner.path.is_ident("name") {
10817                            let s: LitStr = inner.value()?.parse()?;
10818                            out.index_name = Some(s.value());
10819                            return Ok(());
10820                        }
10821                        if inner.path.is_ident("method") {
10822                            let s: LitStr = inner.value()?.parse()?;
10823                            let v = s.value();
10824                            match v.as_str() {
10825                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10826                                    out.index_method = v;
10827                                }
10828                                other => {
10829                                    return Err(inner.error(format!(
10830                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10831                                    )));
10832                                }
10833                            }
10834                            return Ok(());
10835                        }
10836                        Err(inner.error(
10837                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10838                        ))
10839                    })?;
10840                }
10841                return Ok(());
10842            }
10843            Err(meta.error("unknown rustango field attribute"))
10844        })?;
10845    }
10846    Ok(out)
10847}
10848
10849/// Parse a signed integer literal, accepting optional leading `-`.
10850fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10851    let expr: syn::Expr = meta.value()?.parse()?;
10852    match expr {
10853        syn::Expr::Lit(syn::ExprLit {
10854            lit: syn::Lit::Int(lit),
10855            ..
10856        }) => lit.base10_parse::<i64>(),
10857        syn::Expr::Unary(syn::ExprUnary {
10858            op: syn::UnOp::Neg(_),
10859            expr,
10860            ..
10861        }) => {
10862            if let syn::Expr::Lit(syn::ExprLit {
10863                lit: syn::Lit::Int(lit),
10864                ..
10865            }) = *expr
10866            {
10867                let v: i64 = lit.base10_parse()?;
10868                Ok(-v)
10869            } else {
10870                Err(syn::Error::new_spanned(expr, "expected integer literal"))
10871            }
10872        }
10873        other => Err(syn::Error::new_spanned(
10874            other,
10875            "expected integer literal (signed)",
10876        )),
10877    }
10878}
10879
10880struct FieldInfo<'a> {
10881    ident: &'a syn::Ident,
10882    column: String,
10883    primary_key: bool,
10884    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10885    /// skip this column when `Auto::Unset` and emit it under
10886    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10887    auto: bool,
10888    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10889    /// the `Column::Value` associated type for typed-column tokens.
10890    value_ty: &'a Type,
10891    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10892    field_type_tokens: TokenStream2,
10893    schema: TokenStream2,
10894    from_row_init: TokenStream2,
10895    /// Variant of [`Self::from_row_init`] that reads the column via
10896    /// `format!("{prefix}__{col}")` so a model can be decoded out of
10897    /// the aliased columns of a JOINed row. Drives slice 9.0d's
10898    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10899    /// helper that `select_related` calls when stitching loaded FKs.
10900    from_aliased_row_init: TokenStream2,
10901    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10902    /// relation helper emit (`Author::<child>_set`) needs to know `T`
10903    /// to point the generated method at the right child model.
10904    fk_inner: Option<Type>,
10905    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10906    /// `kind` (since ForeignKey detection sets `kind` to K's
10907    /// underlying type) but stored separately for clarity at the
10908    /// `FkRelation` construction site, which only sees the FK's
10909    /// surface fields.
10910    fk_pk_kind: DetectedKind,
10911    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10912    /// the bare `ForeignKey<T, K>`. Routes the load_related and
10913    /// fk_pk_access emitters to wrap assignments / accessors in
10914    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10915    /// FK column compiles end-to-end. The DDL writer reads this off
10916    /// the field schema (`nullable` flag); the macro just needs to
10917    /// keep the Rust-side codegen consistent.
10918    nullable: bool,
10919    /// `true` when this column was marked `#[rustango(auto_now)]` —
10920    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10921    /// column instead of the user-supplied value, so `updated_at`
10922    /// always reflects the latest write without the caller having
10923    /// to remember to set it.
10924    auto_now: bool,
10925    /// `true` when this column was marked `#[rustango(auto_now_add)]`
10926    /// — the column is server-set on INSERT (DB DEFAULT) and
10927    /// **immutable** afterwards. `update_on` / `save_on` skip the
10928    /// column entirely so a stale `created_at` value in memory never
10929    /// rewrites the persisted timestamp.
10930    auto_now_add: bool,
10931    /// `true` when this column was marked `#[rustango(soft_delete)]`.
10932    /// Triggers emission of `soft_delete_on(executor)` and
10933    /// `restore_on(executor)` on the model's inherent impl. There is
10934    /// at most one such column per model — emission asserts this.
10935    soft_delete: bool,
10936    /// `Some` when this column was marked
10937    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
10938    /// every INSERT and UPDATE path; the database recomputes the
10939    /// value from `EXPR`. Backlog item #35.
10940    generated_as: Option<String>,
10941    /// `true` when this column was marked
10942    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
10943    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
10944    /// with `Uuid::now_v7()` before binding, so the PK is generated
10945    /// Rust-side and the column is always present in the INSERT
10946    /// statement (no DB DEFAULT requirement). Issue #823.
10947    default_uuid_v7: bool,
10948    /// `Some` when this FK field carried `#[rustango(related_name =
10949    /// "...")]`. Threaded into [`FkRelation::related_name`] and
10950    /// consumed by [`reverse_helper_tokens`] to override the default
10951    /// `<child_snake>_set` accessor name. Follow-up to #816.
10952    related_name: Option<String>,
10953}
10954
10955/// Reject table names that won't survive SQL identifier
10956/// derivation downstream. Postgres' regular-identifier rule
10957/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
10958/// through the framework's unquoted FK / index / constraint name
10959/// emission without surprises. We also disallow leading-digit and
10960/// the empty string for clarity.
10961///
10962/// Reserved-word collisions (`select`, `from`, …) aren't flagged
10963/// here — those produce a runtime error from the SQL parser,
10964/// which is loud enough; statically enumerating reserved words
10965/// across the three supported dialects is more friction than help.
10966///
10967/// Backlog item #65.
10968fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
10969    validate_sql_identifier(name, "table", span)
10970}
10971
10972/// Reject SQL identifiers that compile but break downstream SQL
10973/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
10974/// `kind` is "table" / "column" — used for the error message so users
10975/// see which attribute caused the failure.
10976fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
10977    if name.is_empty() {
10978        return Err(syn::Error::new(
10979            span,
10980            format!("`{kind} = \"\"` is not a valid SQL identifier"),
10981        ));
10982    }
10983    let mut chars = name.chars();
10984    let first = chars.next().unwrap();
10985    if !(first.is_ascii_alphabetic() || first == '_') {
10986        return Err(syn::Error::new(
10987            span,
10988            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
10989        ));
10990    }
10991    for c in chars {
10992        if !(c.is_ascii_alphanumeric() || c == '_') {
10993            return Err(syn::Error::new(
10994                span,
10995                format!(
10996                    "{kind} name `{name}` contains invalid character {c:?} — \
10997                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
10998                     Hyphens in particular break FK / index name derivation \
10999                     downstream; use underscores instead (e.g. `{}`)",
11000                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11001                ),
11002            ));
11003        }
11004    }
11005    Ok(())
11006}
11007
11008fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11009    let root = rustango_root();
11010    let attrs = parse_field_attrs(field)?;
11011    let ident = field
11012        .ident
11013        .as_ref()
11014        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11015    let name = ident.to_string();
11016    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11017    let primary_key = attrs.primary_key;
11018    let DetectedType {
11019        kind,
11020        nullable,
11021        auto: detected_auto,
11022        fk_inner,
11023    } = detect_type(&field.ty)?;
11024    check_bound_compatibility(field, &attrs, kind)?;
11025    let auto = detected_auto;
11026    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11027    // INSERT path: the user must wrap the field in `Auto<T>`, which
11028    // marks the column as DB-default-supplied. The mixin attrs then
11029    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11030    // for `auto_now`, force the value on UPDATE too.
11031    if attrs.auto_uuid {
11032        if kind != DetectedKind::Uuid {
11033            return Err(syn::Error::new_spanned(
11034                field,
11035                "`#[rustango(auto_uuid)]` requires the field type to be \
11036                 `Auto<uuid::Uuid>`",
11037            ));
11038        }
11039        if !detected_auto {
11040            return Err(syn::Error::new_spanned(
11041                field,
11042                "`#[rustango(auto_uuid)]` requires the field type to be \
11043                 wrapped in `Auto<...>` so the macro skips the column on \
11044                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11045            ));
11046        }
11047    }
11048    if attrs.default_uuid_v7 {
11049        if kind != DetectedKind::Uuid {
11050            return Err(syn::Error::new_spanned(
11051                field,
11052                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11053                 `Auto<uuid::Uuid>`",
11054            ));
11055        }
11056        if !detected_auto {
11057            return Err(syn::Error::new_spanned(
11058                field,
11059                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11060                 wrapped in `Auto<...>` so the macro can detect the \
11061                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11062            ));
11063        }
11064        if attrs.auto_uuid {
11065            return Err(syn::Error::new_spanned(
11066                field,
11067                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11068                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11069                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11070                 Pick one.",
11071            ));
11072        }
11073    }
11074    if attrs.auto_now_add || attrs.auto_now {
11075        if kind != DetectedKind::DateTime {
11076            return Err(syn::Error::new_spanned(
11077                field,
11078                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11079                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11080            ));
11081        }
11082        if !detected_auto {
11083            return Err(syn::Error::new_spanned(
11084                field,
11085                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11086                 the field type to be wrapped in `Auto<...>` so the macro skips \
11087                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11088            ));
11089        }
11090    }
11091    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11092        return Err(syn::Error::new_spanned(
11093            field,
11094            "`#[rustango(soft_delete)]` requires the field type to be \
11095             `Option<chrono::DateTime<chrono::Utc>>`",
11096        ));
11097    }
11098    let is_mixin_auto =
11099        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11100    if detected_auto && !primary_key && !is_mixin_auto {
11101        return Err(syn::Error::new_spanned(
11102            field,
11103            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11104             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11105             `auto_now`",
11106        ));
11107    }
11108    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11109        return Err(syn::Error::new_spanned(
11110            field,
11111            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11112             SERIAL / BIGSERIAL already supplies a default sequence.",
11113        ));
11114    }
11115    if fk_inner.is_some() && primary_key {
11116        return Err(syn::Error::new_spanned(
11117            field,
11118            "`ForeignKey<T>` is not allowed on a primary-key field — \
11119             a row's PK is its own identity, not a reference to a parent.",
11120        ));
11121    }
11122    if attrs.generated_as.is_some() {
11123        if primary_key {
11124            return Err(syn::Error::new_spanned(
11125                field,
11126                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11127                 primary-key field — a PK must be writable so the row \
11128                 has an identity at INSERT time.",
11129            ));
11130        }
11131        if attrs.default.is_some() {
11132            return Err(syn::Error::new_spanned(
11133                field,
11134                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11135                 `default = \"…\"` — Postgres rejects DEFAULT on \
11136                 generated columns. The expression IS the default.",
11137            ));
11138        }
11139        if detected_auto {
11140            return Err(syn::Error::new_spanned(
11141                field,
11142                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11143                 an `Auto<T>` field — generated columns are computed \
11144                 by the DB, not server-assigned via a sequence. Use a \
11145                 plain Rust type (e.g. `f64`).",
11146            ));
11147        }
11148        if fk_inner.is_some() {
11149            return Err(syn::Error::new_spanned(
11150                field,
11151                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11152                 ForeignKey field.",
11153            ));
11154        }
11155    }
11156    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11157    let column_lit = column.as_str();
11158    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11159    // dimension that the bare `Vector` Rust type can't carry, so emit
11160    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11161    // fallback of `Vector(0)`.
11162    let field_type_tokens = if kind == DetectedKind::Vector {
11163        let root = rustango_root();
11164        let dims = attrs.vector_dims.unwrap_or(0);
11165        quote!(#root::core::FieldType::Vector(#dims))
11166    } else if kind == DetectedKind::Geometry {
11167        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11168        // SRID that the bare `Point` Rust type can't carry, so emit
11169        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11170        let root = rustango_root();
11171        let srid = attrs.geometry_srid.unwrap_or(0);
11172        quote!(#root::core::FieldType::Geometry(#srid))
11173    } else {
11174        kind.variant_tokens()
11175    };
11176    let max_length = optional_u32(attrs.max_length);
11177    let min = optional_i64(attrs.min);
11178    let max = optional_i64(attrs.max);
11179    let default = optional_str(attrs.default.as_deref());
11180
11181    let unique = attrs.unique;
11182    let generated_as = optional_str(attrs.generated_as.as_deref());
11183    let help_text = optional_str(attrs.help_text.as_deref());
11184    let choices = optional_choices(attrs.choices.as_deref());
11185    let db_comment = optional_str(attrs.db_comment.as_deref());
11186    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11187    let editable = attrs.editable;
11188    let blank = attrs.blank;
11189    let case_insensitive = attrs.case_insensitive;
11190    let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11191    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11192        return Err(syn::Error::new_spanned(
11193            field,
11194            "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11195             or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11196             non-FK column.",
11197        ));
11198    }
11199    let fk_on_delete = match attrs.on_delete.as_deref() {
11200        None => quote!(::core::option::Option::None),
11201        Some(action) => {
11202            let variant = match action {
11203                "cascade" => quote!(Cascade),
11204                "restrict" => quote!(Restrict),
11205                "set_null" => quote!(SetNull),
11206                "set_default" => quote!(SetDefault),
11207                "no_action" => quote!(NoAction),
11208                // parse_field_attrs already validated this — guard against future drift.
11209                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11210            };
11211            quote!(::core::option::Option::Some(
11212                #root::core::OnDeleteAction::#variant
11213            ))
11214        }
11215    };
11216    let schema = quote! {{
11217        let mut f = #root::core::FieldSchema::new(#name, #column_lit, #field_type_tokens);
11218        f.nullable = #nullable;
11219        f.primary_key = #primary_key;
11220        f.relation = #relation;
11221        f.max_length = #max_length;
11222        f.min = #min;
11223        f.max = #max;
11224        f.default = #default;
11225        f.auto = #auto;
11226        f.unique = #unique;
11227        f.generated_as = #generated_as;
11228        f.help_text = #help_text;
11229        f.choices = #choices;
11230        f.db_comment = #db_comment;
11231        f.verbose_name = #verbose_name;
11232        f.editable = #editable;
11233        f.blank = #blank;
11234        f.case_insensitive = #case_insensitive;
11235        f.fk_on_delete = #fk_on_delete;
11236        f.validators = &[ #(#validators_lits),* ];
11237        f
11238    }};
11239
11240    let from_row_init = quote! {
11241        #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11242    };
11243    let from_aliased_row_init = quote! {
11244        #ident: #root::sql::sqlx::Row::try_get(
11245            row,
11246            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11247        )?
11248    };
11249
11250    Ok(FieldInfo {
11251        ident,
11252        column,
11253        primary_key,
11254        auto,
11255        value_ty: &field.ty,
11256        field_type_tokens,
11257        schema,
11258        from_row_init,
11259        from_aliased_row_init,
11260        fk_inner: fk_inner.cloned(),
11261        fk_pk_kind: kind,
11262        nullable,
11263        auto_now: attrs.auto_now,
11264        auto_now_add: attrs.auto_now_add,
11265        soft_delete: attrs.soft_delete,
11266        generated_as: attrs.generated_as.clone(),
11267        default_uuid_v7: attrs.default_uuid_v7,
11268        related_name: attrs.related_name.clone(),
11269    })
11270}
11271
11272fn check_bound_compatibility(
11273    field: &syn::Field,
11274    attrs: &FieldAttrs,
11275    kind: DetectedKind,
11276) -> syn::Result<()> {
11277    if attrs.max_length.is_some() && kind != DetectedKind::String {
11278        return Err(syn::Error::new_spanned(
11279            field,
11280            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11281        ));
11282    }
11283    if attrs.choices.is_some() && kind != DetectedKind::String {
11284        return Err(syn::Error::new_spanned(
11285            field,
11286            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11287             integer-valued enumerations should be modeled with a Rust enum and \
11288             custom (de)serializer for now",
11289        ));
11290    }
11291    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11292        return Err(syn::Error::new_spanned(
11293            field,
11294            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11295        ));
11296    }
11297    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11298        if min > max {
11299            return Err(syn::Error::new_spanned(
11300                field,
11301                format!("`min` ({min}) is greater than `max` ({max})"),
11302            ));
11303        }
11304    }
11305    Ok(())
11306}
11307
11308/// A `&'static [ty]` of `const fn` results. The slice needs its own
11309/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11310fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11311    quote!({
11312        const ITEMS: &[#ty] = &[ #(#items),* ];
11313        ITEMS
11314    })
11315}
11316
11317fn optional_u32(value: Option<u32>) -> TokenStream2 {
11318    if let Some(v) = value {
11319        quote!(::core::option::Option::Some(#v))
11320    } else {
11321        quote!(::core::option::Option::None)
11322    }
11323}
11324
11325fn optional_i64(value: Option<i64>) -> TokenStream2 {
11326    if let Some(v) = value {
11327        quote!(::core::option::Option::Some(#v))
11328    } else {
11329        quote!(::core::option::Option::None)
11330    }
11331}
11332
11333fn optional_str(value: Option<&str>) -> TokenStream2 {
11334    if let Some(v) = value {
11335        quote!(::core::option::Option::Some(#v))
11336    } else {
11337        quote!(::core::option::Option::None)
11338    }
11339}
11340
11341fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11342    let Some(pairs) = pairs else {
11343        return quote!(::core::option::Option::None);
11344    };
11345    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11346    quote!(::core::option::Option::Some(&[#(#entries),*]))
11347}
11348
11349fn relation_tokens(
11350    field: &syn::Field,
11351    attrs: &FieldAttrs,
11352    fk_inner: Option<&syn::Type>,
11353    table: &str,
11354) -> syn::Result<TokenStream2> {
11355    let root = rustango_root();
11356    if let Some(inner) = fk_inner {
11357        if attrs.fk.is_some() || attrs.o2o.is_some() {
11358            return Err(syn::Error::new_spanned(
11359                field,
11360                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11361                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11362            ));
11363        }
11364        let on = attrs.on.as_deref().unwrap_or("id");
11365        return Ok(quote! {
11366            ::core::option::Option::Some(#root::core::Relation::fk(
11367                <#inner as #root::core::Model>::SCHEMA.table,
11368                #on,
11369            ))
11370        });
11371    }
11372    match (&attrs.fk, &attrs.o2o) {
11373        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11374            field,
11375            "`fk` and `o2o` are mutually exclusive",
11376        )),
11377        (Some(to), None) => {
11378            let on = attrs.on.as_deref().unwrap_or("id");
11379            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11380            // the model's own table. Threaded as a literal string at
11381            // macro-expansion time to sidestep the const-eval cycle
11382            // that `Self::SCHEMA.table` would create when referenced
11383            // inside Self::SCHEMA's own initializer.
11384            let resolved = if to == "self" { table } else { to };
11385            Ok(quote! {
11386                ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11387            })
11388        }
11389        (None, Some(to)) => {
11390            let on = attrs.on.as_deref().unwrap_or("id");
11391            let resolved = if to == "self" { table } else { to };
11392            Ok(quote! {
11393                ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11394            })
11395        }
11396        (None, None) => {
11397            if attrs.on.is_some() {
11398                return Err(syn::Error::new_spanned(
11399                    field,
11400                    "`on` requires `fk` or `o2o`",
11401                ));
11402            }
11403            Ok(quote!(::core::option::Option::None))
11404        }
11405    }
11406}
11407
11408/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11409/// about kinds without depending on `rustango-core` (which would require a
11410/// proc-macro/normal split it doesn't have today).
11411#[derive(Clone, Copy, PartialEq, Eq)]
11412enum DetectedKind {
11413    I16,
11414    I32,
11415    I64,
11416    F32,
11417    F64,
11418    Bool,
11419    String,
11420    DateTime,
11421    Date,
11422    Time,
11423    Uuid,
11424    Json,
11425    Decimal,
11426    Binary,
11427    /// `Array<String>` → PG `text[]` (#341).
11428    ArrayText,
11429    /// `Array<i32>` → PG `integer[]` (#341).
11430    ArrayInt,
11431    /// `Array<i64>` → PG `bigint[]` (#341).
11432    ArrayBigInt,
11433    /// `Range<i32>` → PG `int4range` (#343).
11434    RangeInt,
11435    /// `Range<i64>` → PG `int8range` (#343).
11436    RangeBigInt,
11437    /// `Range<Decimal>` → PG `numrange` (#343).
11438    RangeNumeric,
11439    /// `Range<NaiveDate>` → PG `daterange` (#343).
11440    RangeDate,
11441    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11442    RangeDateTime,
11443    /// `HStore` → PG `hstore` (#342).
11444    HStore,
11445    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11446    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11447    /// in at the `FieldType` emission site (not carried on this enum).
11448    Vector,
11449    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11450    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11451    /// threaded in at the `FieldType` emission site.
11452    Geometry,
11453}
11454
11455impl DetectedKind {
11456    fn variant_tokens(self) -> TokenStream2 {
11457        let root = rustango_root();
11458        match self {
11459            Self::I16 => quote!(#root::core::FieldType::I16),
11460            Self::I32 => quote!(#root::core::FieldType::I32),
11461            Self::I64 => quote!(#root::core::FieldType::I64),
11462            Self::F32 => quote!(#root::core::FieldType::F32),
11463            Self::F64 => quote!(#root::core::FieldType::F64),
11464            Self::Bool => quote!(#root::core::FieldType::Bool),
11465            Self::String => quote!(#root::core::FieldType::String),
11466            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11467            Self::Date => quote!(#root::core::FieldType::Date),
11468            Self::Time => quote!(#root::core::FieldType::Time),
11469            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11470            Self::Json => quote!(#root::core::FieldType::Json),
11471            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11472            Self::Binary => quote!(#root::core::FieldType::Binary),
11473            Self::ArrayText => {
11474                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11475            }
11476            Self::ArrayInt => {
11477                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11478            }
11479            Self::ArrayBigInt => {
11480                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11481            }
11482            Self::RangeInt => {
11483                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11484            }
11485            Self::RangeBigInt => {
11486                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11487            }
11488            Self::RangeNumeric => {
11489                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11490            }
11491            Self::RangeDate => {
11492                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11493            }
11494            Self::RangeDateTime => {
11495                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11496            }
11497            Self::HStore => quote!(#root::core::FieldType::HStore),
11498            // Dimension comes from the `vector(dims = N)` attribute,
11499            // applied at the emission site; `0` here is just a fallback.
11500            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11501            // SRID comes from the `geometry(srid = N)` attribute, applied
11502            // at the emission site; `0` here is just a fallback.
11503            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11504        }
11505    }
11506
11507    fn is_integer(self) -> bool {
11508        matches!(self, Self::I16 | Self::I32 | Self::I64)
11509    }
11510
11511    /// `(SqlValue::<Variant>, default expr)` for emitting the
11512    /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11513    /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11514    /// fires only when the parent's `__rustango_pk_value` returns a
11515    /// different variant than expected, which is a compile-time bug —
11516    /// but we still need a value-typed fallback to keep the match
11517    /// total.
11518    fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11519        let root = rustango_root();
11520        match self {
11521            Self::I16 => (quote!(I16), quote!(0i16)),
11522            Self::I32 => (quote!(I32), quote!(0i32)),
11523            Self::I64 => (quote!(I64), quote!(0i64)),
11524            Self::F32 => (quote!(F32), quote!(0f32)),
11525            Self::F64 => (quote!(F64), quote!(0f64)),
11526            Self::Bool => (quote!(Bool), quote!(false)),
11527            Self::String => (quote!(String), quote!(::std::string::String::new())),
11528            Self::DateTime => (
11529                quote!(DateTime),
11530                quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11531            ),
11532            Self::Date => (
11533                quote!(Date),
11534                quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11535            ),
11536            Self::Time => (
11537                quote!(Time),
11538                quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11539            ),
11540            Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11541            Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11542            Self::Decimal => (
11543                quote!(Decimal),
11544                quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11545            ),
11546            Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11547            // Arrays (#341) can never be a foreign-key primary key, so
11548            // this arm is never reached at runtime — but the match must
11549            // stay total. A bare empty `SqlValue::Array` is the dummy.
11550            Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11551                (quote!(Array), quote!(::std::vec::Vec::new()))
11552            }
11553            // Ranges (#343) likewise can't be a FK PK — never reached.
11554            Self::RangeInt
11555            | Self::RangeBigInt
11556            | Self::RangeNumeric
11557            | Self::RangeDate
11558            | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11559            // HStore (#342) can't be a FK PK — never reached.
11560            Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11561            // Vector (#824) can't be a FK PK — never reached.
11562            Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11563            // Geometry (#443) can't be a FK PK — never reached (arm is
11564            // exhaustiveness-only; never interpolated into emitted code).
11565            Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11566        }
11567    }
11568}
11569
11570/// Result of walking a field's Rust type. `kind` is the underlying
11571/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11572/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11573/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11574/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11575#[derive(Clone, Copy)]
11576struct DetectedType<'a> {
11577    kind: DetectedKind,
11578    nullable: bool,
11579    auto: bool,
11580    fk_inner: Option<&'a syn::Type>,
11581}
11582
11583/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11584/// non-`Auto` types — the caller should already have routed Auto-only
11585/// codegen through this helper, so a `None` indicates a macro-internal
11586/// invariant break.
11587fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11588    let Type::Path(TypePath { path, qself: None }) = ty else {
11589        return None;
11590    };
11591    let last = path.segments.last()?;
11592    if last.ident != "Auto" {
11593        return None;
11594    }
11595    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11596        return None;
11597    };
11598    args.args.iter().find_map(|a| match a {
11599        syn::GenericArgument::Type(t) => Some(t),
11600        _ => None,
11601    })
11602}
11603
11604fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11605    let Type::Path(TypePath { path, qself: None }) = ty else {
11606        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11607    };
11608    let last = path
11609        .segments
11610        .last()
11611        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11612
11613    if last.ident == "Option" {
11614        let inner = generic_inner(ty, &last.arguments, "Option")?;
11615        let inner_det = detect_type(inner)?;
11616        if inner_det.nullable {
11617            return Err(syn::Error::new_spanned(
11618                ty,
11619                "nested Option is not supported",
11620            ));
11621        }
11622        if inner_det.auto {
11623            return Err(syn::Error::new_spanned(
11624                ty,
11625                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11626            ));
11627        }
11628        return Ok(DetectedType {
11629            nullable: true,
11630            ..inner_det
11631        });
11632    }
11633
11634    if last.ident == "Auto" {
11635        let inner = generic_inner(ty, &last.arguments, "Auto")?;
11636        let inner_det = detect_type(inner)?;
11637        if inner_det.auto {
11638            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11639        }
11640        if inner_det.nullable {
11641            return Err(syn::Error::new_spanned(
11642                ty,
11643                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11644            ));
11645        }
11646        if inner_det.fk_inner.is_some() {
11647            return Err(syn::Error::new_spanned(
11648                ty,
11649                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11650            ));
11651        }
11652        if !matches!(
11653            inner_det.kind,
11654            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11655        ) {
11656            return Err(syn::Error::new_spanned(
11657                ty,
11658                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11659                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11660                 (DEFAULT now())",
11661            ));
11662        }
11663        return Ok(DetectedType {
11664            auto: true,
11665            ..inner_det
11666        });
11667    }
11668
11669    if last.ident == "ForeignKey" {
11670        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11671        // Resolve the FK column's underlying SQL type from `K`. When the
11672        // user wrote `ForeignKey<T>` without a key parameter, the type
11673        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11674        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11675        // recurse into K so the column DDL emits the right SQL type
11676        // (VARCHAR for String, UUID for Uuid, …) and the load_related
11677        // emitter knows which `SqlValue` variant to match.
11678        let kind = match key_ty {
11679            Some(k) => detect_type(k)?.kind,
11680            None => DetectedKind::I64,
11681        };
11682        return Ok(DetectedType {
11683            kind,
11684            nullable: false,
11685            auto: false,
11686            fk_inner: Some(inner),
11687        });
11688    }
11689
11690    let kind = match last.ident.to_string().as_str() {
11691        "i16" => DetectedKind::I16,
11692        "i32" => DetectedKind::I32,
11693        "i64" => DetectedKind::I64,
11694        "f32" => DetectedKind::F32,
11695        "f64" => DetectedKind::F64,
11696        "bool" => DetectedKind::Bool,
11697        "String" => DetectedKind::String,
11698        "DateTime" => DetectedKind::DateTime,
11699        "NaiveDate" => DetectedKind::Date,
11700        "NaiveTime" => DetectedKind::Time,
11701        "Uuid" => DetectedKind::Uuid,
11702        "Value" => DetectedKind::Json,
11703        "Decimal" => DetectedKind::Decimal,
11704        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11705        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11706        // — that would emit Binary DDL and decode-fail at runtime.
11707        "Vec" => {
11708            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11709            if let Type::Path(TypePath { path, qself: None }) = inner {
11710                if let Some(seg) = path.segments.last() {
11711                    if seg.ident == "u8" && seg.arguments.is_empty() {
11712                        return Ok(DetectedType {
11713                            kind: DetectedKind::Binary,
11714                            nullable: false,
11715                            auto: false,
11716                            fk_inner: None,
11717                        });
11718                    }
11719                }
11720            }
11721            return Err(syn::Error::new_spanned(
11722                ty,
11723                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11724                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11725            ));
11726        }
11727        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11728        // `integer[]` / `bigint[]` (#341).
11729        "Array" => {
11730            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11731            let elem = match inner {
11732                Type::Path(TypePath { path, qself: None }) => {
11733                    path.segments.last().map(|s| s.ident.to_string())
11734                }
11735                _ => None,
11736            };
11737            let kind = match elem.as_deref() {
11738                Some("String") => DetectedKind::ArrayText,
11739                Some("i32") => DetectedKind::ArrayInt,
11740                Some("i64") => DetectedKind::ArrayBigInt,
11741                _ => {
11742                    return Err(syn::Error::new_spanned(
11743                        ty,
11744                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11745                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11746                    ));
11747                }
11748            };
11749            return Ok(DetectedType {
11750                kind,
11751                nullable: false,
11752                auto: false,
11753                fk_inner: None,
11754            });
11755        }
11756        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11757        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11758        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
11759        "Range" => {
11760            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11761            let elem = match inner {
11762                Type::Path(TypePath { path, qself: None }) => {
11763                    path.segments.last().map(|s| s.ident.to_string())
11764                }
11765                _ => None,
11766            };
11767            let kind = match elem.as_deref() {
11768                Some("i32") => DetectedKind::RangeInt,
11769                Some("i64") => DetectedKind::RangeBigInt,
11770                Some("Decimal") => DetectedKind::RangeNumeric,
11771                Some("NaiveDate") => DetectedKind::RangeDate,
11772                Some("DateTime") => DetectedKind::RangeDateTime,
11773                _ => {
11774                    return Err(syn::Error::new_spanned(
11775                        ty,
11776                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11777                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11778                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11779                         (→ tstzrange) are supported (#343)",
11780                    ));
11781                }
11782            };
11783            return Ok(DetectedType {
11784                kind,
11785                nullable: false,
11786                auto: false,
11787                fk_inner: None,
11788            });
11789        }
11790        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11791        // (the `CastValue` impl bridges logical↔stored); the field's own
11792        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11793        // schema just needs `FieldType::String`.
11794        "Cast" => {
11795            return Ok(DetectedType {
11796                kind: DetectedKind::String,
11797                nullable: false,
11798                auto: false,
11799                fk_inner: None,
11800            });
11801        }
11802        // `HStore` → PG `hstore` (#342). No generic
11803        // parameter — always a string→string map.
11804        "HStore" => {
11805            return Ok(DetectedType {
11806                kind: DetectedKind::HStore,
11807                nullable: false,
11808                auto: false,
11809                fk_inner: None,
11810            });
11811        }
11812        // `Vector` → pgvector `vector(N)` (#824). The dimension is
11813        // supplied by `#[rustango(vector(dims = N))]`, not the type.
11814        "Vector" => {
11815            return Ok(DetectedType {
11816                kind: DetectedKind::Vector,
11817                nullable: false,
11818                auto: false,
11819                fk_inner: None,
11820            });
11821        }
11822        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11823        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11824        "Point" => {
11825            return Ok(DetectedType {
11826                kind: DetectedKind::Geometry,
11827                nullable: false,
11828                auto: false,
11829                fk_inner: None,
11830            });
11831        }
11832        other => {
11833            return Err(syn::Error::new_spanned(
11834                ty,
11835                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)"),
11836            ));
11837        }
11838    };
11839    Ok(DetectedType {
11840        kind,
11841        nullable: false,
11842        auto: false,
11843        fk_inner: None,
11844    })
11845}
11846
11847fn generic_inner<'a>(
11848    ty: &'a Type,
11849    arguments: &'a PathArguments,
11850    wrapper: &str,
11851) -> syn::Result<&'a Type> {
11852    let PathArguments::AngleBracketed(args) = arguments else {
11853        return Err(syn::Error::new_spanned(
11854            ty,
11855            format!("{wrapper} requires a generic argument"),
11856        ));
11857    };
11858    args.args
11859        .iter()
11860        .find_map(|a| match a {
11861            GenericArgument::Type(t) => Some(t),
11862            _ => None,
11863        })
11864        .ok_or_else(|| {
11865            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11866        })
11867}
11868
11869/// Like [`generic_inner`] but pulls *two* type args — the first is
11870/// required, the second is optional. Used by the `ForeignKey<T, K>`
11871/// detection where K defaults to `i64` when omitted.
11872fn generic_pair<'a>(
11873    ty: &'a Type,
11874    arguments: &'a PathArguments,
11875    wrapper: &str,
11876) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11877    let PathArguments::AngleBracketed(args) = arguments else {
11878        return Err(syn::Error::new_spanned(
11879            ty,
11880            format!("{wrapper} requires a generic argument"),
11881        ));
11882    };
11883    let mut types = args.args.iter().filter_map(|a| match a {
11884        GenericArgument::Type(t) => Some(t),
11885        _ => None,
11886    });
11887    let first = types.next().ok_or_else(|| {
11888        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11889    })?;
11890    let second = types.next();
11891    Ok((first, second))
11892}
11893
11894fn to_snake_case(s: &str) -> String {
11895    let mut out = String::with_capacity(s.len() + 4);
11896    for (i, ch) in s.chars().enumerate() {
11897        if ch.is_ascii_uppercase() {
11898            if i > 0 {
11899                out.push('_');
11900            }
11901            out.push(ch.to_ascii_lowercase());
11902        } else {
11903            out.push(ch);
11904        }
11905    }
11906    out
11907}
11908
11909// ============================================================
11910//  #[derive(Form)]  —  slice 8.4B
11911// ============================================================
11912
11913/// Per-field `#[form(...)]` attributes recognised by the derive.
11914#[derive(Default)]
11915struct FormFieldAttrs {
11916    min: Option<i64>,
11917    max: Option<i64>,
11918    min_length: Option<u32>,
11919    max_length: Option<u32>,
11920    /// `#[form(clean = "fn_name")]` — per-field clean hook.
11921    /// The named static method on the form struct is called after the
11922    /// field's typed parse + length/range checks; it gets the parsed
11923    /// value by reference and returns `Result<<FieldType>, String>`.
11924    /// On Ok, the returned value replaces the parsed one; on Err, the
11925    /// message is attached to the field error list. Issue #372.
11926    clean: Option<syn::Ident>,
11927}
11928
11929/// Container-level `#[form(...)]` attributes. Currently only the
11930/// cross-field `validate` hook (issue #373).
11931#[derive(Default)]
11932struct FormContainerAttrs {
11933    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
11934    /// After every per-field parse succeeds, the named method on the
11935    /// form struct is called with `&self` and may return
11936    /// `Result<(), FormErrors>`. Errors merge into the field error
11937    /// list. Issue #373.
11938    validate: Option<syn::Ident>,
11939}
11940
11941/// Detected shape of a form field's Rust type.
11942#[derive(Clone, Copy)]
11943enum FormFieldKind {
11944    String,
11945    I16,
11946    I32,
11947    I64,
11948    F32,
11949    F64,
11950    Bool,
11951}
11952
11953impl FormFieldKind {
11954    fn parse_method(self) -> &'static str {
11955        match self {
11956            Self::I16 => "i16",
11957            Self::I32 => "i32",
11958            Self::I64 => "i64",
11959            Self::F32 => "f32",
11960            Self::F64 => "f64",
11961            // String + Bool don't go through `str::parse`; the codegen
11962            // handles them inline.
11963            Self::String | Self::Bool => "",
11964        }
11965    }
11966}
11967
11968fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
11969    let root = rustango_root();
11970    let struct_name = &input.ident;
11971
11972    let Data::Struct(data) = &input.data else {
11973        return Err(syn::Error::new_spanned(
11974            struct_name,
11975            "Form can only be derived on structs",
11976        ));
11977    };
11978    let Fields::Named(named) = &data.fields else {
11979        return Err(syn::Error::new_spanned(
11980            struct_name,
11981            "Form requires a struct with named fields",
11982        ));
11983    };
11984
11985    // #373 — container-level `#[form(validate = "fn")]` hook.
11986    let container = parse_form_container_attrs(input)?;
11987    let post_field_clean: Vec<TokenStream2> = Vec::new();
11988    let _ = post_field_clean;
11989
11990    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
11991    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
11992
11993    for field in &named.named {
11994        let ident = field
11995            .ident
11996            .as_ref()
11997            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11998        let attrs = parse_form_field_attrs(field)?;
11999        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12000
12001        let name_lit = ident.to_string();
12002        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12003        // #372 — append the per-field `clean_<field>` call right after
12004        // the parse block when the attribute is set. The clean fn
12005        // takes &T and returns Result<T, String>; on Err we attach
12006        // the message to the field error list without aborting, so
12007        // the caller sees every field error at once.
12008        let clean_block = if let Some(clean_fn) = &attrs.clean {
12009            quote! {
12010                if __errors.fields().get(#name_lit).is_none() {
12011                    match Self::#clean_fn(&#ident) {
12012                        ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12013                        ::core::result::Result::Err(__msg) => {
12014                            __errors.add(#name_lit, __msg);
12015                        }
12016                    }
12017                }
12018            }
12019        } else {
12020            quote! {}
12021        };
12022        field_blocks.push(quote! {
12023            #parse_block
12024            #clean_block
12025        });
12026        field_idents.push(ident);
12027    }
12028
12029    // #373 — after every per-field parse + clean succeeds, call the
12030    // cross-field validator if declared. Errors merge into the
12031    // outgoing FormErrors via the existing `FormErrors::merge` helper
12032    // (same primitive the serializer cross-field hook uses).
12033    let cross_field_call = if let Some(validate_fn) = &container.validate {
12034        quote! {
12035            if __errors.is_empty() {
12036                let __candidate = Self { #( #field_idents ),* };
12037                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12038                    __errors.merge(__other);
12039                }
12040                if !__errors.is_empty() {
12041                    return ::core::result::Result::Err(__errors);
12042                }
12043                return ::core::result::Result::Ok(__candidate);
12044            }
12045        }
12046    } else {
12047        quote! {}
12048    };
12049
12050    Ok(quote! {
12051        impl #root::forms::Form for #struct_name {
12052            fn parse(
12053                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12054            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12055                let mut __errors = #root::forms::FormErrors::default();
12056                #( #field_blocks )*
12057                #cross_field_call
12058                if !__errors.is_empty() {
12059                    return ::core::result::Result::Err(__errors);
12060                }
12061                ::core::result::Result::Ok(Self {
12062                    #( #field_idents ),*
12063                })
12064            }
12065        }
12066    })
12067}
12068
12069fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12070    let mut out = FormContainerAttrs::default();
12071    for attr in &input.attrs {
12072        if !attr.path().is_ident("form") {
12073            continue;
12074        }
12075        attr.parse_nested_meta(|meta| {
12076            if meta.path.is_ident("validate") {
12077                let s: LitStr = meta.value()?.parse()?;
12078                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12079                return Ok(());
12080            }
12081            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12082        })?;
12083    }
12084    Ok(out)
12085}
12086
12087fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12088    let mut out = FormFieldAttrs::default();
12089    for attr in &field.attrs {
12090        if !attr.path().is_ident("form") {
12091            continue;
12092        }
12093        attr.parse_nested_meta(|meta| {
12094            if meta.path.is_ident("min") {
12095                let lit: syn::LitInt = meta.value()?.parse()?;
12096                out.min = Some(lit.base10_parse::<i64>()?);
12097                return Ok(());
12098            }
12099            if meta.path.is_ident("max") {
12100                let lit: syn::LitInt = meta.value()?.parse()?;
12101                out.max = Some(lit.base10_parse::<i64>()?);
12102                return Ok(());
12103            }
12104            if meta.path.is_ident("min_length") {
12105                let lit: syn::LitInt = meta.value()?.parse()?;
12106                out.min_length = Some(lit.base10_parse::<u32>()?);
12107                return Ok(());
12108            }
12109            if meta.path.is_ident("max_length") {
12110                let lit: syn::LitInt = meta.value()?.parse()?;
12111                out.max_length = Some(lit.base10_parse::<u32>()?);
12112                return Ok(());
12113            }
12114            if meta.path.is_ident("clean") {
12115                let s: LitStr = meta.value()?.parse()?;
12116                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12117                return Ok(());
12118            }
12119            Err(meta.error(
12120                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12121            ))
12122        })?;
12123    }
12124    Ok(out)
12125}
12126
12127fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12128    let Type::Path(TypePath { path, qself: None }) = ty else {
12129        return Err(syn::Error::new(
12130            span,
12131            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12132        ));
12133    };
12134    let last = path
12135        .segments
12136        .last()
12137        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12138
12139    if last.ident == "Option" {
12140        let inner = generic_inner(ty, &last.arguments, "Option")?;
12141        let (kind, nested) = detect_form_field(inner, span)?;
12142        if nested {
12143            return Err(syn::Error::new(
12144                span,
12145                "nested Option in Form fields is not supported",
12146            ));
12147        }
12148        return Ok((kind, true));
12149    }
12150
12151    let kind = match last.ident.to_string().as_str() {
12152        "String" => FormFieldKind::String,
12153        "i16" => FormFieldKind::I16,
12154        "i32" => FormFieldKind::I32,
12155        "i64" => FormFieldKind::I64,
12156        "f32" => FormFieldKind::F32,
12157        "f64" => FormFieldKind::F64,
12158        "bool" => FormFieldKind::Bool,
12159        other => {
12160            return Err(syn::Error::new(
12161                span,
12162                format!(
12163                    "Form field type `{other}` is not supported in v0.8 — use String / \
12164                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12165                ),
12166            ));
12167        }
12168    };
12169    Ok((kind, false))
12170}
12171
12172#[allow(clippy::too_many_lines)]
12173fn render_form_field_parse(
12174    ident: &syn::Ident,
12175    name_lit: &str,
12176    kind: FormFieldKind,
12177    nullable: bool,
12178    attrs: &FormFieldAttrs,
12179) -> TokenStream2 {
12180    // Pull the raw &str from the payload. Uses variable name `data` to
12181    // match the new `Form::parse(data: &HashMap<…>)` signature.
12182    let lookup = quote! {
12183        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12184    };
12185
12186    let parsed_value = match kind {
12187        FormFieldKind::Bool => quote! {
12188            let __v: bool = match __raw {
12189                ::core::option::Option::None => false,
12190                ::core::option::Option::Some(__s) => !matches!(
12191                    __s.to_ascii_lowercase().as_str(),
12192                    "" | "false" | "0" | "off" | "no"
12193                ),
12194            };
12195        },
12196        FormFieldKind::String => {
12197            if nullable {
12198                quote! {
12199                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12200                        ::core::option::Option::None => ::core::option::Option::None,
12201                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12202                            ::core::option::Option::None
12203                        }
12204                        ::core::option::Option::Some(__s) => {
12205                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12206                        }
12207                    };
12208                }
12209            } else {
12210                quote! {
12211                    let __v: ::std::string::String = match __raw {
12212                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12213                            ::core::clone::Clone::clone(__s)
12214                        }
12215                        _ => {
12216                            __errors.add(#name_lit, "This field is required.");
12217                            ::std::string::String::new()
12218                        }
12219                    };
12220                }
12221            }
12222        }
12223        FormFieldKind::I16
12224        | FormFieldKind::I32
12225        | FormFieldKind::I64
12226        | FormFieldKind::F32
12227        | FormFieldKind::F64 => {
12228            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12229            let ty_lit = kind.parse_method();
12230            let default_val = match kind {
12231                FormFieldKind::I16 => quote! { 0i16 },
12232                FormFieldKind::I32 => quote! { 0i32 },
12233                FormFieldKind::I64 => quote! { 0i64 },
12234                FormFieldKind::F32 => quote! { 0f32 },
12235                FormFieldKind::F64 => quote! { 0f64 },
12236                _ => quote! { Default::default() },
12237            };
12238            if nullable {
12239                quote! {
12240                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12241                        ::core::option::Option::None => ::core::option::Option::None,
12242                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12243                            ::core::option::Option::None
12244                        }
12245                        ::core::option::Option::Some(__s) => {
12246                            match __s.parse::<#parse_ty>() {
12247                                ::core::result::Result::Ok(__n) => {
12248                                    ::core::option::Option::Some(__n)
12249                                }
12250                                ::core::result::Result::Err(__e) => {
12251                                    __errors.add(
12252                                        #name_lit,
12253                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12254                                    );
12255                                    ::core::option::Option::None
12256                                }
12257                            }
12258                        }
12259                    };
12260                }
12261            } else {
12262                quote! {
12263                    let __v: #parse_ty = match __raw {
12264                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12265                            match __s.parse::<#parse_ty>() {
12266                                ::core::result::Result::Ok(__n) => __n,
12267                                ::core::result::Result::Err(__e) => {
12268                                    __errors.add(
12269                                        #name_lit,
12270                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12271                                    );
12272                                    #default_val
12273                                }
12274                            }
12275                        }
12276                        _ => {
12277                            __errors.add(#name_lit, "This field is required.");
12278                            #default_val
12279                        }
12280                    };
12281                }
12282            }
12283        }
12284    };
12285
12286    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12287
12288    quote! {
12289        // `mut` so the per-field `clean` hook (#372) can rewrite the
12290        // parsed value in-place when it returns Ok with a normalized
12291        // form (e.g. trim / lowercase).
12292        let mut #ident = {
12293            #lookup
12294            #parsed_value
12295            #validators
12296            __v
12297        };
12298    }
12299}
12300
12301fn render_form_validators(
12302    name_lit: &str,
12303    kind: FormFieldKind,
12304    nullable: bool,
12305    attrs: &FormFieldAttrs,
12306) -> TokenStream2 {
12307    let mut checks: Vec<TokenStream2> = Vec::new();
12308
12309    let val_ref = if nullable {
12310        quote! { __v.as_ref() }
12311    } else {
12312        quote! { ::core::option::Option::Some(&__v) }
12313    };
12314
12315    let is_string = matches!(kind, FormFieldKind::String);
12316    let is_numeric = matches!(
12317        kind,
12318        FormFieldKind::I16
12319            | FormFieldKind::I32
12320            | FormFieldKind::I64
12321            | FormFieldKind::F32
12322            | FormFieldKind::F64
12323    );
12324
12325    if is_string {
12326        if let Some(min_len) = attrs.min_length {
12327            let min_len_usize = min_len as usize;
12328            checks.push(quote! {
12329                if let ::core::option::Option::Some(__s) = #val_ref {
12330                    if __s.len() < #min_len_usize {
12331                        __errors.add(
12332                            #name_lit,
12333                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12334                        );
12335                    }
12336                }
12337            });
12338        }
12339        if let Some(max_len) = attrs.max_length {
12340            let max_len_usize = max_len as usize;
12341            checks.push(quote! {
12342                if let ::core::option::Option::Some(__s) = #val_ref {
12343                    if __s.len() > #max_len_usize {
12344                        __errors.add(
12345                            #name_lit,
12346                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12347                        );
12348                    }
12349                }
12350            });
12351        }
12352    }
12353
12354    if is_numeric {
12355        if let Some(min) = attrs.min {
12356            checks.push(quote! {
12357                if let ::core::option::Option::Some(__n) = #val_ref {
12358                    if (*__n as f64) < (#min as f64) {
12359                        __errors.add(
12360                            #name_lit,
12361                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12362                        );
12363                    }
12364                }
12365            });
12366        }
12367        if let Some(max) = attrs.max {
12368            checks.push(quote! {
12369                if let ::core::option::Option::Some(__n) = #val_ref {
12370                    if (*__n as f64) > (#max as f64) {
12371                        __errors.add(
12372                            #name_lit,
12373                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12374                        );
12375                    }
12376                }
12377            });
12378        }
12379    }
12380
12381    quote! { #( #checks )* }
12382}
12383
12384// ============================================================
12385//  #[derive(ViewSet)]
12386// ============================================================
12387
12388struct ViewSetAttrs {
12389    model: syn::Path,
12390    fields: Option<Vec<String>>,
12391    filter_fields: Vec<String>,
12392    search_fields: Vec<String>,
12393    /// (field_name, desc)
12394    ordering: Vec<(String, bool)>,
12395    page_size: Option<usize>,
12396    read_only: bool,
12397    perms: ViewSetPermsAttrs,
12398    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12399    /// retrieve / create responses through this `#[derive(Serializer)]`
12400    /// type instead of the default field-level projection (requires the
12401    /// `serializer` feature). Tri-dialect.
12402    serializer: Option<syn::Path>,
12403}
12404
12405#[derive(Default)]
12406struct ViewSetPermsAttrs {
12407    list: Vec<String>,
12408    retrieve: Vec<String>,
12409    create: Vec<String>,
12410    update: Vec<String>,
12411    destroy: Vec<String>,
12412}
12413
12414fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12415    let root = rustango_root();
12416    let struct_name = &input.ident;
12417
12418    // Must be a unit struct or an empty named struct.
12419    match &input.data {
12420        Data::Struct(s) => match &s.fields {
12421            Fields::Unit | Fields::Named(_) => {}
12422            Fields::Unnamed(_) => {
12423                return Err(syn::Error::new_spanned(
12424                    struct_name,
12425                    "ViewSet can only be derived on a unit struct or an empty named struct",
12426                ));
12427            }
12428        },
12429        _ => {
12430            return Err(syn::Error::new_spanned(
12431                struct_name,
12432                "ViewSet can only be derived on a struct",
12433            ));
12434        }
12435    }
12436
12437    let attrs = parse_viewset_attrs(input)?;
12438    let model_path = &attrs.model;
12439
12440    // `.fields(&[...])` call — None means skip (use all scalar fields).
12441    let fields_call = if let Some(ref fields) = attrs.fields {
12442        let lits = fields.iter().map(|f| f.as_str());
12443        quote!(.fields(&[ #(#lits),* ]))
12444    } else {
12445        quote!()
12446    };
12447
12448    let filter_fields_call = if attrs.filter_fields.is_empty() {
12449        quote!()
12450    } else {
12451        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12452        quote!(.filter_fields(&[ #(#lits),* ]))
12453    };
12454
12455    let search_fields_call = if attrs.search_fields.is_empty() {
12456        quote!()
12457    } else {
12458        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12459        quote!(.search_fields(&[ #(#lits),* ]))
12460    };
12461
12462    let ordering_call = if attrs.ordering.is_empty() {
12463        quote!()
12464    } else {
12465        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12466            let f = f.as_str();
12467            quote!((#f, #desc))
12468        });
12469        quote!(.ordering(&[ #(#pairs),* ]))
12470    };
12471
12472    let page_size_call = if let Some(n) = attrs.page_size {
12473        quote!(.page_size(#n))
12474    } else {
12475        quote!()
12476    };
12477
12478    let read_only_call = if attrs.read_only {
12479        quote!(.read_only())
12480    } else {
12481        quote!()
12482    };
12483
12484    // `.serializer::<S>()` — reshape responses through a derived
12485    // serializer. Requires the downstream crate to enable the
12486    // `serializer` feature (the method is gated on it).
12487    let serializer_call = if let Some(ref ser) = attrs.serializer {
12488        quote!(.serializer::<#ser>())
12489    } else {
12490        quote!()
12491    };
12492
12493    let perms = &attrs.perms;
12494    let perms_call = if perms.list.is_empty()
12495        && perms.retrieve.is_empty()
12496        && perms.create.is_empty()
12497        && perms.update.is_empty()
12498        && perms.destroy.is_empty()
12499    {
12500        quote!()
12501    } else {
12502        let list_lits = perms.list.iter().map(|s| s.as_str());
12503        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12504        let create_lits = perms.create.iter().map(|s| s.as_str());
12505        let update_lits = perms.update.iter().map(|s| s.as_str());
12506        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12507        quote! {
12508            .permissions(#root::viewset::ViewSetPerms {
12509                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
12510                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12511                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
12512                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
12513                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
12514            })
12515        }
12516    };
12517
12518    Ok(quote! {
12519        impl #struct_name {
12520            /// Build an `axum::Router` with the six standard REST endpoints
12521            /// for this ViewSet, mounted at `prefix`.
12522            ///
12523            /// Accepts any backend's pool — a `sqlx::PgPool`,
12524            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12525            /// enum — because they all `Into<Pool>` (#1273). This is why
12526            /// the derive no longer names `PgPool`, which would not
12527            /// exist on a project built without the `postgres` feature.
12528            pub fn router(
12529                prefix: &str,
12530                pool: impl ::core::convert::Into<#root::sql::Pool>,
12531            ) -> #root::__axum::Router {
12532                #root::viewset::ViewSet::for_model(
12533                    <#model_path as #root::core::Model>::SCHEMA
12534                )
12535                    #fields_call
12536                    #filter_fields_call
12537                    #search_fields_call
12538                    #ordering_call
12539                    #page_size_call
12540                    #perms_call
12541                    #read_only_call
12542                    #serializer_call
12543                    .router_pool(prefix, pool.into())
12544            }
12545        }
12546    })
12547}
12548
12549fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12550    let mut model: Option<syn::Path> = None;
12551    let mut fields: Option<Vec<String>> = None;
12552    let mut filter_fields: Vec<String> = Vec::new();
12553    let mut search_fields: Vec<String> = Vec::new();
12554    let mut ordering: Vec<(String, bool)> = Vec::new();
12555    let mut page_size: Option<usize> = None;
12556    let mut read_only = false;
12557    let mut perms = ViewSetPermsAttrs::default();
12558    let mut serializer: Option<syn::Path> = None;
12559
12560    for attr in &input.attrs {
12561        if !attr.path().is_ident("viewset") {
12562            continue;
12563        }
12564        attr.parse_nested_meta(|meta| {
12565            if meta.path.is_ident("model") {
12566                let path: syn::Path = meta.value()?.parse()?;
12567                model = Some(path);
12568                return Ok(());
12569            }
12570            if meta.path.is_ident("serializer") {
12571                let path: syn::Path = meta.value()?.parse()?;
12572                serializer = Some(path);
12573                return Ok(());
12574            }
12575            if meta.path.is_ident("fields") {
12576                let s: LitStr = meta.value()?.parse()?;
12577                fields = Some(split_field_list(&s.value()));
12578                return Ok(());
12579            }
12580            if meta.path.is_ident("filter_fields") {
12581                let s: LitStr = meta.value()?.parse()?;
12582                filter_fields = split_field_list(&s.value());
12583                return Ok(());
12584            }
12585            if meta.path.is_ident("search_fields") {
12586                let s: LitStr = meta.value()?.parse()?;
12587                search_fields = split_field_list(&s.value());
12588                return Ok(());
12589            }
12590            if meta.path.is_ident("ordering") {
12591                let s: LitStr = meta.value()?.parse()?;
12592                ordering = parse_ordering_list(&s.value());
12593                return Ok(());
12594            }
12595            if meta.path.is_ident("page_size") {
12596                let lit: syn::LitInt = meta.value()?.parse()?;
12597                page_size = Some(lit.base10_parse::<usize>()?);
12598                return Ok(());
12599            }
12600            if meta.path.is_ident("read_only") {
12601                read_only = true;
12602                return Ok(());
12603            }
12604            if meta.path.is_ident("permissions") {
12605                meta.parse_nested_meta(|inner| {
12606                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12607                        let s: LitStr = inner.value()?.parse()?;
12608                        Ok(split_field_list(&s.value()))
12609                    };
12610                    if inner.path.is_ident("list") {
12611                        perms.list = parse_codenames(&inner)?;
12612                    } else if inner.path.is_ident("retrieve") {
12613                        perms.retrieve = parse_codenames(&inner)?;
12614                    } else if inner.path.is_ident("create") {
12615                        perms.create = parse_codenames(&inner)?;
12616                    } else if inner.path.is_ident("update") {
12617                        perms.update = parse_codenames(&inner)?;
12618                    } else if inner.path.is_ident("destroy") {
12619                        perms.destroy = parse_codenames(&inner)?;
12620                    } else {
12621                        return Err(inner.error(
12622                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12623                        ));
12624                    }
12625                    Ok(())
12626                })?;
12627                return Ok(());
12628            }
12629            Err(meta.error(
12630                "unknown viewset attribute (supported: model, fields, filter_fields, \
12631                 search_fields, ordering, page_size, read_only, serializer, permissions(...))",
12632            ))
12633        })?;
12634    }
12635
12636    let model = model.ok_or_else(|| {
12637        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12638    })?;
12639
12640    Ok(ViewSetAttrs {
12641        model,
12642        fields,
12643        filter_fields,
12644        search_fields,
12645        ordering,
12646        page_size,
12647        read_only,
12648        perms,
12649        serializer,
12650    })
12651}
12652
12653// ============================================================ #[derive(Serializer)]
12654
12655struct SerializerContainerAttrs {
12656    model: syn::Path,
12657    /// `#[serializer(validate = "fn_name")]` on the struct — the
12658    /// cross-field validation hook (#436). The named inherent method
12659    /// must take `&self` and return
12660    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12661    /// `validate()` runs every per-field validator first; then calls
12662    /// the cross-field method if declared; aggregates all errors into
12663    /// one `FormErrors`.
12664    cross_validate: Option<syn::Ident>,
12665}
12666
12667#[derive(Default)]
12668struct SerializerFieldAttrs {
12669    read_only: bool,
12670    write_only: bool,
12671    source: Option<String>,
12672    skip: bool,
12673    /// `#[serializer(method = "fn_name")]` — computed field. The macro
12674    /// emits a `from_model` initializer that calls
12675    /// `Self::fn_name(&model)` and stores the return value.
12676    method: Option<String>,
12677    /// `#[serializer(validate = "fn_name")]` — per-field validator
12678    /// callable run by `Self::validate(&self)`. Must return
12679    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12680    /// the field name.
12681    validate: Option<String>,
12682    /// `#[serializer(nested)]` on a field whose type is another
12683    /// `Serializer` — the macro emits `from_model` initializer that
12684    /// reads the parent via `model.<source>.value()` then calls the
12685    /// child serializer's `from_model(parent)`. When the FK is
12686    /// unloaded the field falls back to `Default::default()` (does
12687    /// NOT panic) so a missing prefetch in prod degrades gracefully.
12688    /// Source field on the model defaults to the field name; override
12689    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12690    /// panic-on-unloaded behavior for tests.
12691    nested: bool,
12692    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12693    /// strict behavior: panic when the FK isn't loaded. Useful in
12694    /// test code where forgetting select_related must trip a hard
12695    /// failure rather than render a blank nested object.
12696    nested_strict: bool,
12697    /// `#[serializer(many = TagSerializer)]` — declare the field as
12698    /// a list of nested serializers. Field type must be `Vec<S>`
12699    /// where `S` is the inner serializer. The macro initializes the
12700    /// field to `Vec::new()` in `from_model` and emits a typed
12701    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12702    /// maps each model row through `S::from_model`. Auto-load isn't
12703    /// possible (the M2M / one-to-many accessor is async); callers
12704    /// fetch the children + call the setter post-from_model.
12705    many: Option<syn::Type>,
12706    /// `#[serializer(slug = "name")]` — render a relation as one of
12707    /// its fields.
12708    /// Source field on the model must be a `ForeignKey<T>`; the
12709    /// macro emits `from_model` glue that walks
12710    /// `model.<source>.value()?.<slug>` and clones it. Field type on
12711    /// the serializer is typically `String` (whatever type the slug
12712    /// column has). When the FK is unloaded the field falls back to
12713    /// `Default::default()`, same graceful-degrade contract as
12714    /// `nested`. Source defaults to the field name; override with
12715    /// `source = "..."`. v0.44.
12716    slug: Option<String>,
12717    /// `#[serializer(max_length = N)]` — caps
12718    /// the character count of a string field on write. Overrides the
12719    /// model's `max_length`; when absent the model value is inherited.
12720    max_length: Option<u64>,
12721    /// `#[serializer(min_length = N)]` — minimum character count.
12722    /// Serializer-only (the model has no `min_length` column).
12723    min_length: Option<u64>,
12724    /// `#[serializer(min = N)]` — inclusive
12725    /// integer lower bound; overrides the model's `min` when given.
12726    min: Option<i64>,
12727    /// `#[serializer(max = N)]` — inclusive
12728    /// integer upper bound; overrides the model's `max` when given.
12729    max: Option<i64>,
12730}
12731
12732fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12733    let mut model: Option<syn::Path> = None;
12734    let mut cross_validate: Option<syn::Ident> = None;
12735    for attr in &input.attrs {
12736        if !attr.path().is_ident("serializer") {
12737            continue;
12738        }
12739        attr.parse_nested_meta(|meta| {
12740            if meta.path.is_ident("model") {
12741                let _eq: syn::Token![=] = meta.input.parse()?;
12742                model = Some(meta.input.parse()?);
12743                return Ok(());
12744            }
12745            if meta.path.is_ident("validate") {
12746                // #436 — container-level `validate = "fn_name"` for
12747                // cross-field validation. Field-level
12748                // `#[serializer(validate = "...")]` on a field is
12749                // parsed separately in `parse_serializer_field_attrs`.
12750                let s: LitStr = meta.value()?.parse()?;
12751                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12752                return Ok(());
12753            }
12754            Err(meta.error(
12755                "unknown serializer container attribute \
12756                 (supported: `model`, `validate`)",
12757            ))
12758        })?;
12759    }
12760    let model = model.ok_or_else(|| {
12761        syn::Error::new_spanned(
12762            &input.ident,
12763            "`#[serializer(model = SomeModel)]` is required",
12764        )
12765    })?;
12766    Ok(SerializerContainerAttrs {
12767        model,
12768        cross_validate,
12769    })
12770}
12771
12772fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12773    let mut out = SerializerFieldAttrs::default();
12774    for attr in &field.attrs {
12775        if !attr.path().is_ident("serializer") {
12776            continue;
12777        }
12778        attr.parse_nested_meta(|meta| {
12779            if meta.path.is_ident("read_only") {
12780                out.read_only = true;
12781                return Ok(());
12782            }
12783            if meta.path.is_ident("write_only") {
12784                out.write_only = true;
12785                return Ok(());
12786            }
12787            if meta.path.is_ident("skip") {
12788                out.skip = true;
12789                return Ok(());
12790            }
12791            if meta.path.is_ident("source") {
12792                let s: LitStr = meta.value()?.parse()?;
12793                out.source = Some(s.value());
12794                return Ok(());
12795            }
12796            if meta.path.is_ident("method") {
12797                let s: LitStr = meta.value()?.parse()?;
12798                out.method = Some(s.value());
12799                return Ok(());
12800            }
12801            if meta.path.is_ident("validate") {
12802                let s: LitStr = meta.value()?.parse()?;
12803                out.validate = Some(s.value());
12804                return Ok(());
12805            }
12806            if meta.path.is_ident("many") {
12807                let _eq: syn::Token![=] = meta.input.parse()?;
12808                out.many = Some(meta.input.parse()?);
12809                return Ok(());
12810            }
12811            if meta.path.is_ident("nested") {
12812                out.nested = true;
12813                // Optional strict flag inside parentheses:
12814                //   #[serializer(nested(strict))]
12815                if meta.input.peek(syn::token::Paren) {
12816                    meta.parse_nested_meta(|inner| {
12817                        if inner.path.is_ident("strict") {
12818                            out.nested_strict = true;
12819                            return Ok(());
12820                        }
12821                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12822                    })?;
12823                }
12824                return Ok(());
12825            }
12826            if meta.path.is_ident("slug") {
12827                let s: LitStr = meta.value()?.parse()?;
12828                out.slug = Some(s.value());
12829                return Ok(());
12830            }
12831            if meta.path.is_ident("max_length") {
12832                let lit: syn::LitInt = meta.value()?.parse()?;
12833                out.max_length = Some(lit.base10_parse::<u64>()?);
12834                return Ok(());
12835            }
12836            if meta.path.is_ident("min_length") {
12837                let lit: syn::LitInt = meta.value()?.parse()?;
12838                out.min_length = Some(lit.base10_parse::<u64>()?);
12839                return Ok(());
12840            }
12841            if meta.path.is_ident("min") {
12842                let lit: syn::LitInt = meta.value()?.parse()?;
12843                out.min = Some(lit.base10_parse::<i64>()?);
12844                return Ok(());
12845            }
12846            if meta.path.is_ident("max") {
12847                let lit: syn::LitInt = meta.value()?.parse()?;
12848                out.max = Some(lit.base10_parse::<i64>()?);
12849                return Ok(());
12850            }
12851            Err(meta.error(
12852                "unknown serializer field attribute (supported: \
12853                 `read_only`, `write_only`, `source`, `skip`, `method`, \
12854                 `validate`, `nested`, `many`, `slug`, `max_length`, \
12855                 `min_length`, `min`, `max`)",
12856            ))
12857        })?;
12858    }
12859    // Validate: read_only + write_only is nonsensical
12860    if out.read_only && out.write_only {
12861        return Err(syn::Error::new_spanned(
12862            field,
12863            "a field cannot be both `read_only` and `write_only`",
12864        ));
12865    }
12866    if out.method.is_some() && out.source.is_some() {
12867        return Err(syn::Error::new_spanned(
12868            field,
12869            "`method` and `source` are mutually exclusive — `method` computes \
12870             the value from a method, `source` reads it from a different model field",
12871        ));
12872    }
12873    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12874        return Err(syn::Error::new_spanned(
12875            field,
12876            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12877             — pick one strategy for populating the field",
12878        ));
12879    }
12880    Ok(out)
12881}
12882
12883fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12884    let root = rustango_root();
12885    let struct_name = &input.ident;
12886    let struct_name_lit = struct_name.to_string();
12887
12888    let Data::Struct(data) = &input.data else {
12889        return Err(syn::Error::new_spanned(
12890            struct_name,
12891            "Serializer can only be derived on structs",
12892        ));
12893    };
12894    let Fields::Named(named) = &data.fields else {
12895        return Err(syn::Error::new_spanned(
12896            struct_name,
12897            "Serializer requires a struct with named fields",
12898        ));
12899    };
12900
12901    let container = parse_serializer_container_attrs(input)?;
12902    let model_path = &container.model;
12903
12904    // Classify each field. `ty` is only consumed by the
12905    // `#[cfg(feature = "openapi")]` block below, but we always
12906    // capture it to keep the field-info build a single pass.
12907    #[allow(dead_code)]
12908    struct FieldInfo {
12909        ident: syn::Ident,
12910        ty: syn::Type,
12911        attrs: SerializerFieldAttrs,
12912    }
12913    let mut fields_info: Vec<FieldInfo> = Vec::new();
12914    for field in &named.named {
12915        let ident = field.ident.clone().expect("named field has ident");
12916        let attrs = parse_serializer_field_attrs(field)?;
12917        fields_info.push(FieldInfo {
12918            ident,
12919            ty: field.ty.clone(),
12920            attrs,
12921        });
12922    }
12923
12924    // Generate from_model body: struct literal with each field assigned.
12925    let from_model_fields = fields_info.iter().map(|fi| {
12926        let ident = &fi.ident;
12927        let ty = &fi.ty;
12928        if let Some(_inner) = &fi.attrs.many {
12929            // Many — collection field. Initialize empty; caller
12930            // populates via the macro-emitted set_<field> helper
12931            // after fetching the M2M children.
12932            quote! { #ident: ::std::vec::Vec::new() }
12933        } else if let Some(method) = &fi.attrs.method {
12934            // SerializerMethodField: call Self::<method>(&model) to
12935            // compute the value. Method signature must be
12936            // `fn <method>(model: &T) -> <field type>`.
12937            let method_ident = syn::Ident::new(method, ident.span());
12938            quote! { #ident: Self::#method_ident(model) }
12939        } else if let Some(slug_field) = &fi.attrs.slug {
12940            // v0.44 — SlugRelatedField. Source defaults to the field
12941            // name on this struct; override via `source = "..."`. The
12942            // source field on the model is expected to be a
12943            // `ForeignKey<T>`; the slug field on the parent is named
12944            // by the attribute value. When the FK is unloaded the
12945            // field falls back to `Default::default()` — same
12946            // graceful-degrade contract as `nested`.
12947            let src_name = fi
12948                .attrs
12949                .source
12950                .as_deref()
12951                .unwrap_or(&fi.ident.to_string())
12952                .to_owned();
12953            let src_ident = syn::Ident::new(&src_name, ident.span());
12954            let slug_ident = syn::Ident::new(slug_field, ident.span());
12955            quote! {
12956                #ident: match model.#src_ident.value() {
12957                    ::core::option::Option::Some(__loaded) =>
12958                        ::core::clone::Clone::clone(&__loaded.#slug_ident),
12959                    ::core::option::Option::None =>
12960                        ::core::default::Default::default(),
12961                }
12962            }
12963        } else if fi.attrs.nested {
12964            // Nested serializer. Source defaults to the field name on
12965            // this struct; override via `source = "..."`. The source
12966            // field on the model is expected to be a `ForeignKey<T>`
12967            // whose `.value()` returns `Option<&T>` after lazy-load.
12968            //
12969            // Behavior matrix (tweakable per-field):
12970            //   * FK loaded   → nested object materializes via
12971            //                   ChildSerializer::from_model(parent).
12972            //   * FK unloaded → fall back to ChildSerializer::default()
12973            //                   (so prod doesn't crash on a missing
12974            //                   prefetch — just renders a blank nested
12975            //                   object). Add `#[serializer(nested,
12976            //                   strict)]` to keep the v0.18.1
12977            //                   panic-on-unloaded behavior for tests
12978            //                   that want hard guardrails.
12979            let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
12980            let src_ident = syn::Ident::new(&src_name, ident.span());
12981            if fi.attrs.nested_strict {
12982                let panic_msg = format!(
12983                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
12984                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
12985                );
12986                quote! {
12987                    #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
12988                        model.#src_ident.value().expect(#panic_msg),
12989                    )
12990                }
12991            } else {
12992                quote! {
12993                    #ident: match model.#src_ident.value() {
12994                        ::core::option::Option::Some(__loaded) =>
12995                            <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
12996                        ::core::option::Option::None =>
12997                            ::core::default::Default::default(),
12998                    }
12999                }
13000            }
13001        } else if fi.attrs.write_only || fi.attrs.skip {
13002            // Not read from model — use default
13003            quote! { #ident: ::core::default::Default::default() }
13004        } else if let Some(src) = &fi.attrs.source {
13005            let src_ident = syn::Ident::new(src, ident.span());
13006            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13007        } else {
13008            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13009        }
13010    });
13011
13012    // is_writable predicate (also used by writable_lits /
13013    // writable_source_lits below).
13014    let is_writable = |fi: &&FieldInfo| {
13015        !fi.attrs.read_only
13016            && !fi.attrs.skip
13017            && fi.attrs.method.is_none()
13018            && !fi.attrs.nested
13019            && fi.attrs.many.is_none()
13020            && fi.attrs.slug.is_none()
13021    };
13022
13023    // Declarative field constraints: one block
13024    // per writable field, run inside `validate()`. Each resolves its
13025    // bounds as the serializer attr when given (`#[serializer(max_length
13026    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13027    // `max` / `choices`), then dispatches on the field's JSON value via
13028    // `forms::validators::check_value` (string → length/choices, integer
13029    // → min/max).
13030    let opt_usize = |v: Option<u64>| match v {
13031        Some(n) => {
13032            let n = n as usize;
13033            quote!(::core::option::Option::Some(#n))
13034        }
13035        None => quote!(::core::option::Option::None),
13036    };
13037    let opt_i64 = |v: Option<i64>| match v {
13038        Some(n) => quote!(::core::option::Option::Some(#n)),
13039        None => quote!(::core::option::Option::None),
13040    };
13041    let constraint_blocks: Vec<_> = fields_info
13042        .iter()
13043        .filter(is_writable)
13044        .map(|fi| {
13045            let ident = &fi.ident;
13046            let fname = ident.to_string();
13047            let mname = fi
13048                .attrs
13049                .source
13050                .clone()
13051                .unwrap_or_else(|| fi.ident.to_string());
13052            let attr_max_len = opt_usize(fi.attrs.max_length);
13053            let attr_min_len = opt_usize(fi.attrs.min_length);
13054            let attr_min = opt_i64(fi.attrs.min);
13055            let attr_max = opt_i64(fi.attrs.max);
13056            quote! {
13057                {
13058                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13059                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13060                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13061                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13062                    let __min: ::core::option::Option<i64> =
13063                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13064                    let __max: ::core::option::Option<i64> =
13065                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13066                    let __choices = __sf.and_then(|__f| __f.choices);
13067                    let __v = #root::__serde_json::to_value(&self.#ident)
13068                        .unwrap_or(#root::__serde_json::Value::Null);
13069                    #root::forms::validators::check_value(
13070                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13071                        &mut __errors,
13072                    );
13073                }
13074            }
13075        })
13076        .collect();
13077    let has_constraints = !constraint_blocks.is_empty();
13078
13079    // Per-field validators. Emit a
13080    // `validate(&self)` method that runs each user-defined validator
13081    // and aggregates errors into `FormErrors`.
13082    let validator_calls: Vec<_> = fields_info
13083        .iter()
13084        .filter_map(|fi| {
13085            let ident = &fi.ident;
13086            let name_lit = ident.to_string();
13087            let method = fi.attrs.validate.as_ref()?;
13088            let method_ident = syn::Ident::new(method, ident.span());
13089            Some(quote! {
13090                if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13091                    __errors.add(#name_lit.to_owned(), __e);
13092                }
13093            })
13094        })
13095        .collect();
13096    // #436 — cross-field `validate(&self)`. If the
13097    // container declared `#[serializer(validate = "fn_name")]`,
13098    // the macro-generated `validate(&self)` runs every per-field
13099    // validator first, then calls the user's cross-field method,
13100    // merging its `FormErrors` into the per-field errors. Either
13101    // alone is enough to emit the wrapper.
13102    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13103        quote! {
13104            // Merge cross-field errors into the per-field bucket so
13105            // a single .validate() call surfaces both layers.
13106            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13107                __errors.merge(__cross);
13108            }
13109        }
13110    });
13111    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13112    // Anything to run? Declarative constraints (for any writable field) +
13113    // per-field validators + cross-field hook.
13114    let has_run_validations = has_validators || has_constraints;
13115    // Shared body: declarative field constraints first (length / range /
13116    // choices, serializer-attr-or-model), then per-field validators, then
13117    // the cross-field hook.
13118    let validate_body = quote! {
13119        let mut __errors = #root::forms::FormErrors::default();
13120        #( #constraint_blocks )*
13121        #( #validator_calls )*
13122        #cross_validate_call
13123        if __errors.is_empty() {
13124            ::core::result::Result::Ok(())
13125        } else {
13126            ::core::result::Result::Err(__errors)
13127        }
13128    };
13129    // Inherent `validate(&self)` — kept for back-compat with direct
13130    // `serializer.validate()` calls that don't import `ModelSerializer`.
13131    // Emitted only when the serializer declares per-field/cross-field
13132    // validators (unchanged rule) so it never collides with a
13133    // hand-written inherent `validate`.
13134    let validate_method = if has_validators {
13135        quote! {
13136            impl #struct_name {
13137                /// Run the declarative field constraints, every
13138                /// `#[serializer(validate = "...")]` per-field validator,
13139                /// and (when declared) the container-level cross-field
13140                /// validator. Aggregates errors into `FormErrors` keyed by
13141                /// the field name. Returns `Ok(())` when all pass.
13142                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13143                    #validate_body
13144                }
13145            }
13146        }
13147    } else {
13148        quote! {}
13149    };
13150    // Trait-method override so the type-erased ViewSet write path can
13151    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13152    // there's anything to run (declarative constraints inherited from the
13153    // model count); otherwise the trait's default no-op is used.
13154    let trait_validate_override = if has_run_validations {
13155        quote! {
13156            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13157                #validate_body
13158            }
13159        }
13160    } else {
13161        quote! {}
13162    };
13163
13164    // For every `#[serializer(many = S)]` field, emit a
13165    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13166    // helper that maps the parents through `S::from_model`.
13167    let many_setters: Vec<_> = fields_info
13168        .iter()
13169        .filter_map(|fi| {
13170            let many_ty = fi.attrs.many.as_ref()?;
13171            let ident = &fi.ident;
13172            let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
13173            Some(quote! {
13174                /// Populate this `many` field by mapping each parent model
13175                /// through the inner serializer's `from_model`. Call after
13176                /// fetching the M2M / one-to-many children since
13177                /// `from_model` itself can't await an SQL query.
13178                pub fn #setter(
13179                    &mut self,
13180                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13181                ) -> &mut Self {
13182                    self.#ident = models.iter()
13183                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13184                        .collect();
13185                    self
13186                }
13187            })
13188        })
13189        .collect();
13190    let many_setters_impl = if many_setters.is_empty() {
13191        quote! {}
13192    } else {
13193        quote! {
13194            impl #struct_name {
13195                #( #many_setters )*
13196            }
13197        }
13198    };
13199
13200    // Generate custom Serialize: skip write_only fields
13201    let output_fields: Vec<_> = fields_info
13202        .iter()
13203        .filter(|fi| !fi.attrs.write_only)
13204        .collect();
13205    let output_field_count = output_fields.len();
13206    let serialize_fields = output_fields.iter().map(|fi| {
13207        let ident = &fi.ident;
13208        let name_lit = ident.to_string();
13209        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13210    });
13211
13212    // writable_fields: normal + write_only.
13213    // Exclude:
13214    //   - `read_only` — server-computed.
13215    //   - `skip` — caller sets manually post-from_model.
13216    //   - `method` — computed from a Self::fn(&model) call; accepting
13217    //     it on write is meaningless.
13218    //   - `nested` / `many` — populated from related-model data, not
13219    //     from a field on the wire body.
13220    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13221    // `many` in `writable_fields()`, which made the ViewSet write
13222    // path accept those fields from the JSON body and try to bind
13223    // them to the SQL UPDATE — a silent no-op at best, a type
13224    // mismatch at worst.
13225    // (`is_writable` is defined above, near the constraint codegen.)
13226    let writable_lits: Vec<_> = fields_info
13227        .iter()
13228        .filter(is_writable)
13229        .map(|fi| fi.ident.to_string())
13230        .collect();
13231
13232    // `writable_source_fields`: the MODEL field names of writable
13233    // serializer fields (source-resolved). The ViewSet write path skips
13234    // every model column NOT in this set, so `read_only` / `method` /
13235    // computed fields a client posts are ignored instead of written.
13236    let writable_source_lits: Vec<String> = fields_info
13237        .iter()
13238        .filter(is_writable)
13239        .map(|fi| {
13240            fi.attrs
13241                .source
13242                .clone()
13243                .unwrap_or_else(|| fi.ident.to_string())
13244        })
13245        .collect();
13246
13247    // `from_writable_json`: build a partial instance for input
13248    // validation. Writable fields are parsed from the JSON body (keyed
13249    // by serializer field name); every other field defaults. Per-field
13250    // type errors collect into `FormErrors` keyed by the field name.
13251    // Construction is field-by-field (not `Self::default()`) so it works
13252    // for serializers regardless of a struct-level `Default`.
13253    let from_writable_json_inits: Vec<_> = fields_info
13254        .iter()
13255        .map(|fi| {
13256            let ident = &fi.ident;
13257            let fname = ident.to_string();
13258            let ty = &fi.ty;
13259            if is_writable(&fi) {
13260                quote! {
13261                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13262                        ::core::option::Option::Some(__v) => {
13263                            match #root::__serde_json::from_value::<#ty>(
13264                                ::core::clone::Clone::clone(__v),
13265                            ) {
13266                                ::core::result::Result::Ok(__x) => __x,
13267                                ::core::result::Result::Err(__e) => {
13268                                    __errors.add(#fname.to_owned(), __e.to_string());
13269                                    ::core::default::Default::default()
13270                                }
13271                            }
13272                        }
13273                        ::core::option::Option::None => ::core::default::Default::default(),
13274                    }
13275                }
13276            } else {
13277                quote! { #ident: ::core::default::Default::default() }
13278            }
13279        })
13280        .collect();
13281
13282    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13283    // Only includes fields shown in JSON output (skips write_only). For each
13284    // `Option<T>` field, omit from `required` and add `.nullable()`.
13285    let openapi_impl = {
13286        #[cfg(feature = "openapi")]
13287        {
13288            let property_calls = output_fields.iter().map(|fi| {
13289                let ident = &fi.ident;
13290                let name_lit = ident.to_string();
13291                let ty = &fi.ty;
13292                let nullable_call = if is_option(ty) {
13293                    quote! { .nullable() }
13294                } else {
13295                    quote! {}
13296                };
13297                quote! {
13298                    .property(
13299                        #name_lit,
13300                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13301                            #nullable_call,
13302                    )
13303                }
13304            });
13305            let required_lits: Vec<_> = output_fields
13306                .iter()
13307                .filter(|fi| !is_option(&fi.ty))
13308                .map(|fi| fi.ident.to_string())
13309                .collect();
13310            quote! {
13311                impl #root::openapi::OpenApiSchema for #struct_name {
13312                    fn openapi_schema() -> #root::openapi::Schema {
13313                        #root::openapi::Schema::object()
13314                            #( #property_calls )*
13315                            .required([ #( #required_lits ),* ])
13316                    }
13317                }
13318            }
13319        }
13320        #[cfg(not(feature = "openapi"))]
13321        {
13322            quote! {}
13323        }
13324    };
13325
13326    Ok(quote! {
13327        impl #root::serializer::ModelSerializer for #struct_name {
13328            type Model = #model_path;
13329
13330            fn from_model(model: &Self::Model) -> Self {
13331                Self {
13332                    #( #from_model_fields ),*
13333                }
13334            }
13335
13336            fn writable_fields() -> &'static [&'static str] {
13337                &[ #( #writable_lits ),* ]
13338            }
13339
13340            fn writable_source_fields() -> &'static [&'static str] {
13341                &[ #( #writable_source_lits ),* ]
13342            }
13343
13344            fn from_writable_json(
13345                __body: &#root::__serde_json::Value,
13346            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13347                let mut __errors = #root::forms::FormErrors::default();
13348                let __obj = __body.as_object();
13349                let __out = Self {
13350                    #( #from_writable_json_inits ),*
13351                };
13352                if __errors.is_empty() {
13353                    ::core::result::Result::Ok(__out)
13354                } else {
13355                    ::core::result::Result::Err(__errors)
13356                }
13357            }
13358
13359            #trait_validate_override
13360        }
13361
13362        impl #root::__serde::Serialize for #struct_name {
13363            fn serialize<S>(&self, serializer: S)
13364                -> ::core::result::Result<S::Ok, S::Error>
13365            where
13366                S: #root::__serde::Serializer,
13367            {
13368                use #root::__serde::ser::SerializeStruct;
13369                let mut __state = serializer.serialize_struct(
13370                    #struct_name_lit,
13371                    #output_field_count,
13372                )?;
13373                #( #serialize_fields )*
13374                __state.end()
13375            }
13376        }
13377
13378        #openapi_impl
13379
13380        #validate_method
13381
13382        #many_setters_impl
13383    })
13384}
13385
13386/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13387/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13388/// when the feature is off.
13389#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13390fn is_option(ty: &syn::Type) -> bool {
13391    if let syn::Type::Path(p) = ty {
13392        if let Some(last) = p.path.segments.last() {
13393            return last.ident == "Option";
13394        }
13395    }
13396    false
13397}
13398
13399#[cfg(test)]
13400mod main_attr_tests {
13401    use super::*;
13402
13403    fn expand(args: &str) -> String {
13404        let body = quote! { async fn main() { run().await } };
13405        expand_main(args.parse().unwrap(), body)
13406            .expect("expansion failed")
13407            .to_string()
13408    }
13409
13410    /// Asserts on the expansion rather than on `parse_logging`,
13411    /// because the defect was a subscriber reaching the output, not
13412    /// an argument being misread (#1465).
13413    #[test]
13414    fn logging_false_emits_no_subscriber() {
13415        let out = expand("logging = false");
13416        assert!(
13417            !out.contains("tracing_subscriber"),
13418            "`logging = false` must not install a subscriber:\n{out}",
13419        );
13420        assert!(
13421            out.contains("block_on"),
13422            "the runtime must still be built:\n{out}",
13423        );
13424    }
13425
13426    #[test]
13427    fn default_still_installs_a_subscriber() {
13428        // The control. Without it, an `expand_main` that dropped the
13429        // prologue unconditionally would pass the test above while
13430        // leaving every default project with no logging at all.
13431        let out = expand("");
13432        assert!(
13433            out.contains("tracing_subscriber"),
13434            "the default entrypoint must still install one:\n{out}",
13435        );
13436    }
13437
13438    #[test]
13439    fn logging_false_composes_with_flavor() {
13440        let out = expand("flavor = \"current_thread\", logging = false");
13441        assert!(!out.contains("tracing_subscriber"), "{out}");
13442        assert!(out.contains("new_current_thread"), "{out}");
13443    }
13444
13445    #[test]
13446    fn logging_true_is_the_default_shape() {
13447        // `logging = true` is accepted and means the default, so the
13448        // match must not fire on the substring `logging`.
13449        assert!(parse_logging(&"logging = true".parse().unwrap()));
13450        assert!(!parse_logging(&"logging=false".parse().unwrap()));
13451    }
13452}