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/// * `allow_anonymous` — flag; write actions without codenames are
89///   intended, so the mount warning is skipped.
90/// * `serializer = SomeSerializer` — render list / retrieve / create
91///   responses through a `#[derive(Serializer)]` type instead of the
92///   default field-level projection (`read_only` / `source` / `method`
93///   / `write_only` overrides apply). Tri-dialect. Requires the
94///   `serializer` feature.
95/// * `permissions(list = "...", retrieve = "...", create = "...",
96///   update = "...", destroy = "...")` — codenames required per action.
97#[proc_macro_derive(ViewSet, attributes(viewset))]
98pub fn derive_viewset(input: TokenStream) -> TokenStream {
99    let input = parse_macro_input!(input as DeriveInput);
100    expand_viewset(&input)
101        .unwrap_or_else(syn::Error::into_compile_error)
102        .into()
103}
104
105/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
106/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
107/// that walks every named field and:
108///
109/// * Parses the string value into the field's Rust type (`String`,
110///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
111///   nullable case).
112/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
113///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
114///   validators in declaration order, returning `FormError::Parse`
115///   on the first failure.
116///
117/// Example:
118///
119/// ```ignore
120/// #[derive(Form)]
121/// pub struct CreateItemForm {
122///     #[form(min_length = 1, max_length = 64)]
123///     pub name: String,
124///     #[form(min = 0, max = 150)]
125///     pub age: i32,
126///     pub active: bool,
127///     pub email: Option<String>,
128/// }
129///
130/// let parsed = CreateItemForm::parse(&form_map)?;
131/// ```
132#[proc_macro_derive(Form, attributes(form))]
133pub fn derive_form(input: TokenStream) -> TokenStream {
134    let input = parse_macro_input!(input as DeriveInput);
135    expand_form(&input)
136        .unwrap_or_else(syn::Error::into_compile_error)
137        .into()
138}
139
140/// Derive `rustango::serializer::ModelSerializer` for a struct.
141/// (intra-doc link disabled — the macro crate doesn't depend on
142/// `rustango` itself, so rustdoc can't resolve the path.)
143///
144/// # Container attribute (required)
145/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
146///
147/// # Field attributes
148/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
149/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
150/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
151/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
152/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
153/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
154/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
155/// - `#[serializer(slug = "name")]` — render a relation as one of its fields: clones `model.<source>.value()?.name`; excluded from `writable_fields()` (v0.44)
156/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
157/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
158///   write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
159///   no attr is given, override it when given (`min_length` is serializer-only)
160///
161/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
162#[proc_macro_derive(Serializer, attributes(serializer))]
163pub fn derive_serializer(input: TokenStream) -> TokenStream {
164    let input = parse_macro_input!(input as DeriveInput);
165    expand_serializer(&input)
166        .unwrap_or_else(syn::Error::into_compile_error)
167        .into()
168}
169
170/// Bake every `*.json` migration file in a directory into the binary
171/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
172/// of `(name, json_content)` pairs, lex-sorted by file stem.
173///
174/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
175/// behaviour as `migrate(pool, dir)` but with no filesystem access.
176/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
177/// (i.e. the crate that invokes the macro). Default is
178/// `"./migrations"` if no argument is supplied.
179///
180/// ```ignore
181/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
182/// // or:
183/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
184///
185/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
186/// ```
187///
188/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
189/// file's `name` field must equal its file stem, every `prev`
190/// reference must point to another migration in the same directory,
191/// and the JSON must parse. A broken chain — orphan `prev`, missing
192/// predecessor, malformed file — fails at macro-expansion time with
193/// a clear `compile_error!`. *No other Rust web framework
194/// validates migration chains at compile time*: Cot's migrations are
195/// imperative Rust code (no static chain), Loco's are SeaORM
196/// up/down (same), Rwf's are raw SQL (no chain at all).
197///
198/// Each migration is included via `include_str!` so cargo's rebuild
199/// detection picks up file *content* changes. **Caveat:** cargo
200/// doesn't watch directory listings, so adding or removing a
201/// migration file inside the dir won't auto-trigger a rebuild — run
202/// `cargo clean` (or just bump any other source file) when you add
203/// new migrations during embedded development.
204#[proc_macro]
205pub fn embed_migrations(input: TokenStream) -> TokenStream {
206    expand_embed_migrations(input.into())
207        .unwrap_or_else(syn::Error::into_compile_error)
208        .into()
209}
210
211/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
212/// compile time against typed columns. Issue #269 / T1.7.
213///
214/// Each invocation lowers to the equivalent typed-column method call:
215///
216/// ```ignore
217/// // These expand identically:
218/// Q!(User.email__icontains = "alice")
219/// User::email.ilike("%alice%")
220/// ```
221///
222/// Field-name typos fail the build (the macro emits `User::no_such_field`
223/// which doesn't exist) — the headline ergonomic win of this slice over
224/// filters keyed by a plain string.
225///
226/// # Supported lookup suffixes
227///
228/// * bare `=` / `__exact` → `.eq(value)`
229/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
230/// * `__ne` → `.ne(value)`
231/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
232/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
233/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
234/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
235/// * `__in` → `.is_in(iterable)`
236/// * `__not_in` → `.not_in(iterable)`
237/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
238/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
239/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
240///
241/// Unknown suffixes fail the build with a `compile_error!` pointing at
242/// the lookup token.
243///
244/// # Combine
245///
246/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
247/// `.and()` / `.or()` / `.not()` methods:
248///
249/// ```ignore
250/// User::objects()
251///     .where_(
252///         Q!(User.active = true)
253///             .and(Q!(User.email__icontains = "alice"))
254///     )
255///     .fetch(&pool).await?;
256/// ```
257///
258/// All emitted code routes through existing per-dialect writers — no new
259/// SQL emission machinery. Tri-dialect support is inherent.
260#[allow(non_snake_case)]
261#[proc_macro]
262pub fn Q(input: TokenStream) -> TokenStream {
263    expand_q(input.into())
264        .unwrap_or_else(syn::Error::into_compile_error)
265        .into()
266}
267
268/// `#[rustango::main]` — the runserver entrypoint. Wraps
269/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
270/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
271/// functions are zero-boilerplate:
272///
273/// ```ignore
274/// #[rustango::main]
275/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
276///     rustango::server::Builder::from_env().await?
277///         .migrate("migrations").await?
278///         .api(my_app::urls::api())
279///         .seed_with(my_app::seed::run).await?
280///         .serve("0.0.0.0:8080").await
281/// }
282/// ```
283///
284/// Optional `flavor = "current_thread"` passes through to
285/// `#[tokio::main]`; default is the multi-threaded runtime.
286///
287/// `logging = false` suppresses the default subscriber, for apps
288/// that install their own. Needed whenever the body reaches
289/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
290/// macro installs gets there first, and `tracing` keeps the first
291/// one, so the `[logging]` section would otherwise be read and
292/// discarded (#1465).
293///
294/// ```ignore
295/// #[rustango::main(logging = false)]
296/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
297///     rustango::manage::Cli::new()
298///         .with_settings_from_env()
299///         .with_logging()          // now the first installer
300///         .run().await
301/// }
302/// ```
303///
304/// Pulls `tracing-subscriber` into the rustango crate behind the
305/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
306/// out get plain `#[tokio::main]` ergonomics without the dependency.
307#[proc_macro_attribute]
308pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
309    expand_main(args.into(), item.into())
310        .unwrap_or_else(syn::Error::into_compile_error)
311        .into()
312}
313
314fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
315    let mut input: syn::ItemFn = syn::parse2(item)?;
316    if input.sig.asyncness.is_none() {
317        return Err(syn::Error::new(
318            input.sig.ident.span(),
319            "`#[rustango::main]` must wrap an `async fn`",
320        ));
321    }
322
323    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
324    // to `#[tokio::main]`. Tokio's proc-macro internally emits
325    // `::tokio::*` paths that resolve against the user crate's deps,
326    // so calling it through the rustango re-export still requires the
327    // user to add tokio to their own Cargo.toml. Building the
328    // runtime ourselves keeps the dep transitive through the
329    // `runtime` feature on rustango.
330    //
331    // Parse optional `flavor = "current_thread"` / `flavor =
332    // "multi_thread"` from the attribute args. Unknown args are
333    // tolerated (forward-compat with tokio's own arg surface).
334    let root = rustango_root();
335    let flavor = parse_flavor(&args);
336    let builder_call = match flavor {
337        Flavor::CurrentThread => quote! {
338            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
339        },
340        Flavor::MultiThread => quote! {
341            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
342        },
343    };
344
345    // Detach the user body and rewrite `main` as a sync fn that
346    // builds the runtime and blocks on the async body.
347    let user_body = input.block.clone();
348    input.sig.asyncness = None;
349
350    // `logging = false` drops the block entirely, so a later
351    // `Cli::with_logging()` is the first installer and its
352    // `[logging]` settings actually take effect (#1465).
353    let logging_prologue = if parse_logging(&args) {
354        quote! {
355            use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
356            // Colour only when stdout is a terminal, and never under
357            // `NO_COLOR` — `Color::Auto`'s rule, called rather than
358            // copied.
359            //
360            // This was inlined at first, on a comment claiming
361            // `rustango::logging` is gated on `admin` + `tenancy`. It is
362            // not: `pub mod logging` is ungated and its contents are
363            // `runtime`-gated — the same feature that gates this macro
364            // (`pub use rustango_macros::main` is `#[cfg(feature =
365            // "runtime")]`). So wherever this expands, `Color` is
366            // reachable, and the copy was justified by a gate that does
367            // not exist. `should_colour` carries a deliberate
368            // `NO_COLOR`-set-but-empty subtlety that the inline happened
369            // to match today and nothing kept matching tomorrow.
370            //
371            // Without any of it the subscriber inherits
372            // tracing-subscriber's default, `cfg!(feature = "ansi")` —
373            // true since the framework turned that feature on — so
374            // `./app > app.log` and every container redirecting stdout
375            // collected escape codes. `Setup::install` has its own
376            // check; this is the default entrypoint and had none.
377            let __ansi = #root::logging::Color::Auto.should_colour();
378            // `try_init` so duplicate installers (e.g. tests already
379            // holding a subscriber) don't panic.
380            let _ = tracing_subscriber::fmt()
381                .with_env_filter(
382                    EnvFilter::try_from_default_env()
383                        .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
384                )
385                .with_ansi(__ansi)
386                .try_init();
387        }
388    } else {
389        quote! {}
390    };
391
392    input.block = syn::parse2(quote! {{
393        { #logging_prologue }
394        let __rt = #builder_call
395            .enable_all()
396            .build()
397            .expect("failed to build tokio runtime");
398        __rt.block_on(async move #user_body)
399    }})?;
400
401    Ok(quote! {
402        #input
403    })
404}
405
406enum Flavor {
407    MultiThread,
408    CurrentThread,
409}
410
411fn parse_flavor(args: &TokenStream2) -> Flavor {
412    // Cheap parser: look for the literal token sequence
413    // `flavor = "current_thread"`. Everything else (including
414    // bare `multi_thread` or no args) defaults to multi-thread.
415    let s = args.to_string();
416    if s.contains("current_thread") {
417        Flavor::CurrentThread
418    } else {
419        Flavor::MultiThread
420    }
421}
422
423/// `false` when the attribute carries `logging = false` — the caller
424/// installs their own subscriber, usually via `Cli::with_logging`
425/// (#1465). Same cheap token match as `parse_flavor`.
426fn parse_logging(args: &TokenStream2) -> bool {
427    !args.to_string().replace(' ', "").contains("logging=false")
428}
429
430/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
431struct QInput {
432    base_path: syn::Path,
433    field: syn::Ident,
434    value: syn::Expr,
435}
436
437impl syn::parse::Parse for QInput {
438    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
439        let base_path: syn::Path = input.parse()?;
440        input.parse::<syn::Token![.]>()?;
441        let field: syn::Ident = input.parse()?;
442        input.parse::<syn::Token![=]>()?;
443        let value: syn::Expr = input.parse()?;
444        Ok(QInput {
445            base_path,
446            field,
447            value,
448        })
449    }
450}
451
452fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
453    let q: QInput = syn::parse2(input)?;
454    let root = rustango_root();
455    let field_str = q.field.to_string();
456    let field_span = q.field.span();
457    let (base, suffix) = match field_str.find("__") {
458        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
459        None => (field_str.as_str(), ""),
460    };
461    if base.is_empty() {
462        return Err(syn::Error::new(
463            field_span,
464            "Q!(): field name is empty before `__` suffix",
465        ));
466    }
467    let base_ident = syn::Ident::new(base, field_span);
468    let value = &q.value;
469    let path = &q.base_path;
470
471    // Most suffixes map directly to a Column method with the value
472    // forwarded unchanged. Some need value-shape massaging (wildcards
473    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
474    // ISNULL). Unknown suffixes fail the build.
475    let expanded = match suffix {
476        "" | "exact" => quote! {
477            #root::core::Column::eq(#path::#base_ident, #value)
478        },
479        "ne" => quote! {
480            #root::core::Column::ne(#path::#base_ident, #value)
481        },
482        "gt" => quote! {
483            #root::core::Column::gt(#path::#base_ident, #value)
484        },
485        "gte" => quote! {
486            #root::core::Column::gte(#path::#base_ident, #value)
487        },
488        "lt" => quote! {
489            #root::core::Column::lt(#path::#base_ident, #value)
490        },
491        "lte" => quote! {
492            #root::core::Column::lte(#path::#base_ident, #value)
493        },
494        // The escaped-LIKE lookups delegate to the typed Column
495        // methods, which escape the value and pair it with the
496        // *Escaped ops so `%` / `_` match literally on every dialect
497        // (#1257) — the macro no longer owns any pattern wrapping.
498        "iexact" => quote! {
499            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
500        },
501        "contains" => quote! {
502            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
503        },
504        "icontains" => quote! {
505            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
506        },
507        "startswith" => quote! {
508            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
509        },
510        "istartswith" => quote! {
511            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
512        },
513        "endswith" => quote! {
514            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
515        },
516        "iendswith" => quote! {
517            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
518        },
519        "in" => quote! {
520            #root::core::Column::is_in(#path::#base_ident, #value)
521        },
522        "not_in" => quote! {
523            #root::core::Column::not_in(#path::#base_ident, #value)
524        },
525        "isnull" => {
526            // Must be a bool literal at macro time so we can route to
527            // is_null vs is_not_null without a runtime branch.
528            let b = match value {
529                syn::Expr::Lit(syn::ExprLit {
530                    lit: syn::Lit::Bool(b),
531                    ..
532                }) => b.value(),
533                _ => {
534                    return Err(syn::Error::new_spanned(
535                        value,
536                        "Q!(): `__isnull` requires a `true` or `false` literal",
537                    ));
538                }
539            };
540            if b {
541                quote! { #root::core::Column::is_null(#path::#base_ident) }
542            } else {
543                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
544            }
545        }
546        "between" => {
547            // Accept a tuple literal `(lo, hi)`.
548            let tuple = match value {
549                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
550                _ => {
551                    return Err(syn::Error::new_spanned(
552                        value,
553                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
554                    ));
555                }
556            };
557            let lo = &tuple.elems[0];
558            let hi = &tuple.elems[1];
559            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
560        }
561        "regex" => quote! {
562            #root::core::Column::regex(#path::#base_ident, #value)
563        },
564        "iregex" => quote! {
565            #root::core::Column::iregex(#path::#base_ident, #value)
566        },
567        _ => {
568            return Err(syn::Error::new(
569                field_span,
570                format!(
571                    "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",
572                    suffix
573                ),
574            ));
575        }
576    };
577    Ok(expanded)
578}
579
580fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
581    // Default to "./migrations" if invoked without args.
582    let path_str = if input.is_empty() {
583        "./migrations".to_string()
584    } else {
585        let lit: LitStr = syn::parse2(input)?;
586        lit.value()
587    };
588
589    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
590        syn::Error::new(
591            proc_macro2::Span::call_site(),
592            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
593        )
594    })?;
595    let abs = std::path::Path::new(&manifest).join(&path_str);
596
597    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
598    if abs.is_dir() {
599        let read = std::fs::read_dir(&abs).map_err(|e| {
600            syn::Error::new(
601                proc_macro2::Span::call_site(),
602                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
603            )
604        })?;
605        for entry in read.flatten() {
606            let path = entry.path();
607            if !path.is_file() {
608                continue;
609            }
610            if path.extension().and_then(|s| s.to_str()) != Some("json") {
611                continue;
612            }
613            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
614                continue;
615            };
616            entries.push((stem.to_owned(), path));
617        }
618    }
619    entries.sort_by(|a, b| a.0.cmp(&b.0));
620
621    // Compile-time chain validation: read each migration's JSON,
622    // pull `name` and `prev` (file-stem-keyed for the chain check),
623    // and verify every `prev` points to another migration in the
624    // slice. Mismatches between the file stem and the embedded
625    // `name` field — and broken `prev` chains — fail at MACRO
626    // EXPANSION time so a misshapen migration set never compiles.
627    //
628    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
629    // migrations + a Rust proc-macro that reads them is a combo no
630    // other Rust web framework matches (Cot's are imperative Rust
631    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
632    // a static chain to validate).
633    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
634    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
635    for (stem, path) in &entries {
636        let raw = std::fs::read_to_string(path).map_err(|e| {
637            syn::Error::new(
638                proc_macro2::Span::call_site(),
639                format!(
640                    "embed_migrations!: cannot read {} for chain validation: {e}",
641                    path.display()
642                ),
643            )
644        })?;
645        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
646            syn::Error::new(
647                proc_macro2::Span::call_site(),
648                format!(
649                    "embed_migrations!: {} is not valid JSON: {e}",
650                    path.display()
651                ),
652            )
653        })?;
654        let name = json
655            .get("name")
656            .and_then(|v| v.as_str())
657            .ok_or_else(|| {
658                syn::Error::new(
659                    proc_macro2::Span::call_site(),
660                    format!(
661                        "embed_migrations!: {} is missing the `name` field",
662                        path.display()
663                    ),
664                )
665            })?
666            .to_owned();
667        if name != *stem {
668            return Err(syn::Error::new(
669                proc_macro2::Span::call_site(),
670                format!(
671                    "embed_migrations!: file stem `{stem}` does not match the migration's \
672                     `name` field `{name}` — rename the file or fix the JSON",
673                ),
674            ));
675        }
676        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
677        chain_names.push(name.clone());
678        prev_refs.push((name, prev));
679    }
680
681    let name_set: std::collections::HashSet<&str> =
682        chain_names.iter().map(String::as_str).collect();
683    for (name, prev) in &prev_refs {
684        if let Some(p) = prev {
685            if !name_set.contains(p.as_str()) {
686                return Err(syn::Error::new(
687                    proc_macro2::Span::call_site(),
688                    format!(
689                        "embed_migrations!: broken migration chain — `{name}` declares \
690                         prev=`{p}` but no migration with that name exists in {}",
691                        abs.display()
692                    ),
693                ));
694            }
695        }
696    }
697
698    let pairs: Vec<TokenStream2> = entries
699        .iter()
700        .map(|(name, path)| {
701            let path_lit = path.display().to_string();
702            quote! { (#name, ::core::include_str!(#path_lit)) }
703        })
704        .collect();
705
706    Ok(quote! {
707        {
708            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
709            __RUSTANGO_EMBEDDED
710        }
711    })
712}
713
714fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
715    let root = rustango_root();
716    let struct_name = &input.ident;
717
718    let Data::Struct(data) = &input.data else {
719        return Err(syn::Error::new_spanned(
720            struct_name,
721            "Model can only be derived on structs",
722        ));
723    };
724    let Fields::Named(named) = &data.fields else {
725        return Err(syn::Error::new_spanned(
726            struct_name,
727            "Model requires a struct with named fields",
728        ));
729    };
730
731    let container = parse_container_attrs(input)?;
732    let table = container
733        .table
734        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
735    let model_name = struct_name.to_string();
736
737    let collected = collect_fields(named, &table)?;
738
739    // Validate that #[rustango(display = "…")] names a real field.
740    if let Some((ref display, span)) = container.display {
741        if !collected.field_names.iter().any(|n| n == display) {
742            return Err(syn::Error::new(
743                span,
744                format!("`display = \"{display}\"` does not match any field on this struct"),
745            ));
746        }
747    }
748    let display = container.display.map(|(name, _)| name);
749    let app_label = container.app.clone();
750
751    // Validate admin field-name lists against declared field names.
752    // Note: `list_display` is intentionally NOT validated here. As of
753    // v0.32 it may also reference inventory-registered computed
754    // fields (via `register_admin_computed!`) whose existence the
755    // macro can't see at compile time — they're submitted from any
756    // crate that depends on rustango. The runtime list-view resolves
757    // unknown names against the inventory + silently drops the
758    // truly-bogus ones, which is the cheaper trade-off versus
759    // forcing a per-Model attr to opt out.
760    if let Some(admin) = &container.admin {
761        for (label, list) in [
762            ("search_fields", &admin.search_fields),
763            ("readonly_fields", &admin.readonly_fields),
764            ("list_filter", &admin.list_filter),
765        ] {
766            if let Some((names, span)) = list {
767                for name in names {
768                    if !collected.field_names.iter().any(|n| n == name) {
769                        return Err(syn::Error::new(
770                            *span,
771                            format!(
772                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
773                            ),
774                        ));
775                    }
776                }
777            }
778        }
779        if let Some((pairs, span)) = &admin.ordering {
780            for (name, _) in pairs {
781                if !collected.field_names.iter().any(|n| n == name) {
782                    return Err(syn::Error::new(
783                        *span,
784                        format!(
785                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
786                        ),
787                    ));
788                }
789            }
790        }
791        if let Some((groups, span)) = &admin.fieldsets {
792            for (_, fields) in groups {
793                for name in fields {
794                    if !collected.field_names.iter().any(|n| n == name) {
795                        return Err(syn::Error::new(
796                            *span,
797                            format!(
798                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
799                            ),
800                        ));
801                    }
802                }
803            }
804        }
805    }
806    if let Some(audit) = &container.audit {
807        if let Some((names, span)) = &audit.track {
808            for name in names {
809                if !collected.field_names.iter().any(|n| n == name) {
810                    return Err(syn::Error::new(
811                        *span,
812                        format!(
813                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
814                        ),
815                    ));
816                }
817            }
818        }
819    }
820
821    // Issue #291 / T2.5 — validate each `default_order` column name
822    // against the model's collected fields. Typos fail at macro-expand
823    // time, not at the database.
824    for (col, _desc, span) in &container.default_order {
825        if !collected.field_names.iter().any(|n| n == col) {
826            return Err(syn::Error::new(
827                *span,
828                format!(
829                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
830                ),
831            ));
832        }
833    }
834
835    // Build the audit_track list for ModelSchema: None when no audit attr,
836    // Some(empty) when audit present without track, Some(names) when explicit.
837    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
838        audit
839            .track
840            .as_ref()
841            .map(|(names, _)| names.clone())
842            .unwrap_or_default()
843    });
844
845    // The upsert conflict target: a container-level composite unique,
846    // never a field `index(unique)` or a partial index PG can't target.
847    let upsert_unique: Option<Vec<String>> = container
848        .indexes
849        .iter()
850        .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
851        .map(|i| i.columns.clone());
852
853    // Merge field-level indexes into the container's index list.
854    let mut all_indexes: Vec<IndexAttr> = container.indexes;
855    for field in &named.named {
856        let ident = field.ident.as_ref().expect("named");
857        // The same default column as `process_field` (#1937).
858        let col = ident_name(ident);
859        // Re-parse field attrs to check for index flag
860        if let Ok(fa) = parse_field_attrs(field) {
861            if fa.index {
862                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
863                let auto_name = if fa.index_unique {
864                    format!("{table}_{col_name}_uq_idx")
865                } else {
866                    format!("{table}_{col_name}_idx")
867                };
868                all_indexes.push(IndexAttr {
869                    name: fa.index_name.or(Some(auto_name)),
870                    columns: vec![col_name],
871                    unique: fa.index_unique,
872                    method: fa.index_method,
873                    where_clause: None,
874                    include: Vec::new(),
875                });
876            }
877        }
878    }
879
880    let model_impl = model_impl_tokens(
881        struct_name,
882        &model_name,
883        &table,
884        display.as_deref(),
885        app_label.as_deref(),
886        container.admin.as_ref(),
887        &container.default_order,
888        &collected.field_schemas,
889        collected.soft_delete_column.as_deref(),
890        container.permissions,
891        audit_track_names.as_deref(),
892        &container.m2m,
893        &all_indexes,
894        &container.checks,
895        &container.excludes,
896        &container.composite_fks,
897        &container.generic_fks,
898        container.scope.as_deref(),
899        container.is_view,
900        container.verbose_name.as_deref(),
901        container.verbose_name_plural.as_deref(),
902        container.managed,
903        container.base_manager_name.as_deref(),
904        container.order_with_respect_to.as_deref(),
905        container.proxy,
906        &container.required_db_features,
907        container.required_db_vendor.as_deref(),
908        container.default_related_name.as_deref(),
909        container.db_table_comment.as_deref(),
910        container
911            .get_latest_by
912            .as_ref()
913            .map(|(c, d)| (c.as_str(), *d)),
914        &container.extra_permissions,
915        &container.default_permissions,
916        &container.global_scopes,
917        &container.reverse_has_relations,
918        &container.generic_has_relations,
919    );
920    let module_ident = column_module_ident(struct_name);
921    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
922    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
923        let track_set: Option<std::collections::HashSet<&str>> = audit
924            .track
925            .as_ref()
926            .map(|(names, _)| names.iter().map(String::as_str).collect());
927        collected
928            .column_entries
929            .iter()
930            .filter(|c| {
931                track_set
932                    .as_ref()
933                    .map_or(true, |s| s.contains(c.name.as_str()))
934            })
935            .collect()
936    });
937    let inherent_impl = inherent_impl_tokens(
938        struct_name,
939        &collected,
940        collected.primary_key.as_ref(),
941        &column_consts,
942        audited_fields.as_deref(),
943        upsert_unique.as_deref(),
944        &container.manager_fns,
945    );
946    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
947    let from_row_impl = from_row_impl_tokens(
948        struct_name,
949        &collected.from_row_inits,
950        &collected.from_row_inits_my,
951    );
952    let reverse_helpers = reverse_helper_tokens(
953        struct_name,
954        &collected.fk_relations,
955        container.default_related_name.as_deref(),
956    );
957    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
958    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
959    // Issue #817 — `#[rustango(through(...))]` accessors.
960    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
961    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
962    let reverse_has_accessors =
963        reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
964    let generic_fk_accessors = generic_fk_accessor_tokens(
965        struct_name,
966        &container.generic_fks,
967        &collected.column_entries,
968    );
969
970    // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
971    // emits an empty extension trait so users can add methods via
972    // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
973    // trait declaration. See `crates/rustango/src/manager.rs` for the
974    // pattern this replaces.
975    let manager_trait = container.manager_ext.as_ref().map(|name| {
976        let model_name_str = struct_name.to_string();
977        let doc = format!(
978            "Empty marker trait for [`{model_name_str}`], from \
979             `#[rustango(manager(ext = ...))]`. Put query shortcuts on your \
980             own extension trait over `QuerySet<{model_name_str}>`."
981        );
982        quote! {
983            #[doc = #doc]
984            pub trait #name: ::core::marker::Sized {}
985        }
986    });
987
988    Ok(quote! {
989        #model_impl
990        #inherent_impl
991        #from_row_impl
992        #column_module
993        #reverse_helpers
994        #m2m_accessors
995        #generic_m2m_accessors
996        #through_accessors
997        #reverse_has_accessors
998        #generic_fk_accessors
999        #manager_trait
1000
1001        #root::core::inventory::submit! {
1002            // `module_path!()` evaluates at the registration site,
1003            // so a Model declared in `crate::blog::models` records
1004            // `"<crate>::blog::models"` and `resolved_app_label()`
1005            // can infer "blog" without an explicit attribute.
1006            #root::core::ModelEntry::new(
1007                <#struct_name as #root::core::Model>::SCHEMA,
1008                ::core::module_path!(),
1009            )
1010            .with_audited(
1011                <#struct_name as #root::core::Model>::__rustango_audited_update,
1012                <#struct_name as #root::core::Model>::__rustango_audited_delete,
1013            )
1014            .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1015        }
1016    })
1017}
1018
1019/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1020/// matches `field_name` against the model's FK fields and, for a
1021/// match, decodes the FK target via the parent's macro-generated
1022/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
1023/// `ForeignKey::Loaded` on `self`.
1024///
1025/// Always emitted (with empty arms for FK-less models, which
1026/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1027/// trait bound on `fetch_on` is universally satisfied — users
1028/// never have to think about implementing it.
1029fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1030    let root = rustango_root();
1031    let arms = fk_relations.iter().map(|rel| {
1032        let parent_ty = &rel.parent_type;
1033        let field_ident = &rel.field_ident;
1034        let fk_name = ident_name(field_ident);
1035        let fk_col = fk_name.as_str();
1036        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1037        let assign = if rel.nullable {
1038            quote! {
1039                self.#field_ident = ::core::option::Option::Some(
1040                    #root::sql::ForeignKey::loaded(_pk, _parent),
1041                );
1042            }
1043        } else {
1044            quote! {
1045                self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1046            }
1047        };
1048        quote! {
1049            #fk_col => {
1050                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1051                // Audit #451 — multi-hop `select_related("a__b__c")`:
1052                // stitch the deeper relation onto this parent first,
1053                // decoding it at the accumulated `__next_alias`. The
1054                // parent type also impls `LoadRelated`, so this recurses
1055                // the FK chain to arbitrary depth.
1056                if let ::core::option::Option::Some(__r) = __rest {
1057                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1058                        &mut _parent, row, __r, &__next_alias,
1059                    )?;
1060                }
1061                // Loud-in-debug, default-in-release: a divergence
1062                // between the FK field's declared `K` (drives the
1063                // expected `SqlValue::<Variant>`) and the parent's
1064                // `__rustango_pk_value` output is a macro-internal
1065                // invariant break — surfacing the panic in dev
1066                // catches it before users hit silent PK=0 corruption.
1067                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1068                    #root::core::SqlValue::#variant_ident(v) => v,
1069                    _other => {
1070                        ::core::debug_assert!(
1071                            false,
1072                            "rustango macro bug: load_related on FK `{}` expected \
1073                             SqlValue::{} from parent's __rustango_pk_value but got \
1074                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1075                            #fk_col,
1076                            ::core::stringify!(#variant_ident),
1077                            _other,
1078                        );
1079                        #default_expr
1080                    }
1081                };
1082                #assign
1083                ::core::result::Result::Ok(true)
1084            }
1085        }
1086    });
1087    quote! {
1088        #[cfg(feature = "postgres")]
1089        impl #root::sql::LoadRelated for #struct_name {
1090            #[allow(unused_variables)]
1091            fn __rustango_load_related(
1092                &mut self,
1093                row: &#root::sql::sqlx::postgres::PgRow,
1094                field_name: &str,
1095                alias: &str,
1096            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1097                // Audit #451 — split the multi-hop path: `base` is the FK
1098                // on THIS model, `__rest` (if any) is the remaining chain
1099                // to stitch onto the loaded parent. `__next_alias` is the
1100                // accumulated join alias the parent's columns live under
1101                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1102                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1103                    match field_name.split_once("__") {
1104                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1105                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1106                    };
1107                let __next_alias: ::std::string::String = match __rest {
1108                    ::core::option::Option::Some(__r) => {
1109                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1110                        ::std::format!("{}__{}", alias, __rb)
1111                    }
1112                    ::core::option::Option::None => ::std::string::String::new(),
1113                };
1114                match __base {
1115                    #( #arms )*
1116                    _ => ::core::result::Result::Ok(false),
1117                }
1118            }
1119        }
1120    }
1121}
1122
1123/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1124/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1125/// which expands to a `LoadRelatedMy` impl when rustango is built with
1126/// the `mysql` feature, and to nothing otherwise. The decoded parent
1127/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1128/// decoder, also batch8) so the dual emission is symmetric across
1129/// backends.
1130fn load_related_impl_my_tokens(
1131    struct_name: &syn::Ident,
1132    fk_relations: &[FkRelation],
1133) -> TokenStream2 {
1134    let root = rustango_root();
1135    let arms = fk_relations.iter().map(|rel| {
1136        let parent_ty = &rel.parent_type;
1137        let field_ident = &rel.field_ident;
1138        let fk_name = ident_name(field_ident);
1139        let fk_col = fk_name.as_str();
1140        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1141        let assign = if rel.nullable {
1142            quote! {
1143                __self.#field_ident = ::core::option::Option::Some(
1144                    #root::sql::ForeignKey::loaded(_pk, _parent),
1145                );
1146            }
1147        } else {
1148            quote! {
1149                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1150            }
1151        };
1152        // `self` IS hygiene-tracked through macro_rules — emitted from
1153        // a different context than the `&mut self` parameter inside
1154        // the macro_rules-expanded fn. Pass it through as `__self`
1155        // and let the macro_rules rebind it to the receiver.
1156        quote! {
1157            #fk_col => {
1158                let mut _parent: #parent_ty =
1159                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1160                // Audit #451 — multi-hop: stitch the deeper relation onto
1161                // the parent at the accumulated alias (see PG twin).
1162                if let ::core::option::Option::Some(__r) = __rest {
1163                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1164                        &mut _parent, row, __r, &__next_alias,
1165                    )?;
1166                }
1167                // See note in `load_related_impl_tokens` (PG twin) —
1168                // the same loud-in-debug invariant guard.
1169                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1170                    #root::core::SqlValue::#variant_ident(v) => v,
1171                    _other => {
1172                        ::core::debug_assert!(
1173                            false,
1174                            "rustango macro bug: load_related on FK `{}` expected \
1175                             SqlValue::{} from parent's __rustango_pk_value but got \
1176                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1177                            #fk_col,
1178                            ::core::stringify!(#variant_ident),
1179                            _other,
1180                        );
1181                        #default_expr
1182                    }
1183                };
1184                #assign
1185                ::core::result::Result::Ok(true)
1186            }
1187        }
1188    });
1189    quote! {
1190        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1191            #( #arms )*
1192        });
1193    }
1194}
1195
1196/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1197/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1198/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1199fn load_related_impl_sqlite_tokens(
1200    struct_name: &syn::Ident,
1201    fk_relations: &[FkRelation],
1202) -> TokenStream2 {
1203    let root = rustango_root();
1204    let arms = fk_relations.iter().map(|rel| {
1205        let parent_ty = &rel.parent_type;
1206        let field_ident = &rel.field_ident;
1207        let fk_name = ident_name(field_ident);
1208        let fk_col = fk_name.as_str();
1209        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1210        let assign = if rel.nullable {
1211            quote! {
1212                __self.#field_ident = ::core::option::Option::Some(
1213                    #root::sql::ForeignKey::loaded(_pk, _parent),
1214                );
1215            }
1216        } else {
1217            quote! {
1218                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1219            }
1220        };
1221        quote! {
1222            #fk_col => {
1223                let mut _parent: #parent_ty =
1224                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1225                // Audit #451 — multi-hop: stitch the deeper relation onto
1226                // the parent at the accumulated alias (see PG twin).
1227                if let ::core::option::Option::Some(__r) = __rest {
1228                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1229                        &mut _parent, row, __r, &__next_alias,
1230                    )?;
1231                }
1232                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1233                    #root::core::SqlValue::#variant_ident(v) => v,
1234                    _other => {
1235                        ::core::debug_assert!(
1236                            false,
1237                            "rustango macro bug: load_related on FK `{}` expected \
1238                             SqlValue::{} from parent's __rustango_pk_value but got \
1239                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1240                            #fk_col,
1241                            ::core::stringify!(#variant_ident),
1242                            _other,
1243                        );
1244                        #default_expr
1245                    }
1246                };
1247                #assign
1248                ::core::result::Result::Ok(true)
1249            }
1250        }
1251    });
1252    quote! {
1253        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1254            #( #arms )*
1255        });
1256    }
1257}
1258
1259/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1260/// matches `field_name` against the model's FK fields and returns
1261/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1262/// group children by parent PK.
1263///
1264/// Always emitted (with `_ => None` for FK-less models) so the
1265/// trait bound on `fetch_with_prefetch` is universally satisfied.
1266fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1267    let root = rustango_root();
1268    let arms = fk_relations.iter().map(|rel| {
1269        let field_ident = &rel.field_ident;
1270        let fk_name = ident_name(field_ident);
1271        let fk_col = fk_name.as_str();
1272        if rel.pk_kind == DetectedKind::I64 {
1273            // i64 FK — return the stored PK so prefetch_related can
1274            // group children by it. Nullable variant unwraps via
1275            // `as_ref().map(...)`: an unset (NULL) FK column yields
1276            // `None` and that child sits out of the grouping (correct
1277            // semantics — it has no parent to attach to).
1278            if rel.nullable {
1279                quote! {
1280                    #fk_col => self.#field_ident
1281                        .as_ref()
1282                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1283                }
1284            } else {
1285                quote! {
1286                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1287                }
1288            }
1289        } else {
1290            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1291            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1292            // i64-keyed grouping path — the trait signature is
1293            // `Option<i64>` and a non-i64 PK can't lower into it.
1294            // The FK still works for everything else (CRUD, lazy
1295            // load via `.get()`, select_related JOINs); only the
1296            // bulk prefetch grouper needs the integer key.
1297            quote! {
1298                #fk_col => ::core::option::Option::None,
1299            }
1300        }
1301    });
1302    // PK-type-agnostic version: every FK arm emits an
1303    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1304    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1305    // opt OUT of the legacy i64 method (it returns None) but opt IN
1306    // here.
1307    let value_arms = fk_relations.iter().map(|rel| {
1308        let field_ident = &rel.field_ident;
1309        let fk_name = ident_name(field_ident);
1310        let fk_col = fk_name.as_str();
1311        if rel.nullable {
1312            quote! {
1313                #fk_col => self.#field_ident
1314                    .as_ref()
1315                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1316                        #root::sql::ForeignKey::pk(fk)
1317                    )),
1318            }
1319        } else {
1320            quote! {
1321                #fk_col => ::core::option::Option::Some(
1322                    ::core::convert::Into::<#root::core::SqlValue>::into(
1323                        self.#field_ident.pk()
1324                    )
1325                ),
1326            }
1327        }
1328    });
1329    quote! {
1330        impl #root::sql::FkPkAccess for #struct_name {
1331            #[allow(unused_variables)]
1332            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1333                match field_name {
1334                    #( #arms )*
1335                    _ => ::core::option::Option::None,
1336                }
1337            }
1338            #[allow(unused_variables)]
1339            fn __rustango_fk_pk_value(
1340                &self,
1341                field_name: &str,
1342            ) -> ::core::option::Option<#root::core::SqlValue> {
1343                match field_name {
1344                    #( #value_arms )*
1345                    _ => ::core::option::Option::None,
1346                }
1347            }
1348        }
1349    }
1350}
1351
1352/// For every `ForeignKey<Parent>` field on `Child`, emit
1353/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1354/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1355/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1356/// — the canonical reverse-FK fetch. One round trip, no N+1.
1357///
1358/// **PG-only emission**: the accessor is bounded on
1359/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1360/// both of which are gated behind the `postgres` cargo feature. The
1361/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1362/// model derive itself compiles on tri-dialect / sqlite-only
1363/// downstream builds — the accessor just isn't materialised. A tri-
1364/// dialect `_set_pool` variant is a separate follow-up.
1365fn reverse_helper_tokens(
1366    child_ident: &syn::Ident,
1367    fk_relations: &[FkRelation],
1368    default_related_name: Option<&str>,
1369) -> TokenStream2 {
1370    let root = rustango_root();
1371    if fk_relations.is_empty() {
1372        return TokenStream2::new();
1373    }
1374    // Method-name resolution per FK (issue #816 + follow-up):
1375    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1376    //      itself — wins over everything else.
1377    //   2. Container-level `default_related_name = "..."` on the
1378    //      child. Applies to every FK on this model that didn't
1379    //      override.
1380    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1381    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1382    //      edge cases.
1383    //
1384    // The PG-on-executor variant keeps the resolved name; the
1385    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1386    // framework's convention for the `&Pool` flavor of every helper).
1387    let default_pg_suffix = default_related_name
1388        .map(str::to_owned)
1389        .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1390    let impls = fk_relations.iter().map(|rel| {
1391        let pg_suffix = rel
1392            .related_name
1393            .clone()
1394            .unwrap_or_else(|| default_pg_suffix.clone());
1395        let pool_suffix = format!("{}_pool", pg_suffix);
1396        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1397        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1398        let parent_ty = &rel.parent_type;
1399        let fk_name = ident_name(&rel.field_ident);
1400        let fk_col = fk_name.as_str();
1401        let doc = format!(
1402            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1403             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1404             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1405             further `{child_ident}::objects()` filters via direct queryset use."
1406        );
1407        let pool_doc = format!(
1408            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1409             [`#root::sql::Pool`] and dispatches per backend so the \
1410             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1411             Use this from framework code that holds a `&Pool` (admin, \
1412             tenancy resolver, viewset handlers); reach for the executor- \
1413             bound variant when you already have a typed `sqlx::Executor`."
1414        );
1415        quote! {
1416            #[cfg(feature = "postgres")]
1417            impl #parent_ty {
1418                #[doc = #doc]
1419                ///
1420                /// # Errors
1421                /// Returns [`#root::sql::ExecError`] for SQL-writing
1422                /// or driver failures.
1423                pub async fn #pg_method_ident<'_c, _E>(
1424                    &self,
1425                    _executor: _E,
1426                ) -> ::core::result::Result<
1427                    ::std::vec::Vec<#child_ident>,
1428                    #root::sql::ExecError,
1429                >
1430                where
1431                    _E: #root::sql::sqlx::Executor<
1432                        '_c,
1433                        Database = #root::sql::sqlx::Postgres,
1434                    >,
1435                {
1436                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1437                    #root::query::QuerySet::<#child_ident>::new()
1438                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1439                        .fetch_on(_executor)
1440                        .await
1441                }
1442            }
1443
1444            impl #parent_ty {
1445                #[doc = #pool_doc]
1446                ///
1447                /// # Errors
1448                /// Returns [`#root::sql::ExecError`] for SQL-writing
1449                /// or driver failures.
1450                pub async fn #pool_method_ident(
1451                    &self,
1452                    pool: &#root::sql::Pool,
1453                ) -> ::core::result::Result<
1454                    ::std::vec::Vec<#child_ident>,
1455                    #root::sql::ExecError,
1456                > {
1457                    use #root::sql::FetcherPool as _;
1458                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1459                    #root::query::QuerySet::<#child_ident>::new()
1460                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1461                        .fetch(pool)
1462                        .await
1463                }
1464            }
1465        }
1466    });
1467    quote! { #( #impls )* }
1468}
1469
1470/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1471/// relation declared on the model.
1472/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1473/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1474///
1475/// Closes #239 + #240 — `comment.content_object` /
1476/// `comment.content_object = post` ergonomics on top of the existing
1477/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1478///
1479/// `column_entries` is passed so we can resolve each `ct_column` /
1480/// `pk_column` SQL name back to its Rust field ident — the macro
1481/// only sees the column-side strings in the attribute, but the
1482/// emitted accessor needs to read the actual struct field.
1483fn generic_fk_accessor_tokens(
1484    struct_name: &syn::Ident,
1485    generic_fks: &[GenericFkAttr],
1486    column_entries: &[ColumnEntry],
1487) -> TokenStream2 {
1488    let root = rustango_root();
1489    if generic_fks.is_empty() {
1490        return TokenStream2::new();
1491    }
1492    let methods = generic_fks.iter().filter_map(|gfk| {
1493        // Resolve `ct_column` + `pk_column` to the struct's Rust
1494        // field idents. A typo (column name doesn't match any field)
1495        // emits no method for that registration — the user will see
1496        // the compiler reject the SCHEMA literal anyway, so there's
1497        // a clear error path without us double-reporting.
1498        let ct_ident = column_entries
1499            .iter()
1500            .find(|c| c.column == gfk.ct_column)
1501            .map(|c| c.ident.clone())?;
1502        let pk_ident = column_entries
1503            .iter()
1504            .find(|c| c.column == gfk.pk_column)
1505            .map(|c| c.ident.clone())?;
1506
1507        let accessor_ident =
1508            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1509        let setter_ident =
1510            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1511        let name_literal = gfk.name.as_str();
1512
1513        Some(quote! {
1514            #[doc = concat!(
1515                "Resolve the polymorphic `",
1516                #name_literal,
1517                "` relation. Reads `self.",
1518                stringify!(#ct_ident),
1519                "` + `self.",
1520                stringify!(#pk_ident),
1521                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1522                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1523                #name_literal,
1524                "\", ...))]`."
1525            )]
1526            pub async fn #accessor_ident(
1527                &self,
1528                pool: &#root::sql::Pool,
1529            ) -> ::core::result::Result<
1530                ::core::option::Option<#root::__serde_json::Value>,
1531                #root::sql::ExecError,
1532            > {
1533                let gfk = #root::contenttypes::GenericForeignKey::new(
1534                    self.#ct_ident as i64,
1535                    self.#pk_ident as i64,
1536                );
1537                gfk.get_object(pool).await
1538            }
1539
1540            #[doc = concat!(
1541                "Set the polymorphic `",
1542                #name_literal,
1543                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1544                stringify!(#ct_ident),
1545                "` and `self.",
1546                stringify!(#pk_ident),
1547                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1548                #name_literal,
1549                "\", ...))]`."
1550            )]
1551            pub async fn #setter_ident<T: #root::core::Model>(
1552                &mut self,
1553                pool: &#root::sql::Pool,
1554                target_pk: i64,
1555            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1556                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1557                    pool,
1558                    target_pk,
1559                ).await?;
1560                self.#ct_ident = gfk.content_type_id as _;
1561                self.#pk_ident = gfk.object_pk as _;
1562                ::core::result::Result::Ok(())
1563            }
1564        })
1565    });
1566    quote! {
1567        impl #struct_name {
1568            #( #methods )*
1569        }
1570    }
1571}
1572
1573fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1574    let root = rustango_root();
1575    if m2m_relations.is_empty() {
1576        return TokenStream2::new();
1577    }
1578    let methods = m2m_relations.iter().map(|rel| {
1579        let method_name = format!("{}_m2m", rel.name);
1580        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1581        let through = rel.through.as_str();
1582        let src_col = rel.src.as_str();
1583        let dst_col = rel.dst.as_str();
1584        quote! {
1585            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1586                #root::sql::M2MManager {
1587                    src_pk: self.__rustango_pk_value(),
1588                    through: #through,
1589                    src_col: #src_col,
1590                    dst_col: #dst_col,
1591                }
1592            }
1593        }
1594    });
1595    quote! {
1596        impl #struct_name {
1597            #( #methods )*
1598        }
1599    }
1600}
1601
1602/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1603/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1604fn generic_m2m_accessor_tokens(
1605    struct_name: &syn::Ident,
1606    relations: &[GenericM2MAttr],
1607) -> TokenStream2 {
1608    let root = rustango_root();
1609    if relations.is_empty() {
1610        return TokenStream2::new();
1611    }
1612    let methods = relations.iter().map(|rel| {
1613        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1614        let through = rel.through.as_str();
1615        let pk_col = rel.pk_column.as_str();
1616        let ct_col = rel.ct_column.as_str();
1617        let related_col = rel.related_column.as_str();
1618        quote! {
1619            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1620                #root::sql::GenericM2MManager {
1621                    src_pk: self.__rustango_pk_value(),
1622                    src_schema: <Self as #root::core::Model>::SCHEMA,
1623                    through: #through,
1624                    pk_col: #pk_col,
1625                    ct_col: #ct_col,
1626                    dst_col: #related_col,
1627                }
1628            }
1629        }
1630    });
1631    quote! {
1632        impl #struct_name {
1633            #( #methods )*
1634        }
1635    }
1636}
1637
1638/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1639/// associated functions for each `#[rustango(reverse_has(...))]`
1640/// attribute. Issue #830.
1641///
1642/// The two emitted functions return ready-to-use `WhereExpr` nodes
1643/// that downstream callers drop into
1644/// `QuerySet::<Self>::where_raw(...)`:
1645///
1646/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1647///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1648///   `whereHas` parity (without the closure-style sub-predicate
1649///   refinement — that's a follow-up; users can layer additional
1650///   predicates by constructing the SelectQuery themselves and
1651///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1652/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1653///   `whereDoesntHave` parity.
1654///
1655/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1656/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1657/// machinery threads the outer-table reference through automatically
1658/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1659fn reverse_has_accessor_tokens(
1660    struct_name: &syn::Ident,
1661    reverse_has_relations: &[ReverseHasAttr],
1662) -> TokenStream2 {
1663    let root = rustango_root();
1664    if reverse_has_relations.is_empty() {
1665        return TokenStream2::new();
1666    }
1667    let methods = reverse_has_relations.iter().map(|rel| {
1668        let exists_name = format!("{}_exists_expr", rel.name);
1669        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1670        let count_name = format!("{}_count", rel.name);
1671        let fetch_name = format!("{}_fetch", rel.name);
1672        let first_name = format!("{}_first", rel.name);
1673        let pluck_name = format!("{}_pluck", rel.name);
1674        let accessor_name = rel.name.as_str();
1675        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1676        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1677        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1678        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1679        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1680        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1681        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1682        let child = &rel.child;
1683        let child_fk_column = rel.child_fk_column.as_str();
1684        let self_pk_column = rel.self_pk_column.as_str();
1685        let exists_doc = format!(
1686            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1687             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1688             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1689             filter to {struct_name}s with at least one matching child.",
1690        );
1691        let not_exists_doc = format!(
1692            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1693             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1694             <outer>.{self_pk_column})`. Drop into \
1695             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1696             {struct_name}s with **no** matching child.",
1697        );
1698        let count_doc = format!(
1699            "Eloquent `$model->{name}->count()` analog — returns \
1700             the number of `{child}` rows whose `{child_fk_column}` \
1701             matches this `{struct_name}` instance's primary key. \
1702             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1703             {child_fk_column} = <self.pk>`.",
1704            name = rel.name,
1705        );
1706        let accessor_doc = format!(
1707            "Eloquent `$model->{name}` accessor — returns a \
1708             `QuerySet<{child}>` filtered to rows whose \
1709             `{child_fk_column}` matches this `{struct_name}` \
1710             instance's primary key. **Chainable**: compose `.filter()` \
1711             / `.order_by()` / `.limit()` etc. on top, then call \
1712             `.fetch(&pool)` (the QuerySet trait method) when \
1713             done. For the simple \"fetch all\" hot path with no \
1714             further composition, prefer the bare-name \
1715             `{name}_fetch(&pool)` companion.",
1716            name = rel.name,
1717        );
1718        let fetch_doc = format!(
1719            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1720             over `{name}(&self).fetch(&pool)`. Use this when \
1721             you don't need further `.filter()` / `.order_by()` \
1722             composition; falls back to the chainable accessor when \
1723             you do. Avoids the `_pool` suffix on the most common \
1724             call-site shape.",
1725            name = rel.name,
1726        );
1727        quote! {
1728            #[doc = #accessor_doc]
1729            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1730                #root::query::QuerySet::<#child>::new()
1731                    .filter(#child_fk_column, self.__rustango_pk_value())
1732            }
1733
1734            #[doc = #fetch_doc]
1735            pub async fn #fetch_ident(
1736                &self,
1737                pool: &#root::sql::Pool,
1738            ) -> ::core::result::Result<
1739                ::std::vec::Vec<#child>,
1740                #root::sql::ExecError,
1741            > {
1742                use #root::sql::FetcherPool as _;
1743                self.#accessor_ident().fetch(pool).await
1744            }
1745
1746            /// Eloquent `$model->relation->first()` / `hasOne`
1747            /// semantics — bare-name shortcut over
1748            /// `self.<name>().first(&pool)`. Returns `None` when no
1749            /// child rows match. Useful when the relation is
1750            /// nominally many-to-one in shape but at most one row is
1751            /// expected (latest comment, primary tag, etc.).
1752            pub async fn #first_ident(
1753                &self,
1754                pool: &#root::sql::Pool,
1755            ) -> ::core::result::Result<
1756                ::core::option::Option<#child>,
1757                #root::sql::ExecError,
1758            > {
1759                self.#accessor_ident().first(pool).await
1760            }
1761
1762            /// Eloquent `$model->relation->pluck($col)` — project a
1763            /// single column from the child rows into a `Vec<U>`.
1764            /// Skips the typed `Child` decode, which is cheaper when
1765            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1766            pub async fn #pluck_ident<U>(
1767                &self,
1768                col: &'static str,
1769                pool: &#root::sql::Pool,
1770            ) -> ::core::result::Result<
1771                ::std::vec::Vec<U>,
1772                #root::sql::ExecError,
1773            >
1774            where
1775                U: #root::sql::FlatScalar,
1776            {
1777                self.#accessor_ident()
1778                    .values_list_flat(col)
1779                    .fetch::<U>(pool)
1780                    .await
1781            }
1782
1783            #[doc = #exists_doc]
1784            pub fn #exists_ident() -> #root::core::WhereExpr {
1785                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1786                let child_schema =
1787                    <#child as #root::core::Model>::SCHEMA;
1788                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1789                    lhs: Expr::Column(#child_fk_column),
1790                    op: Op::Eq,
1791                    rhs: Expr::OuterRef(#self_pk_column),
1792                });
1793                WhereExpr::Exists(::std::boxed::Box::new(inner))
1794            }
1795
1796            #[doc = #not_exists_doc]
1797            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1798                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1799                let child_schema =
1800                    <#child as #root::core::Model>::SCHEMA;
1801                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1802                    lhs: Expr::Column(#child_fk_column),
1803                    op: Op::Eq,
1804                    rhs: Expr::OuterRef(#self_pk_column),
1805                });
1806                WhereExpr::NotExists(::std::boxed::Box::new(inner))
1807            }
1808
1809            #[doc = #count_doc]
1810            pub async fn #count_ident(
1811                &self,
1812                pool: &#root::sql::Pool,
1813            ) -> ::core::result::Result<
1814                i64,
1815                #root::sql::ExecError,
1816            > {
1817                use #root::sql::CounterPool as _;
1818                #root::query::QuerySet::<#child>::new()
1819                    .filter(#child_fk_column, self.__rustango_pk_value())
1820                    .count(pool)
1821                    .await
1822            }
1823        }
1824    });
1825    quote! {
1826        impl #struct_name {
1827            #( #methods )*
1828        }
1829    }
1830}
1831
1832/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1833/// `#[rustango(through(...))]` attribute. Issue #817.
1834///
1835/// Each accessor builds a correlated subquery via
1836/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1837/// intermediate table, filters on the FK column pointing at this
1838/// model, and projects the intermediate PK. The outer queryset
1839/// filters the far model by its FK-to-intermediate column being in
1840/// that set.
1841///
1842/// The returned `QuerySet<Far>` is **chainable** — the subquery
1843/// lives inside a `where_raw` clause so the user's later
1844/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1845/// disturb it.
1846///
1847/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1848/// SQLite — no LATERAL or backend-specific syntax involved.
1849fn through_accessor_tokens(
1850    struct_name: &syn::Ident,
1851    through_relations: &[ThroughAttr],
1852) -> TokenStream2 {
1853    let root = rustango_root();
1854    if through_relations.is_empty() {
1855        return TokenStream2::new();
1856    }
1857    let methods = through_relations.iter().map(|rel| {
1858        let method_name = format!("{}_through", rel.name);
1859        let count_name = format!("{}_through_count", rel.name);
1860        let fetch_name = format!("{}_through_fetch", rel.name);
1861        let first_name = format!("{}_through_first", rel.name);
1862        let pluck_name = format!("{}_through_pluck", rel.name);
1863        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1864        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1865        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1866        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1867        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1868        let far = &rel.far;
1869        let intermediate = &rel.intermediate;
1870        let far_fk_column = rel.far_fk_column.as_str();
1871        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1872        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1873        let doc = format!(
1874            "Eloquent `hasManyThrough` accessor — returns a \
1875             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1876             instance through the intermediate `{intermediate}` table. \
1877             Generated SQL shape: \
1878             `… WHERE {far_fk_column} IN (SELECT \
1879             {intermediate_pk_column} FROM <{intermediate}> WHERE \
1880             {intermediate_fk_column} = self.pk)`. Chainable like any \
1881             other QuerySet — compose `.filter()` / `.order_by()` / \
1882             `.limit()` etc. on top.",
1883        );
1884        let count_doc = format!(
1885            "Eloquent `$model->{name}->count()` analog for the \
1886             through-relation — returns the number of `{far}` rows \
1887             reachable through `{intermediate}`. Equivalent to \
1888             `self.{name}_through().count(pool)` but spelled \
1889             as a bare instance method for parity with the \
1890             `reverse_has` `<name>_count` shape.",
1891            name = rel.name,
1892        );
1893        quote! {
1894            #[doc = #doc]
1895            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1896                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1897                let intermediate_schema =
1898                    <#intermediate as #root::core::Model>::SCHEMA;
1899                let sub = SelectQuery::new(intermediate_schema)
1900                    .where_clause(WhereExpr::Predicate(Filter::new(
1901                        #intermediate_fk_column,
1902                        Op::Eq,
1903                        self.__rustango_pk_value(),
1904                    )))
1905                    .projection(::std::vec![#intermediate_pk_column]);
1906                #root::query::QuerySet::<#far>::new().where_raw(
1907                    WhereExpr::InSubquery {
1908                        column: #far_fk_column,
1909                        negated: false,
1910                        subquery: ::std::boxed::Box::new(sub),
1911                    },
1912                )
1913            }
1914
1915            #[doc = #count_doc]
1916            pub async fn #count_ident(
1917                &self,
1918                pool: &#root::sql::Pool,
1919            ) -> ::core::result::Result<
1920                i64,
1921                #root::sql::ExecError,
1922            > {
1923                use #root::sql::CounterPool as _;
1924                self.#method_ident().count(pool).await
1925            }
1926
1927            /// Eloquent `$model->relation->get()` for the
1928            /// through-relation — bare-name hot-path over
1929            /// `self.<name>_through().fetch(&pool)`. Use this
1930            /// when you don't need further `.filter()` /
1931            /// `.order_by()` composition; falls back to the
1932            /// chainable accessor when you do. Avoids the `_pool`
1933            /// suffix on the most common call-site shape.
1934            pub async fn #fetch_ident(
1935                &self,
1936                pool: &#root::sql::Pool,
1937            ) -> ::core::result::Result<
1938                ::std::vec::Vec<#far>,
1939                #root::sql::ExecError,
1940            > {
1941                use #root::sql::FetcherPool as _;
1942                self.#method_ident().fetch(pool).await
1943            }
1944
1945            /// Eloquent `hasOneThrough` analog — bare-name shortcut
1946            /// over `self.<name>_through().first(&pool)`. Returns
1947            /// `None` when no far rows are reachable through the
1948            /// intermediate. Useful when at most one row is
1949            /// expected (latest comment by country, primary tag,
1950            /// etc.).
1951            pub async fn #first_ident(
1952                &self,
1953                pool: &#root::sql::Pool,
1954            ) -> ::core::result::Result<
1955                ::core::option::Option<#far>,
1956                #root::sql::ExecError,
1957            > {
1958                self.#method_ident().first(pool).await
1959            }
1960
1961            /// Pluck a single column from the far rows into `Vec<U>`
1962            /// — cheaper than the typed `<Far>` decode when only one
1963            /// scalar column is needed.
1964            pub async fn #pluck_ident<U>(
1965                &self,
1966                col: &'static str,
1967                pool: &#root::sql::Pool,
1968            ) -> ::core::result::Result<
1969                ::std::vec::Vec<U>,
1970                #root::sql::ExecError,
1971            >
1972            where
1973                U: #root::sql::FlatScalar,
1974            {
1975                self.#method_ident()
1976                    .values_list_flat(col)
1977                    .fetch::<U>(pool)
1978                    .await
1979            }
1980        }
1981    });
1982    quote! {
1983        impl #struct_name {
1984            #( #methods )*
1985        }
1986    }
1987}
1988
1989struct ColumnEntry {
1990    /// The struct field ident, used both for the inherent const name on
1991    /// the model and for the inner column type's name.
1992    ident: syn::Ident,
1993    /// The struct's field type, used as `Column::Value`.
1994    value_ty: Type,
1995    /// Rust-side field name (e.g. `"id"`).
1996    name: String,
1997    /// SQL-side column name (e.g. `"user_id"`).
1998    column: String,
1999    /// `#root::core::FieldType::I64` etc.
2000    field_type_tokens: TokenStream2,
2001    /// A bare or optional `Uuid` field; MySQL decodes it from text.
2002    uuid_column: bool,
2003}
2004
2005struct CollectedFields {
2006    field_schemas: Vec<TokenStream2>,
2007    from_row_inits: Vec<TokenStream2>,
2008    /// Aliased counterparts of `from_row_inits` — read columns via
2009    /// `format!("{prefix}__{col}")` aliases so a Model can be
2010    /// decoded from a JOINed row's projected target columns.
2011    from_aliased_row_inits: Vec<TokenStream2>,
2012    /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2013    from_row_inits_my: Vec<TokenStream2>,
2014    from_aliased_row_inits_my: Vec<TokenStream2>,
2015    /// Static column-name list — used by the simple insert path
2016    /// (no `Auto<T>` fields). Aligned with `insert_values`.
2017    insert_columns: Vec<TokenStream2>,
2018    /// Static `Into<SqlValue>` expressions, one per field. Aligned
2019    /// with `insert_columns`. Used by the simple insert path only.
2020    insert_values: Vec<TokenStream2>,
2021    /// Per-field push expressions for the dynamic (Auto-aware)
2022    /// insert path. Each statement either unconditionally pushes
2023    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2024    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2025    insert_pushes: Vec<TokenStream2>,
2026    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2027    /// when `has_auto == false`.
2028    returning_cols: Vec<TokenStream2>,
2029    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2030    /// field. Run after `insert_returning` to populate the model.
2031    auto_assigns: Vec<TokenStream2>,
2032    /// `(ident, column_literal)` pairs for every Auto field. Used by
2033    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2034    /// instead of `self`.
2035    auto_field_idents: Vec<(syn::Ident, String)>,
2036    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2037    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2038    /// back into the struct after insert (MySQL has no RETURNING → the
2039    /// field stays at its placeholder until the next read).
2040    generated_field_idents: Vec<(syn::Ident, String)>,
2041    /// First `Auto<T>` field the database fills, with its inner `T`: the
2042    /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2043    first_db_auto: Option<(syn::Ident, Type)>,
2044    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2045    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2046    /// `columns`).
2047    bulk_pushes_no_auto: Vec<TokenStream2>,
2048    /// Bulk-insert per-row pushes for **all fields including Auto**.
2049    /// Used by the all-Auto-Set bulk path (Auto col included with the
2050    /// caller-supplied value).
2051    bulk_pushes_all: Vec<TokenStream2>,
2052    /// Column-name literals for non-Auto fields only (paired with
2053    /// `bulk_pushes_no_auto`).
2054    bulk_columns_no_auto: Vec<TokenStream2>,
2055    /// Column-name literals for every field including Auto (paired
2056    /// with `bulk_pushes_all`).
2057    bulk_columns_all: Vec<TokenStream2>,
2058    /// Fills each `default_uuid_v7` field of `_row` in place, so a
2059    /// mutable bulk insert hands the PKs back (#1934).
2060    bulk_fill_in_place: Vec<TokenStream2>,
2061    /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2062    rust_filled_idents: Vec<syn::Ident>,
2063    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2064    /// + the loop that asserts every row matches. One pair per Auto
2065    /// field. Empty when `has_auto == false`.
2066    bulk_auto_uniformity: Vec<TokenStream2>,
2067    /// Identifier of the first Auto field, used as the witness for
2068    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2069    first_auto_ident: Option<syn::Ident>,
2070    /// `true` if any field on the struct is `Auto<T>`.
2071    has_auto: bool,
2072    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2073    /// Gates `save()` codegen — only Auto PKs let us infer
2074    /// insert-vs-update from the in-memory value.
2075    pk_is_auto: bool,
2076    /// `Assignment` constructors for every non-PK column. Drives the
2077    /// UPDATE branch of `save()`.
2078    update_assignments: Vec<TokenStream2>,
2079    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2080    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2081    upsert_update_columns: Vec<TokenStream2>,
2082    primary_key: Option<(syn::Ident, String)>,
2083    column_entries: Vec<ColumnEntry>,
2084    /// Rust-side field names, in declaration order. Used to validate
2085    /// container attributes like `display = "…"`.
2086    field_names: Vec<String>,
2087    /// FK fields on this child model. Drives the reverse-relation
2088    /// helper emit — for each FK, the macro adds an inherent
2089    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2090    /// method on the parent type.
2091    fk_relations: Vec<FkRelation>,
2092    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2093    /// the model has one. Drives emission of the `soft_delete_on` /
2094    /// `restore_on` inherent methods. At most one such column per
2095    /// model is allowed; collect_fields rejects duplicates.
2096    soft_delete_column: Option<String>,
2097    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2098    /// companion to `soft_delete_column` for emitting predicates
2099    /// that need to read the field off `&self` (e.g. `trashed()`).
2100    soft_delete_field_ident: Option<syn::Ident>,
2101}
2102
2103#[derive(Clone)]
2104struct FkRelation {
2105    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2106    /// helper is emitted as `impl <ParentType> { … }`.
2107    parent_type: Type,
2108    /// Rust field ident of the FK. Its name is the key for select_related,
2109    /// prefetch and filters; the SQL column may differ (#1936).
2110    field_ident: syn::Ident,
2111    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2112    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2113    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2114    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2115    pk_kind: DetectedKind,
2116    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2117    /// FK column). Drives the `Some(...)` wrapping in load_related
2118    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2119    /// the codegen matches the field's declared shape.
2120    nullable: bool,
2121    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2122    /// override. When set, the reverse helper picks this name instead
2123    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2124    /// #816 (issue's "Related" note re: per-FK override).
2125    related_name: Option<String>,
2126}
2127
2128fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2129    let root = rustango_root();
2130    let cap = named.named.len();
2131    let mut out = CollectedFields {
2132        field_schemas: Vec::with_capacity(cap),
2133        from_row_inits: Vec::with_capacity(cap),
2134        from_aliased_row_inits: Vec::with_capacity(cap),
2135        from_row_inits_my: Vec::with_capacity(cap),
2136        from_aliased_row_inits_my: Vec::with_capacity(cap),
2137        insert_columns: Vec::with_capacity(cap),
2138        insert_values: Vec::with_capacity(cap),
2139        insert_pushes: Vec::with_capacity(cap),
2140        returning_cols: Vec::new(),
2141        auto_assigns: Vec::new(),
2142        auto_field_idents: Vec::new(),
2143        generated_field_idents: Vec::new(),
2144        first_db_auto: None,
2145        bulk_pushes_no_auto: Vec::with_capacity(cap),
2146        bulk_pushes_all: Vec::with_capacity(cap),
2147        bulk_columns_no_auto: Vec::with_capacity(cap),
2148        bulk_columns_all: Vec::with_capacity(cap),
2149        bulk_fill_in_place: Vec::new(),
2150        rust_filled_idents: Vec::new(),
2151        bulk_auto_uniformity: Vec::new(),
2152        first_auto_ident: None,
2153        has_auto: false,
2154        pk_is_auto: false,
2155        update_assignments: Vec::with_capacity(cap),
2156        upsert_update_columns: Vec::with_capacity(cap),
2157        primary_key: None,
2158        column_entries: Vec::with_capacity(cap),
2159        field_names: Vec::with_capacity(cap),
2160        fk_relations: Vec::new(),
2161        soft_delete_column: None,
2162        soft_delete_field_ident: None,
2163    };
2164
2165    for field in &named.named {
2166        let info = process_field(field, table)?;
2167        out.field_names.push(ident_name(info.ident));
2168        out.field_schemas.push(info.schema);
2169        out.from_row_inits.push(info.from_row_init);
2170        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2171        out.from_row_inits_my.push(info.from_row_init_my);
2172        out.from_aliased_row_inits_my
2173            .push(info.from_aliased_row_init_my);
2174        if let Some(parent_ty) = info.fk_inner.clone() {
2175            out.fk_relations.push(FkRelation {
2176                parent_type: parent_ty,
2177                field_ident: info.ident.clone(),
2178                pk_kind: info.fk_pk_kind,
2179                nullable: info.nullable,
2180                related_name: info.related_name.clone(),
2181            });
2182        }
2183        if info.soft_delete {
2184            if out.soft_delete_column.is_some() {
2185                return Err(syn::Error::new_spanned(
2186                    field,
2187                    "only one field may be marked `#[rustango(soft_delete)]`",
2188                ));
2189            }
2190            out.soft_delete_column = Some(info.column.clone());
2191            out.soft_delete_field_ident = Some(info.ident.clone());
2192        }
2193        let column = info.column.as_str();
2194        let ident = info.ident;
2195        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2196        // skip every write path — Postgres recomputes the value
2197        // from EXPR. Push only the column-entry record (so typed
2198        // column constants still exist for filtering / projection)
2199        // and the schema literal (already pushed above) and move
2200        // on. No insert_columns/values, no insert_pushes, no
2201        // bulk_*, no update_assignments, no upsert_update_columns,
2202        // no returning_cols.
2203        if info.generated_as.is_some() {
2204            out.column_entries.push(ColumnEntry {
2205                ident: ident.clone(),
2206                value_ty: info.value_ty.clone(),
2207                name: ident_name(ident),
2208                column: info.column.clone(),
2209                field_type_tokens: info.field_type_tokens,
2210                uuid_column: info.uuid_column,
2211            });
2212            // #1028 — refresh the DB-computed value after insert on
2213            // RETURNING-capable backends. The column joins `returning_cols`
2214            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2215            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2216            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2217            // it keeps the placeholder until the next read.
2218            out.returning_cols.push(quote!(#column));
2219            out.generated_field_idents
2220                .push((ident.clone(), info.column.clone()));
2221            continue;
2222        }
2223        out.insert_columns.push(quote!(#column));
2224        out.insert_values.push(quote! {
2225            ::core::convert::Into::<#root::core::SqlValue>::into(
2226                ::core::clone::Clone::clone(&self.#ident)
2227            )
2228        });
2229        if info.auto {
2230            out.has_auto = true;
2231            if out.first_auto_ident.is_none() {
2232                out.first_auto_ident = Some(ident.clone());
2233            }
2234            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2235            // before binding, so the value is already in
2236            // `self.#ident` after the insert_push — RETURNING is
2237            // redundant. Skip adding this column to returning_cols /
2238            // auto_assigns / auto_field_idents to avoid (a) an
2239            // unnecessary RETURNING column on every dialect, and (b)
2240            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2241            // integer PK.
2242            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2243            // for the same reason: they are filled Rust-side below and
2244            // bound, so the value is already in `self` and RETURNING
2245            // would be a redundant column on every dialect.
2246            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2247                if out.first_db_auto.is_none() {
2248                    out.first_db_auto = auto_inner_type(info.value_ty)
2249                        .cloned()
2250                        .map(|ty| (ident.clone(), ty));
2251                }
2252                out.returning_cols.push(quote!(#column));
2253                out.auto_field_idents
2254                    .push((ident.clone(), info.column.clone()));
2255                out.auto_assigns.push(quote! {
2256                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2257                });
2258            }
2259            if info.default_uuid_v7 {
2260                // Rust-side UUIDv7 generation (issue #823, Eloquent
2261                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2262                // and bind; Auto::Set → bind the user's value. The
2263                // column is ALWAYS present in the INSERT statement —
2264                // no RETURNING / no DB DEFAULT needed.
2265                out.insert_pushes.push(quote! {
2266                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2267                        self.#ident = #root::sql::Auto::Set(
2268                            #root::__uuid::Uuid::now_v7(),
2269                        );
2270                    }
2271                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2272                        _columns.push(#column);
2273                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2274                            ::core::clone::Clone::clone(_v)
2275                        ));
2276                    }
2277                });
2278            } else if info.auto_now_add || info.auto_now {
2279                // Rust-side write timestamp (#1464), mirroring
2280                // `default_uuid_v7` above. `auto_now` has always bound
2281                // `Utc::now()` on UPDATE but not on INSERT, so the
2282                // first write of an `updated_at` took the DB default
2283                // while every later one took the clock — the same
2284                // column, filled two different ways.
2285                //
2286                // The default is the problem. `CREATE TABLE IF NOT
2287                // EXISTS` leaves an upgraded table's
2288                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2289                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2290                // statement that changes it — so every insert relying
2291                // on that default wrote the legacy shape forever, while
2292                // the migrate sweep converted the rows around it. The
2293                // column went permanently mixed and `ORDER BY` inverted
2294                // the admin audit log (#1616 rework review,
2295                // correctness-001 / dialects-005 / security-002).
2296                //
2297                // Binding it here makes the stale default unreachable
2298                // rather than trying to migrate it: the timestamp comes
2299                // from the application, not the database.
2300                out.insert_pushes.push(quote! {
2301                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2302                        self.#ident = #root::sql::Auto::Set(
2303                            #root::__chrono::Utc::now(),
2304                        );
2305                    }
2306                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2307                        _columns.push(#column);
2308                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2309                            ::core::clone::Clone::clone(_v)
2310                        ));
2311                    }
2312                });
2313            } else {
2314                out.insert_pushes.push(quote! {
2315                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2316                        _columns.push(#column);
2317                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2318                            ::core::clone::Clone::clone(_v)
2319                        ));
2320                    }
2321                });
2322            }
2323            // …except the timestamp columns, which must appear in BOTH
2324            // paths (#1464). The path is chosen by the *first* Auto
2325            // field — in practice the PK — so a bulk insert of rows
2326            // with an unset PK took the no-auto branch and dropped the
2327            // timestamp column, falling back to the DB default that
2328            // this change exists to stop depending on.
2329            //
2330            // `rows` is borrowed immutably here, so this fills per row
2331            // at push time rather than writing back into the struct. An
2332            // explicitly-Set value is still honoured; only `Unset`
2333            // takes the clock.
2334            // `default_uuid_v7` likewise: its column has no DB default (#1934).
2335            if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2336                out.rust_filled_idents.push(ident.clone());
2337            }
2338            let rust_fill = if info.default_uuid_v7 {
2339                Some(quote!(#root::__uuid::Uuid::now_v7()))
2340            } else if info.auto_now_add || info.auto_now {
2341                Some(quote!(#root::__chrono::Utc::now()))
2342            } else {
2343                None
2344            };
2345            // Bulk: Auto fields appear in the all-Set path. An unset filled
2346            // field takes its fill there too, not NULL (#1950).
2347            out.bulk_columns_all.push(quote!(#column));
2348            if let Some(fill) = &rust_fill {
2349                let push = quote! {
2350                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2351                        match &_row.#ident {
2352                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2353                            #root::sql::Auto::Unset => #fill,
2354                        }
2355                    ));
2356                };
2357                out.bulk_columns_no_auto.push(quote!(#column));
2358                out.bulk_pushes_no_auto.push(push.clone());
2359                out.bulk_pushes_all.push(push);
2360            } else {
2361                out.bulk_pushes_all.push(quote! {
2362                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2363                        ::core::clone::Clone::clone(&_row.#ident)
2364                    ));
2365                });
2366            }
2367            if info.default_uuid_v7 {
2368                out.bulk_fill_in_place.push(quote! {
2369                    if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2370                        _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2371                    }
2372                });
2373            }
2374            // Uniformity check: every row's Auto state must match the
2375            // first row's. Mixed Set/Unset within one bulk_insert is
2376            // rejected here so the column list stays consistent.
2377            // A filled field may be unset on any row, so it is exempt.
2378            let ident_clone = ident.clone();
2379            if rust_fill.is_none() {
2380                out.bulk_auto_uniformity.push(quote! {
2381                    for _r in rows.iter().skip(1) {
2382                        if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2383                            return ::core::result::Result::Err(
2384                                #root::sql::ExecError::Sql(
2385                                    #root::sql::SqlError::BulkAutoMixed
2386                                )
2387                            );
2388                        }
2389                    }
2390                });
2391            }
2392        } else {
2393            out.insert_pushes.push(quote! {
2394                _columns.push(#column);
2395                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2396                    ::core::clone::Clone::clone(&self.#ident)
2397                ));
2398            });
2399            // Bulk: non-Auto fields appear in BOTH paths.
2400            out.bulk_columns_no_auto.push(quote!(#column));
2401            out.bulk_columns_all.push(quote!(#column));
2402            let push_expr = quote! {
2403                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2404                    ::core::clone::Clone::clone(&_row.#ident)
2405                ));
2406            };
2407            out.bulk_pushes_no_auto.push(push_expr.clone());
2408            out.bulk_pushes_all.push(push_expr);
2409        }
2410        if info.primary_key {
2411            if out.primary_key.is_some() {
2412                return Err(syn::Error::new_spanned(
2413                    field,
2414                    "only one field may be marked `#[rustango(primary_key)]`",
2415                ));
2416            }
2417            out.primary_key = Some((ident.clone(), info.column.clone()));
2418            if info.auto {
2419                out.pk_is_auto = true;
2420            }
2421        } else if info.auto_now_add {
2422            // Immutable post-insert: skip from UPDATE entirely.
2423        } else if info.auto_now {
2424            // `auto_now` columns: bind `chrono::Utc::now()` on every
2425            // UPDATE so the column is always overridden with the
2426            // wall-clock at write time, regardless of what value the
2427            // user left in the struct field.
2428            out.update_assignments.push(quote! {
2429                #root::core::Assignment::new(
2430                    #column,
2431                    ::core::convert::Into::<#root::core::SqlValue>::into(
2432                        #root::__chrono::Utc::now()
2433                    ),
2434                )
2435            });
2436            out.upsert_update_columns.push(quote!(#column));
2437        } else {
2438            out.update_assignments.push(quote! {
2439                #root::core::Assignment::new(
2440                    #column,
2441                    ::core::convert::Into::<#root::core::SqlValue>::into(
2442                        ::core::clone::Clone::clone(&self.#ident)
2443                    ),
2444                )
2445            });
2446            out.upsert_update_columns.push(quote!(#column));
2447        }
2448        out.column_entries.push(ColumnEntry {
2449            ident: ident.clone(),
2450            value_ty: info.value_ty.clone(),
2451            name: ident_name(ident),
2452            column: info.column.clone(),
2453            field_type_tokens: info.field_type_tokens,
2454            uuid_column: info.uuid_column,
2455        });
2456    }
2457    Ok(out)
2458}
2459
2460fn model_impl_tokens(
2461    struct_name: &syn::Ident,
2462    model_name: &str,
2463    table: &str,
2464    display: Option<&str>,
2465    app_label: Option<&str>,
2466    admin: Option<&AdminAttrs>,
2467    default_order: &[(String, bool, proc_macro2::Span)],
2468    field_schemas: &[TokenStream2],
2469    soft_delete_column: Option<&str>,
2470    permissions: bool,
2471    audit_track: Option<&[String]>,
2472    m2m_relations: &[M2MAttr],
2473    indexes: &[IndexAttr],
2474    checks: &[CheckAttr],
2475    excludes: &[ExcludeAttr],
2476    composite_fks: &[CompositeFkAttr],
2477    generic_fks: &[GenericFkAttr],
2478    scope: Option<&str>,
2479    is_view: bool,
2480    verbose_name: Option<&str>,
2481    verbose_name_plural: Option<&str>,
2482    managed: bool,
2483    base_manager_name: Option<&str>,
2484    order_with_respect_to: Option<&str>,
2485    proxy: bool,
2486    required_db_features: &[String],
2487    required_db_vendor: Option<&str>,
2488    default_related_name: Option<&str>,
2489    db_table_comment: Option<&str>,
2490    get_latest_by: Option<(&str, bool)>,
2491    extra_permissions: &[(String, String)],
2492    default_permissions: &[String],
2493    global_scopes: &[GlobalScopeAttr],
2494    reverse_has_relations: &[ReverseHasAttr],
2495    generic_has_relations: &[GenericHasAttr],
2496) -> TokenStream2 {
2497    let root = rustango_root();
2498    let display_tokens = if let Some(name) = display {
2499        quote!(::core::option::Option::Some(#name))
2500    } else {
2501        quote!(::core::option::Option::None)
2502    };
2503    let app_label_tokens = if let Some(name) = app_label {
2504        quote!(::core::option::Option::Some(#name))
2505    } else {
2506        quote!(::core::option::Option::None)
2507    };
2508    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2509        quote!(::core::option::Option::Some(#col))
2510    } else {
2511        quote!(::core::option::Option::None)
2512    };
2513    let audit_track_tokens = match audit_track {
2514        None => quote!(::core::option::Option::None),
2515        Some(names) => {
2516            let lits = names.iter().map(|n| n.as_str());
2517            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2518        }
2519    };
2520    let admin_tokens = admin_config_tokens(admin);
2521    // Default `tenant` so single-tenant projects (no `scope` attr
2522    // anywhere) keep the v0.24.x behavior. Container-attr parser
2523    // already validated the value is "registry" or "tenant".
2524    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2525        Some("registry") => quote!(#root::core::ModelScope::Registry),
2526        _ => quote!(#root::core::ModelScope::Tenant),
2527    };
2528    let verbose_name_tokens = optional_str(verbose_name);
2529    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2530    let base_manager_name_tokens = optional_str(base_manager_name);
2531    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2532    let required_db_features_lits: Vec<&str> =
2533        required_db_features.iter().map(String::as_str).collect();
2534    let required_db_vendor_tokens = optional_str(required_db_vendor);
2535    let default_related_name_tokens = optional_str(default_related_name);
2536    let db_table_comment_tokens = optional_str(db_table_comment);
2537    let get_latest_by_tokens = match get_latest_by {
2538        Some((col, desc)) => {
2539            quote!(::core::option::Option::Some((#col, #desc)))
2540        }
2541        None => quote!(::core::option::Option::None),
2542    };
2543    let extra_permission_tokens: Vec<_> = extra_permissions
2544        .iter()
2545        .map(|(c, l)| quote!((#c, #l)))
2546        .collect();
2547    let default_permission_tokens: Vec<_> = default_permissions
2548        .iter()
2549        .map(|action| quote!(#action))
2550        .collect();
2551    let indexes_tokens = indexes.iter().map(|idx| {
2552        // When no explicit `name = "..."` was given, derive a stable,
2553        // collision-free name from the table + columns
2554        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2555        // would clash the moment a model declares two unnamed indexes.
2556        // Capped at Postgres's 63-char identifier limit.
2557        let derived_name = idx.name.clone().unwrap_or_else(|| {
2558            let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2559            if n.len() > 63 {
2560                n.truncate(63);
2561            }
2562            n
2563        });
2564        let name = derived_name.as_str();
2565        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2566        let unique = idx.unique;
2567        // Map the parsed method string onto the IndexMethod enum
2568        // variant — kept at the codegen layer so the IR doesn't
2569        // carry the string form.
2570        let method_variant = match idx.method.as_str() {
2571            "gin" => quote!(#root::core::IndexMethod::Gin),
2572            "gist" => quote!(#root::core::IndexMethod::Gist),
2573            "brin" => quote!(#root::core::IndexMethod::Brin),
2574            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2575            "hash" => quote!(#root::core::IndexMethod::Hash),
2576            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2577            _ => quote!(#root::core::IndexMethod::BTree),
2578        };
2579        let where_clause = match &idx.where_clause {
2580            Some(s) => quote!(::core::option::Option::Some(#s)),
2581            None => quote!(::core::option::Option::None),
2582        };
2583        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2584        quote! {{
2585            let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2586            i.unique = #unique;
2587            i.method = #method_variant;
2588            i.where_clause = #where_clause;
2589            i.include = &[ #(#include_lits),* ];
2590            i
2591        }}
2592    });
2593    let checks_tokens = checks.iter().map(|c| {
2594        let name = c.name.as_str();
2595        let expr = c.expr.as_str();
2596        quote!(#root::core::CheckConstraint::new(#name, #expr))
2597    });
2598    let excludes_tokens = excludes.iter().map(|e| {
2599        let name = e.name.as_str();
2600        let using = e.using.as_str();
2601        let element_tokens = e.elements.iter().map(|(col, op)| {
2602            let col_s = col.as_str();
2603            let op_s = op.as_str();
2604            quote!((#col_s, #op_s))
2605        });
2606        let where_tokens = match e.where_clause.as_deref() {
2607            Some(w) => quote!(::core::option::Option::Some(#w)),
2608            None => quote!(::core::option::Option::None),
2609        };
2610        quote! {{
2611            let mut e = #root::core::ExclusionConstraint::new(
2612                #name,
2613                #using,
2614                &[ #(#element_tokens),* ],
2615            );
2616            e.where_clause = #where_tokens;
2617            e
2618        }}
2619    });
2620    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2621        let name = rel.name.as_str();
2622        let to = rel.to.as_str();
2623        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2624        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2625        quote! {
2626            #root::core::CompositeFkRelation::new(
2627                #name,
2628                #to,
2629                &[ #(#from_cols),* ],
2630                &[ #(#on_cols),* ],
2631            )
2632        }
2633    });
2634    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2635        let name = rel.name.as_str();
2636        let ct_col = rel.ct_column.as_str();
2637        let pk_col = rel.pk_column.as_str();
2638        quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2639    });
2640    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2641    // no `#[rustango(default_order = "...")]` attribute was supplied.
2642    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2643        let col_lit = col.as_str();
2644        quote! { (#col_lit, #desc) }
2645    });
2646
2647    // Issue #820 — `global_scopes` slice literal. Empty when no
2648    // `#[rustango(global_scope(...))]` attribute was supplied. The
2649    // `apply` path is re-emitted verbatim so it resolves in the
2650    // consumer's scope; the name is stored as a string literal.
2651    let global_scope_tokens = global_scopes.iter().map(|s| {
2652        let name = s.name.as_str();
2653        // The slice sits in a nested `const`, where `Self` does not resolve.
2654        let mut apply = s.apply.clone();
2655        if let Some(first) = apply.segments.first_mut() {
2656            if first.ident == "Self" {
2657                first.ident = struct_name.clone();
2658            }
2659        }
2660        quote!(#root::core::GlobalScope::new(#name, #apply))
2661    });
2662
2663    let m2m_tokens = m2m_relations.iter().map(|rel| {
2664        let name = rel.name.as_str();
2665        let to = rel.to.as_str();
2666        let through = rel.through.as_str();
2667        let src = rel.src.as_str();
2668        let dst = rel.dst.as_str();
2669        let auto_create = rel.auto_create;
2670        quote! {{
2671            let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2672            m.auto_create = #auto_create;
2673            m
2674        }}
2675    });
2676    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2677    // when the model declares `#[rustango(reverse_has(...))]`. Each
2678    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2679    // a const expression. Models without reverse_has fall through to
2680    // the trait's empty default — no override emitted.
2681    let reverse_relations_override = if reverse_has_relations.is_empty() {
2682        quote!()
2683    } else {
2684        let entries = reverse_has_relations.iter().map(|rel| {
2685            let name = rel.name.as_str();
2686            let child = &rel.child;
2687            let child_fk_column = rel.child_fk_column.as_str();
2688            let self_pk_column = rel.self_pk_column.as_str();
2689            quote! {
2690                #root::core::ReverseRelation::new(
2691                    #name,
2692                    <#child as #root::core::Model>::SCHEMA,
2693                    #child_fk_column,
2694                    #self_pk_column,
2695                )
2696            }
2697        });
2698        quote! {
2699            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2700                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2701                RELS
2702            }
2703        }
2704    };
2705    // Issue #830 — `Model::generic_reverse_relations()` override for
2706    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2707    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2708        quote!()
2709    } else {
2710        let entries = generic_has_relations.iter().map(|rel| {
2711            let name = rel.name.as_str();
2712            let child = &rel.child;
2713            let ct_column = rel.ct_column.as_str();
2714            let pk_column = rel.pk_column.as_str();
2715            let self_pk_column = rel.self_pk_column.as_str();
2716            quote! {
2717                #root::core::GenericReverseRelation::new(
2718                    #name,
2719                    <#child as #root::core::Model>::SCHEMA,
2720                    #ct_column,
2721                    #pk_column,
2722                    #self_pk_column,
2723                )
2724            }
2725        });
2726        quote! {
2727            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2728                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2729                RELS
2730            }
2731        }
2732    };
2733    let fields_slice = const_slice(
2734        &quote!(#root::core::FieldSchema),
2735        field_schemas.iter().cloned(),
2736    );
2737    let m2m_slice = const_slice(&quote!(#root::core::M2MRelation), m2m_tokens);
2738    let indexes_slice = const_slice(&quote!(#root::core::IndexSchema), indexes_tokens);
2739    let checks_slice = const_slice(&quote!(#root::core::CheckConstraint), checks_tokens);
2740    let excludes_slice = const_slice(&quote!(#root::core::ExclusionConstraint), excludes_tokens);
2741    let composite_fk_slice = const_slice(
2742        &quote!(#root::core::CompositeFkRelation),
2743        composite_fk_tokens,
2744    );
2745    let generic_fk_slice = const_slice(&quote!(#root::core::GenericRelation), generic_fk_tokens);
2746    let audited_update_override = if audit_track.is_some() {
2747        quote! {
2748            fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2749                ::core::option::Option::Some(Self::__rustango_update_audited)
2750            }
2751            fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2752                ::core::option::Option::Some(Self::__rustango_delete_audited)
2753            }
2754            fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2755                ::core::option::Option::Some(Self::__rustango_create_audited)
2756            }
2757        }
2758    } else {
2759        quote!()
2760    };
2761    let global_scope_slice = const_slice(&quote!(#root::core::GlobalScope), global_scope_tokens);
2762    quote! {
2763        impl #root::core::Model for #struct_name {
2764            const SCHEMA: &'static #root::core::ModelSchema = &{
2765                #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2766                    name: #model_name,
2767                    table: #table,
2768                    fields: #fields_slice,
2769                    display: #display_tokens,
2770                    app_label: #app_label_tokens,
2771                    admin: #admin_tokens,
2772                    soft_delete_column: #soft_delete_tokens,
2773                    permissions: #permissions,
2774                    audit_track: #audit_track_tokens,
2775                    m2m: #m2m_slice,
2776                    indexes: #indexes_slice,
2777                    check_constraints: #checks_slice,
2778                    exclusion_constraints: #excludes_slice,
2779                    composite_relations: #composite_fk_slice,
2780                    generic_relations: #generic_fk_slice,
2781                    scope: #scope_tokens,
2782                    default_order: &[ #(#default_order_tokens),* ],
2783                    is_view: #is_view,
2784                    verbose_name: #verbose_name_tokens,
2785                    verbose_name_plural: #verbose_name_plural_tokens,
2786                    managed: #managed,
2787                    base_manager_name: #base_manager_name_tokens,
2788                    order_with_respect_to: #order_with_respect_to_tokens,
2789                    proxy: #proxy,
2790                    required_db_features: &[ #(#required_db_features_lits),* ],
2791                    required_db_vendor: #required_db_vendor_tokens,
2792                    default_related_name: #default_related_name_tokens,
2793                    db_table_comment: #db_table_comment_tokens,
2794                    get_latest_by: #get_latest_by_tokens,
2795                    extra_permissions: &[ #(#extra_permission_tokens),* ],
2796                    default_permissions: &[ #(#default_permission_tokens),* ],
2797                    global_scopes: #global_scope_slice,
2798                })
2799            };
2800
2801            #reverse_relations_override
2802            #generic_reverse_relations_override
2803            #audited_update_override
2804        }
2805    }
2806}
2807
2808/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2809/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2810/// otherwise a static reference to a populated `AdminConfig`.
2811fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2812    let root = rustango_root();
2813    let Some(admin) = admin else {
2814        return quote!(::core::option::Option::None);
2815    };
2816
2817    let list_display = admin
2818        .list_display
2819        .as_ref()
2820        .map(|(v, _)| v.as_slice())
2821        .unwrap_or(&[]);
2822    let list_display_lits = list_display.iter().map(|s| s.as_str());
2823
2824    let search_fields = admin
2825        .search_fields
2826        .as_ref()
2827        .map(|(v, _)| v.as_slice())
2828        .unwrap_or(&[]);
2829    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2830
2831    let readonly_fields = admin
2832        .readonly_fields
2833        .as_ref()
2834        .map(|(v, _)| v.as_slice())
2835        .unwrap_or(&[]);
2836    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2837
2838    let list_filter = admin
2839        .list_filter
2840        .as_ref()
2841        .map(|(v, _)| v.as_slice())
2842        .unwrap_or(&[]);
2843    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2844
2845    let actions = admin
2846        .actions
2847        .as_ref()
2848        .map(|(v, _)| v.as_slice())
2849        .unwrap_or(&[]);
2850    let actions_lits = actions.iter().map(|s| s.as_str());
2851
2852    let fieldsets = admin
2853        .fieldsets
2854        .as_ref()
2855        .map(|(v, _)| v.as_slice())
2856        .unwrap_or(&[]);
2857    let fieldset_tokens = const_slice(
2858        &quote!(#root::core::Fieldset),
2859        fieldsets.iter().map(|(title, fields)| {
2860            let title = title.as_str();
2861            let field_lits = fields.iter().map(String::as_str);
2862            quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
2863        }),
2864    );
2865
2866    let list_display_links = admin
2867        .list_display_links
2868        .as_ref()
2869        .map(|(v, _)| v.as_slice())
2870        .unwrap_or(&[]);
2871    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2872
2873    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2874    let actions_on_top = admin.actions_on_top.unwrap_or(true);
2875    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2876    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2877
2878    let prepopulated = admin
2879        .prepopulated_fields
2880        .as_ref()
2881        .map(|(v, _)| v.as_slice())
2882        .unwrap_or(&[]);
2883    let prepopulated_tokens = const_slice(
2884        &quote!(#root::core::PrepopulatedField),
2885        prepopulated.iter().map(|(target, sources)| {
2886            let target = target.as_str();
2887            let source_lits = sources.iter().map(String::as_str);
2888            quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
2889        }),
2890    );
2891
2892    let raw_id_fields = admin
2893        .raw_id_fields
2894        .as_ref()
2895        .map(|(v, _)| v.as_slice())
2896        .unwrap_or(&[]);
2897    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2898
2899    let autocomplete_fields = admin
2900        .autocomplete_fields
2901        .as_ref()
2902        .map(|(v, _)| v.as_slice())
2903        .unwrap_or(&[]);
2904    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2905
2906    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2907    let list_select_related_tokens = match admin.list_select_related.as_deref() {
2908        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2909        Some("none") => quote!(#root::core::ListSelectRelated::None),
2910        Some(raw) => {
2911            let names: Vec<&str> = raw
2912                .split(',')
2913                .map(str::trim)
2914                .filter(|s| !s.is_empty())
2915                .collect();
2916            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2917        }
2918    };
2919
2920    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2921    // a Vec<(String, String)>. Empty / unset → no overrides.
2922    let formfield_pairs: Vec<(&str, &str)> = admin
2923        .formfield_overrides
2924        .as_ref()
2925        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2926        .unwrap_or_default();
2927    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2928        let field = *field;
2929        let widget = *widget;
2930        quote!((#field, #widget))
2931    });
2932
2933    let list_per_page = admin.list_per_page.unwrap_or(0);
2934
2935    let ordering_pairs = admin
2936        .ordering
2937        .as_ref()
2938        .map(|(v, _)| v.as_slice())
2939        .unwrap_or(&[]);
2940    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2941        let name = name.as_str();
2942        let desc = *desc;
2943        quote!((#name, #desc))
2944    });
2945
2946    quote! {
2947        ::core::option::Option::Some({
2948            const ADMIN: &#root::core::AdminConfig = &{
2949                #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
2950                    list_display: &[ #( #list_display_lits ),* ],
2951                    search_fields: &[ #( #search_fields_lits ),* ],
2952                    list_per_page: #list_per_page,
2953                    ordering: &[ #( #ordering_tokens ),* ],
2954                    readonly_fields: &[ #( #readonly_fields_lits ),* ],
2955                    list_filter: &[ #( #list_filter_lits ),* ],
2956                    actions: &[ #( #actions_lits ),* ],
2957                    fieldsets: #fieldset_tokens,
2958                    list_display_links: &[ #( #list_display_links_lits ),* ],
2959                    search_help_text: #search_help_text,
2960                    actions_on_top: #actions_on_top,
2961                    actions_on_bottom: #actions_on_bottom,
2962                    date_hierarchy: #date_hierarchy,
2963                    prepopulated_fields: #prepopulated_tokens,
2964                    raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2965                    autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2966                    list_select_related: #list_select_related_tokens,
2967                    formfield_overrides: &[ #( #formfield_tokens ),* ],
2968                })
2969            };
2970            ADMIN
2971        })
2972    }
2973}
2974
2975fn inherent_impl_tokens(
2976    struct_name: &syn::Ident,
2977    fields: &CollectedFields,
2978    primary_key: Option<&(syn::Ident, String)>,
2979    column_consts: &TokenStream2,
2980    audited_fields: Option<&[&ColumnEntry]>,
2981    upsert_unique: Option<&[String]>,
2982    manager_fns: &[syn::Ident],
2983) -> TokenStream2 {
2984    let root = rustango_root();
2985    // Audit-emit fragments threaded into write paths. Non-empty only
2986    // when the model carries `#[rustango(audit(...))]`. They reborrow
2987    // `_executor` (a `&mut PgConnection` for audited models — the
2988    // macro switches the signature below) so the data write and the
2989    // audit INSERT both run on the same caller-supplied connection.
2990    let executor_passes_to_data_write = if audited_fields.is_some() {
2991        quote!(&mut *_executor)
2992    } else {
2993        quote!(_executor)
2994    };
2995    let executor_param = if audited_fields.is_some() {
2996        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2997    } else {
2998        quote!(_executor: _E)
2999    };
3000    let executor_generics = if audited_fields.is_some() {
3001        quote!()
3002    } else {
3003        quote!(<'_c, _E>)
3004    };
3005    let executor_where = if audited_fields.is_some() {
3006        quote!()
3007    } else {
3008        quote! {
3009            where
3010                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
3011        }
3012    };
3013    // For audited models the `_on` methods take `&mut PgConnection`, so
3014    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
3015    // must acquire a connection first. Non-audited models keep the
3016    // direct delegation since `&PgPool` IS an Executor.
3017    let pool_to_save_on = if audited_fields.is_some() {
3018        quote! {
3019            let mut _conn = pool.acquire().await?;
3020            self.save_on(&mut *_conn).await
3021        }
3022    } else {
3023        quote!(self.save_on(pool).await)
3024    };
3025    let pool_to_insert_on = if audited_fields.is_some() {
3026        quote! {
3027            let mut _conn = pool.acquire().await?;
3028            self.insert_on(&mut *_conn).await
3029        }
3030    } else {
3031        quote!(self.insert_on(pool).await)
3032    };
3033    let pool_to_delete_on = if audited_fields.is_some() {
3034        quote! {
3035            let mut _conn = pool.acquire().await?;
3036            self.delete_on(&mut *_conn).await
3037        }
3038    } else {
3039        quote!(self.delete_on(pool).await)
3040    };
3041    let pool_to_bulk_insert_on = if audited_fields.is_some() {
3042        quote! {
3043            let mut _tx = pool.begin().await?;
3044            Self::bulk_insert_on(rows, &mut *_tx).await?;
3045            _tx.commit().await?;
3046            ::core::result::Result::Ok(())
3047        }
3048    } else {
3049        quote!(Self::bulk_insert_on(rows, pool).await)
3050    };
3051    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
3052    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
3053    // directly, but `upsert_on` for audited models takes
3054    // `&mut PgConnection` (the audit emit needs a real connection).
3055    // Add the missing acquire shim to keep audited Auto-PK upsert
3056    // compiling.
3057    let pool_to_upsert_on = if audited_fields.is_some() {
3058        quote! {
3059            let mut _tx = pool.begin().await?;
3060            self.upsert_on(&mut *_tx).await?;
3061            _tx.commit().await?;
3062            ::core::result::Result::Ok(())
3063        }
3064    } else {
3065        quote!(self.upsert_on(pool).await)
3066    };
3067
3068    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3069    // (audit-on-connection over &Pool needs a bi-dialect transaction
3070    // helper, deferred). Two body shapes:
3071    // - has_auto: build InsertQuery skipping Auto::Unset columns,
3072    //   request Auto cols in `returning`, dispatch via
3073    //   `insert_returning_pool`, then on the returned `PgRow` /
3074    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
3075    //   row OR drop the single i64 into the first Auto field on MySQL
3076    //   (multi-Auto models on MySQL error at runtime since
3077    //   `LAST_INSERT_ID()` only reports one)
3078    // - non-Auto: build InsertQuery with explicit columns/values and
3079    //   call `insert_pool` (no returning needed)
3080    // pool_insert_method body for the audited Auto-PK case is moved
3081    // to after audit_pair_tokens / audit_pk_to_string (they live
3082    // ~150 lines below). This block keeps the non-audited and
3083    // non-Auto branches in place — the audited Auto-PK arm is
3084    // computed below and merged via the dispatch helper variable.
3085    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3086        // Audited models with explicit (non-Auto) PKs go through
3087        // the non-Auto insert path below — the audit emit is one
3088        // round-trip after the INSERT inside the same tx via
3089        // audit::save_one_with_audit? No, INSERT semantics
3090        // differ. For non-Auto PK + audited, route through a
3091        // dedicated insert + audit emit on the same tx, but defer
3092        // the macro emission to the audit-bundle-aware block below
3093        // — this `quote!()` placeholder gets overwritten there.
3094        quote!()
3095    } else if audited_fields.is_some() && fields.has_auto {
3096        // Audited Auto-PK insert_pool — assembled after the audit
3097        // bundles. Placeholder; real emission below.
3098        quote!()
3099    } else if fields.has_auto {
3100        let pushes = &fields.insert_pushes;
3101        let returning_cols = &fields.returning_cols;
3102        // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3103        let filled = &fields.rust_filled_idents;
3104        let was_unset: Vec<syn::Ident> = filled
3105            .iter()
3106            .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3107            .collect();
3108        let insert_or_ignore_call = quote! {
3109            #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3110            let mut _columns: ::std::vec::Vec<&'static str> =
3111                ::std::vec::Vec::new();
3112            let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3113                ::std::vec::Vec::new();
3114            #( #pushes )*
3115            let _query = #root::core::InsertQuery::new(
3116                <Self as #root::core::Model>::SCHEMA,
3117                _columns,
3118                _values,
3119            );
3120            let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3121            if !matches!(_result, ::core::result::Result::Ok(true)) {
3122                #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3123            }
3124            _result
3125        };
3126        // When every `Auto<T>` field is filled Rust-side
3127        // (`default_uuid_v7`, issue #823), there is no column to read
3128        // back from the database — `returning_cols` is empty. Route
3129        // through plain `insert_pool` instead of
3130        // `insert_returning_pool` to skip the redundant RETURNING /
3131        // LAST_INSERT_ID round-trip.
3132        if fields.returning_cols.is_empty() {
3133            quote! {
3134                /// Insert this row against either backend. Every
3135                /// `Auto<T>` PK on this model is filled Rust-side
3136                /// (e.g. `default_uuid_v7`) before binding, so no
3137                /// RETURNING round-trip is needed.
3138                ///
3139                /// # Errors
3140                /// As [`Self::insert`].
3141                pub async fn insert_pool(
3142                    &mut self,
3143                    pool: &#root::sql::Pool,
3144                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3145                    let mut _columns: ::std::vec::Vec<&'static str> =
3146                        ::std::vec::Vec::new();
3147                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3148                        ::std::vec::Vec::new();
3149                    #( #pushes )*
3150                    let _query = #root::core::InsertQuery::new(
3151                        <Self as #root::core::Model>::SCHEMA,
3152                        _columns,
3153                        _values,
3154                    );
3155                    #root::sql::insert_pool(pool, &_query).await
3156                }
3157
3158                /// Eloquent `Model::insertOrIgnore()` — same shape
3159                /// as the auto-PK branch above. Returns `Ok(true)`
3160                /// when inserted, `Ok(false)` when a conflict caused
3161                /// the INSERT to silently skip.
3162                ///
3163                /// # Errors
3164                /// As [`Self::insert`].
3165                pub async fn insert_or_ignore(
3166                    &mut self,
3167                    pool: &#root::sql::Pool,
3168                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3169                    #insert_or_ignore_call
3170                }
3171            }
3172        } else {
3173            quote! {
3174                /// Insert this row against either backend, populating any
3175                /// `Auto<T>` PK from the auto-assigned value.
3176                ///
3177                /// # Errors
3178                /// As [`Self::insert`].
3179                pub async fn insert_pool(
3180                    &mut self,
3181                    pool: &#root::sql::Pool,
3182                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3183                    let mut _columns: ::std::vec::Vec<&'static str> =
3184                        ::std::vec::Vec::new();
3185                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3186                        ::std::vec::Vec::new();
3187                    #( #pushes )*
3188                    let _query = #root::core::InsertQuery::new(
3189                        <Self as #root::core::Model>::SCHEMA,
3190                        _columns,
3191                        _values,
3192                    )
3193                    .returning(::std::vec![ #( #returning_cols ),* ]);
3194                    let _result = #root::sql::insert_returning_pool(
3195                        pool, &_query,
3196                    ).await?;
3197                    #root::sql::apply_auto_pk(_result, self)
3198                }
3199
3200                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3201                /// row or silently skip on unique-constraint
3202                /// violation. Returns `Ok(true)` when a row was
3203                /// inserted, `Ok(false)` when a conflict caused the
3204                /// INSERT to silently skip.
3205                ///
3206                /// **Caveat on auto-PK models**: when the row is
3207                /// skipped (conflict), this instance's `Auto<T>`
3208                /// fields stay `Unset` — no PK is back-populated
3209                /// because the server didn't auto-assign one. For
3210                /// "insert then read back the PK or the existing
3211                /// row's PK", use the `upsert` family or
3212                /// `get_or_create`.
3213                ///
3214                /// # Errors
3215                /// As [`Self::insert`].
3216                pub async fn insert_or_ignore(
3217                    &mut self,
3218                    pool: &#root::sql::Pool,
3219                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3220                    #insert_or_ignore_call
3221                }
3222            }
3223        }
3224    } else {
3225        let insert_columns = &fields.insert_columns;
3226        let insert_values = &fields.insert_values;
3227        quote! {
3228            /// Insert this row into its table against either backend.
3229            /// Equivalent to [`Self::insert`] but takes
3230            /// [`#root::sql::Pool`].
3231            ///
3232            /// # Errors
3233            /// As [`Self::insert`].
3234            pub async fn insert_pool(
3235                &self,
3236                pool: &#root::sql::Pool,
3237            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3238                let _query = #root::core::InsertQuery::new(
3239                    <Self as #root::core::Model>::SCHEMA,
3240                    ::std::vec![ #( #insert_columns ),* ],
3241                    ::std::vec![ #( #insert_values ),* ],
3242                );
3243                #root::sql::insert_pool(pool, &_query).await
3244            }
3245
3246            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3247            /// or silently skip on unique-constraint violation. Maps
3248            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3249            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3250            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3251            /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3252            ///
3253            /// Returns `Ok(true)` when a row was inserted,
3254            /// `Ok(false)` when a conflict caused the INSERT to
3255            /// silently skip.
3256            ///
3257            /// Use for "create-if-absent" patterns where you don't
3258            /// need the row back. For "find-or-create with the row
3259            /// returned", use the queryset-level
3260            /// `crate::sql::get_or_create` free function.
3261            ///
3262            /// # Errors
3263            /// As [`Self::insert`], plus any dialect-specific
3264            /// translation error from the ConflictClause writer.
3265            pub async fn insert_or_ignore(
3266                &self,
3267                pool: &#root::sql::Pool,
3268            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3269                let _query = #root::core::InsertQuery::new(
3270                    <Self as #root::core::Model>::SCHEMA,
3271                    ::std::vec![ #( #insert_columns ),* ],
3272                    ::std::vec![ #( #insert_values ),* ],
3273                );
3274                #root::sql::insert_or_ignore(pool, &_query).await
3275            }
3276        }
3277    };
3278
3279    // pool_save_method moved to after audit_pair_tokens /
3280    // audit_pk_to_string (they live ~70 lines below) — needed for
3281    // the audited branch which builds an UpdateQuery + PendingEntry
3282    // and dispatches via audit::save_one_with_audit.
3283
3284    // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3285    // are computed (they live ~80 lines below).
3286
3287    // Build the (column, JSON value) pair list used by every
3288    // snapshot-style audit emission. Reused across delete_on,
3289    // soft_delete_on, restore_on, and (later) bulk paths. Empty
3290    // when the model isn't audited.
3291    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3292        .map(|tracked| {
3293            tracked
3294                .iter()
3295                .map(|c| {
3296                    let column_lit = c.column.as_str();
3297                    let ident = &c.ident;
3298                    quote! {
3299                        (
3300                            #column_lit,
3301                            #root::__serde_json::to_value(&self.#ident)
3302                                .unwrap_or(#root::__serde_json::Value::Null),
3303                        )
3304                    }
3305                })
3306                .collect()
3307        })
3308        .unwrap_or_default();
3309    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3310        if fields.pk_is_auto {
3311            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3312        } else {
3313            quote!(::std::format!("{}", &self.#pk_ident))
3314        }
3315    } else {
3316        quote!(::std::string::String::new())
3317    };
3318    // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3319    // is built, so every audited write path records the same shape.
3320    let delete_audited_body = if primary_key.is_some() {
3321        quote! {
3322            ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3323                pool,
3324                query,
3325                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3326            ))
3327        }
3328    } else {
3329        quote! {
3330            let _ = (pool, query);
3331            ::std::boxed::Box::pin(async {
3332                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3333                    table: <Self as #root::core::Model>::SCHEMA.table,
3334                })
3335            })
3336        }
3337    };
3338    let update_audited_body = if primary_key.is_some() {
3339        quote! {
3340            ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3341                pool,
3342                query,
3343                move |_r: &Self| _r.__rustango_audit_entry(op),
3344            ))
3345        }
3346    } else {
3347        quote! {
3348            let _ = (pool, query, op);
3349            ::std::boxed::Box::pin(async {
3350                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3351                    table: <Self as #root::core::Model>::SCHEMA.table,
3352                })
3353            })
3354        }
3355    };
3356    let audit_entry_method = if audited_fields.is_some() {
3357        let pairs = audit_pair_tokens.iter();
3358        let pk_str = audit_pk_to_string.clone();
3359        quote! {
3360            /// Snapshot audit entry for `operation` on this row.
3361            #[doc(hidden)]
3362            pub fn __rustango_audit_entry(
3363                &self,
3364                operation: #root::audit::AuditOp,
3365            ) -> #root::audit::PendingEntry {
3366                #root::audit::PendingEntry {
3367                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3368                    entity_pk: #pk_str,
3369                    operation,
3370                    source: #root::audit::current_source(),
3371                    changes: #root::audit::snapshot_changes(&[
3372                        #( #pairs ),*
3373                    ]),
3374                }
3375            }
3376
3377            /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3378            #[doc(hidden)]
3379            pub fn __rustango_update_audited<'a>(
3380                pool: &'a #root::sql::Pool,
3381                query: &'a #root::core::UpdateQuery,
3382                op: #root::audit::AuditOp,
3383            ) -> ::std::pin::Pin<::std::boxed::Box<
3384                dyn ::core::future::Future<
3385                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3386                > + ::core::marker::Send + 'a,
3387            >> {
3388                #update_audited_body
3389            }
3390
3391            /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3392            #[doc(hidden)]
3393            pub fn __rustango_create_audited<'a, 't>(
3394                tx: &'a mut #root::sql::PoolTx<'t>,
3395                pk: #root::core::SqlValue,
3396            ) -> ::std::pin::Pin<::std::boxed::Box<
3397                dyn ::core::future::Future<
3398                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3399                > + ::core::marker::Send + 'a,
3400            >> {
3401                ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3402                    tx,
3403                    <Self as #root::core::Model>::SCHEMA,
3404                    pk,
3405                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3406                ))
3407            }
3408
3409            /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3410            #[doc(hidden)]
3411            pub fn __rustango_delete_audited<'a>(
3412                pool: &'a #root::sql::Pool,
3413                query: &'a #root::core::DeleteQuery,
3414            ) -> ::std::pin::Pin<::std::boxed::Box<
3415                dyn ::core::future::Future<
3416                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3417                > + ::core::marker::Send + 'a,
3418            >> {
3419                #delete_audited_body
3420            }
3421        }
3422    } else {
3423        quote!()
3424    };
3425    // Builds `_audit_entry`; `written` is the soft-delete column's new
3426    // value, recorded in place of the stale `self` field.
3427    let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3428        if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3429            quote! {
3430                let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3431                _audit_entry.set_tracked(#col, #v);
3432            }
3433        } else {
3434            quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3435        }
3436    };
3437    let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3438        .unwrap_or(#root::__serde_json::Value::Null));
3439    let restored_json = quote!(#root::__serde_json::Value::Null);
3440    // `_on` emit; `affected` paths skip the row when nothing changed.
3441    let make_op_emit =
3442        |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3443            if audited_fields.is_none() {
3444                return quote!();
3445            }
3446            let entry = make_entry(op_path, written);
3447            let emit = quote! {
3448                #entry
3449                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3450            };
3451            if affected {
3452                quote!(if _affected > 0 { #emit })
3453            } else {
3454                emit
3455            }
3456        };
3457    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3458    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3459    let audit_softdelete_emit = make_op_emit(
3460        quote!(#root::audit::AuditOp::SoftDelete),
3461        Some(soft_deleted_json.clone()),
3462        true,
3463    );
3464    let audit_restore_emit = make_op_emit(
3465        quote!(#root::audit::AuditOp::Restore),
3466        Some(restored_json.clone()),
3467        true,
3468    );
3469    // `&Pool` update runner for soft_delete / restore: one tx with the
3470    // audit row when audited, a plain UPDATE otherwise.
3471    let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3472        if audited_fields.is_some() {
3473            let entry = make_entry(op_path, Some(written));
3474            quote! {
3475                #entry
3476                #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3477            }
3478        } else {
3479            quote!(#root::sql::update_pool(pool, &_query).await)
3480        }
3481    };
3482    let pool_softdelete_run =
3483        make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3484    let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3485
3486    // `save_pool(&Pool)` — emitted for every model with a PK.
3487    // Audited Auto-PK models are deferred (the Auto::Unset →
3488    // insert_pool path needs the audited-insert flow from a future
3489    // batch). Three body shapes:
3490    // - non-audited, plain PK: build UpdateQuery + dispatch through
3491    //   sql::update_pool
3492    // - non-audited, Auto-PK: same, but Auto::Unset routes to
3493    //   self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3494    // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3495    //   through audit::save_one_with_audit (per-backend tx wraps
3496    //   UPDATE + audit emit atomically). Snapshot-style audit (post-
3497    //   write field values) — diff-style audit (with pre-UPDATE
3498    //   SELECT for `before` values) needs per-tracked-column codegen
3499    //   that doesn't fit the runtime-helper pattern; legacy &PgPool
3500    //   `save` keeps the diff for now.
3501    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3502        let pk_column_lit = pk_col.as_str();
3503        let assignments = &fields.update_assignments;
3504        if audited_fields.is_some() {
3505            if fields.pk_is_auto {
3506                // Auto-PK + audited: defer. The Auto::Unset insert
3507                // path needs a transactional INSERT + LAST_INSERT_ID
3508                // + audit emit flow — that's a follow-up batch.
3509                quote!()
3510            } else {
3511                let pairs = audit_pair_tokens.iter();
3512                let pairs2 = audit_pair_tokens.iter();
3513                let pk_str = audit_pk_to_string.clone();
3514                let pk_str2 = audit_pk_to_string.clone();
3515                quote! {
3516                    /// Save (UPDATE) this row against either backend
3517                    /// with audit emission inside the same transaction.
3518                    /// Bi-dialect counterpart of [`Self::save`] for
3519                    /// audited models with non-`Auto<T>` PKs.
3520                    ///
3521                    /// Captures **post-write** field state (snapshot
3522                    /// audit). The legacy &PgPool [`Self::save`]
3523                    /// captures BEFORE+AFTER for true diff audit;
3524                    /// porting that to the &Pool path needs runtime
3525                    /// per-tracked-column decoding and is deferred.
3526                    ///
3527                    /// # Errors
3528                    /// As [`Self::save`].
3529                    pub async fn save_pool(
3530                        &mut self,
3531                        pool: &#root::sql::Pool,
3532                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3533                        let _query = #root::core::UpdateQuery::new(
3534                            <Self as #root::core::Model>::SCHEMA,
3535                            ::std::vec![ #( #assignments ),* ],
3536                            #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3537                                #pk_column_lit,
3538                                #root::core::Op::Eq,
3539                                ::core::convert::Into::<#root::core::SqlValue>::into(
3540                                    ::core::clone::Clone::clone(&self.#pk_ident)
3541                                ),
3542                            )),
3543                        );
3544                        let _audit_entry = #root::audit::PendingEntry {
3545                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3546                            entity_pk: #pk_str,
3547                            operation: #root::audit::AuditOp::Update,
3548                            source: #root::audit::current_source(),
3549                            changes: #root::audit::snapshot_changes(&[
3550                                #( #pairs ),*
3551                            ]),
3552                        };
3553                        let _affected = #root::audit::save_one_with_audit(
3554                            pool, &_query, &_audit_entry,
3555                        ).await?;
3556                        ::core::result::Result::Ok(_affected)
3557                    }
3558
3559                    /// `save_pool` narrowed to a Rust-field allowlist — issue #66.
3560                    /// Audit emission shrinks to the same column set so
3561                    /// the audit log reflects exactly what was written.
3562                    ///
3563                    /// # Errors
3564                    /// As [`Self::save_pool`], plus
3565                    /// [`#root::core::QueryError::UnknownField`] wrapped
3566                    /// in `ExecError::Query` for unknown field names.
3567                    pub async fn save_partial(
3568                        &mut self,
3569                        fields: &[&str],
3570                        pool: &#root::sql::Pool,
3571                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3572                        if fields.is_empty() {
3573                            #root::__tracing::warn!(
3574                                target: "rustango::save_partial",
3575                                model = <Self as #root::core::Model>::SCHEMA.name,
3576                                "save_partial called with empty field list — no-op"
3577                            );
3578                            return ::core::result::Result::Ok(0);
3579                        }
3580                        let _schema = <Self as #root::core::Model>::SCHEMA;
3581                        let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3582                            ::std::collections::HashSet::with_capacity(fields.len());
3583                        for f in fields {
3584                            match _schema.field(f) {
3585                                ::core::option::Option::Some(fs) => {
3586                                    _wanted_cols.insert(fs.column);
3587                                }
3588                                ::core::option::Option::None => {
3589                                    return ::core::result::Result::Err(
3590                                        #root::sql::ExecError::Query(
3591                                            #root::core::QueryError::UnknownField {
3592                                                model: _schema.name,
3593                                                field: (*f).to_owned(),
3594                                            }
3595                                        )
3596                                    );
3597                                }
3598                            }
3599                        }
3600                        let _full: ::std::vec::Vec<#root::core::Assignment> =
3601                            ::std::vec![ #( #assignments ),* ];
3602                        let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3603                            .into_iter()
3604                            .filter(|a| _wanted_cols.contains(a.column))
3605                            .collect();
3606                        if _filtered.is_empty() {
3607                            #root::__tracing::warn!(
3608                                target: "rustango::save_partial",
3609                                model = _schema.name,
3610                                "save_partial: every named field maps to a non-assignable column — no-op"
3611                            );
3612                            return ::core::result::Result::Ok(0);
3613                        }
3614                        let _query = #root::core::UpdateQuery::new(
3615                            _schema,
3616                            _filtered,
3617                            #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3618                                #pk_column_lit,
3619                                #root::core::Op::Eq,
3620                                ::core::convert::Into::<#root::core::SqlValue>::into(
3621                                    ::core::clone::Clone::clone(&self.#pk_ident)
3622                                ),
3623                            )),
3624                        );
3625                        // Narrow the audit snapshot to the same column set.
3626                        let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3627                            ::std::vec![ #( #pairs2 ),* ];
3628                        let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3629                            _all_pairs
3630                                .into_iter()
3631                                .filter(|(col, _)| _wanted_cols.contains(col))
3632                                .collect();
3633                        let _audit_entry = #root::audit::PendingEntry {
3634                            entity_table: _schema.table,
3635                            entity_pk: #pk_str2,
3636                            operation: #root::audit::AuditOp::Update,
3637                            source: #root::audit::current_source(),
3638                            changes: #root::audit::snapshot_changes(&_narrowed),
3639                        };
3640                        let _affected = #root::audit::save_one_with_audit(
3641                            pool, &_query, &_audit_entry,
3642                        ).await?;
3643                        ::core::result::Result::Ok(_affected)
3644                    }
3645
3646                    /// Typed-column counterpart of [`Self::save_partial`] —
3647                    /// issue #67. `fields` is a tuple of [`Column`]
3648                    /// constants whose `Model` matches `Self`; typos and
3649                    /// model mismatches surface at *compile time*
3650                    /// (`Author::name` inside a `Post::save_partial_typed`
3651                    /// call is a type error, no runtime check).
3652                    ///
3653                    /// ```ignore
3654                    /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3655                    /// ```
3656                    ///
3657                    /// Lowers to [`Self::save_partial`] under the hood;
3658                    /// audit narrowing + every other semantic is identical.
3659                    ///
3660                    /// [`Column`]: #root::core::Column
3661                    ///
3662                    /// # Errors
3663                    /// As [`Self::save_partial`].
3664                    pub async fn save_partial_typed<
3665                        L: #root::core::TypedFieldList<Self>,
3666                    >(
3667                        &mut self,
3668                        fields: L,
3669                        pool: &#root::sql::Pool,
3670                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3671                        let _names = fields.rust_field_names();
3672                        let _refs: ::std::vec::Vec<&str> =
3673                            _names.iter().copied().collect();
3674                        self.save_partial(&_refs, pool).await
3675                    }
3676                }
3677            }
3678        } else {
3679            let dispatch_unset = if fields.pk_is_auto {
3680                quote! {
3681                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3682                        return self.insert_pool(pool).await.map(|()| 1u64);
3683                    }
3684                }
3685            } else {
3686                quote!()
3687            };
3688            quote! {
3689                /// Save this row to its table against either backend.
3690                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3691                /// `UPDATE` keyed on the PK.
3692                ///
3693                /// # Errors
3694                /// As [`Self::save`].
3695                pub async fn save_pool(
3696                    &mut self,
3697                    pool: &#root::sql::Pool,
3698                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3699                    #dispatch_unset
3700                    let _query = #root::core::UpdateQuery::new(
3701                        <Self as #root::core::Model>::SCHEMA,
3702                        ::std::vec![ #( #assignments ),* ],
3703                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3704                            #pk_column_lit,
3705                            #root::core::Op::Eq,
3706                            ::core::convert::Into::<#root::core::SqlValue>::into(
3707                                ::core::clone::Clone::clone(&self.#pk_ident)
3708                            ),
3709                        )),
3710                    );
3711                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3712                    ::core::result::Result::Ok(_affected)
3713                }
3714
3715                /// Save (UPDATE) only the listed Rust-side fields,
3716                /// leaving every other column untouched. Issue #66.
3717                ///
3718                /// `fields` are Rust-side struct field names; the macro
3719                /// resolves each to its SQL column. Unknown field
3720                /// names return [`#root::core::QueryError::UnknownField`]
3721                /// wrapped in `ExecError::Query`. An empty list is a
3722                /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3723                ///
3724                /// Use this when:
3725                /// * you only mutated a couple of fields on a wide row
3726                ///   (avoid re-writing every column on every save), or
3727                /// * two writers diverged after their initial read and
3728                ///   you want to preserve the other writer's changes to
3729                ///   columns you didn't touch.
3730                ///
3731                /// Auto-PK models with an unset PK return
3732                /// [`#root::core::QueryError::UnknownField`] with
3733                /// field name `<pk>` — `save_partial` is an
3734                /// UPDATE-only path. Call [`Self::insert_pool`]
3735                /// (or [`Self::save_pool`] which dispatches based on
3736                /// PK state) for the INSERT case.
3737                ///
3738                /// # Errors
3739                /// As [`Self::save_pool`], plus `UnknownField` for
3740                /// unknown / empty / Auto-Unset cases.
3741                pub async fn save_partial(
3742                    &mut self,
3743                    fields: &[&str],
3744                    pool: &#root::sql::Pool,
3745                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3746                    if fields.is_empty() {
3747                        #root::__tracing::warn!(
3748                            target: "rustango::save_partial",
3749                            model = <Self as #root::core::Model>::SCHEMA.name,
3750                            "save_partial called with empty field list — no-op"
3751                        );
3752                        return ::core::result::Result::Ok(0);
3753                    }
3754                    let _schema = <Self as #root::core::Model>::SCHEMA;
3755                    // Validate field names against the schema.
3756                    let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3757                        ::std::collections::HashSet::with_capacity(fields.len());
3758                    for f in fields {
3759                        match _schema.field(f) {
3760                            ::core::option::Option::Some(fs) => {
3761                                _wanted_cols.insert(fs.column);
3762                            }
3763                            ::core::option::Option::None => {
3764                                return ::core::result::Result::Err(
3765                                    #root::sql::ExecError::Query(
3766                                        #root::core::QueryError::UnknownField {
3767                                            model: _schema.name,
3768                                            field: (*f).to_owned(),
3769                                        }
3770                                    )
3771                                );
3772                            }
3773                        }
3774                    }
3775                    // Build the full assignment vec, then keep only the
3776                    // assignments whose column is in `_wanted_cols`.
3777                    let _full: ::std::vec::Vec<#root::core::Assignment> =
3778                        ::std::vec![ #( #assignments ),* ];
3779                    let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3780                        .into_iter()
3781                        .filter(|a| _wanted_cols.contains(a.column))
3782                        .collect();
3783                    if _filtered.is_empty() {
3784                        // All field names valid, but they all map to
3785                        // non-assignable slots (PK column, computed/
3786                        // virtual fields, relations without an
3787                        // assignment). Same no-op as an empty list.
3788                        #root::__tracing::warn!(
3789                            target: "rustango::save_partial",
3790                            model = _schema.name,
3791                            "save_partial: every named field maps to a non-assignable column — no-op"
3792                        );
3793                        return ::core::result::Result::Ok(0);
3794                    }
3795                    let _query = #root::core::UpdateQuery::new(
3796                        _schema,
3797                        _filtered,
3798                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3799                            #pk_column_lit,
3800                            #root::core::Op::Eq,
3801                            ::core::convert::Into::<#root::core::SqlValue>::into(
3802                                ::core::clone::Clone::clone(&self.#pk_ident)
3803                            ),
3804                        )),
3805                    );
3806                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3807                    ::core::result::Result::Ok(_affected)
3808                }
3809
3810                /// Typed-column counterpart of [`Self::save_partial`] —
3811                /// issue #67. `fields` is a tuple of [`Column`]
3812                /// constants whose `Model` matches `Self`; typos and
3813                /// model mismatches surface at *compile time*
3814                /// (`Author::name` inside a `Post::save_partial_typed`
3815                /// call is a type error, no runtime check).
3816                ///
3817                /// ```ignore
3818                /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3819                /// ```
3820                ///
3821                /// Lowers to [`Self::save_partial`] under the hood — the
3822                /// tuple is reduced to a `&[&str]` slice of Rust-side
3823                /// field names and forwarded.
3824                ///
3825                /// [`Column`]: #root::core::Column
3826                ///
3827                /// # Errors
3828                /// As [`Self::save_partial`].
3829                pub async fn save_partial_typed<
3830                    L: #root::core::TypedFieldList<Self>,
3831                >(
3832                    &mut self,
3833                    fields: L,
3834                    pool: &#root::sql::Pool,
3835                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3836                    let _names = fields.rust_field_names();
3837                    let _refs: ::std::vec::Vec<&str> =
3838                        _names.iter().copied().collect();
3839                    self.save_partial(&_refs, pool).await
3840                }
3841            }
3842        }
3843    } else {
3844        quote!()
3845    };
3846
3847    // Audited `insert_pool` (overrides the placeholder set higher up
3848    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3849    // audited models get insert_pool routing through
3850    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3851    // + auto-PK readback + audit emit). Snapshot-style audit (the
3852    // PendingEntry's `changes` carries post-write field values).
3853    let pool_insert_method = if audited_fields.is_some() {
3854        if let Some(_) = primary_key {
3855            let pushes = if fields.has_auto {
3856                fields.insert_pushes.clone()
3857            } else {
3858                // For non-Auto-PK models, the macro normally builds
3859                // {columns, values} from fields.insert_columns +
3860                // fields.insert_values rather than insert_pushes.
3861                // Map those into the pushes shape.
3862                fields
3863                    .insert_columns
3864                    .iter()
3865                    .zip(&fields.insert_values)
3866                    .map(|(col, val)| {
3867                        quote! {
3868                            _columns.push(#col);
3869                            _values.push(#val);
3870                        }
3871                    })
3872                    .collect()
3873            };
3874            // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
3875            let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
3876            {
3877                fields.returning_cols.clone()
3878            } else {
3879                // Non-Auto-PK: still need RETURNING something for the
3880                // audit helper's contract (it errors on empty
3881                // returning). Return the PK column so the audit row
3882                // can carry the assigned PK back. Some non-Auto PKs
3883                // are server-side-default (e.g. UUIDv4 default), so
3884                // RETURNING is genuinely useful.
3885                primary_key
3886                    .map(|(_, col)| {
3887                        let lit = col.as_str();
3888                        vec![quote!(#lit)]
3889                    })
3890                    .unwrap_or_default()
3891            };
3892            quote! {
3893                /// Insert this row against either backend with audit
3894                /// emission inside the same transaction. Bi-dialect
3895                /// counterpart of [`Self::insert`] for audited models.
3896                ///
3897                /// Snapshot-style audit (post-write field values).
3898                ///
3899                /// # Errors
3900                /// As [`Self::insert`].
3901                pub async fn insert_pool(
3902                    &mut self,
3903                    pool: &#root::sql::Pool,
3904                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3905                    let mut _columns: ::std::vec::Vec<&'static str> =
3906                        ::std::vec::Vec::new();
3907                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3908                        ::std::vec::Vec::new();
3909                    #( #pushes )*
3910                    let _query = #root::core::InsertQuery::new(
3911                        <Self as #root::core::Model>::SCHEMA,
3912                        _columns,
3913                        _values,
3914                    )
3915                    .returning(::std::vec![ #( #returning_cols ),* ]);
3916                    #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
3917                        _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
3918                    })
3919                    .await
3920                }
3921            }
3922        } else {
3923            quote!()
3924        }
3925    } else {
3926        // Keep the non-audited pool_insert_method we built earlier.
3927        pool_insert_method
3928    };
3929
3930    // Update audited save_pool: now that insert_pool is wired for
3931    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3932    // insert_pool. Non-audited save_pool already does this.
3933    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3934    // Replaces the snapshot-only emission with a per-backend transaction
3935    // body that:
3936    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3937    //     column), capturing BEFORE values
3938    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3939    //  3. builds AFTER pairs from &self
3940    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3941    //     AuditOp::Update + diff_changes(...) on the same tx connection
3942    //  5. commits
3943    //
3944    // Per-backend arms inline the SQL string + placeholder shape, then
3945    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3946    // is polymorphic over Row type — the same tokens work against
3947    // PgRow and MySqlRow as long as the field's Rust type implements
3948    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3949    // primitives + chrono/uuid/serde_json::Value all do).
3950    let pool_save_method = if let Some(tracked) = audited_fields {
3951        if let Some((pk_ident, pk_col)) = primary_key {
3952            let pk_column_lit = pk_col.as_str();
3953            // Two iterators — quote!'s `#(#var)*` consumes the
3954            // iterator, and we need to splice the same after-pairs
3955            // sequence into both per-backend arms.
3956            let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3957            let pk_str = audit_pk_to_string.clone();
3958            // Per-tracked-column BEFORE-pair token list. Each entry
3959            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3960            // The Row alias resolves to PgRow / MySqlRow per call site,
3961            // so the same template generates both the PG and MySQL bodies.
3962            let mk_before_pairs =
3963                |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
3964                    tracked
3965                        .iter()
3966                        .map(|c| {
3967                            let getter = getter(c);
3968                            let column_lit = c.column.as_str();
3969                            let value_ty = &c.value_ty;
3970                            quote! {
3971                                (
3972                                    #column_lit,
3973                                    match #getter::<#value_ty>(
3974                                        _audit_before_row, #column_lit,
3975                                    ) {
3976                                        ::core::result::Result::Ok(v) => {
3977                                            #root::__serde_json::to_value(&v)
3978                                                .unwrap_or(#root::__serde_json::Value::Null)
3979                                        }
3980                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3981                                    },
3982                                )
3983                            }
3984                        })
3985                        .collect()
3986                };
3987            let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
3988            let before_pairs_my = mk_before_pairs(&|c| {
3989                if c.uuid_column {
3990                    quote!(#root::sql::try_get_flat_my)
3991                } else {
3992                    quote!(#root::sql::try_get_returning_my)
3993                }
3994            });
3995            let before_pairs_sqlite =
3996                mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
3997            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
3998            let pk_value_for_bind = if fields.pk_is_auto {
3999                quote!(self.#pk_ident.get().copied().unwrap_or_default())
4000            } else {
4001                quote!(::core::clone::Clone::clone(&self.#pk_ident))
4002            };
4003            let assignments = &fields.update_assignments;
4004            let unset_dispatch = if fields.has_auto {
4005                quote! {
4006                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
4007                        return self.insert_pool(pool).await.map(|()| 1u64);
4008                    }
4009                }
4010            } else {
4011                quote!()
4012            };
4013            quote! {
4014                /// Save this row against either backend with audit
4015                /// emission (diff-style: BEFORE+AFTER) inside the
4016                /// same transaction. Auto::Unset PK routes to
4017                /// insert_pool. Bi-dialect counterpart of
4018                /// [`Self::save`] for audited models.
4019                ///
4020                /// The audit row's `changes` JSON contains one
4021                /// `{ "field": { "before": …, "after": … } }` entry
4022                /// per tracked column whose value actually changed
4023                /// — same shape as the existing &PgPool save() emits.
4024                ///
4025                /// # Errors
4026                /// As [`Self::save`].
4027                pub async fn save_pool(
4028                    &mut self,
4029                    pool: &#root::sql::Pool,
4030                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4031                    #unset_dispatch
4032                    let _query = #root::core::UpdateQuery::new(
4033                        <Self as #root::core::Model>::SCHEMA,
4034                        ::std::vec![ #( #assignments ),* ],
4035                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4036                            #pk_column_lit,
4037                            #root::core::Op::Eq,
4038                            ::core::convert::Into::<#root::core::SqlValue>::into(
4039                                ::core::clone::Clone::clone(&self.#pk_ident)
4040                            ),
4041                        )),
4042                    );
4043                    let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
4044                        ::std::vec![ #( #after_pairs_pg ),* ];
4045                    let _before_query = #root::audit::before_image_query(
4046                        <Self as #root::core::Model>::SCHEMA,
4047                        #pk_column_lit,
4048                        ::core::convert::Into::<#root::core::SqlValue>::into(
4049                            #pk_value_for_bind,
4050                        ),
4051                        &[ #( #tracked_cols ),* ],
4052                    );
4053                    #root::audit::save_one_with_diff(
4054                        pool,
4055                        &_query,
4056                        &_before_query,
4057                        <Self as #root::core::Model>::SCHEMA.table,
4058                        #pk_str,
4059                        _after_pairs,
4060                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4061                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4062                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4063                    ).await
4064                }
4065            }
4066        } else {
4067            quote!()
4068        }
4069    } else {
4070        pool_save_method
4071    };
4072
4073    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4074    // body shapes:
4075    // - non-audited: simple dispatch through `sql::delete_pool`
4076    // - audited: routes through `audit::delete_one_with_audit`,
4077    //   which opens a per-backend transaction wrapping DELETE +
4078    //   audit emit so the data write and audit row commit atomically.
4079    let pool_delete_method = {
4080        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4081        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4082        if let Some(pk_ident) = pk_ident_for_pool {
4083            if audited_fields.is_some() {
4084                quote! {
4085                    /// Delete this row against either backend with audit
4086                    /// emission inside the same transaction. Bi-dialect
4087                    /// counterpart of [`Self::delete`] for audited models.
4088                    ///
4089                    /// # Errors
4090                    /// As [`Self::delete`].
4091                    pub async fn delete_pool(
4092                        &self,
4093                        pool: &#root::sql::Pool,
4094                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4095                        let _query = #root::core::DeleteQuery::by_pk(
4096                            <Self as #root::core::Model>::SCHEMA,
4097                            #pk_column_lit,
4098                            ::core::convert::Into::<#root::core::SqlValue>::into(
4099                                ::core::clone::Clone::clone(&self.#pk_ident)
4100                            ),
4101                        );
4102                        let _audit_entry =
4103                            self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4104                        #root::audit::delete_one_with_audit(
4105                            pool, &_query, &_audit_entry,
4106                        ).await
4107                    }
4108                }
4109            } else {
4110                quote! {
4111                    /// Delete the row identified by this instance's primary key
4112                    /// against either backend. Equivalent to [`Self::delete`] but
4113                    /// takes [`#root::sql::Pool`] and dispatches per backend.
4114                    ///
4115                    /// # Errors
4116                    /// As [`Self::delete`].
4117                    pub async fn delete_pool(
4118                        &self,
4119                        pool: &#root::sql::Pool,
4120                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4121                        let _query = #root::core::DeleteQuery::by_pk(
4122                            <Self as #root::core::Model>::SCHEMA,
4123                            #pk_column_lit,
4124                            ::core::convert::Into::<#root::core::SqlValue>::into(
4125                                ::core::clone::Clone::clone(&self.#pk_ident)
4126                            ),
4127                        );
4128                        #root::sql::delete_pool(pool, &_query).await
4129                    }
4130                }
4131            }
4132        } else {
4133            quote!()
4134        }
4135    };
4136
4137    // Bulk writers on audited models write one audit row per affected
4138    // row, in the write's transaction (#1747).
4139    let (bulk_delete_run, bulk_update_run, truncate_run) = if audited_fields.is_some() {
4140        (
4141            quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4142                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4143            })
4144            .await),
4145            quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4146                _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4147            })
4148            .await),
4149            quote!(#root::audit::truncate_with_audit(pool, _table, &_sql).await),
4150        )
4151    } else {
4152        (
4153            quote!(#root::sql::delete_pool(pool, &_query).await),
4154            quote!(#root::sql::update_pool(pool, &_query).await),
4155            quote!(#root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await),
4156        )
4157    };
4158
4159    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4160    // matching this instance's PK and overwrite the in-memory state
4161    // with the freshly-fetched columns.
4162    // Issue #825. Only emitted when the model declares a PK; non-PK
4163    // models can't address a specific row.
4164    //
4165    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4166    // every field from `self`; resets the PK to `Auto::Unset` when
4167    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4168    // a fresh autoincrement value. Non-Auto PKs preserve the source
4169    // PK — the caller must overwrite before insert. Pure-Rust, no
4170    // I/O, no dialect surface.
4171    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4172        let other_field_clones: Vec<TokenStream2> = fields
4173            .column_entries
4174            .iter()
4175            .filter(|c| &c.ident != pk_ident)
4176            .map(|c| {
4177                let ident = &c.ident;
4178                quote! {
4179                    #ident: ::core::clone::Clone::clone(&self.#ident)
4180                }
4181            })
4182            .collect();
4183        let pk_clone_token = if fields.pk_is_auto {
4184            quote! { #pk_ident: #root::sql::Auto::Unset }
4185        } else {
4186            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4187        };
4188        let replicate_doc = if fields.pk_is_auto {
4189            quote! {
4190                /// Eloquent-style `replicate()` — return a clone of this
4191                /// row with the primary key reset to [`Auto::Unset`] so
4192                /// the copy is ready for `insert_pool` / `save_pool` to
4193                /// allocate a fresh autoincrement value. Every other
4194                /// field is `Clone`d verbatim. Issue #825.
4195                ///
4196                /// `auto_now_add` / `auto_now` timestamp fields are
4197                /// **not** reset (Eloquent's `replicate` doesn't reset
4198                /// them either) — pass them through the normal insert
4199                /// path if you want fresh values, or assign them
4200                /// explicitly after the call.
4201            }
4202        } else {
4203            quote! {
4204                /// Eloquent-style `replicate()` — clone this row
4205                /// verbatim. Because the primary key is **not** an
4206                /// `Auto<T>`, the clone keeps the source PK; the
4207                /// caller must overwrite `copy.<pk>` before inserting
4208                /// to avoid a unique-key violation. Issue #825.
4209            }
4210        };
4211        // 2026-06-07 — field-name / shortcut collision guard.
4212        //
4213        // The macro emits both `pub const <field>: <field>_col = ...`
4214        // (per-field typed-column const, used by the typed-builder
4215        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4216        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4217        // model has a field named e.g. `count`, both items would land
4218        // in the same inherent impl with the same name, and the
4219        // compiler rejects the derive with "duplicate definitions".
4220        //
4221        // Drop the conflicting shortcut for that model. Callers can
4222        // still reach the same behavior via
4223        // `QuerySet::<Model>::default().count(&pool)`.
4224        let column_names: ::std::collections::HashSet<String> = fields
4225            .column_entries
4226            .iter()
4227            .map(|c| ident_name(&c.ident))
4228            .collect();
4229        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4230            if column_names.contains(name) {
4231                quote! {}
4232            } else {
4233                tokens
4234            }
4235        };
4236        let count_method = emit_if_no_field_collision(
4237            "count",
4238            quote! {
4239                /// Count rows of this model — `SELECT COUNT(*) FROM
4240                /// <table>`. Eloquent `Model::count()` parity.
4241                ///
4242                /// Skipped on models that already declare a field named
4243                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4244                /// in that case.
4245                ///
4246                /// # Errors
4247                /// As [`CounterPool::count`].
4248                ///
4249                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4250                pub async fn count(
4251                    pool: &#root::sql::Pool,
4252                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4253                    use #root::sql::CounterPool as _;
4254                    #root::query::QuerySet::<Self>::default()
4255                        .count(pool)
4256                        .await
4257                }
4258            },
4259        );
4260        let value_method = emit_if_no_field_collision(
4261            "value",
4262            quote! {
4263                /// Pluck a single scalar from the first row.
4264                /// Eloquent `Model::query()->value($col)` parity.
4265                ///
4266                /// Skipped on models that already declare a field named
4267                /// `value`. Drop into
4268                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4269                ///
4270                /// # Errors
4271                /// As `ValuesFlatQuerySet::first`.
4272                pub async fn value<U>(
4273                    col: &str,
4274                    pool: &#root::sql::Pool,
4275                ) -> ::core::result::Result<
4276                    ::core::option::Option<U>,
4277                    #root::sql::ExecError,
4278                >
4279                where
4280                    U: #root::sql::FlatScalar,
4281                {
4282                    let _col_static: &'static str = Self::__resolve_col(col)?;
4283                    #root::query::QuerySet::<Self>::default()
4284                        .values_list_flat(_col_static)
4285                        .first::<U>(pool)
4286                        .await
4287                }
4288            },
4289        );
4290        let sum_method = emit_if_no_field_collision(
4291            "sum",
4292            quote! {
4293                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4294                /// Skipped on models that already declare a field named
4295                /// `sum`.
4296                ///
4297                /// # Errors
4298                /// As [`#root::sql::fetch_aggregate_pool`].
4299                pub async fn sum<U>(
4300                    col: &str,
4301                    pool: &#root::sql::Pool,
4302                ) -> ::core::result::Result<
4303                    ::core::option::Option<U>,
4304                    #root::sql::ExecError,
4305                >
4306                where
4307                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4308                        + #root::sql::MaybeMyFromRow
4309                        + #root::sql::MaybeSqliteFromRow
4310                        + ::core::marker::Send
4311                        + ::core::marker::Unpin,
4312                {
4313                    #root::query::QuerySet::<Self>::default()
4314                        .sum::<U>(col, pool)
4315                        .await
4316                }
4317            },
4318        );
4319        let avg_method = emit_if_no_field_collision(
4320            "avg",
4321            quote! {
4322                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4323                /// models that already declare a field named `avg`.
4324                ///
4325                /// # Errors
4326                /// As [`#root::sql::fetch_aggregate_pool`].
4327                pub async fn avg<U>(
4328                    col: &str,
4329                    pool: &#root::sql::Pool,
4330                ) -> ::core::result::Result<
4331                    ::core::option::Option<U>,
4332                    #root::sql::ExecError,
4333                >
4334                where
4335                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4336                        + #root::sql::MaybeMyFromRow
4337                        + #root::sql::MaybeSqliteFromRow
4338                        + ::core::marker::Send
4339                        + ::core::marker::Unpin,
4340                {
4341                    #root::query::QuerySet::<Self>::default()
4342                        .avg::<U>(col, pool)
4343                        .await
4344                }
4345            },
4346        );
4347        let min_method = emit_if_no_field_collision(
4348            "min",
4349            quote! {
4350                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4351                /// models that already declare a field named `min`.
4352                ///
4353                /// # Errors
4354                /// As [`#root::sql::fetch_aggregate_pool`].
4355                pub async fn min<U>(
4356                    col: &str,
4357                    pool: &#root::sql::Pool,
4358                ) -> ::core::result::Result<
4359                    ::core::option::Option<U>,
4360                    #root::sql::ExecError,
4361                >
4362                where
4363                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4364                        + #root::sql::MaybeMyFromRow
4365                        + #root::sql::MaybeSqliteFromRow
4366                        + ::core::marker::Send
4367                        + ::core::marker::Unpin,
4368                {
4369                    #root::query::QuerySet::<Self>::default()
4370                        .min::<U>(col, pool)
4371                        .await
4372                }
4373            },
4374        );
4375        let max_method = emit_if_no_field_collision(
4376            "max",
4377            quote! {
4378                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4379                /// models that already declare a field named `max`.
4380                ///
4381                /// # Errors
4382                /// As [`#root::sql::fetch_aggregate_pool`].
4383                pub async fn max<U>(
4384                    col: &str,
4385                    pool: &#root::sql::Pool,
4386                ) -> ::core::result::Result<
4387                    ::core::option::Option<U>,
4388                    #root::sql::ExecError,
4389                >
4390                where
4391                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4392                        + #root::sql::MaybeMyFromRow
4393                        + #root::sql::MaybeSqliteFromRow
4394                        + ::core::marker::Send
4395                        + ::core::marker::Unpin,
4396                {
4397                    #root::query::QuerySet::<Self>::default()
4398                        .max::<U>(col, pool)
4399                        .await
4400                }
4401            },
4402        );
4403        let first_method = emit_if_no_field_collision(
4404            "first",
4405            quote! {
4406                /// First row of this model. Eloquent `Model::first()`.
4407                /// Skipped on models that already declare a field named
4408                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4409                ///
4410                /// # Errors
4411                /// As `QuerySet::first`.
4412                pub async fn first(
4413                    pool: &#root::sql::Pool,
4414                ) -> ::core::result::Result<
4415                    ::core::option::Option<Self>,
4416                    #root::sql::ExecError,
4417                > {
4418                    #root::query::QuerySet::<Self>::default()
4419                        .first(pool)
4420                        .await
4421                }
4422            },
4423        );
4424        let last_method = emit_if_no_field_collision(
4425            "last",
4426            quote! {
4427                /// Last row of this model by primary-key DESC.
4428                /// Eloquent `Model::query()->latest('id')->first()`
4429                /// parity — fetches the highest-PK row without
4430                /// requiring the caller to spell the PK column.
4431                /// Returns `None` on an empty table.
4432                ///
4433                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4434                /// Skipped on models that already declare a field
4435                /// named `last`.
4436                ///
4437                /// # Errors
4438                /// As `QuerySet::last`.
4439                pub async fn last(
4440                    pool: &#root::sql::Pool,
4441                ) -> ::core::result::Result<
4442                    ::core::option::Option<Self>,
4443                    #root::sql::ExecError,
4444                > {
4445                    #root::query::QuerySet::<Self>::default()
4446                        .last(pool)
4447                        .await
4448                }
4449            },
4450        );
4451        quote! {
4452            /// Re-SELECT this row by its primary key and overwrite
4453            /// every in-memory field with the freshly-fetched value.
4454            /// Issue #825.
4455            ///
4456            /// Use this when the row may have been modified by another
4457            /// process / connection / job since you read it — e.g. after
4458            /// a queued task callback, or to re-sync stale UI state
4459            /// before re-saving.
4460            ///
4461            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4462            /// when the primary key no longer matches any row (e.g.
4463            /// the row was deleted concurrently).
4464            ///
4465            /// # Errors
4466            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4467            /// the PK no longer exists.
4468            ///
4469            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4470            pub async fn refresh_from_db(
4471                &mut self,
4472                pool: &#root::sql::Pool,
4473            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4474                use #root::sql::FetcherPool as _;
4475                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4476                    ::core::clone::Clone::clone(&self.#pk_ident),
4477                );
4478                let mut _rows: ::std::vec::Vec<Self> =
4479                    #root::query::QuerySet::<Self>::default()
4480                        .filter(::core::stringify!(#pk_ident), _pk_val)
4481                        .limit(1)
4482                        .fetch(pool)
4483                        .await?;
4484                match _rows.into_iter().next() {
4485                    ::core::option::Option::Some(_fresh) => {
4486                        *self = _fresh;
4487                        ::core::result::Result::Ok(())
4488                    }
4489                    ::core::option::Option::None => ::core::result::Result::Err(
4490                        #root::sql::ExecError::Driver(
4491                            #root::sql::sqlx::Error::RowNotFound,
4492                        ),
4493                    ),
4494                }
4495            }
4496
4497            /// Atomically increment the integer column `col` by
4498            /// `by` for this row. Equivalent to
4499            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4500            ///
4501            /// **Doesn't mutate `self`** — the in-memory copy is now
4502            /// stale; call [`Self::refresh_from_db`] /
4503            /// [`Self::fresh`] to re-sync. Returns the rows-
4504            /// affected count (0 when the PK doesn't match any row,
4505            /// 1 on success).
4506            ///
4507            /// `col` is the Rust field name as a string; unknown
4508            /// fields surface as `UnknownField` at runtime. Negative
4509            /// `by` values atomically decrement (see also
4510            /// [`Self::decrement`]).
4511            ///
4512            /// # Errors
4513            /// As [`UpdaterPool::execute_pool`].
4514            ///
4515            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4516            pub async fn increment(
4517                &self,
4518                col: &str,
4519                by: i64,
4520                pool: &#root::sql::Pool,
4521            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4522                Self::__increment_one(self, col, by, pool).await
4523            }
4524
4525            /// Sibling of [`Self::increment`] — atomically
4526            /// decrement this row's `col` by `by`. Eloquent
4527            /// `$model->decrement($col, $by)` parity. Equivalent to
4528            /// `self.increment(col, -by, &pool)`; the separate name
4529            /// keeps call sites readable.
4530            ///
4531            /// # Errors
4532            /// As [`Self::increment`].
4533            pub async fn decrement(
4534                &self,
4535                col: &str,
4536                by: i64,
4537                pool: &#root::sql::Pool,
4538            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4539                Self::__increment_one(self, col, -by, pool).await
4540            }
4541
4542            /// Bulk-increment: add `by` to `col` on every row of the
4543            /// table. Eloquent `Model::query()->increment($col, $by)`
4544            /// parity. Use for counters, score adjustments, view
4545            /// rollups.
4546            ///
4547            /// # Errors
4548            /// As [`UpdaterPool::execute_pool`].
4549            ///
4550            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4551            pub async fn increment_each(
4552                col: &str,
4553                by: i64,
4554                pool: &#root::sql::Pool,
4555            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4556                Self::__increment_all(col, by, pool).await
4557            }
4558
4559            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4560            ///
4561            /// # Errors
4562            /// As [`Self::increment_each`].
4563            pub async fn decrement_each(
4564                col: &str,
4565                by: i64,
4566                pool: &#root::sql::Pool,
4567            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4568                Self::__increment_all(col, -by, pool).await
4569            }
4570
4571            /// Internal: forward to
4572            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4573            /// One-line wrapper kept as a per-Model method so the
4574            /// macro's emitted `increment` / `decrement` instance
4575            /// calls don't have to thread `Self` through manually.
4576            #[doc(hidden)]
4577            pub async fn __increment_one(
4578                this: &Self,
4579                col: &str,
4580                by: i64,
4581                pool: &#root::sql::Pool,
4582            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4583                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4584                    ::core::clone::Clone::clone(&this.#pk_ident),
4585                );
4586                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4587                    ::core::stringify!(#pk_ident),
4588                    _pk_val,
4589                    col,
4590                    by,
4591                    pool,
4592                )
4593                .await
4594            }
4595
4596            /// Internal: run
4597            /// [`#root::sql::model_shortcuts::increment_all_query`].
4598            #[doc(hidden)]
4599            pub async fn __increment_all(
4600                col: &str,
4601                by: i64,
4602                pool: &#root::sql::Pool,
4603            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4604                let _query =
4605                    #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4606                #bulk_update_run
4607            }
4608
4609            /// Internal: forward to
4610            /// [`#root::sql::model_shortcuts::resolve_col`].
4611            #[doc(hidden)]
4612            pub fn __resolve_col(
4613                col: &str,
4614            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4615                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4616            }
4617
4618            /// Internal: forward to
4619            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4620            #[doc(hidden)]
4621            #[must_use]
4622            pub fn __add_signed_expr(
4623                col_static: &'static str,
4624                signed_by: i64,
4625            ) -> #root::core::Expr {
4626                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4627            }
4628
4629            /// Re-SELECT this row by its primary key and return a
4630            /// **new** instance with the freshly-fetched fields.
4631            /// Eloquent `Model::fresh()` parity — non-mutating
4632            /// counterpart of [`Self::refresh_from_db`].
4633            ///
4634            /// Returns `Ok(None)` when the row was deleted
4635            /// concurrently — vs [`Self::refresh_from_db`]
4636            /// which surfaces that as `RowNotFound` because
4637            /// in-place mutation has nothing to write to.
4638            ///
4639            /// Useful when you want to compare the in-memory
4640            /// instance against the persisted state (audit-style
4641            /// diffs, conflict detection) without mutating the
4642            /// reference you already hold.
4643            ///
4644            /// # Errors
4645            /// As [`FetcherPool::fetch`].
4646            ///
4647            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4648            pub async fn fresh(
4649                &self,
4650                pool: &#root::sql::Pool,
4651            ) -> ::core::result::Result<
4652                ::core::option::Option<Self>,
4653                #root::sql::ExecError,
4654            > {
4655                use #root::sql::FetcherPool as _;
4656                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4657                    ::core::clone::Clone::clone(&self.#pk_ident),
4658                );
4659                let _rows: ::std::vec::Vec<Self> =
4660                    #root::query::QuerySet::<Self>::default()
4661                        .filter(::core::stringify!(#pk_ident), _pk_val)
4662                        .limit(1)
4663                        .fetch(pool)
4664                        .await?;
4665                ::core::result::Result::Ok(_rows.into_iter().next())
4666            }
4667
4668            #replicate_doc
4669            #[must_use]
4670            pub fn replicate(&self) -> Self {
4671                Self {
4672                    #pk_clone_token,
4673                    #( #other_field_clones, )*
4674                }
4675            }
4676
4677            #first_method
4678
4679            #last_method
4680
4681            /// Throwing counterpart of [`Self::first`] —
4682            /// errors with `RowNotFound` when the table is empty.
4683            /// Eloquent `Model::firstOrFail()` parity.
4684            ///
4685            /// # Errors
4686            /// As [`Self::first`]; additionally
4687            /// [`sqlx::Error::RowNotFound`] on empty tables.
4688            ///
4689            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4690            pub async fn first_or_fail(
4691                pool: &#root::sql::Pool,
4692            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4693                // Route through the queryset rather than `Self::first`,
4694                // which is suppressed on models with a field named
4695                // `first` (field/shortcut collision guard).
4696                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4697                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4698                    ::core::option::Option::None => ::core::result::Result::Err(
4699                        #root::sql::ExecError::Driver(
4700                            #root::sql::sqlx::Error::RowNotFound,
4701                        ),
4702                    ),
4703                }
4704            }
4705
4706            /// Single-column projection — `SELECT <col> FROM
4707            /// <table>`. Returns `Vec<U>` where each element is the
4708            /// decoded value of the column. Eloquent
4709            /// `Model::pluck($column)` parity.
4710            ///
4711            /// Thin wrapper over `QuerySet::<Self>::default()
4712            /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4713            /// `FlatScalar`; use `Option<_>` for a nullable column.
4714            ///
4715            /// # Errors
4716            /// As `ValuesFlatQuerySet::fetch`.
4717            pub async fn pluck<U>(
4718                col: &'static str,
4719                pool: &#root::sql::Pool,
4720            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4721            where
4722                U: #root::sql::FlatScalar,
4723            {
4724                #root::query::QuerySet::<Self>::default()
4725                    .values_list_flat(col)
4726                    .fetch::<U>(pool)
4727                    .await
4728            }
4729
4730            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4731            /// stream every row of this model in batches of `n`,
4732            /// invoking the callback once per batch. Stable PK-ASC
4733            /// ordering so the LIMIT/OFFSET pagination is
4734            /// deterministic across drivers.
4735            ///
4736            /// The callback is async — it can do further DB work
4737            /// (writes, related-row lookups, queue dispatch) per
4738            /// batch. Return `Err(...)` from the callback to abort
4739            /// the iteration early; the error bubbles up.
4740            ///
4741            /// **When to use**: bulk processing flows that can't
4742            /// fit the whole table in memory — sending newsletters,
4743            /// running data migrations, computing summary
4744            /// statistics. For small / known-bounded tables, plain
4745            /// `Self::all(&pool)` is simpler.
4746            ///
4747            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4748            /// large tables. For multi-million-row scans prefer
4749            /// keyset-by-PK (the standard "WHERE id > last_seen"
4750            /// shape) over `chunk(...)`.
4751            ///
4752            /// Skipped on models without a primary key — chunking
4753            /// needs a stable order to avoid skipping / repeating
4754            /// rows across batches.
4755            pub async fn chunk<F, Fut>(
4756                n: i64,
4757                pool: &#root::sql::Pool,
4758                mut cb: F,
4759            ) -> ::core::result::Result<(), #root::sql::ExecError>
4760            where
4761                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4762                Fut: ::core::future::Future<
4763                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4764                >,
4765            {
4766                use #root::sql::FetcherPool as _;
4767                let pk_col = match Self::primary_key_column() {
4768                    ::core::option::Option::Some(c) => c,
4769                    ::core::option::Option::None => {
4770                        return ::core::result::Result::Ok(());
4771                    }
4772                };
4773                let mut offset: i64 = 0;
4774                loop {
4775                    let rows: ::std::vec::Vec<Self> =
4776                        #root::query::QuerySet::<Self>::default()
4777                            .order_by(&[(pk_col, false)])
4778                            .limit(n)
4779                            .offset(offset)
4780                            .fetch(pool)
4781                            .await?;
4782                    if rows.is_empty() {
4783                        return ::core::result::Result::Ok(());
4784                    }
4785                    let len = rows.len() as i64;
4786                    cb(rows).await?;
4787                    if len < n {
4788                        return ::core::result::Result::Ok(());
4789                    }
4790                    offset += n;
4791                }
4792            }
4793
4794            /// Eloquent `Model::chunkById($n, fn (...))` — same
4795            /// per-batch callback shape as [`Self::chunk`], but uses
4796            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4797            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4798            /// — the right choice for multi-million-row sweeps.
4799            ///
4800            /// Requires the primary key to be a signed integer type
4801            /// (`i64` / `i32`); the keyset comparison rides on
4802            /// `__rustango_pk_value()` lowering through
4803            /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4804            /// non-integer PKs (UUID, String) — those should use
4805            /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4806            /// keyset loop.
4807            ///
4808            /// Callback errors abort iteration; the error bubbles up
4809            /// unchanged. Empty table → callback invoked zero times.
4810            pub async fn chunk_by_id<F, Fut>(
4811                n: i64,
4812                pool: &#root::sql::Pool,
4813                mut cb: F,
4814            ) -> ::core::result::Result<(), #root::sql::ExecError>
4815            where
4816                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4817                Fut: ::core::future::Future<
4818                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4819                >,
4820            {
4821                use #root::sql::FetcherPool as _;
4822                let pk_col = match Self::primary_key_column() {
4823                    ::core::option::Option::Some(c) => c,
4824                    ::core::option::Option::None => {
4825                        return ::core::result::Result::Ok(());
4826                    }
4827                };
4828                // Track the largest PK seen so the next batch picks
4829                // up from there. `i64::MIN` as the sentinel — the
4830                // very first iteration's `> MIN` matches every row,
4831                // so the loop entry is uniform with subsequent
4832                // iterations.
4833                let mut last_seen: i64 = i64::MIN;
4834                loop {
4835                    let key = ::std::format!("{}__gt", pk_col);
4836                    let rows: ::std::vec::Vec<Self> =
4837                        #root::query::QuerySet::<Self>::default()
4838                            .filter(key.as_str(), last_seen)
4839                            .order_by(&[(pk_col, false)])
4840                            .limit(n)
4841                            .fetch(pool)
4842                            .await?;
4843                    if rows.is_empty() {
4844                        return ::core::result::Result::Ok(());
4845                    }
4846                    let len = rows.len() as i64;
4847                    // Capture the last row's PK BEFORE moving rows
4848                    // into the callback.
4849                    let max_pk = match rows
4850                        .last()
4851                        .map(|r| r.__rustango_pk_value())
4852                    {
4853                        ::core::option::Option::Some(
4854                            #root::core::SqlValue::I64(v),
4855                        ) => v,
4856                        ::core::option::Option::Some(
4857                            #root::core::SqlValue::I32(v),
4858                        ) => i64::from(v),
4859                        _ => return ::core::result::Result::Ok(()),
4860                    };
4861                    cb(rows).await?;
4862                    if len < n {
4863                        return ::core::result::Result::Ok(());
4864                    }
4865                    last_seen = max_pk;
4866                }
4867            }
4868
4869            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4870            /// per-row callback companion to [`Self::chunk`].
4871            /// Streams every row in keyset-paginated batches of
4872            /// `batch` size, calling `cb` once per row.
4873            ///
4874            /// Inherits the keyset-paginated scan of
4875            /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4876            /// non-integer PKs are silently a no-op).
4877            ///
4878            /// ```ignore
4879            /// Post::each(500, &pool, |p| async move {
4880            ///     reindex(p).await?;
4881            ///     Ok(())
4882            /// }).await?;
4883            /// ```
4884            ///
4885            /// Return `Err(...)` from the callback to abort
4886            /// iteration; the error bubbles up unchanged.
4887            pub async fn each<F, Fut>(
4888                batch: i64,
4889                pool: &#root::sql::Pool,
4890                mut cb: F,
4891            ) -> ::core::result::Result<(), #root::sql::ExecError>
4892            where
4893                F: ::core::ops::FnMut(Self) -> Fut,
4894                Fut: ::core::future::Future<
4895                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4896                >,
4897            {
4898                use #root::sql::FetcherPool as _;
4899                let pk_col = match Self::primary_key_column() {
4900                    ::core::option::Option::Some(c) => c,
4901                    ::core::option::Option::None => {
4902                        return ::core::result::Result::Ok(());
4903                    }
4904                };
4905                let mut last_seen: i64 = i64::MIN;
4906                loop {
4907                    let key = ::std::format!("{}__gt", pk_col);
4908                    let rows: ::std::vec::Vec<Self> =
4909                        #root::query::QuerySet::<Self>::default()
4910                            .filter(key.as_str(), last_seen)
4911                            .order_by(&[(pk_col, false)])
4912                            .limit(batch)
4913                            .fetch(pool)
4914                            .await?;
4915                    if rows.is_empty() {
4916                        return ::core::result::Result::Ok(());
4917                    }
4918                    let len = rows.len() as i64;
4919                    let max_pk = match rows
4920                        .last()
4921                        .map(|r| r.__rustango_pk_value())
4922                    {
4923                        ::core::option::Option::Some(
4924                            #root::core::SqlValue::I64(v),
4925                        ) => v,
4926                        ::core::option::Option::Some(
4927                            #root::core::SqlValue::I32(v),
4928                        ) => i64::from(v),
4929                        _ => return ::core::result::Result::Ok(()),
4930                    };
4931                    for row in rows {
4932                        cb(row).await?;
4933                    }
4934                    if len < batch {
4935                        return ::core::result::Result::Ok(());
4936                    }
4937                    last_seen = max_pk;
4938                }
4939            }
4940
4941            /// Delete every row of this model — `TRUNCATE TABLE
4942            /// <table> RESTART IDENTITY CASCADE` on Postgres,
4943            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4944            /// support `TRUNCATE` inside foreign-key constraints
4945            /// or — for SQLite — at all). Eloquent `Model::truncate()`
4946            /// parity.
4947            ///
4948            /// **Use only in tests / fixture-reset flows.** Production
4949            /// writes through this would silently bypass the
4950            /// `pre_delete` / `post_delete` signals (no per-row hooks
4951            /// fire on a TRUNCATE / bulk DELETE FROM). An audited model
4952            /// gets one bulk audit entry, not one per row.
4953            ///
4954            /// # Errors
4955            /// As [`raw_execute_pool`].
4956            ///
4957            /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4958            pub async fn truncate(
4959                pool: &#root::sql::Pool,
4960            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4961                let _table = <Self as #root::core::Model>::SCHEMA.table;
4962                let _dialect = pool.dialect();
4963                let _quoted = _dialect.quote_ident(_table);
4964                let _sql = if _dialect.name() == "postgres" {
4965                    ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4966                } else {
4967                    ::std::format!("DELETE FROM {}", _quoted)
4968                };
4969                #truncate_run
4970            }
4971
4972            /// Bulk-delete every row whose primary key is in
4973            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4974            /// Returns the affected row count.
4975            ///
4976            /// Eloquent `Model::destroy([1, 2, 3])` parity.
4977            /// Empty `pks` is a no-op (returns 0).
4978            ///
4979            /// Accepts any iterable whose elements are
4980            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4981            /// `[i64; N]`, etc.
4982            ///
4983            /// # Errors
4984            /// As `delete_pool`.
4985            pub async fn destroy<V>(
4986                pks: impl ::core::iter::IntoIterator<Item = V>,
4987                pool: &#root::sql::Pool,
4988            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4989            where
4990                V: ::core::convert::Into<#root::core::SqlValue>,
4991            {
4992                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4993                    pks.into_iter().map(::core::convert::Into::into).collect();
4994                if _values.is_empty() {
4995                    return ::core::result::Result::Ok(0);
4996                }
4997                let _pk = <Self as #root::core::Model>::SCHEMA
4998                    .primary_key()
4999                    .ok_or_else(|| {
5000                        #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
5001                    })?;
5002                let _query = #root::query::QuerySet::<Self>::default()
5003                    .filter_op(
5004                        _pk.name,
5005                        #root::core::Op::In,
5006                        #root::core::SqlValue::List(_values),
5007                    )
5008                    .compile_delete()?;
5009                #bulk_delete_run
5010            }
5011
5012            /// Fetch every row where `<col> = <val>`. Eloquent
5013            /// `Model::where($col, $val)->get()` parity.
5014            ///
5015            /// Thin wrapper over `QuerySet::<Self>::default()
5016            /// .filter(col, val).fetch(pool)`. For one row,
5017            /// use [`Self::first_where`]; for a chain that
5018            /// needs further `.filter()` / `.order_by()` /
5019            /// `.limit()`, drop down to `Self::query().filter(...)`
5020            /// directly.
5021            ///
5022            /// `val` accepts any value `Into<SqlValue>` so plain
5023            /// strings, ints, UUIDs, etc. all work.
5024            ///
5025            /// # Errors
5026            /// As [`FetcherPool::fetch`].
5027            ///
5028            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5029            pub async fn where_(
5030                col: &str,
5031                val: impl ::core::convert::Into<#root::core::SqlValue>,
5032                pool: &#root::sql::Pool,
5033            ) -> ::core::result::Result<
5034                ::std::vec::Vec<Self>,
5035                #root::sql::ExecError,
5036            > {
5037                use #root::sql::FetcherPool as _;
5038                #root::query::QuerySet::<Self>::default()
5039                    .filter(col, val)
5040                    .fetch(pool)
5041                    .await
5042            }
5043
5044            /// Fetch every row where `<col> IN (vals)`. Eloquent
5045            /// `Model::whereIn($col, $vals)->get()` parity. Empty
5046            /// `vals` returns no rows (matches SQL's empty-IN
5047            /// semantics).
5048            ///
5049            /// `vals` accepts any iterable whose items are
5050            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5051            /// etc.
5052            ///
5053            /// # Errors
5054            /// As [`FetcherPool::fetch`].
5055            ///
5056            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5057            pub async fn where_in<V>(
5058                col: &str,
5059                vals: impl ::core::iter::IntoIterator<Item = V>,
5060                pool: &#root::sql::Pool,
5061            ) -> ::core::result::Result<
5062                ::std::vec::Vec<Self>,
5063                #root::sql::ExecError,
5064            >
5065            where
5066                V: ::core::convert::Into<#root::core::SqlValue>,
5067            {
5068                use #root::sql::FetcherPool as _;
5069                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5070                    vals.into_iter().map(::core::convert::Into::into).collect();
5071                if _values.is_empty() {
5072                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5073                }
5074                let _key = ::std::format!("{}__in", col);
5075                #root::query::QuerySet::<Self>::default()
5076                    .filter(&_key, #root::core::SqlValue::List(_values))
5077                    .fetch(pool)
5078                    .await
5079            }
5080
5081            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5082            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5083            /// `vals` returns every row (matches SQL's empty-NOT-IN
5084            /// semantics — vacuously true for every row).
5085            ///
5086            /// # Errors
5087            /// As [`FetcherPool::fetch`].
5088            ///
5089            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5090            pub async fn where_not_in<V>(
5091                col: &str,
5092                vals: impl ::core::iter::IntoIterator<Item = V>,
5093                pool: &#root::sql::Pool,
5094            ) -> ::core::result::Result<
5095                ::std::vec::Vec<Self>,
5096                #root::sql::ExecError,
5097            >
5098            where
5099                V: ::core::convert::Into<#root::core::SqlValue>,
5100            {
5101                use #root::sql::FetcherPool as _;
5102                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5103                    vals.into_iter().map(::core::convert::Into::into).collect();
5104                if _values.is_empty() {
5105                    return #root::query::QuerySet::<Self>::default()
5106                        .fetch(pool)
5107                        .await;
5108                }
5109                let _key = ::std::format!("{}__not_in", col);
5110                #root::query::QuerySet::<Self>::default()
5111                    .filter(&_key, #root::core::SqlValue::List(_values))
5112                    .fetch(pool)
5113                    .await
5114            }
5115
5116            /// Fetch every row where `<col> IS NULL`. Eloquent
5117            /// `Model::whereNull($col)->get()` parity.
5118            ///
5119            /// # Errors
5120            /// As [`FetcherPool::fetch`].
5121            ///
5122            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5123            pub async fn where_null(
5124                col: &str,
5125                pool: &#root::sql::Pool,
5126            ) -> ::core::result::Result<
5127                ::std::vec::Vec<Self>,
5128                #root::sql::ExecError,
5129            > {
5130                use #root::sql::FetcherPool as _;
5131                let _key = ::std::format!("{}__isnull", col);
5132                #root::query::QuerySet::<Self>::default()
5133                    .filter(&_key, #root::core::SqlValue::Bool(true))
5134                    .fetch(pool)
5135                    .await
5136            }
5137
5138            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5139            /// `Model::whereNotNull($col)->get()` parity.
5140            ///
5141            /// # Errors
5142            /// As [`FetcherPool::fetch`].
5143            ///
5144            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5145            pub async fn where_not_null(
5146                col: &str,
5147                pool: &#root::sql::Pool,
5148            ) -> ::core::result::Result<
5149                ::std::vec::Vec<Self>,
5150                #root::sql::ExecError,
5151            > {
5152                use #root::sql::FetcherPool as _;
5153                let _key = ::std::format!("{}__isnull", col);
5154                #root::query::QuerySet::<Self>::default()
5155                    .filter(&_key, #root::core::SqlValue::Bool(false))
5156                    .fetch(pool)
5157                    .await
5158            }
5159
5160            /// Fetch up to `n` rows in random order. Eloquent
5161            /// `Model::inRandomOrder()->limit($n)->get()` /
5162            /// `Model::query()->inRandomOrder()->get()->take($n)`
5163            /// parity. **Performance caveat**: random ordering
5164            /// forces a full table scan + per-row random key sort;
5165            /// the optimizer cannot use an index. Prefer a
5166            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5167            ///
5168            /// # Errors
5169            /// As [`FetcherPool::fetch`].
5170            ///
5171            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5172            pub async fn random_n(
5173                n: i64,
5174                pool: &#root::sql::Pool,
5175            ) -> ::core::result::Result<
5176                ::std::vec::Vec<Self>,
5177                #root::sql::ExecError,
5178            > {
5179                use #root::sql::FetcherPool as _;
5180                #root::query::QuerySet::<Self>::default()
5181                    .order_random()
5182                    .limit(n)
5183                    .fetch(pool)
5184                    .await
5185            }
5186
5187            /// Fetch one row in random order. Eloquent
5188            /// `Model::inRandomOrder()->first()` parity. Same
5189            /// performance caveat as [`Self::random_n`].
5190            ///
5191            /// # Errors
5192            /// As [`FetcherPool::fetch`].
5193            ///
5194            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5195            pub async fn random(
5196                pool: &#root::sql::Pool,
5197            ) -> ::core::result::Result<
5198                ::core::option::Option<Self>,
5199                #root::sql::ExecError,
5200            > {
5201                ::core::result::Result::Ok(
5202                    Self::random_n(1, pool).await?.into_iter().next(),
5203                )
5204            }
5205
5206            /// Fetch every row ordered ASC by `field`. Eloquent
5207            /// `Model::oldest($field)->get()` parity — the multi-row
5208            /// counterpart of [`Self::earliest`].
5209            ///
5210            /// # Errors
5211            /// As [`FetcherPool::fetch`].
5212            ///
5213            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5214            pub async fn oldest(
5215                field: &str,
5216                pool: &#root::sql::Pool,
5217            ) -> ::core::result::Result<
5218                ::std::vec::Vec<Self>,
5219                #root::sql::ExecError,
5220            > {
5221                use #root::sql::FetcherPool as _;
5222                #root::query::QuerySet::<Self>::default()
5223                    .order_by(&[(field, false)])
5224                    .fetch(pool)
5225                    .await
5226            }
5227
5228            /// Fetch every row ordered DESC by `field`. Eloquent
5229            /// `Model::latest($field)->get()` parity — the multi-row
5230            /// counterpart of [`Self::latest`].
5231            ///
5232            /// # Errors
5233            /// As [`FetcherPool::fetch`].
5234            ///
5235            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5236            pub async fn newest(
5237                field: &str,
5238                pool: &#root::sql::Pool,
5239            ) -> ::core::result::Result<
5240                ::std::vec::Vec<Self>,
5241                #root::sql::ExecError,
5242            > {
5243                use #root::sql::FetcherPool as _;
5244                #root::query::QuerySet::<Self>::default()
5245                    .order_by(&[(field, true)])
5246                    .fetch(pool)
5247                    .await
5248            }
5249
5250            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5251            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5252            /// Routes through the existing `__year` lookup suffix
5253            /// (issue #829).
5254            ///
5255            /// # Errors
5256            /// As [`FetcherPool::fetch`].
5257            ///
5258            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5259            pub async fn where_year(
5260                col: &str,
5261                year: i64,
5262                pool: &#root::sql::Pool,
5263            ) -> ::core::result::Result<
5264                ::std::vec::Vec<Self>,
5265                #root::sql::ExecError,
5266            > {
5267                use #root::sql::FetcherPool as _;
5268                let _key = ::std::format!("{}__year", col);
5269                #root::query::QuerySet::<Self>::default()
5270                    .filter(&_key, #root::core::SqlValue::I64(year))
5271                    .fetch(pool)
5272                    .await
5273            }
5274
5275            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5276            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5277            /// `month` is 1–12.
5278            ///
5279            /// # Errors
5280            /// As [`FetcherPool::fetch`].
5281            ///
5282            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5283            pub async fn where_month(
5284                col: &str,
5285                month: i64,
5286                pool: &#root::sql::Pool,
5287            ) -> ::core::result::Result<
5288                ::std::vec::Vec<Self>,
5289                #root::sql::ExecError,
5290            > {
5291                use #root::sql::FetcherPool as _;
5292                let _key = ::std::format!("{}__month", col);
5293                #root::query::QuerySet::<Self>::default()
5294                    .filter(&_key, #root::core::SqlValue::I64(month))
5295                    .fetch(pool)
5296                    .await
5297            }
5298
5299            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5300            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5301            /// `day` is 1–31.
5302            ///
5303            /// # Errors
5304            /// As [`FetcherPool::fetch`].
5305            ///
5306            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5307            pub async fn where_day(
5308                col: &str,
5309                day: i64,
5310                pool: &#root::sql::Pool,
5311            ) -> ::core::result::Result<
5312                ::std::vec::Vec<Self>,
5313                #root::sql::ExecError,
5314            > {
5315                use #root::sql::FetcherPool as _;
5316                let _key = ::std::format!("{}__day", col);
5317                #root::query::QuerySet::<Self>::default()
5318                    .filter(&_key, #root::core::SqlValue::I64(day))
5319                    .fetch(pool)
5320                    .await
5321            }
5322
5323            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5324            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5325            /// `hour` is 0–23.
5326            ///
5327            /// # Errors
5328            /// As [`FetcherPool::fetch`].
5329            ///
5330            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5331            pub async fn where_hour(
5332                col: &str,
5333                hour: i64,
5334                pool: &#root::sql::Pool,
5335            ) -> ::core::result::Result<
5336                ::std::vec::Vec<Self>,
5337                #root::sql::ExecError,
5338            > {
5339                use #root::sql::FetcherPool as _;
5340                let _key = ::std::format!("{}__hour", col);
5341                #root::query::QuerySet::<Self>::default()
5342                    .filter(&_key, #root::core::SqlValue::I64(hour))
5343                    .fetch(pool)
5344                    .await
5345            }
5346
5347            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5348            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5349            /// `minute` is 0–59.
5350            ///
5351            /// # Errors
5352            /// As [`FetcherPool::fetch`].
5353            ///
5354            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5355            pub async fn where_minute(
5356                col: &str,
5357                minute: i64,
5358                pool: &#root::sql::Pool,
5359            ) -> ::core::result::Result<
5360                ::std::vec::Vec<Self>,
5361                #root::sql::ExecError,
5362            > {
5363                use #root::sql::FetcherPool as _;
5364                let _key = ::std::format!("{}__minute", col);
5365                #root::query::QuerySet::<Self>::default()
5366                    .filter(&_key, #root::core::SqlValue::I64(minute))
5367                    .fetch(pool)
5368                    .await
5369            }
5370
5371            /// Fetch every row where `<col> LIKE <pattern>` —
5372            /// caller-supplied pattern (must include `%` / `_`
5373            /// wildcards manually). Eloquent `Model::whereLike`
5374            /// parity.
5375            ///
5376            /// # Errors
5377            /// As [`FetcherPool::fetch`].
5378            ///
5379            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5380            pub async fn where_like(
5381                col: &str,
5382                pattern: impl ::core::convert::Into<::std::string::String>,
5383                pool: &#root::sql::Pool,
5384            ) -> ::core::result::Result<
5385                ::std::vec::Vec<Self>,
5386                #root::sql::ExecError,
5387            > {
5388                use #root::sql::FetcherPool as _;
5389                let _key = ::std::format!("{}__like", col);
5390                #root::query::QuerySet::<Self>::default()
5391                    .filter(
5392                        &_key,
5393                        #root::core::SqlValue::String(pattern.into()),
5394                    )
5395                    .fetch(pool)
5396                    .await
5397            }
5398
5399            /// Fetch every row where `<col> ILIKE <pattern>` —
5400            /// case-insensitive LIKE (PG native, MySQL/SQLite
5401            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5402            ///
5403            /// # Errors
5404            /// As [`FetcherPool::fetch`].
5405            ///
5406            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5407            pub async fn where_ilike(
5408                col: &str,
5409                pattern: impl ::core::convert::Into<::std::string::String>,
5410                pool: &#root::sql::Pool,
5411            ) -> ::core::result::Result<
5412                ::std::vec::Vec<Self>,
5413                #root::sql::ExecError,
5414            > {
5415                use #root::sql::FetcherPool as _;
5416                let _key = ::std::format!("{}__ilike", col);
5417                #root::query::QuerySet::<Self>::default()
5418                    .filter(
5419                        &_key,
5420                        #root::core::SqlValue::String(pattern.into()),
5421                    )
5422                    .fetch(pool)
5423                    .await
5424            }
5425
5426            /// Fetch every row where `<col>` starts with `prefix`
5427            /// (auto-appends `%`). Eloquent
5428            /// `whereLike("col", "$prefix%")` parity.
5429            ///
5430            /// # Errors
5431            /// As [`FetcherPool::fetch`].
5432            ///
5433            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5434            pub async fn where_starts_with(
5435                col: &str,
5436                prefix: impl ::core::convert::Into<::std::string::String>,
5437                pool: &#root::sql::Pool,
5438            ) -> ::core::result::Result<
5439                ::std::vec::Vec<Self>,
5440                #root::sql::ExecError,
5441            > {
5442                use #root::sql::FetcherPool as _;
5443                let _key = ::std::format!("{}__startswith", col);
5444                #root::query::QuerySet::<Self>::default()
5445                    .filter(
5446                        &_key,
5447                        #root::core::SqlValue::String(prefix.into()),
5448                    )
5449                    .fetch(pool)
5450                    .await
5451            }
5452
5453            /// Fetch every row where `<col>` ends with `suffix`
5454            /// (auto-prepends `%`). Eloquent
5455            /// `whereLike("col", "%$suffix")` parity.
5456            ///
5457            /// # Errors
5458            /// As [`FetcherPool::fetch`].
5459            ///
5460            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5461            pub async fn where_ends_with(
5462                col: &str,
5463                suffix: impl ::core::convert::Into<::std::string::String>,
5464                pool: &#root::sql::Pool,
5465            ) -> ::core::result::Result<
5466                ::std::vec::Vec<Self>,
5467                #root::sql::ExecError,
5468            > {
5469                use #root::sql::FetcherPool as _;
5470                let _key = ::std::format!("{}__endswith", col);
5471                #root::query::QuerySet::<Self>::default()
5472                    .filter(
5473                        &_key,
5474                        #root::core::SqlValue::String(suffix.into()),
5475                    )
5476                    .fetch(pool)
5477                    .await
5478            }
5479
5480            /// Fetch every row where `<col>` contains `substr`
5481            /// (auto-wraps with `%`).
5482            /// Eloquent `whereLike("col", "%$substr%")` parity.
5483            ///
5484            /// # Errors
5485            /// As [`FetcherPool::fetch`].
5486            ///
5487            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5488            pub async fn where_contains(
5489                col: &str,
5490                substr: impl ::core::convert::Into<::std::string::String>,
5491                pool: &#root::sql::Pool,
5492            ) -> ::core::result::Result<
5493                ::std::vec::Vec<Self>,
5494                #root::sql::ExecError,
5495            > {
5496                use #root::sql::FetcherPool as _;
5497                let _key = ::std::format!("{}__contains", col);
5498                #root::query::QuerySet::<Self>::default()
5499                    .filter(
5500                        &_key,
5501                        #root::core::SqlValue::String(substr.into()),
5502                    )
5503                    .fetch(pool)
5504                    .await
5505            }
5506
5507            /// Fetch every row where `<col> > val`. Eloquent
5508            /// `Model::where($col, ">", $val)->get()` parity.
5509            ///
5510            /// # Errors
5511            /// As [`FetcherPool::fetch`].
5512            ///
5513            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5514            pub async fn where_gt(
5515                col: &str,
5516                val: impl ::core::convert::Into<#root::core::SqlValue>,
5517                pool: &#root::sql::Pool,
5518            ) -> ::core::result::Result<
5519                ::std::vec::Vec<Self>,
5520                #root::sql::ExecError,
5521            > {
5522                use #root::sql::FetcherPool as _;
5523                let _key = ::std::format!("{}__gt", col);
5524                #root::query::QuerySet::<Self>::default()
5525                    .filter(&_key, val)
5526                    .fetch(pool)
5527                    .await
5528            }
5529
5530            /// Fetch every row where `<col> >= val`. Eloquent
5531            /// `Model::where($col, ">=", $val)->get()` parity.
5532            ///
5533            /// # Errors
5534            /// As [`FetcherPool::fetch`].
5535            ///
5536            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5537            pub async fn where_gte(
5538                col: &str,
5539                val: impl ::core::convert::Into<#root::core::SqlValue>,
5540                pool: &#root::sql::Pool,
5541            ) -> ::core::result::Result<
5542                ::std::vec::Vec<Self>,
5543                #root::sql::ExecError,
5544            > {
5545                use #root::sql::FetcherPool as _;
5546                let _key = ::std::format!("{}__gte", col);
5547                #root::query::QuerySet::<Self>::default()
5548                    .filter(&_key, val)
5549                    .fetch(pool)
5550                    .await
5551            }
5552
5553            /// Fetch every row where `<col> < val`. Eloquent
5554            /// `Model::where($col, "<", $val)->get()` parity.
5555            ///
5556            /// # Errors
5557            /// As [`FetcherPool::fetch`].
5558            ///
5559            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5560            pub async fn where_lt(
5561                col: &str,
5562                val: impl ::core::convert::Into<#root::core::SqlValue>,
5563                pool: &#root::sql::Pool,
5564            ) -> ::core::result::Result<
5565                ::std::vec::Vec<Self>,
5566                #root::sql::ExecError,
5567            > {
5568                use #root::sql::FetcherPool as _;
5569                let _key = ::std::format!("{}__lt", col);
5570                #root::query::QuerySet::<Self>::default()
5571                    .filter(&_key, val)
5572                    .fetch(pool)
5573                    .await
5574            }
5575
5576            /// Fetch every row where `<col> <= val`. Eloquent
5577            /// `Model::where($col, "<=", $val)->get()` parity.
5578            ///
5579            /// # Errors
5580            /// As [`FetcherPool::fetch`].
5581            ///
5582            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5583            pub async fn where_lte(
5584                col: &str,
5585                val: impl ::core::convert::Into<#root::core::SqlValue>,
5586                pool: &#root::sql::Pool,
5587            ) -> ::core::result::Result<
5588                ::std::vec::Vec<Self>,
5589                #root::sql::ExecError,
5590            > {
5591                use #root::sql::FetcherPool as _;
5592                let _key = ::std::format!("{}__lte", col);
5593                #root::query::QuerySet::<Self>::default()
5594                    .filter(&_key, val)
5595                    .fetch(pool)
5596                    .await
5597            }
5598
5599            /// Fetch every row where `<col> <> val`. Eloquent
5600            /// `Model::where($col, "!=", $val)->get()` parity.
5601            ///
5602            /// # Errors
5603            /// As [`FetcherPool::fetch`].
5604            ///
5605            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5606            pub async fn where_ne(
5607                col: &str,
5608                val: impl ::core::convert::Into<#root::core::SqlValue>,
5609                pool: &#root::sql::Pool,
5610            ) -> ::core::result::Result<
5611                ::std::vec::Vec<Self>,
5612                #root::sql::ExecError,
5613            > {
5614                use #root::sql::FetcherPool as _;
5615                let _key = ::std::format!("{}__ne", col);
5616                #root::query::QuerySet::<Self>::default()
5617                    .filter(&_key, val)
5618                    .fetch(pool)
5619                    .await
5620            }
5621
5622            /// Fetch every row matching `<col> = val` for ANY of the
5623            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5624            /// parity. Empty `cols` returns no rows.
5625            ///
5626            /// Resolves each `&str` column to its SCHEMA-registered
5627            /// `&'static str` once and builds a single OR-composed
5628            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5629            ///
5630            /// # Errors
5631            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5632            /// when any column is not declared on the model.
5633            ///
5634            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5635            pub async fn where_any(
5636                cols: &[&str],
5637                val: impl ::core::convert::Into<#root::core::SqlValue>,
5638                pool: &#root::sql::Pool,
5639            ) -> ::core::result::Result<
5640                ::std::vec::Vec<Self>,
5641                #root::sql::ExecError,
5642            > {
5643                Self::__where_multi(cols, val, false, pool).await
5644            }
5645
5646            /// Fetch every row matching `<col> = val` for ALL listed
5647            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5648            /// Empty `cols` returns every row (vacuous AND).
5649            ///
5650            /// # Errors
5651            /// As [`Self::where_any`].
5652            pub async fn where_all(
5653                cols: &[&str],
5654                val: impl ::core::convert::Into<#root::core::SqlValue>,
5655                pool: &#root::sql::Pool,
5656            ) -> ::core::result::Result<
5657                ::std::vec::Vec<Self>,
5658                #root::sql::ExecError,
5659            > {
5660                Self::__where_multi(cols, val, true, pool).await
5661            }
5662
5663            /// Internal: build a Q expression composing `cols` via
5664            /// AND (`all`) or OR (`!all`), then fetch. Backs
5665            /// `where_any` / `where_all`.
5666            #[doc(hidden)]
5667            pub async fn __where_multi(
5668                cols: &[&str],
5669                val: impl ::core::convert::Into<#root::core::SqlValue>,
5670                all: bool,
5671                pool: &#root::sql::Pool,
5672            ) -> ::core::result::Result<
5673                ::std::vec::Vec<Self>,
5674                #root::sql::ExecError,
5675            > {
5676                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5677                    .await
5678            }
5679
5680            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5681            /// parity. PK-ordered
5682            /// is NOT guaranteed without an explicit `order_by` —
5683            /// drop into `Self::query()` for that.
5684            ///
5685            /// # Errors
5686            /// As [`FetcherPool::fetch`].
5687            ///
5688            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5689            pub async fn take(
5690                n: i64,
5691                pool: &#root::sql::Pool,
5692            ) -> ::core::result::Result<
5693                ::std::vec::Vec<Self>,
5694                #root::sql::ExecError,
5695            > {
5696                use #root::sql::FetcherPool as _;
5697                #root::query::QuerySet::<Self>::default()
5698                    .limit(n)
5699                    .fetch(pool)
5700                    .await
5701            }
5702
5703            /// Fetch the page-th window of `per_page` rows
5704            /// (1-indexed). Eloquent
5705            /// `Model::query()->forPage($page, $perPage)->get()`
5706            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5707            /// LIMIT per_page`; for large offsets this is O(N) —
5708            /// prefer keyset pagination via PK on hot paths.
5709            ///
5710            /// # Errors
5711            /// As [`FetcherPool::fetch`].
5712            ///
5713            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5714            pub async fn for_page(
5715                page: i64,
5716                per_page: i64,
5717                pool: &#root::sql::Pool,
5718            ) -> ::core::result::Result<
5719                ::std::vec::Vec<Self>,
5720                #root::sql::ExecError,
5721            > {
5722                use #root::sql::FetcherPool as _;
5723                let _offset = #root::list_params::page_offset(page, per_page);
5724                #root::query::QuerySet::<Self>::default()
5725                    .limit(per_page)
5726                    .offset(_offset)
5727                    .fetch(pool)
5728                    .await
5729            }
5730
5731            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5732            /// one page of rows AND the total row count in one
5733            /// call. Returns `(rows, total)`. Two queries: a
5734            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5735            /// for the total.
5736            ///
5737            /// Useful for paginated UIs that need both the visible
5738            /// rows AND a "Page X of Y" / total-count widget. For
5739            /// large tables prefer keyset pagination + cached count;
5740            /// every call to `paginate` re-counts the full table.
5741            ///
5742            /// Same 1-indexed `page` convention as [`Self::for_page`].
5743            ///
5744            /// # Errors
5745            /// As [`Self::for_page`] and [`Self::count`].
5746            pub async fn paginate(
5747                page: i64,
5748                per_page: i64,
5749                pool: &#root::sql::Pool,
5750            ) -> ::core::result::Result<
5751                (::std::vec::Vec<Self>, i64),
5752                #root::sql::ExecError,
5753            > {
5754                // Route through the queryset rather than `Self::count`:
5755                // the `count` inherent method is suppressed on models
5756                // that declare a field named `count` (field/shortcut
5757                // collision guard), and `paginate` must still compile
5758                // for those models.
5759                let total = {
5760                    use #root::sql::CounterPool as _;
5761                    #root::query::QuerySet::<Self>::default()
5762                        .count(pool)
5763                        .await?
5764                };
5765                let rows = Self::for_page(page, per_page, pool).await?;
5766                ::core::result::Result::Ok((rows, total))
5767            }
5768
5769            /// Bulk-update — set `set_col = set_val` on every row
5770            /// matching `where_col = where_val`. Returns affected row
5771            /// count. Eloquent
5772            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5773            /// parity.
5774            ///
5775            /// For multi-column updates drop into the queryset
5776            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5777            ///
5778            /// # Errors
5779            /// As [`UpdaterPool::execute_pool`].
5780            ///
5781            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5782            pub async fn update_where(
5783                where_col: &str,
5784                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5785                set_col: &str,
5786                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5787                pool: &#root::sql::Pool,
5788            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5789                let _query = #root::query::QuerySet::<Self>::default()
5790                    .filter(where_col, where_val)
5791                    .update()
5792                    .set(set_col, set_val)
5793                    .compile()?;
5794                #bulk_update_run
5795            }
5796
5797            /// Bulk-delete — remove every row matching
5798            /// `where_col = where_val`. Returns affected row count.
5799            /// Eloquent
5800            /// `Model::where($where_col, $where_val)->delete()` parity.
5801            ///
5802            /// # Errors
5803            /// As [`#root::sql::delete_pool`].
5804            pub async fn delete_where(
5805                where_col: &str,
5806                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5807                pool: &#root::sql::Pool,
5808            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5809                let _query = #root::query::QuerySet::<Self>::default()
5810                    .filter_op(where_col, #root::core::Op::Eq, where_val)
5811                    .compile_delete()?;
5812                #bulk_delete_run
5813            }
5814
5815            /// Bulk-update — set `set_col = set_val` on EVERY row of
5816            /// this model's table. **No WHERE clause** — use with
5817            /// care. Eloquent `Model::query()->update([$col => $val])`
5818            /// parity.
5819            ///
5820            /// Use for backfills, one-shot reset flows, etc. The
5821            /// returned count is rows affected.
5822            ///
5823            /// # Errors
5824            /// As [`UpdaterPool::execute_pool`].
5825            ///
5826            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5827            pub async fn update_all(
5828                set_col: &str,
5829                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5830                pool: &#root::sql::Pool,
5831            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5832                let _query = #root::query::QuerySet::<Self>::default()
5833                    .update()
5834                    .set(set_col, set_val)
5835                    .compile()?;
5836                #bulk_update_run
5837            }
5838
5839            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5840            /// Eloquent `Model::whereNotLike` parity. Pattern is
5841            /// passed verbatim — caller controls `%` / `_`.
5842            ///
5843            /// # Errors
5844            /// As [`FetcherPool::fetch`].
5845            ///
5846            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5847            pub async fn where_not_like(
5848                col: &str,
5849                pattern: impl ::core::convert::Into<::std::string::String>,
5850                pool: &#root::sql::Pool,
5851            ) -> ::core::result::Result<
5852                ::std::vec::Vec<Self>,
5853                #root::sql::ExecError,
5854            > {
5855                use #root::sql::FetcherPool as _;
5856                let _key = ::std::format!("{}__not_like", col);
5857                #root::query::QuerySet::<Self>::default()
5858                    .filter(
5859                        &_key,
5860                        #root::core::SqlValue::String(pattern.into()),
5861                    )
5862                    .fetch(pool)
5863                    .await
5864            }
5865
5866            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5867            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5868            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5869            ///
5870            /// # Errors
5871            /// As [`FetcherPool::fetch`].
5872            ///
5873            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5874            pub async fn where_not_ilike(
5875                col: &str,
5876                pattern: impl ::core::convert::Into<::std::string::String>,
5877                pool: &#root::sql::Pool,
5878            ) -> ::core::result::Result<
5879                ::std::vec::Vec<Self>,
5880                #root::sql::ExecError,
5881            > {
5882                use #root::sql::FetcherPool as _;
5883                let _key = ::std::format!("{}__not_ilike", col);
5884                #root::query::QuerySet::<Self>::default()
5885                    .filter(
5886                        &_key,
5887                        #root::core::SqlValue::String(pattern.into()),
5888                    )
5889                    .fetch(pool)
5890                    .await
5891            }
5892
5893            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5894            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5895            /// parity.
5896            ///
5897            /// # Errors
5898            /// As [`FetcherPool::fetch`].
5899            ///
5900            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5901            pub async fn where_not_between(
5902                col: &str,
5903                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5904                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5905                pool: &#root::sql::Pool,
5906            ) -> ::core::result::Result<
5907                ::std::vec::Vec<Self>,
5908                #root::sql::ExecError,
5909            > {
5910                use #root::sql::FetcherPool as _;
5911                let _key = ::std::format!("{}__not_between", col);
5912                let _vals = #root::core::SqlValue::List(::std::vec![
5913                    ::core::convert::Into::into(lo),
5914                    ::core::convert::Into::into(hi),
5915                ]);
5916                #root::query::QuerySet::<Self>::default()
5917                    .filter(&_key, _vals)
5918                    .fetch(pool)
5919                    .await
5920            }
5921
5922            /// Returns the SQL table name for this model. Eloquent
5923            /// `$model->getTable()` parity.
5924            #[must_use]
5925            pub fn table_name() -> &'static str {
5926                <Self as #root::core::Model>::SCHEMA.table
5927            }
5928
5929            /// Returns the SQL column name of this model's primary
5930            /// key, or `None` when the model has no
5931            /// `#[rustango(primary_key)]`. Eloquent
5932            /// `$model->getKeyName()` parity.
5933            #[must_use]
5934            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5935                <Self as #root::core::Model>::SCHEMA
5936                    .primary_key()
5937                    .map(|f| f.column)
5938            }
5939
5940            /// Fetch every row where `<col> BETWEEN lo AND hi`
5941            /// (inclusive on both ends — same as SQL). Eloquent
5942            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5943            ///
5944            /// # Errors
5945            /// As [`FetcherPool::fetch`].
5946            ///
5947            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5948            pub async fn where_between(
5949                col: &str,
5950                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5951                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5952                pool: &#root::sql::Pool,
5953            ) -> ::core::result::Result<
5954                ::std::vec::Vec<Self>,
5955                #root::sql::ExecError,
5956            > {
5957                use #root::sql::FetcherPool as _;
5958                let _key = ::std::format!("{}__between", col);
5959                let _vals = #root::core::SqlValue::List(::std::vec![
5960                    ::core::convert::Into::into(lo),
5961                    ::core::convert::Into::into(hi),
5962                ]);
5963                #root::query::QuerySet::<Self>::default()
5964                    .filter(&_key, _vals)
5965                    .fetch(pool)
5966                    .await
5967            }
5968
5969            /// Fetch the first row where `<col> = <val>`. Returns
5970            /// `Ok(None)` when no row matches. Eloquent
5971            /// `Model::firstWhere($col, $val)` parity.
5972            ///
5973            /// Thin wrapper over `QuerySet::<Self>::default()
5974            /// .filter(col, val).first(pool)`. Use this when you
5975            /// want one row identified by a non-PK column (e.g.
5976            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5977            ///
5978            /// `val` accepts any value `Into<SqlValue>` so plain
5979            /// strings, ints, UUIDs, etc. all work.
5980            ///
5981            /// # Errors
5982            /// As `QuerySet::first`.
5983            pub async fn first_where(
5984                col: &str,
5985                val: impl ::core::convert::Into<#root::core::SqlValue>,
5986                pool: &#root::sql::Pool,
5987            ) -> ::core::result::Result<
5988                ::core::option::Option<Self>,
5989                #root::sql::ExecError,
5990            > {
5991                #root::query::QuerySet::<Self>::default()
5992                    .filter(col, val)
5993                    .first(pool)
5994                    .await
5995            }
5996
5997            #value_method
5998
5999            /// Fetch the row with the largest `field` value —
6000            /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
6001            /// `Ok(None)` for an empty table. Eloquent
6002            /// `Model::latest($field)->first()` parity.
6003            /// Thin wrapper over `QuerySet::<Self>::default()
6004            /// .latest(field, pool)`.
6005            ///
6006            /// **Field name** is the Rust field ident as a string
6007            /// (not the SQL column). It is a runtime `&str`, so an
6008            /// unknown one compiles and surfaces as
6009            /// `ExecError::Query(QueryError::UnknownField)` when the
6010            /// query runs.
6011            ///
6012            /// # Errors
6013            /// As `QuerySet::latest`.
6014            pub async fn latest(
6015                field: &str,
6016                pool: &#root::sql::Pool,
6017            ) -> ::core::result::Result<
6018                ::core::option::Option<Self>,
6019                #root::sql::ExecError,
6020            > {
6021                #root::query::QuerySet::<Self>::default()
6022                    .latest(field, pool)
6023                    .await
6024            }
6025
6026            /// Sibling of [`Self::latest`] — fetches the row
6027            /// with the smallest `field` value (`ORDER BY <field>
6028            /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
6029            /// ->first()` parity.
6030            ///
6031            /// # Errors
6032            /// As [`Self::latest`].
6033            pub async fn earliest(
6034                field: &str,
6035                pool: &#root::sql::Pool,
6036            ) -> ::core::result::Result<
6037                ::core::option::Option<Self>,
6038                #root::sql::ExecError,
6039            > {
6040                #root::query::QuerySet::<Self>::default()
6041                    .earliest(field, pool)
6042                    .await
6043            }
6044
6045            #count_method
6046
6047            /// `true` when the table contains at least one row.
6048            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6049            /// over `QuerySet::<Self>::default().exists(pool)`.
6050            ///
6051            /// # Errors
6052            /// As [`ExistsPool::exists`].
6053            ///
6054            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6055            pub async fn exists(
6056                pool: &#root::sql::Pool,
6057            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6058                use #root::sql::ExistsPool as _;
6059                #root::query::QuerySet::<Self>::default()
6060                    .exists(pool)
6061                    .await
6062            }
6063
6064            /// Inverse of [`Self::exists`] — returns `true` when
6065            /// the table has zero rows. Eloquent
6066            /// `Model::doesntExist()` parity.
6067            ///
6068            /// # Errors
6069            /// As [`Self::exists`].
6070            pub async fn doesnt_exist(
6071                pool: &#root::sql::Pool,
6072            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6073                Self::exists(pool).await.map(|e| !e)
6074            }
6075
6076            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6077            /// `true` when a row with primary key `pk` exists in
6078            /// the table. Sugar over
6079            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6080            ///
6081            /// Differs from [`Self::exists`] (which checks "any row
6082            /// in the table") by checking a specific PK existence.
6083            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6084            /// because the row is never materialized — the SQL is
6085            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6086            ///
6087            /// # Errors
6088            /// As [`#root::sql::ExistsPool::contains_pk`].
6089            pub async fn contains_pk(
6090                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6091                pool: &#root::sql::Pool,
6092            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6093                use #root::sql::ExistsPool as _;
6094                #root::query::QuerySet::<Self>::default()
6095                    .contains_pk(pool, pk)
6096                    .await
6097            }
6098
6099            #sum_method
6100            #avg_method
6101            #min_method
6102            #max_method
6103
6104            /// Fetch every row of this model from `pool`. Eloquent
6105            /// `Model::all()` parity — a thin wrapper over
6106            /// `QuerySet::<Self>::default().fetch(pool)`.
6107            ///
6108            /// **Use with care on large tables** — there's no
6109            /// pagination or limit; the entire table is materialized
6110            /// into memory. For anything beyond fixture / lookup
6111            /// tables, page through `QuerySet::<Self>::default()
6112            /// .order_by(...).limit(N).offset(M).fetch(pool)`
6113            /// or stream via `.iterator(chunk_size)`.
6114            ///
6115            /// # Errors
6116            /// As [`FetcherPool::fetch`].
6117            ///
6118            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6119            pub async fn all(
6120                pool: &#root::sql::Pool,
6121            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6122            {
6123                use #root::sql::FetcherPool as _;
6124                #root::query::QuerySet::<Self>::default()
6125                    .fetch(pool)
6126                    .await
6127            }
6128
6129            /// Look up every row whose primary key is in `pks`.
6130            /// Returns the matching rows in **inventory** order — NOT
6131            /// the order of `pks`. Empty `pks` returns an empty
6132            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6133            /// with a list) parity.
6134            ///
6135            /// Thin wrapper over `QuerySet::<Self>::default()
6136            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6137            /// Caller-supplied PKs that don't match a row are
6138            /// silently skipped (the returned vec is shorter than
6139            /// the input list). For an order-preserving / "fail
6140            /// when any missing" variant, build the queryset
6141            /// explicitly with `in_bulk` instead.
6142            ///
6143            /// Accepts any iterable whose elements are
6144            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6145            /// `Vec<Uuid>`, etc.
6146            ///
6147            /// # Errors
6148            /// As [`FetcherPool::fetch`].
6149            ///
6150            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6151            pub async fn find_many<V>(
6152                pks: impl ::core::iter::IntoIterator<Item = V>,
6153                pool: &#root::sql::Pool,
6154            ) -> ::core::result::Result<
6155                ::std::vec::Vec<Self>,
6156                #root::sql::ExecError,
6157            >
6158            where
6159                V: ::core::convert::Into<#root::core::SqlValue>,
6160            {
6161                use #root::sql::FetcherPool as _;
6162                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6163                    pks.into_iter().map(::core::convert::Into::into).collect();
6164                if _values.is_empty() {
6165                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6166                }
6167                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6168                #root::query::QuerySet::<Self>::default()
6169                    .filter(&_key, #root::core::SqlValue::List(_values))
6170                    .fetch(pool)
6171                    .await
6172            }
6173
6174            /// Look up the row whose primary key equals `pk`. Returns
6175            /// `Ok(None)` when no row matches, rather than erroring.
6176            /// Eloquent `Model::find`
6177            /// shape — accepts any value `Into<SqlValue>`.
6178            ///
6179            /// One-liner shortcut for the common
6180            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6181            /// .limit(1).fetch(pool).await?.into_iter().next()`
6182            /// dance.
6183            ///
6184            /// # Errors
6185            /// As [`FetcherPool::fetch`].
6186            ///
6187            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6188            pub async fn find(
6189                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6190                pool: &#root::sql::Pool,
6191            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6192            {
6193                use #root::sql::FetcherPool as _;
6194                let _pk_val: #root::core::SqlValue = pk.into();
6195                let mut _rows: ::std::vec::Vec<Self> =
6196                    #root::query::QuerySet::<Self>::default()
6197                        .filter(::core::stringify!(#pk_ident), _pk_val)
6198                        .limit(1)
6199                        .fetch(pool)
6200                        .await?;
6201                ::core::result::Result::Ok(_rows.into_iter().next())
6202            }
6203
6204            /// Look up the row whose primary key equals `pk`. Errors
6205            /// when no row matches — the throwing counterpart of
6206            /// [`Self::find`]. Eloquent `Model::findOrFail`
6207            /// parity.
6208            ///
6209            /// Translates the miss into
6210            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6211            /// so callers can `?`-bubble straight through the typical
6212            /// `ExecError` error chain.
6213            ///
6214            /// # Errors
6215            /// As [`Self::find`]; additionally
6216            /// [`sqlx::Error::RowNotFound`] when no row matches.
6217            ///
6218            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6219            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6220            pub async fn find_or_fail(
6221                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6222                pool: &#root::sql::Pool,
6223            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6224                match Self::find(pk, pool).await? {
6225                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6226                    ::core::option::Option::None => ::core::result::Result::Err(
6227                        #root::sql::ExecError::Driver(
6228                            #root::sql::sqlx::Error::RowNotFound,
6229                        ),
6230                    ),
6231                }
6232            }
6233
6234            /// Look up multiple rows by primary key, **failing** if
6235            /// any requested PK is missing. Eloquent
6236            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6237            /// inventory order (NOT request order — same as
6238            /// [`Self::find_many`]).
6239            ///
6240            /// Empty `pks` returns an empty `Vec` (no rows requested
6241            /// → nothing to fail on).
6242            ///
6243            /// Differs from [`Self::find_many`] in that this method
6244            /// requires every PK to resolve. If even one is missing,
6245            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6246            /// in `ExecError::Driver`). Rows are deduped at the SQL
6247            /// layer, so passing the same PK twice in `pks` counts as
6248            /// one expected row.
6249            ///
6250            /// # Errors
6251            /// As [`Self::find_many`], plus `RowNotFound` when the
6252            /// returned row count is less than the count of distinct
6253            /// requested PKs.
6254            pub async fn find_many_or_fail<V>(
6255                pks: impl ::core::iter::IntoIterator<Item = V>,
6256                pool: &#root::sql::Pool,
6257            ) -> ::core::result::Result<
6258                ::std::vec::Vec<Self>,
6259                #root::sql::ExecError,
6260            >
6261            where
6262                V: ::core::convert::Into<#root::core::SqlValue>,
6263            {
6264                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6265                    pks.into_iter().map(::core::convert::Into::into).collect();
6266                if _values.is_empty() {
6267                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6268                }
6269                // Dedup the requested PK list before counting so
6270                // duplicate-PK requests don't false-fail (the SQL
6271                // `IN (...)` clause naturally dedups too).
6272                let mut _seen: ::std::collections::HashSet<
6273                    ::std::string::String,
6274                > = ::std::collections::HashSet::new();
6275                for v in &_values {
6276                    _seen.insert(v.to_display_string());
6277                }
6278                let _expected = _seen.len();
6279                let _rows = Self::find_many(_values, pool).await?;
6280                if _rows.len() < _expected {
6281                    return ::core::result::Result::Err(
6282                        #root::sql::ExecError::Driver(
6283                            #root::sql::sqlx::Error::RowNotFound,
6284                        ),
6285                    );
6286                }
6287                ::core::result::Result::Ok(_rows)
6288            }
6289
6290            /// Find by primary key or run `fallback` to produce a
6291            /// default row to return. Eloquent
6292            /// `Model::findOr($pk, fn() => …)` parity.
6293            ///
6294            /// Unlike [`Self::find_or_fail`] (which raises on
6295            /// miss), this is the "give me something sensible"
6296            /// branch: typical use is "fetch the user's row, else
6297            /// fall back to an anonymous/guest stub".
6298            ///
6299            /// `fallback` runs only when no row matches — the DB
6300            /// round-trip happens unconditionally.
6301            ///
6302            /// # Errors
6303            /// As [`Self::find`].
6304            pub async fn find_or<F>(
6305                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6306                pool: &#root::sql::Pool,
6307                fallback: F,
6308            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6309            where
6310                F: ::core::ops::FnOnce() -> Self,
6311            {
6312                ::core::result::Result::Ok(
6313                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6314                )
6315            }
6316
6317            /// Same as [`Self::find_or`] but also returns a `bool`
6318            /// indicating whether the row was found in the DB
6319            /// (`true`) or freshly built from `fallback` (`false`).
6320            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6321            /// in PHP the returned model also exposes `->exists`;
6322            /// here that's surfaced as the second tuple element.
6323            ///
6324            /// Useful for edit-or-create form handlers where the
6325            /// caller needs to know whether to PATCH or POST when
6326            /// the user submits the form.
6327            ///
6328            /// # Errors
6329            /// As [`Self::find`].
6330            pub async fn find_or_new<F>(
6331                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6332                pool: &#root::sql::Pool,
6333                fallback: F,
6334            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6335            where
6336                F: ::core::ops::FnOnce() -> Self,
6337            {
6338                match Self::find(pk, pool).await? {
6339                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6340                    ::core::option::Option::None => {
6341                        ::core::result::Result::Ok((fallback(), false))
6342                    }
6343                }
6344            }
6345
6346            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6347            /// [`Self::find_or_new`] but **persists** the new row
6348            /// when the PK isn't found. Returns `(row, exists: bool)`
6349            /// — `exists=true` when the PK was found,
6350            /// `false` when a fresh row was inserted.
6351            ///
6352            /// ```ignore
6353            /// let (post, found) = Post::find_or_insert(
6354            ///     pk,
6355            ///     &pool,
6356            ///     || Post { id: Auto::default(), title: "new".into() },
6357            /// ).await?;
6358            /// // `found` true → returned existing row;
6359            /// // `found` false → fallback was inserted, `post.id` populated.
6360            /// ```
6361            ///
6362            /// **Caveat**: two concurrent calls that both miss the
6363            /// find can both INSERT, violating uniqueness. Wrap in a
6364            /// transaction or rely on a UNIQUE constraint + handle
6365            /// the conflict error if you need race-free semantics.
6366            ///
6367            /// # Errors
6368            /// As [`Self::find`] and [`Self::save_pool`].
6369            pub async fn find_or_insert<F>(
6370                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6371                pool: &#root::sql::Pool,
6372                fallback: F,
6373            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6374            where
6375                F: ::core::ops::FnOnce() -> Self,
6376            {
6377                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6378                    return ::core::result::Result::Ok((_row, true));
6379                }
6380                let mut _new = fallback();
6381                _new.save_pool(pool).await?;
6382                ::core::result::Result::Ok((_new, false))
6383            }
6384
6385            /// Fetch the first row of the table, or run `fallback`
6386            /// when the table is empty. Eloquent
6387            /// `Model::firstOr(fn() => …)` parity.
6388            ///
6389            /// # Errors
6390            /// As [`Self::first`].
6391            pub async fn first_or<F>(
6392                pool: &#root::sql::Pool,
6393                fallback: F,
6394            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6395            where
6396                F: ::core::ops::FnOnce() -> Self,
6397            {
6398                // Route through the queryset rather than `Self::first`,
6399                // which is suppressed on models with a field named
6400                // `first` (field/shortcut collision guard).
6401                ::core::result::Result::Ok(
6402                    #root::query::QuerySet::<Self>::default()
6403                        .first(pool)
6404                        .await?
6405                        .unwrap_or_else(fallback),
6406                )
6407            }
6408
6409            /// Fetch exactly one row matching `<col> = <val>`. Errors
6410            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6411            /// or more than one matches
6412            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6413            /// Eloquent `Model::sole($col, $val)` parity.
6414            ///
6415            /// # Errors
6416            /// As [`Self::where_`] plus the explicit
6417            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6418            pub async fn sole(
6419                col: &str,
6420                val: impl ::core::convert::Into<#root::core::SqlValue>,
6421                pool: &#root::sql::Pool,
6422            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6423                let mut _rows = Self::where_(col, val, pool).await?;
6424                match _rows.len() {
6425                    0 => ::core::result::Result::Err(
6426                        #root::sql::ExecError::Driver(
6427                            #root::sql::sqlx::Error::RowNotFound,
6428                        ),
6429                    ),
6430                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6431                    n => ::core::result::Result::Err(
6432                        #root::sql::ExecError::MultipleRowsReturned {
6433                            op: "sole",
6434                            table: <Self as #root::core::Model>::SCHEMA.name,
6435                            count: n,
6436                        },
6437                    ),
6438                }
6439            }
6440        }
6441    } else {
6442        quote!()
6443    };
6444
6445    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6446    // the non-audited `_pool` methods but execute against an open
6447    // `PoolTx` so the writes participate in the caller's transaction.
6448    // Auditing inside TX is deferred; these always use the plain
6449    // executor primitives regardless of whether the model is audited.
6450    let tx_insert_method = if fields.has_auto {
6451        let pushes = &fields.insert_pushes;
6452        let returning_cols = &fields.returning_cols;
6453        quote! {
6454            /// Insert this row inside an open transaction, populating
6455            /// any `Auto<T>` PK from the auto-assigned value. Works
6456            /// against any backend that `tx` wraps.
6457            ///
6458            /// # Errors
6459            /// As [`Self::insert_pool`].
6460            pub async fn insert_tx(
6461                &mut self,
6462                tx: &mut #root::sql::PoolTx<'_>,
6463            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6464                let mut _columns: ::std::vec::Vec<&'static str> =
6465                    ::std::vec::Vec::new();
6466                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6467                    ::std::vec::Vec::new();
6468                #( #pushes )*
6469                let _query = #root::core::InsertQuery::new(
6470                    <Self as #root::core::Model>::SCHEMA,
6471                    _columns,
6472                    _values,
6473                )
6474                .returning(::std::vec![ #( #returning_cols ),* ]);
6475                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6476                #root::sql::apply_auto_pk(_result, self)
6477            }
6478        }
6479    } else {
6480        let insert_columns = &fields.insert_columns;
6481        let insert_values = &fields.insert_values;
6482        quote! {
6483            /// Insert this row inside an open transaction.
6484            ///
6485            /// # Errors
6486            /// As [`Self::insert_pool`].
6487            pub async fn insert_tx(
6488                &self,
6489                tx: &mut #root::sql::PoolTx<'_>,
6490            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6491                let _query = #root::core::InsertQuery::new(
6492                    <Self as #root::core::Model>::SCHEMA,
6493                    ::std::vec![ #( #insert_columns ),* ],
6494                    ::std::vec![ #( #insert_values ),* ],
6495                );
6496                #root::sql::insert_tx(tx, &_query).await
6497            }
6498        }
6499    };
6500
6501    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6502        let pk_column_lit = pk_col.as_str();
6503        let assignments = &fields.update_assignments;
6504        let dispatch_unset = if fields.pk_is_auto {
6505            quote! {
6506                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6507                    return self.insert_tx(tx).await.map(|()| 1u64);
6508                }
6509            }
6510        } else {
6511            quote!()
6512        };
6513        quote! {
6514            /// Save this row inside an open transaction. `INSERT` when
6515            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6516            /// PK. Works against any backend that `tx` wraps.
6517            ///
6518            /// # Errors
6519            /// As [`Self::save_pool`].
6520            pub async fn save_tx(
6521                &mut self,
6522                tx: &mut #root::sql::PoolTx<'_>,
6523            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6524                #dispatch_unset
6525                let _query = #root::core::UpdateQuery::new(
6526                    <Self as #root::core::Model>::SCHEMA,
6527                    ::std::vec![ #( #assignments ),* ],
6528                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6529                        #pk_column_lit,
6530                        #root::core::Op::Eq,
6531                        ::core::convert::Into::<#root::core::SqlValue>::into(
6532                            ::core::clone::Clone::clone(&self.#pk_ident)
6533                        ),
6534                    )),
6535                );
6536                let _affected = #root::sql::update_tx(tx, &_query).await?;
6537                ::core::result::Result::Ok(_affected)
6538            }
6539        }
6540    } else {
6541        quote!()
6542    };
6543
6544    let tx_delete_method = {
6545        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6546        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6547        if let Some(pk_ident) = pk_ident_for_tx {
6548            quote! {
6549                /// Delete the row identified by this instance's PK
6550                /// inside an open transaction. Works against any backend
6551                /// that `tx` wraps.
6552                ///
6553                /// # Errors
6554                /// As [`Self::delete_pool`].
6555                pub async fn delete_tx(
6556                    &self,
6557                    tx: &mut #root::sql::PoolTx<'_>,
6558                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6559                    let _query = #root::core::DeleteQuery::by_pk(
6560                        <Self as #root::core::Model>::SCHEMA,
6561                        #pk_column_lit,
6562                        ::core::convert::Into::<#root::core::SqlValue>::into(
6563                            ::core::clone::Clone::clone(&self.#pk_ident)
6564                        ),
6565                    );
6566                    #root::sql::delete_tx(tx, &_query).await
6567                }
6568            }
6569        } else {
6570            quote!()
6571        }
6572    };
6573
6574    // Update emission captures both BEFORE and AFTER state — runs an
6575    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6576    // each tracked field's prior value, then after the UPDATE diffs
6577    // against the in-memory `&self`. `diff_changes` drops unchanged
6578    // columns so the JSON only contains the actual delta.
6579    //
6580    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6581    // and binds `_audit_before_pairs`; `audit_update_post` runs
6582    // after the UPDATE and emits the PendingEntry.
6583    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6584        audited_fields
6585    {
6586        if tracked.is_empty() {
6587            (quote!(), quote!())
6588        } else {
6589            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6590            let pk_column_lit_for_select =
6591                primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6592            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6593                if fields.pk_is_auto {
6594                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6595                } else {
6596                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6597                }
6598            } else {
6599                quote!(0_i64)
6600            };
6601            let before_pairs = tracked.iter().map(|c| {
6602                let column_lit = c.column.as_str();
6603                let value_ty = &c.value_ty;
6604                quote! {
6605                    (
6606                        #column_lit,
6607                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6608                            &_audit_before_row, #column_lit,
6609                        ) {
6610                            ::core::result::Result::Ok(v) => {
6611                                #root::__serde_json::to_value(&v)
6612                                    .unwrap_or(#root::__serde_json::Value::Null)
6613                            }
6614                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6615                        },
6616                    )
6617                }
6618            });
6619            let after_pairs = tracked.iter().map(|c| {
6620                let column_lit = c.column.as_str();
6621                let ident = &c.ident;
6622                quote! {
6623                    (
6624                        #column_lit,
6625                        #root::__serde_json::to_value(&self.#ident)
6626                            .unwrap_or(#root::__serde_json::Value::Null),
6627                    )
6628                }
6629            });
6630            let pk_str = audit_pk_to_string.clone();
6631            let pre = quote! {
6632                let _audit_before_query = #root::audit::before_image_query(
6633                    <Self as #root::core::Model>::SCHEMA,
6634                    #pk_column_lit_for_select,
6635                    ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6636                    &[ #( #tracked_cols ),* ],
6637                );
6638                // A failed pre-read must not let the UPDATE run unaudited (#1907).
6639                let _audit_before_pairs:
6640                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6641                    #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6642                        .await?
6643                        .into_iter()
6644                        .next()
6645                        .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6646            };
6647            let post = quote! {
6648                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6649                    let _audit_after:
6650                        ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6651                        ::std::vec![ #( #after_pairs ),* ];
6652                    if let ::core::option::Option::Some(_audit_entry) =
6653                        #root::audit::PendingEntry::update_diff(
6654                            <Self as #root::core::Model>::SCHEMA.table,
6655                            #pk_str,
6656                            &_audit_before,
6657                            &_audit_after,
6658                        )
6659                    {
6660                        #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6661                    }
6662                }
6663            };
6664            (pre, post)
6665        }
6666    } else {
6667        (quote!(), quote!())
6668    };
6669
6670    // Bulk-insert audit: one `Create` entry per row once RETURNING has
6671    // set its PK, written in one batched INSERT.
6672    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6673        quote! {
6674            let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6675                .iter()
6676                .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6677                .collect();
6678            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6679        }
6680    } else {
6681        quote!()
6682    };
6683
6684    let save_method = if fields.pk_is_auto {
6685        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6686        let pk_column_lit = pk_column.as_str();
6687        let assignments = &fields.update_assignments;
6688        let upsert_cols = &fields.upsert_update_columns;
6689        let upsert_pushes = &fields.insert_pushes;
6690        let upsert_returning = &fields.returning_cols;
6691        let upsert_auto_assigns = &fields.auto_assigns;
6692        // Conflict target: prefer the first declared `unique_together`
6693        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6694        // `BIGSERIAL` and never collide on insert, so a PK-only target
6695        // would silently turn `upsert()` into "always-insert" for
6696        // surrogate-PK models with composite UNIQUE constraints — see
6697        // `RolePermission` / `UserRole` / `UserPermission` in the
6698        // tenancy permission engine. When no `unique_together` is
6699        // declared we keep the PK target (the original behaviour).
6700        let upsert_target_columns: Vec<String> =
6701            upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6702        let upsert_target_lits = upsert_target_columns
6703            .iter()
6704            .map(String::as_str)
6705            .collect::<Vec<_>>();
6706        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6707            quote!(#root::core::ConflictClause::DoNothing)
6708        } else {
6709            quote!(#root::core::ConflictClause::DoUpdate {
6710                target: ::std::vec![ #( #upsert_target_lits ),* ],
6711                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6712            })
6713        };
6714        // An upsert that may hit a conflict is recorded as `Update`; only
6715        // an unset PK with a PK-only target is surely a `Create`.
6716        let (audit_upsert_pre, audit_upsert_emit) = if audited_fields.is_some() {
6717            (
6718                if upsert_target_columns == [pk_column.clone()] {
6719                    quote! {
6720                        let _audit_op = if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6721                            #root::audit::AuditOp::Create
6722                        } else {
6723                            #root::audit::AuditOp::Update
6724                        };
6725                    }
6726                } else {
6727                    quote!(let _audit_op = #root::audit::AuditOp::Update;)
6728                },
6729                quote! {
6730                    let _audit_entry = self.__rustango_audit_entry(_audit_op);
6731                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6732                },
6733            )
6734        } else {
6735            (quote!(), quote!())
6736        };
6737        Some(quote! {
6738            /// Insert this row if its `Auto<T>` primary key is
6739            /// `Unset`, otherwise update the existing row matching the
6740            /// PK. The caller doesn't need to pick `insert` vs the
6741            /// bulk-update path manually.
6742            ///
6743            /// On the insert branch, populates the PK from `RETURNING`
6744            /// (same behavior as `insert`). On the update branch,
6745            /// writes every non-PK column back; if no row matches the
6746            /// PK, returns `Ok(())` silently.
6747            ///
6748            /// Only generated when the primary key is declared as
6749            /// `Auto<T>`. Models with a manually-managed PK must use
6750            /// `insert` or the QuerySet update builder.
6751            ///
6752            /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6753            /// driver-specific `PgPool`, so it does not exist at all on a
6754            /// build without the `postgres` feature: selecting `sqlite`
6755            /// makes the method vanish rather than fail with anything that
6756            /// names the cause.
6757            ///
6758            /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6759            /// all three backends. The naming is inverted — the short name
6760            /// is the narrow one — and that is tracked in #1293.
6761            ///
6762            /// # Errors
6763            /// Returns [`#root::sql::ExecError`] for SQL-writing
6764            /// or driver failures.
6765            #[cfg(feature = "postgres")]
6766            pub async fn save(
6767                &mut self,
6768                pool: &#root::sql::sqlx::PgPool,
6769            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6770                #pool_to_save_on
6771            }
6772
6773            /// Like [`Self::save`] but accepts any sqlx executor —
6774            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6775            /// escape hatch for tenant-scoped writes: schema-mode
6776            /// tenants share the registry pool but rely on a per-
6777            /// checkout `SET search_path`, so passing `&PgPool` would
6778            /// silently hit the wrong schema. Acquire a connection
6779            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6780            ///
6781            /// # Errors
6782            /// As [`Self::save`].
6783            #[cfg(feature = "postgres")]
6784            pub async fn save_on #executor_generics (
6785                &mut self,
6786                #executor_param,
6787            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6788            #executor_where
6789            {
6790                // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6791                // the rows-affected count (0 when the PK no longer exists).
6792                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6793                    return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6794                }
6795                #audit_update_pre
6796                let _query = #root::core::UpdateQuery::new(
6797                    <Self as #root::core::Model>::SCHEMA,
6798                    ::std::vec![ #( #assignments ),* ],
6799                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6800                        #pk_column_lit,
6801                        #root::core::Op::Eq,
6802                        ::core::convert::Into::<#root::core::SqlValue>::into(
6803                            ::core::clone::Clone::clone(&self.#pk_ident)
6804                        ),
6805                    )),
6806                );
6807                let _affected = #root::sql::__macro_internals::update_on(
6808                    #executor_passes_to_data_write,
6809                    &_query,
6810                ).await?;
6811                #audit_update_post
6812                ::core::result::Result::Ok(_affected)
6813            }
6814
6815            /// Per-call override for the audit source. Runs
6816            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6817            /// scope so the resulting audit entry records `source`
6818            /// instead of the task-local default. Useful for seed
6819            /// scripts and one-off CLI tools that don't sit inside an
6820            /// admin handler. The override applies only to this call;
6821            /// no global state changes.
6822            ///
6823            /// # Errors
6824            /// As [`Self::save_on`].
6825            #[cfg(feature = "postgres")]
6826            pub async fn save_on_with #executor_generics (
6827                &mut self,
6828                #executor_param,
6829                source: #root::audit::AuditSource,
6830            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6831            #executor_where
6832            {
6833                #root::audit::with_source(source, self.save_on(_executor)).await
6834            }
6835
6836            /// Insert this row or update it in-place if the primary key already
6837            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6838            ///
6839            /// With `Auto::Unset` PK the server assigns a new key and no conflict
6840            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6841            /// inserted if absent or all non-PK columns are overwritten if present.
6842            ///
6843            /// # Errors
6844            /// As [`Self::insert_on`].
6845            #[cfg(feature = "postgres")]
6846            pub async fn upsert(
6847                &mut self,
6848                pool: &#root::sql::sqlx::PgPool,
6849            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6850                #pool_to_upsert_on
6851            }
6852
6853            /// Like [`Self::upsert`] but accepts any sqlx executor.
6854            /// See [`Self::save_on`] for tenancy-scoped rationale.
6855            ///
6856            /// # Errors
6857            /// As [`Self::upsert`].
6858            #[cfg(feature = "postgres")]
6859            pub async fn upsert_on #executor_generics (
6860                &mut self,
6861                #executor_param,
6862            ) -> ::core::result::Result<(), #root::sql::ExecError>
6863            #executor_where
6864            {
6865                let mut _columns: ::std::vec::Vec<&'static str> =
6866                    ::std::vec::Vec::new();
6867                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6868                    ::std::vec::Vec::new();
6869                #( #upsert_pushes )*
6870                let query = #root::core::InsertQuery::new(
6871                    <Self as #root::core::Model>::SCHEMA,
6872                    _columns,
6873                    _values,
6874                )
6875                .returning(::std::vec![ #( #upsert_returning ),* ])
6876                .on_conflict(#conflict_clause);
6877                #audit_upsert_pre
6878                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6879                    #executor_passes_to_data_write,
6880                    &query,
6881                ).await?;
6882                let _returning_row = &_returning_row_v;
6883                #( #upsert_auto_assigns )*
6884                #audit_upsert_emit
6885                ::core::result::Result::Ok(())
6886            }
6887        })
6888    } else {
6889        None
6890    };
6891
6892    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6893        let pk_column_lit = pk_column.as_str();
6894        // Optional `soft_delete_on` / `restore_on` companions when the
6895        // model has a `#[rustango(soft_delete)]` column. They land
6896        // alongside the regular `delete_on` so callers have both
6897        // options — a hard delete (audit-tracked as a real DELETE) and
6898        // a logical delete (audit-tracked as an UPDATE setting the
6899        // deleted_at column to NOW()).
6900        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6901            let col_lit = col;
6902            let sd_field_ident = fields
6903                .soft_delete_field_ident
6904                .clone()
6905                .expect("soft_delete_column without ident");
6906            quote! {
6907                /// Soft-delete this row by setting its
6908                /// `#[rustango(soft_delete)]` column to `NOW()`.
6909                /// The row stays in the table; query helpers can
6910                /// filter it out by checking the column for `IS NOT
6911                /// NULL`.
6912                ///
6913                /// # Errors
6914                /// As [`Self::delete`].
6915                #[cfg(feature = "postgres")]
6916                pub async fn soft_delete_on #executor_generics (
6917                    &self,
6918                    #executor_param,
6919                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6920                #executor_where
6921                {
6922                    let _now = #root::__chrono::Utc::now();
6923                    let _query = #root::soft_delete::__mark_query(
6924                        <Self as #root::core::Model>::SCHEMA,
6925                        #col_lit,
6926                        #pk_column_lit,
6927                        ::core::convert::Into::<#root::core::SqlValue>::into(
6928                            ::core::clone::Clone::clone(&self.#pk_ident)
6929                        ),
6930                        ::core::option::Option::Some(_now),
6931                    );
6932                    let _affected = #root::sql::__macro_internals::update_on(
6933                        #executor_passes_to_data_write,
6934                        &_query,
6935                    ).await?;
6936                    #audit_softdelete_emit
6937                    ::core::result::Result::Ok(_affected)
6938                }
6939
6940                /// Inverse of [`Self::soft_delete_on`] — clears the
6941                /// soft-delete column back to NULL so the row is
6942                /// considered live again.
6943                ///
6944                /// # Errors
6945                /// As [`Self::delete`].
6946                #[cfg(feature = "postgres")]
6947                pub async fn restore_on #executor_generics (
6948                    &self,
6949                    #executor_param,
6950                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6951                #executor_where
6952                {
6953                    let _query = #root::soft_delete::__mark_query(
6954                        <Self as #root::core::Model>::SCHEMA,
6955                        #col_lit,
6956                        #pk_column_lit,
6957                        ::core::convert::Into::<#root::core::SqlValue>::into(
6958                            ::core::clone::Clone::clone(&self.#pk_ident)
6959                        ),
6960                        ::core::option::Option::None,
6961                    );
6962                    let _affected = #root::sql::__macro_internals::update_on(
6963                        #executor_passes_to_data_write,
6964                        &_query,
6965                    ).await?;
6966                    #audit_restore_emit
6967                    ::core::result::Result::Ok(_affected)
6968                }
6969
6970                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6971                /// — takes [`#root::sql::Pool`] and dispatches per
6972                /// backend. Eloquent `Model::delete()` semantics on
6973                /// soft-delete-enabled models (closes #821 partial).
6974                ///
6975                /// Sets the `#[rustango(soft_delete)]` column to
6976                /// `NOW()` on every backend. Query helpers
6977                /// (`QuerySet::active()` / `only_trashed()`,
6978                /// `soft_delete::active_filter` /
6979                /// `compose_with_active`) filter trashed rows out by
6980                /// reading `IS NULL` on the same column.
6981                ///
6982                /// # Errors
6983                /// As [`#root::sql::update_pool`].
6984                pub async fn soft_delete(
6985                    &self,
6986                    pool: &#root::sql::Pool,
6987                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6988                    let _now = #root::__chrono::Utc::now();
6989                    let _query = #root::soft_delete::__mark_query(
6990                        <Self as #root::core::Model>::SCHEMA,
6991                        #col_lit,
6992                        #pk_column_lit,
6993                        ::core::convert::Into::<#root::core::SqlValue>::into(
6994                            ::core::clone::Clone::clone(&self.#pk_ident)
6995                        ),
6996                        ::core::option::Option::Some(_now),
6997                    );
6998                    #pool_softdelete_run
6999                }
7000
7001                /// Tri-dialect counterpart of [`Self::restore_on`].
7002                /// Clears the `#[rustango(soft_delete)]` column back
7003                /// to `NULL`, marking the row live again. Eloquent
7004                /// `Model::restore()` parity.
7005                ///
7006                /// # Errors
7007                /// As [`#root::sql::update_pool`].
7008                pub async fn restore(
7009                    &self,
7010                    pool: &#root::sql::Pool,
7011                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7012                    let _query = #root::soft_delete::__mark_query(
7013                        <Self as #root::core::Model>::SCHEMA,
7014                        #col_lit,
7015                        #pk_column_lit,
7016                        ::core::convert::Into::<#root::core::SqlValue>::into(
7017                            ::core::clone::Clone::clone(&self.#pk_ident)
7018                        ),
7019                        ::core::option::Option::None,
7020                    );
7021                    #pool_restore_run
7022                }
7023
7024                /// Hard-delete this row, ignoring the soft-delete
7025                /// column. Eloquent `Model::forceDelete()` parity —
7026                /// the escape hatch when you need to actually purge
7027                /// data (GDPR, fixture cleanup, etc.).
7028                ///
7029                /// Equivalent to [`Self::delete_pool`] (the framework's
7030                /// non-soft delete) — exposed under the Eloquent name
7031                /// for muscle-memory + so soft-delete-enabled models
7032                /// have all three operations (soft / restore / force)
7033                /// in one place.
7034                ///
7035                /// # Errors
7036                /// As [`Self::delete_pool`].
7037                pub async fn force_delete(
7038                    &self,
7039                    pool: &#root::sql::Pool,
7040                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7041                    Self::delete_pool(self, pool).await
7042                }
7043
7044                /// Returns `true` when this row is soft-deleted (its
7045                /// `#[rustango(soft_delete)]` column is currently
7046                /// set — Eloquent `$model->trashed()` parity).
7047                ///
7048                /// Pure in-memory predicate over `&self`; does not
7049                /// hit the database. Useful in admin/template code
7050                /// like `{% if post.trashed() %}…{% endif %}` and in
7051                /// guard clauses on restore/force-delete flows.
7052                pub fn trashed(&self) -> bool {
7053                    ::core::option::Option::is_some(&self.#sd_field_ident)
7054                }
7055
7056                /// Fetch every row whose `#[rustango(soft_delete)]`
7057                /// column is `NULL` (a.k.a. the live, non-trashed
7058                /// rows). Eloquent's default `Model::all()` behavior on
7059                /// a soft-delete model (Eloquent auto-scopes trashed
7060                /// rows out; rustango doesn't have auto-scoping yet —
7061                /// see issue #820 — so this is the explicit shortcut).
7062                ///
7063                /// One-liner over `QuerySet::<Self>::default()
7064                /// .active().fetch(pool)`. Closes #821 partial.
7065                ///
7066                /// # Errors
7067                /// As [`#root::sql::FetcherPool::fetch`].
7068                pub async fn active(
7069                    pool: &#root::sql::Pool,
7070                ) -> ::core::result::Result<
7071                    ::std::vec::Vec<Self>,
7072                    #root::sql::ExecError,
7073                > {
7074                    use #root::sql::FetcherPool as _;
7075                    #root::query::QuerySet::<Self>::default()
7076                        .active()
7077                        .fetch(pool)
7078                        .await
7079                }
7080
7081                /// Fetch ONLY soft-deleted rows. Eloquent
7082                /// `Model::onlyTrashed()->get()` parity — drives the
7083                /// admin "Trash" page, restore flows, GDPR purge
7084                /// scans, etc.
7085                ///
7086                /// One-liner over `QuerySet::<Self>::default()
7087                /// .only_trashed().fetch(pool)`. Closes #821
7088                /// partial.
7089                ///
7090                /// # Errors
7091                /// As [`#root::sql::FetcherPool::fetch`].
7092                pub async fn only_trashed(
7093                    pool: &#root::sql::Pool,
7094                ) -> ::core::result::Result<
7095                    ::std::vec::Vec<Self>,
7096                    #root::sql::ExecError,
7097                > {
7098                    use #root::sql::FetcherPool as _;
7099                    #root::query::QuerySet::<Self>::default()
7100                        .only_trashed()
7101                        .fetch(pool)
7102                        .await
7103                }
7104
7105                /// Fetch every row, both live and soft-deleted.
7106                /// Eloquent `Model::withTrashed()->get()` parity.
7107                ///
7108                /// Today every queryset already includes trashed rows
7109                /// (rustango has no global-scope tracking yet — issue
7110                /// #820), so this is functionally equivalent to
7111                /// [`Self::all`]. Exposed as a named shortcut so
7112                /// soft-delete-aware code reads `Model::with_trashed_pool`
7113                /// rather than `Model::all_pool` — keeps intent visible
7114                /// in callers and stays correct when auto-scoping lands.
7115                ///
7116                /// Closes #821 partial.
7117                ///
7118                /// # Errors
7119                /// As [`#root::sql::FetcherPool::fetch`].
7120                pub async fn with_trashed(
7121                    pool: &#root::sql::Pool,
7122                ) -> ::core::result::Result<
7123                    ::std::vec::Vec<Self>,
7124                    #root::sql::ExecError,
7125                > {
7126                    use #root::sql::FetcherPool as _;
7127                    #root::query::QuerySet::<Self>::default()
7128                        .with_trashed()
7129                        .fetch(pool)
7130                        .await
7131                }
7132
7133            }
7134        } else {
7135            quote!()
7136        };
7137        quote! {
7138            /// Delete the row identified by this instance's primary key.
7139            ///
7140            /// Returns the number of rows affected (0 or 1).
7141            ///
7142            /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7143            /// a driver-specific `PgPool`, so it does not exist on a build
7144            /// without the `postgres` feature. [`Self::delete_pool`] takes
7145            /// `rustango::sql::Pool` and works on all three. The naming is
7146            /// inverted; tracked in #1293.
7147            ///
7148            /// # Errors
7149            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7150            /// driver failures.
7151            #[cfg(feature = "postgres")]
7152            pub async fn delete(
7153                &self,
7154                pool: &#root::sql::sqlx::PgPool,
7155            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7156                #pool_to_delete_on
7157            }
7158
7159            /// Like [`Self::delete`] but accepts any sqlx executor —
7160            /// for tenant-scoped deletes against an explicitly-acquired
7161            /// connection. See [`Self::save_on`] for the rationale.
7162            ///
7163            /// # Errors
7164            /// As [`Self::delete`].
7165            #[cfg(feature = "postgres")]
7166            pub async fn delete_on #executor_generics (
7167                &self,
7168                #executor_param,
7169            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7170            #executor_where
7171            {
7172                let query = #root::core::DeleteQuery::by_pk(
7173                    <Self as #root::core::Model>::SCHEMA,
7174                    #pk_column_lit,
7175                    ::core::convert::Into::<#root::core::SqlValue>::into(
7176                        ::core::clone::Clone::clone(&self.#pk_ident)
7177                    ),
7178                );
7179                let _affected = #root::sql::__macro_internals::delete_on(
7180                    #executor_passes_to_data_write,
7181                    &query,
7182                ).await?;
7183                #audit_delete_emit
7184                ::core::result::Result::Ok(_affected)
7185            }
7186
7187            /// Per-call audit-source override for [`Self::delete_on`].
7188            /// See [`Self::save_on_with`] for shape rationale.
7189            ///
7190            /// # Errors
7191            /// As [`Self::delete_on`].
7192            #[cfg(feature = "postgres")]
7193            pub async fn delete_on_with #executor_generics (
7194                &self,
7195                #executor_param,
7196                source: #root::audit::AuditSource,
7197            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7198            #executor_where
7199            {
7200                #root::audit::with_source(source, self.delete_on(_executor)).await
7201            }
7202            #pool_delete_method
7203            #pool_insert_method
7204            #audit_entry_method
7205            #pool_save_method
7206            #refresh_replicate_methods
7207            #tx_delete_method
7208            #tx_insert_method
7209            #tx_save_method
7210            #soft_delete_methods
7211
7212            /// Returns `true` when `other` represents the same DB
7213            /// row as `self` — i.e. their primary keys compare
7214            /// equal. Eloquent `$model->is($other)` parity.
7215            ///
7216            /// Because both arguments are typed `&Self`, the
7217            /// model/table check is automatic — `Post::is` cannot
7218            /// be invoked against a `Comment` at compile time. Only
7219            /// the PK has to be compared at runtime.
7220            pub fn is(&self, other: &Self) -> bool {
7221                self.#pk_ident == other.#pk_ident
7222            }
7223
7224            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7225            /// parity.
7226            pub fn is_not(&self, other: &Self) -> bool {
7227                self.#pk_ident != other.#pk_ident
7228            }
7229
7230            /// Returns this row's primary-key value as an
7231            /// [`#root::core::SqlValue`]. Eloquent
7232            /// `$model->getKey()` parity.
7233            ///
7234            /// Useful when you need to thread the PK through a
7235            /// generic `Into<SqlValue>`-bound API without knowing the
7236            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7237            #[must_use]
7238            pub fn get_key(&self) -> #root::core::SqlValue {
7239                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7240            }
7241
7242        }
7243    });
7244
7245    let insert_method = if fields.has_auto {
7246        let pushes = &fields.insert_pushes;
7247        let returning_cols = &fields.returning_cols;
7248        let auto_assigns = &fields.auto_assigns;
7249        quote! {
7250            /// Insert this row into its table. Skips columns whose
7251            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7252            /// sequence fills them in, then reads each `Auto` column
7253            /// back via `RETURNING` and stores it on `self`.
7254            ///
7255            /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7256            /// a driver-specific `PgPool`, so it does not exist on a build
7257            /// without the `postgres` feature. [`Self::insert_pool`] takes
7258            /// `rustango::sql::Pool` and works on all three. The naming is
7259            /// inverted; tracked in #1293.
7260            ///
7261            /// # Errors
7262            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7263            /// driver failures.
7264            #[cfg(feature = "postgres")]
7265            pub async fn insert(
7266                &mut self,
7267                pool: &#root::sql::sqlx::PgPool,
7268            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7269                #pool_to_insert_on
7270            }
7271
7272            /// Like [`Self::insert`] but accepts any sqlx executor.
7273            /// See [`Self::save_on`] for tenancy-scoped rationale.
7274            ///
7275            /// # Errors
7276            /// As [`Self::insert`].
7277            #[cfg(feature = "postgres")]
7278            pub async fn insert_on #executor_generics (
7279                &mut self,
7280                #executor_param,
7281            ) -> ::core::result::Result<(), #root::sql::ExecError>
7282            #executor_where
7283            {
7284                let mut _columns: ::std::vec::Vec<&'static str> =
7285                    ::std::vec::Vec::new();
7286                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7287                    ::std::vec::Vec::new();
7288                #( #pushes )*
7289                let query = #root::core::InsertQuery::new(
7290                    <Self as #root::core::Model>::SCHEMA,
7291                    _columns,
7292                    _values,
7293                )
7294                .returning(::std::vec![ #( #returning_cols ),* ]);
7295                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7296                    #executor_passes_to_data_write,
7297                    &query,
7298                ).await?;
7299                let _returning_row = &_returning_row_v;
7300                #( #auto_assigns )*
7301                #audit_insert_emit
7302                ::core::result::Result::Ok(())
7303            }
7304
7305            /// Per-call audit-source override for [`Self::insert_on`].
7306            /// See [`Self::save_on_with`] for shape rationale.
7307            ///
7308            /// # Errors
7309            /// As [`Self::insert_on`].
7310            #[cfg(feature = "postgres")]
7311            pub async fn insert_on_with #executor_generics (
7312                &mut self,
7313                #executor_param,
7314                source: #root::audit::AuditSource,
7315            ) -> ::core::result::Result<(), #root::sql::ExecError>
7316            #executor_where
7317            {
7318                #root::audit::with_source(source, self.insert_on(_executor)).await
7319            }
7320        }
7321    } else {
7322        let insert_columns = &fields.insert_columns;
7323        let insert_values = &fields.insert_values;
7324        quote! {
7325            /// Insert this row into its table.
7326            ///
7327            /// # Errors
7328            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7329            /// driver failures.
7330            #[cfg(feature = "postgres")]
7331            pub async fn insert(
7332                &self,
7333                pool: &#root::sql::sqlx::PgPool,
7334            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7335                self.insert_on(pool).await
7336            }
7337
7338            /// Like [`Self::insert`] but accepts any sqlx executor.
7339            /// See [`Self::save_on`] for tenancy-scoped rationale.
7340            ///
7341            /// # Errors
7342            /// As [`Self::insert`].
7343            #[cfg(feature = "postgres")]
7344            pub async fn insert_on<'_c, _E>(
7345                &self,
7346                _executor: _E,
7347            ) -> ::core::result::Result<(), #root::sql::ExecError>
7348            where
7349                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7350            {
7351                let query = #root::core::InsertQuery::new(
7352                    <Self as #root::core::Model>::SCHEMA,
7353                    ::std::vec![ #( #insert_columns ),* ],
7354                    ::std::vec![ #( #insert_values ),* ],
7355                );
7356                #root::sql::__macro_internals::insert_on(_executor, &query).await
7357            }
7358        }
7359    };
7360
7361    let bulk_insert_method = if fields.has_auto {
7362        let cols_no_auto = &fields.bulk_columns_no_auto;
7363        let cols_all = &fields.bulk_columns_all;
7364        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7365        let pushes_all = &fields.bulk_pushes_all;
7366        let returning_cols = &fields.returning_cols;
7367        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7368        let uniformity = &fields.bulk_auto_uniformity;
7369        let fill_in_place = &fields.bulk_fill_in_place;
7370        // Nothing to read back when every Auto field is filled Rust-side (#1934).
7371        let read_back = if fields.returning_cols.is_empty() {
7372            quote!()
7373        } else {
7374            quote! {
7375                if _returned.len() != rows.len() {
7376                    return ::core::result::Result::Err(
7377                        #root::sql::ExecError::Sql(
7378                            #root::sql::SqlError::BulkInsertReturningMismatch {
7379                                expected: rows.len(),
7380                                actual: _returned.len(),
7381                            }
7382                        )
7383                    );
7384                }
7385                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7386                    #auto_assigns_for_row
7387                }
7388            }
7389        };
7390        // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7391        let first_auto_ident = fields
7392            .first_db_auto
7393            .as_ref()
7394            .map(|(ident, _)| ident)
7395            .or(fields.first_auto_ident.as_ref())
7396            .expect("has_auto implies first_auto_ident is Some");
7397        quote! {
7398            /// Bulk-insert `rows` in a single round-trip. Every row's
7399            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7400            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7401            /// (caller-supplied values). Mixed Set/Unset is rejected
7402            /// — call `insert` per row for that case.
7403            ///
7404            /// Empty slice is a no-op. Each row's `Auto` fields are
7405            /// populated from the `RETURNING` clause in input order
7406            /// before this returns.
7407            ///
7408            /// # Errors
7409            /// Returns [`#root::sql::ExecError`] for validation,
7410            /// SQL-writing, mixed-Auto rejection, or driver failures.
7411            #[cfg(feature = "postgres")]
7412            pub async fn bulk_insert(
7413                rows: &mut [Self],
7414                pool: &#root::sql::sqlx::PgPool,
7415            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7416                #pool_to_bulk_insert_on
7417            }
7418
7419            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7420            /// See [`Self::save_on`] for tenancy-scoped rationale.
7421            ///
7422            /// # Errors
7423            /// As [`Self::bulk_insert`].
7424            #[cfg(feature = "postgres")]
7425            pub async fn bulk_insert_on #executor_generics (
7426                rows: &mut [Self],
7427                #executor_param,
7428            ) -> ::core::result::Result<(), #root::sql::ExecError>
7429            #executor_where
7430            {
7431                if rows.is_empty() {
7432                    return ::core::result::Result::Ok(());
7433                }
7434                let _first_unset = matches!(
7435                    rows[0].#first_auto_ident,
7436                    #root::sql::Auto::Unset
7437                );
7438                #( #uniformity )*
7439                for _row in rows.iter_mut() {
7440                    #( #fill_in_place )*
7441                }
7442
7443                let mut _all_rows: ::std::vec::Vec<
7444                    ::std::vec::Vec<#root::core::SqlValue>,
7445                > = ::std::vec::Vec::with_capacity(rows.len());
7446                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7447                    for _row in rows.iter() {
7448                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7449                            ::std::vec::Vec::new();
7450                        #( #pushes_no_auto )*
7451                        _all_rows.push(_row_vals);
7452                    }
7453                    ::std::vec![ #( #cols_no_auto ),* ]
7454                } else {
7455                    for _row in rows.iter() {
7456                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7457                            ::std::vec::Vec::new();
7458                        #( #pushes_all )*
7459                        _all_rows.push(_row_vals);
7460                    }
7461                    ::std::vec![ #( #cols_all ),* ]
7462                };
7463
7464                let _query = #root::core::BulkInsertQuery::new(
7465                    <Self as #root::core::Model>::SCHEMA,
7466                    _columns,
7467                    _all_rows,
7468                )
7469                .returning(::std::vec![ #( #returning_cols ),* ]);
7470                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7471                    #executor_passes_to_data_write,
7472                    &_query,
7473                ).await?;
7474                #read_back
7475                #audit_bulk_insert_emit
7476                ::core::result::Result::Ok(())
7477            }
7478        }
7479    } else {
7480        let cols_all = &fields.bulk_columns_all;
7481        let pushes_all = &fields.bulk_pushes_all;
7482        quote! {
7483            /// Bulk-insert `rows` in a single round-trip. Every row's
7484            /// fields are written verbatim — there are no `Auto<T>`
7485            /// fields on this model.
7486            ///
7487            /// Empty slice is a no-op.
7488            ///
7489            /// # Errors
7490            /// Returns [`#root::sql::ExecError`] for validation,
7491            /// SQL-writing, or driver failures.
7492            #[cfg(feature = "postgres")]
7493            pub async fn bulk_insert(
7494                rows: &[Self],
7495                pool: &#root::sql::sqlx::PgPool,
7496            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7497                #pool_to_bulk_insert_on
7498            }
7499
7500            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7501            /// See [`Self::save_on`] for tenancy-scoped rationale.
7502            ///
7503            /// # Errors
7504            /// As [`Self::bulk_insert`].
7505            #[cfg(feature = "postgres")]
7506            pub async fn bulk_insert_on #executor_generics (
7507                rows: &[Self],
7508                #executor_param,
7509            ) -> ::core::result::Result<(), #root::sql::ExecError>
7510            #executor_where
7511            {
7512                if rows.is_empty() {
7513                    return ::core::result::Result::Ok(());
7514                }
7515                let mut _all_rows: ::std::vec::Vec<
7516                    ::std::vec::Vec<#root::core::SqlValue>,
7517                > = ::std::vec::Vec::with_capacity(rows.len());
7518                for _row in rows.iter() {
7519                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7520                        ::std::vec::Vec::new();
7521                    #( #pushes_all )*
7522                    _all_rows.push(_row_vals);
7523                }
7524                let _query = #root::core::BulkInsertQuery::new(
7525                    <Self as #root::core::Model>::SCHEMA,
7526                    ::std::vec![ #( #cols_all ),* ],
7527                    _all_rows,
7528                );
7529                let _ = #root::sql::__macro_internals::bulk_insert_on(
7530                    #executor_passes_to_data_write,
7531                    &_query,
7532                ).await?;
7533                #audit_bulk_insert_emit
7534                ::core::result::Result::Ok(())
7535            }
7536        }
7537    };
7538
7539    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7540    // (no postgres-feature gate); routes through the existing
7541    // `bulk_insert_pool` + per-dialect conflict writer.
7542    //
7543    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7544    // sequence picks the PK for fresh inserts; the UPDATE branch never
7545    // touches the Auto column.
7546    // No RETURNING on MySQL, so which rows landed is unknown: refuse on
7547    // audited models rather than write unaudited (#1747).
7548    let refuse_unaudited_bulk_insert = quote! {
7549        if <Self as #root::core::Model>::SCHEMA.audit_track.is_some() {
7550            return ::core::result::Result::Err(#root::sql::ExecError::AuditUnsupported {
7551                table: <Self as #root::core::Model>::SCHEMA.table,
7552                reason: "conflict-handling bulk inserts cannot tell which rows landed",
7553            });
7554        }
7555    };
7556    let bulk_upsert_pool_method = {
7557        // Pick the "no Auto" columns when the model has Auto fields,
7558        // else every column.
7559        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7560            (
7561                fields.bulk_columns_no_auto.clone(),
7562                fields.bulk_pushes_no_auto.clone(),
7563            )
7564        } else {
7565            (
7566                fields.bulk_columns_all.clone(),
7567                fields.bulk_pushes_all.clone(),
7568            )
7569        };
7570        quote! {
7571            /// Tri-dialect bulk insert that updates on conflict — the
7572            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7573            ///
7574            /// Per-row values are extracted and lowered into a
7575            /// [`#root::core::BulkInsertQuery`] with
7576            /// `on_conflict = DoUpdate { target, update_columns }`. The
7577            /// writer dispatches per-dialect:
7578            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7579            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7580            ///
7581            /// `target` names the column(s) whose unique constraint
7582            /// defines the conflict (typically a `unique` or
7583            /// `unique_together` natural-key column, NOT the `Auto<T>`
7584            /// PK). `update_cols` names the columns to overwrite on
7585            /// conflict — every other column is left untouched on the
7586            /// existing row.
7587            ///
7588            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7589            /// picks the PK on insert; the update path never touches
7590            /// the Auto column). Auto-set rows trigger a hard error.
7591            /// Empty slice is a no-op.
7592            ///
7593            /// # Errors
7594            /// Returns [`#root::sql::ExecError`] for validation,
7595            /// SQL-writing, or driver failures.
7596            pub async fn bulk_upsert_pool(
7597                rows: &[Self],
7598                target: &[&'static str],
7599                update_cols: &[&'static str],
7600                pool: &#root::sql::Pool,
7601            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7602                #refuse_unaudited_bulk_insert
7603                if rows.is_empty() {
7604                    return ::core::result::Result::Ok(());
7605                }
7606                let mut _all_rows: ::std::vec::Vec<
7607                    ::std::vec::Vec<#root::core::SqlValue>,
7608                > = ::std::vec::Vec::with_capacity(rows.len());
7609                for _row in rows.iter() {
7610                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7611                        ::std::vec::Vec::new();
7612                    #( #upsert_pushes )*
7613                    _all_rows.push(_row_vals);
7614                }
7615                let _query = #root::core::BulkInsertQuery::new(
7616                    <Self as #root::core::Model>::SCHEMA,
7617                    ::std::vec![ #( #upsert_cols ),* ],
7618                    _all_rows,
7619                )
7620                .on_conflict_do_update(target, update_cols);
7621                #root::sql::bulk_insert_pool(pool, &_query).await
7622            }
7623
7624            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7625            /// skip rows that would violate a unique constraint. Issue
7626            /// #267 / T1.5. Same per-dialect dispatch as
7627            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7628            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7629            /// <pivot> = <pivot>` (MySQL no-op write).
7630            ///
7631            /// # Errors
7632            /// As [`Self::bulk_upsert_pool`].
7633            pub async fn bulk_insert_or_ignore_pool(
7634                rows: &[Self],
7635                pool: &#root::sql::Pool,
7636            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7637                #refuse_unaudited_bulk_insert
7638                if rows.is_empty() {
7639                    return ::core::result::Result::Ok(());
7640                }
7641                let mut _all_rows: ::std::vec::Vec<
7642                    ::std::vec::Vec<#root::core::SqlValue>,
7643                > = ::std::vec::Vec::with_capacity(rows.len());
7644                for _row in rows.iter() {
7645                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7646                        ::std::vec::Vec::new();
7647                    #( #upsert_pushes )*
7648                    _all_rows.push(_row_vals);
7649                }
7650                let _query = #root::core::BulkInsertQuery::new(
7651                    <Self as #root::core::Model>::SCHEMA,
7652                    ::std::vec![ #( #upsert_cols ),* ],
7653                    _all_rows,
7654                )
7655                .on_conflict_do_nothing();
7656                #root::sql::bulk_insert_pool(pool, &_query).await
7657            }
7658        }
7659    };
7660
7661    // Ergonomic `Model::bulk_update(objs, fields)`.
7662    // The SQL/IR/executor stack
7663    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7664    // `write_bulk_update_*` writers) already existed; what was missing
7665    // was the per-model constructor that maps `&[Self]` + a runtime
7666    // column list into rows of `[pk, col_vals…]` so callers don't
7667    // hand-build the IR. Emitted only when the model has a primary key
7668    // (the PK is the join key and can't itself be updated).
7669    let bulk_update_rows_run = if audited_fields.is_some() {
7670        quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7671            _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7672        })
7673        .await)
7674    } else {
7675        quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7676    };
7677    let bulk_update_method = match &fields.primary_key {
7678        None => quote! {},
7679        Some((pk_ident, pk_col)) => {
7680            // One pair of match arms per non-PK column: a validation arm
7681            // resolving the runtime name to its `&'static str` column,
7682            // and a value arm pushing that field off the row.
7683            let mut col_arms: Vec<TokenStream2> = Vec::new();
7684            let mut val_arms: Vec<TokenStream2> = Vec::new();
7685            for entry in &fields.column_entries {
7686                if &entry.column == pk_col {
7687                    continue;
7688                }
7689                let col = &entry.column;
7690                let ident = &entry.ident;
7691                col_arms.push(quote! { #col => #col, });
7692                val_arms.push(quote! {
7693                    #col => _row_vals.push(
7694                        ::core::convert::Into::<#root::core::SqlValue>::into(
7695                            ::core::clone::Clone::clone(&_o.#ident)
7696                        )
7697                    ),
7698                });
7699            }
7700            quote! {
7701                /// Write per-row-different values for the named `fields`
7702                /// across every object in `objs` in a single statement,
7703                /// matched by primary key.
7704                ///
7705                /// `fields` names the **columns** to update. The primary
7706                /// key identifies each row and cannot itself be updated
7707                /// (pass it and you get
7708                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7709                /// Empty `objs` or `fields` is a no-op returning `0`.
7710                /// Objects whose PK matches no row are simply not updated.
7711                /// Returns the number of rows affected.
7712                ///
7713                /// Tri-dialect: lowers to one
7714                /// [`#root::core::BulkUpdateQuery`] and dispatches
7715                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7716                /// a CTE + correlated subquery on SQLite, an inner
7717                /// `JOIN (VALUES …)` on MySQL.
7718                ///
7719                /// # Errors
7720                /// [`#root::core::QueryError::UnknownField`] for a name
7721                /// that isn't a column on this model,
7722                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7723                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7724                /// SQL-writing / driver failures.
7725                pub async fn bulk_update(
7726                    objs: &[Self],
7727                    fields: &[&str],
7728                    pool: &#root::sql::Pool,
7729                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7730                    if objs.is_empty() || fields.is_empty() {
7731                        return ::core::result::Result::Ok(0);
7732                    }
7733                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7734                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7735                        ::std::vec::Vec::with_capacity(fields.len());
7736                    for &_f in fields {
7737                        let _col: &'static str = match _f {
7738                            #pk_col => {
7739                                return ::core::result::Result::Err(
7740                                    ::core::convert::Into::into(
7741                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7742                                            model: _model_name,
7743                                            field: ::std::string::ToString::to_string(_f),
7744                                        }
7745                                    )
7746                                );
7747                            }
7748                            #( #col_arms )*
7749                            _ => {
7750                                return ::core::result::Result::Err(
7751                                    ::core::convert::Into::into(
7752                                        #root::core::QueryError::UnknownField {
7753                                            model: _model_name,
7754                                            field: ::std::string::ToString::to_string(_f),
7755                                        }
7756                                    )
7757                                );
7758                            }
7759                        };
7760                        // A model whose only field is the primary key emits no
7761                        // non-PK arms, so every arm above diverges and this push
7762                        // is genuinely unreachable — correct, since such a model
7763                        // has nothing to bulk-update. Scoped to the statement
7764                        // rather than the fn so a real unreachability elsewhere
7765                        // in this body still warns. Without it every PK-only
7766                        // model warns in the *user's* crate, which they cannot
7767                        // act on (#1290).
7768                        #[allow(unreachable_code)]
7769                        _update_columns.push(_col);
7770                    }
7771                    let mut _rows: ::std::vec::Vec<
7772                        ::std::vec::Vec<#root::core::SqlValue>,
7773                    > = ::std::vec::Vec::with_capacity(objs.len());
7774                    for _o in objs.iter() {
7775                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7776                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7777                        // PK first — the writers expect `[pk, …update cols]`.
7778                        _row_vals.push(
7779                            ::core::convert::Into::<#root::core::SqlValue>::into(
7780                                ::core::clone::Clone::clone(&_o.#pk_ident)
7781                            )
7782                        );
7783                        for &_f in fields {
7784                            match _f {
7785                                #( #val_arms )*
7786                                // Unreachable: every name was validated
7787                                // against the same arm set above.
7788                                _ => {}
7789                            }
7790                        }
7791                        _rows.push(_row_vals);
7792                    }
7793                    let _query = #root::core::BulkUpdateQuery::new(
7794                        <Self as #root::core::Model>::SCHEMA,
7795                        _update_columns,
7796                        _rows,
7797                    );
7798                    #bulk_update_rows_run
7799                }
7800            }
7801        }
7802    };
7803
7804    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7805        quote! {
7806            /// Hidden runtime accessor for the primary-key value as a
7807            /// [`SqlValue`]. Used by reverse-relation helpers
7808            /// (`<parent>::<child>_set`) emitted from sibling models'
7809            /// FK fields. Not part of the public API.
7810            #[doc(hidden)]
7811            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7812                ::core::convert::Into::<#root::core::SqlValue>::into(
7813                    ::core::clone::Clone::clone(&self.#pk_ident)
7814                )
7815            }
7816        }
7817    });
7818
7819    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7820        quote! {
7821            impl #root::sql::HasPkValue for #struct_name {
7822                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7823                    ::core::convert::Into::<#root::core::SqlValue>::into(
7824                        ::core::clone::Clone::clone(&self.#pk_ident)
7825                    )
7826                }
7827            }
7828        }
7829    });
7830
7831    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7832
7833    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7834    // dispatch to the right per-backend body without the macro emitting
7835    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7836    // so audited models with non-Auto PKs (which still go through
7837    // `insert_one_with_audit` → `apply_auto_pk`) link.
7838    let assign_auto_pk_pool_impl = {
7839        let auto_assigns = &fields.auto_assigns;
7840        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7841        // so the body is structurally identical to `auto_assigns` —
7842        // only the helper name swaps from `try_get_returning` to
7843        // `try_get_returning_sqlite` so the closure typechecks against
7844        // a `SqliteRow` instead of a `PgRow`.
7845        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7846            .auto_field_idents
7847            .iter()
7848            .map(|(ident, column)| {
7849                quote! {
7850                    self.#ident = #root::sql::try_get_returning_sqlite(
7851                        _returning_row, #column
7852                    )?;
7853                }
7854            })
7855            .collect();
7856        // #1028 — decode each `generated_as` column from the same
7857        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7858        // own type drives the decode (no `Auto<T>` wrapper).
7859        let generated_assigns: Vec<TokenStream2> = fields
7860            .generated_field_idents
7861            .iter()
7862            .map(|(ident, column)| {
7863                quote! {
7864                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7865                }
7866            })
7867            .collect();
7868        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7869            .generated_field_idents
7870            .iter()
7871            .map(|(ident, column)| {
7872                quote! {
7873                    self.#ident = #root::sql::try_get_returning_sqlite(
7874                        _returning_row, #column
7875                    )?;
7876                }
7877            })
7878            .collect();
7879        let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
7880            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7881            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7882            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7883            // those models can't use AUTO_INCREMENT). The trait is only
7884            // touched on the MySQL arm at runtime, so PG-only consumers
7885            // never see the bound failure.
7886            //
7887            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7888            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7889            // runtime with "multi-column RETURNING". MySQL has no
7890            // multi-column RETURNING semantic and a follow-up SELECT
7891            // would need cross-trait plumbing. Pragmatic shape: succeed
7892            // with the FIRST Auto field populated from LAST_INSERT_ID();
7893            // any other Auto fields stay `Auto::Unset`. Callers that
7894            // need the DB-defaulted timestamp / UUID can re-fetch the
7895            // row by PK after `save_pool`. Fixes the cookbook chapter
7896            // 12 dialect divergence.
7897            quote! {
7898                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7899                    ::rustango_from_mysql_auto_id(_id)?;
7900                self.#first = #root::sql::Auto::Set(_converted);
7901                ::core::result::Result::Ok(())
7902            }
7903        } else {
7904            quote! {
7905                let _ = _id;
7906                ::core::result::Result::Ok(())
7907            }
7908        };
7909        quote! {
7910            impl #root::sql::AssignAutoPkPool for #struct_name {
7911                fn __rustango_assign_from_pg_row(
7912                    &mut self,
7913                    _returning_row: &#root::sql::PgReturningRow,
7914                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7915                    #( #auto_assigns )*
7916                    #( #generated_assigns )*
7917                    ::core::result::Result::Ok(())
7918                }
7919                fn __rustango_assign_from_mysql_id(
7920                    &mut self,
7921                    _id: i64,
7922                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7923                    #mysql_body
7924                }
7925                fn __rustango_assign_from_sqlite_row(
7926                    &mut self,
7927                    _returning_row: &#root::sql::SqliteReturningRow,
7928                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7929                    #( #auto_assigns_sqlite )*
7930                    #( #generated_assigns_sqlite )*
7931                    ::core::result::Result::Ok(())
7932                }
7933            }
7934        }
7935    };
7936
7937    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7938    let aliased_row_helper = quote! {
7939        /// Decode a row's aliased target columns (produced by
7940        /// `select_related`'s LEFT JOIN) into a fresh instance of
7941        /// this model. Reads each column via
7942        /// `format!("{prefix}__{col}")`, matching the alias the
7943        /// SELECT writer emitted. Slice 9.0d.
7944        #[doc(hidden)]
7945        #[cfg(feature = "postgres")]
7946        pub fn __rustango_from_aliased_row(
7947            row: &#root::sql::sqlx::postgres::PgRow,
7948            prefix: &str,
7949        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7950            ::core::result::Result::Ok(Self {
7951                #( #from_aliased_row_inits ),*
7952            })
7953        }
7954    };
7955    // v0.23.0-batch8 — MySQL counterpart, gated through the
7956    // cfg-aware macro_rules so PG-only builds expand to nothing.
7957    let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
7958    let aliased_row_helper_my = quote! {
7959        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7960            #( #from_aliased_row_inits_my ),*
7961        });
7962    };
7963
7964    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7965    // pattern + cfg gate on the `sqlite` feature.
7966    let aliased_row_helper_sqlite = quote! {
7967        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7968            #( #from_aliased_row_inits ),*
7969        });
7970    };
7971
7972    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7973    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7974    let load_related_impl_sqlite =
7975        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7976
7977    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7978    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7979    // default `Self::objects()`. Each accessor returns a fresh
7980    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7981    // methods the user defined.
7982    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7983        .iter()
7984        .map(|fn_ident| {
7985            let model_name_str = struct_name.to_string();
7986            let fn_name_str = fn_ident.to_string();
7987            let doc = format!(
7988                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7989                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7990                 equivalent to `Self::objects()`. Chains with any \
7991                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7992                 extension methods."
7993            );
7994            quote! {
7995                #[doc = #doc]
7996                #[must_use]
7997                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7998                    #root::query::QuerySet::new()
7999                }
8000            }
8001        })
8002        .collect();
8003
8004    quote! {
8005        impl #struct_name {
8006            /// Start a new `QuerySet` over this model.
8007            #[must_use]
8008            pub fn objects() -> #root::query::QuerySet<#struct_name> {
8009                #root::query::QuerySet::new()
8010            }
8011
8012            /// Eloquent-shape alias of [`Self::objects`]. Returns
8013            /// a fresh `QuerySet<Self>` ready for `.filter()` /
8014            /// `.where_()` / etc. Matches Laravel muscle-memory:
8015            ///
8016            /// ```ignore
8017            /// // Eloquent:    Post::query()->where('published', true)
8018            /// // rustango:    Post::query().filter("published", true)
8019            /// //         or:  Post::objects().filter("published", true)
8020            /// ```
8021            ///
8022            /// Both names point at the same underlying constructor;
8023            /// neither is preferred.
8024            #[must_use]
8025            pub fn query() -> #root::query::QuerySet<#struct_name> {
8026                #root::query::QuerySet::new()
8027            }
8028
8029            #( #extra_manager_fns )*
8030
8031            #insert_method
8032
8033            #bulk_insert_method
8034
8035            #bulk_upsert_pool_method
8036
8037            #bulk_update_method
8038
8039            #save_method
8040
8041            #pk_methods
8042
8043            #pk_value_helper
8044
8045            #aliased_row_helper
8046
8047            #column_consts
8048        }
8049
8050        #aliased_row_helper_my
8051
8052        #aliased_row_helper_sqlite
8053
8054        #load_related_impl
8055
8056        #load_related_impl_my
8057
8058        #load_related_impl_sqlite
8059
8060        #has_pk_value_impl
8061
8062        #fk_pk_access_impl
8063
8064        #assign_auto_pk_pool_impl
8065    }
8066}
8067
8068/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8069/// `auto_assigns` but reading from `_returning_row` and writing to
8070/// `_row_mut` instead of `self`.
8071fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8072    let root = rustango_root();
8073    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
8074        let col_lit = column.as_str();
8075        quote! {
8076            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8077                _returning_row,
8078                #col_lit,
8079            )?;
8080        }
8081    });
8082    quote! { #( #lines )* }
8083}
8084
8085/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8086fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8087    let lines = entries.iter().map(|e| {
8088        let ident = &e.ident;
8089        let col_ty = column_type_ident(ident);
8090        quote! {
8091            #[allow(non_upper_case_globals)]
8092            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8093        }
8094    });
8095    quote! { #(#lines)* }
8096}
8097
8098/// Emit a hidden per-model module carrying one zero-sized type per field,
8099/// each with a `Column` impl pointing back at the model.
8100fn column_module_tokens(
8101    module_ident: &syn::Ident,
8102    struct_name: &syn::Ident,
8103    entries: &[ColumnEntry],
8104) -> TokenStream2 {
8105    let root = rustango_root();
8106    let items = entries.iter().map(|e| {
8107        let col_ty = column_type_ident(&e.ident);
8108        let value_ty = &e.value_ty;
8109        let name = &e.name;
8110        let column = &e.column;
8111        let field_type_tokens = &e.field_type_tokens;
8112        quote! {
8113            #[derive(::core::clone::Clone, ::core::marker::Copy)]
8114            pub struct #col_ty;
8115
8116            impl #root::core::Column for #col_ty {
8117                type Model = super::#struct_name;
8118                type Value = #value_ty;
8119                const NAME: &'static str = #name;
8120                const COLUMN: &'static str = #column;
8121                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8122            }
8123        }
8124    });
8125    quote! {
8126        #[doc(hidden)]
8127        #[allow(non_camel_case_types, non_snake_case)]
8128        pub mod #module_ident {
8129            // Re-import the parent scope so field types referencing
8130            // sibling models (e.g. `ForeignKey<Author>`) resolve
8131            // inside this submodule. Without this we'd hit
8132            // `proc_macro_derive_resolution_fallback` warnings.
8133            #[allow(unused_imports)]
8134            use super::*;
8135            #(#items)*
8136        }
8137    }
8138}
8139
8140fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8141    let name = ident_name(field_ident);
8142    syn::Ident::new(&format!("{name}_col"), field_ident.span())
8143}
8144
8145/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8146fn ident_name(ident: &syn::Ident) -> String {
8147    syn::ext::IdentExt::unraw(ident).to_string()
8148}
8149
8150/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8151fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8152    match syn::parse_str::<syn::Ident>(name) {
8153        Ok(mut ident) => {
8154            ident.set_span(span);
8155            ident
8156        }
8157        Err(_) => syn::Ident::new_raw(name, span),
8158    }
8159}
8160
8161fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8162    syn::Ident::new(
8163        &format!("__rustango_cols_{struct_name}"),
8164        struct_name.span(),
8165    )
8166}
8167
8168fn from_row_impl_tokens(
8169    struct_name: &syn::Ident,
8170    from_row_inits: &[TokenStream2],
8171    from_row_inits_my: &[TokenStream2],
8172) -> TokenStream2 {
8173    let root = rustango_root();
8174    // The Postgres impl is always emitted — every rustango build pulls in
8175    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8176    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8177    // macro_rules whose body collapses to nothing when rustango is built
8178    // without the `mysql` feature. No user-facing feature shim required.
8179    //
8180    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8181    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8182    // back into a comma-separated list inside square brackets. Since each
8183    // entry is already in `field: expr` shape, the existing tokens slot in.
8184    quote! {
8185        #[cfg(feature = "postgres")]
8186        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8187            for #struct_name
8188        {
8189            fn from_row(
8190                row: &'r #root::sql::sqlx::postgres::PgRow,
8191            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8192                ::core::result::Result::Ok(Self {
8193                    #( #from_row_inits ),*
8194                })
8195            }
8196        }
8197
8198        #root::__impl_my_from_row!(#struct_name, |row| {
8199            #( #from_row_inits_my ),*
8200        });
8201
8202        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8203            #( #from_row_inits ),*
8204        });
8205    }
8206}
8207
8208struct ContainerAttrs {
8209    table: Option<String>,
8210    display: Option<(String, proc_macro2::Span)>,
8211    /// Explicit app label from `#[rustango(app = "blog")]`.
8212    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8213    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8214    /// at runtime — this attribute is the override for cases where
8215    /// the inference is wrong (e.g. a model that conceptually belongs
8216    /// to one app but is physically in another module).
8217    app: Option<String>,
8218    /// Per-model admin knobs from
8219    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8220    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8221    /// admin code falls back to `AdminConfig::DEFAULT`.
8222    admin: Option<AdminAttrs>,
8223    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8224    /// `None` when the model isn't audited — write paths emit no
8225    /// audit entries. When present, single-row writes capture
8226    /// before/after for the listed fields and bulk writes batch
8227    /// snapshots into one INSERT into `rustango_audit_log`.
8228    audit: Option<AuditAttrs>,
8229    /// `true` when `#[rustango(permissions)]` is present. Signals that
8230    /// `auto_create_permissions` should seed the four CRUD codenames for
8231    /// this model.
8232    permissions: bool,
8233    /// Many-to-many relations declared via
8234    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8235    ///                 src = "post_id", dst = "tag_id"))]`.
8236    m2m: Vec<M2MAttr>,
8237    /// Polymorphic M2M relations declared via
8238    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8239    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8240    ///   related_column = "tag_id"))]` (issue #818).
8241    generic_m2m: Vec<GenericM2MAttr>,
8242    /// Composite indexes declared via
8243    /// `#[rustango(index("col1, col2"))]` or
8244    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8245    /// Single-column indexes from `#[rustango(index)]` on fields are
8246    /// accumulated here during field collection.
8247    indexes: Vec<IndexAttr>,
8248    /// Table-level CHECK constraints declared via
8249    /// `#[rustango(check(name = "…", expr = "…"))]`.
8250    checks: Vec<CheckAttr>,
8251    /// Table-level PG `EXCLUDE` constraints declared via
8252    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8253    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8254    /// migration writer renders them on Postgres and skips with a
8255    /// warning on MySQL/SQLite. Issue #319.
8256    excludes: Vec<ExcludeAttr>,
8257    /// Composite (multi-column) FKs declared via
8258    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8259    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8260    composite_fks: Vec<CompositeFkAttr>,
8261    /// Generic ("any model") FKs declared via
8262    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8263    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8264    generic_fks: Vec<GenericFkAttr>,
8265    /// Where this model lives in a tenancy deployment, declared via
8266    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8267    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8268    /// to partition diff output between registry-scoped and
8269    /// tenant-scoped migration files.
8270    scope: Option<String>,
8271    /// Custom-Manager extension-trait name from
8272    /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8273    /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8274    /// adjacent to the model so users can write
8275    /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8276    /// and discover the convention from the model definition.
8277    manager_ext: Option<syn::Ident>,
8278    /// Extra QuerySet accessor names from
8279    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8280    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8281    /// the default `Self::objects()`. Multiple attributes allowed.
8282    manager_fns: Vec<syn::Ident>,
8283    /// Default ordering declared via `#[rustango(default_order =
8284    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8285    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8286    /// `-` prefix means descending; the `+` prefix or no prefix means
8287    /// ascending.
8288    default_order: Vec<(String, bool, proc_macro2::Span)>,
8289    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8290    /// Routes the emitted schema's `is_view = true` so the migration
8291    /// snapshot skips this model (its underlying SQL view is operator-
8292    /// managed, not rustango-managed).
8293    is_view: bool,
8294    /// `#[rustango(managed = false)]` — who owns the table.
8295    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8296    /// the migration snapshot skips this model so `makemigrations` /
8297    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8298    /// TABLE` against it (operator-managed schema).
8299    managed: bool,
8300    /// Name of the model's base manager, from
8301    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8302    /// `ModelSchema::base_manager_name`. Declarative-only today.
8303    base_manager_name: Option<String>,
8304    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8305    /// field this model's instances are ordered relative to.
8306    /// Declarative-only today; threaded onto
8307    /// `ModelSchema::order_with_respect_to`.
8308    order_with_respect_to: Option<String>,
8309    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8310    /// Marks the model as a proxy that
8311    /// shares its DB table with another struct. Threaded into
8312    /// `ModelSchema::proxy` so future codegen can skip table-owning
8313    /// behavior for proxies.
8314    proxy: bool,
8315    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8316    /// Each comma-separated capability token surfaces on
8317    /// `ModelSchema::required_db_features` so `manage check --deploy`
8318    /// can warn when the active dialect lacks one.
8319    required_db_features: Vec<String>,
8320    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8321    /// Normalized to the dialect name `manage check --deploy`
8322    /// compares against `Settings.database.backend`. Aliases
8323    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8324    /// stored under the canonical name.
8325    required_db_vendor: Option<String>,
8326    /// `#[rustango(default_related_name = "...")]`. Threaded into
8327    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8328    /// name to use when an FK / M2M field doesn't override it.
8329    /// Today rustango doesn't auto-emit reverse managers; the
8330    /// metadata is the foundation for that work.
8331    default_related_name: Option<String>,
8332    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8333    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8334    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8335    /// inline `COMMENT='...'`, SQLite: no-op).
8336    db_table_comment: Option<String>,
8337    /// `#[rustango(get_latest_by = "created_at")]` /
8338    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8339    /// `(column, descending)` where `descending = true` when the
8340    /// attribute value starts with `-`. Threaded into
8341    /// `ModelSchema::get_latest_by`.
8342    get_latest_by: Option<(String, bool)>,
8343    /// Extra `(codename, label)` permissions
8344    /// from `#[rustango(extra_permissions = "approve:Can approve,
8345    /// archive:Can archive")]`. Comma-separated `codename:label`
8346    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8347    extra_permissions: Vec<(String, String)>,
8348    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8349    /// `"view"`) the framework
8350    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8351    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8352    /// Validated at parse time; unknown actions fail with a span-pointing
8353    /// error.
8354    default_permissions: Vec<String>,
8355    /// `#[rustango(verbose_name = "blog post")]` — the
8356    /// human-readable singular label for the model. Threaded into
8357    /// `ModelSchema::verbose_name` so admin section headers /
8358    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8359    /// over the Rust struct identifier.
8360    verbose_name: Option<String>,
8361    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8362    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8363    /// When unset, `display_label_plural()` auto-suffixes `s`.
8364    verbose_name_plural: Option<String>,
8365    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8366    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8367    /// a name (used by `QuerySet::without_global_scope`) with a
8368    /// `fn() -> WhereExpr` path that the macro emits into
8369    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8370    global_scopes: Vec<GlobalScopeAttr>,
8371    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8372    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8373    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8374    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8375    /// entry emits an inherent `<name>_through(&self)` accessor that
8376    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8377    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8378    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8379    through_relations: Vec<ThroughAttr>,
8380    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8381    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8382    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8383    /// Each entry emits two associated functions on the parent —
8384    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8385    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8386    /// over a correlated subquery against the child table. Users
8387    /// drop the result into `QuerySet::where_raw(...)`.
8388    reverse_has_relations: Vec<ReverseHasAttr>,
8389    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8390    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8391    /// entry so the relation-existence family resolves polymorphic
8392    /// children by name.
8393    generic_has_relations: Vec<GenericHasAttr>,
8394}
8395
8396/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8397/// declaration. Each entry becomes one `core::GlobalScope` in the
8398/// emitted schema literal; `apply` resolves at macro-expand time
8399/// against the consumer's scope so the function must be in scope at
8400/// the use site. Issue #820.
8401struct GlobalScopeAttr {
8402    name: String,
8403    apply: syn::Path,
8404}
8405
8406/// Parsed `#[rustango(through(...))]` declaration. Issue
8407/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8408/// `hasManyThrough` / `hasOneThrough` parity.
8409///
8410/// `Country hasManyThrough Post via User` declares as:
8411///
8412/// ```ignore
8413/// #[rustango(through(
8414///     name                   = "posts",
8415///     far                    = "Post",
8416///     far_fk_column          = "author_id",
8417///     intermediate           = "User",
8418///     intermediate_fk_column = "country_id",
8419/// ))]
8420/// struct Country { ... }
8421/// ```
8422///
8423/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8424/// which returns a queryset filtered via a correlated subquery —
8425/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8426/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8427/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8428/// because the subquery lives inside a `WhereExpr::InSubquery` node
8429/// and the outer queryset's pending list stays empty.
8430///
8431/// All four required arguments use **SQL column / table names**
8432/// (not Rust field names) to sidestep the multi-hop-filter substrate
8433/// gap. Once that substrate lands, a higher-level Rust-field-name
8434/// shorthand can be added without breaking this surface.
8435struct ThroughAttr {
8436    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8437    name: String,
8438    /// Far model type identifier. `far = "Post"` → returns
8439    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8440    /// the derive expands.
8441    far: syn::Ident,
8442    /// SQL column on the far model's table that references the
8443    /// intermediate model's primary key. For `Post`'s
8444    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8445    /// default `<field>_id` convention) or whatever the user
8446    /// declared via `#[rustango(db_column = "...")]`.
8447    far_fk_column: String,
8448    /// Intermediate model type identifier. Needed to look up its
8449    /// `SCHEMA` so the subquery's `FROM` clause points at the
8450    /// intermediate table. `intermediate = "User"`.
8451    intermediate: syn::Ident,
8452    /// SQL column on the intermediate's table that references the
8453    /// source (this) model's primary key. For `User`'s
8454    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8455    intermediate_fk_column: String,
8456    /// SQL primary-key column on the intermediate's table — the
8457    /// column the subquery projects. Optional; defaults to `"id"`
8458    /// (rustango's default PK column name). Override when the
8459    /// intermediate declares a custom PK column.
8460    intermediate_pk_column: String,
8461}
8462
8463/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8464/// child_fk_column = "..."))]` declaration. Issue
8465/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8466/// `whereHas` / `whereDoesntHave` parity.
8467///
8468/// `Post hasMany Comment` declares as:
8469///
8470/// ```ignore
8471/// #[rustango(reverse_has(
8472///     name             = "comments",
8473///     child            = "Comment",
8474///     child_fk_column  = "post_id",
8475/// ))]
8476/// struct Post { ... }
8477/// ```
8478///
8479/// The macro emits two associated functions on `Post`:
8480///
8481/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8482///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8483///   <outer>.<self_pk_column>)`.
8484/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8485///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8486///
8487/// User code:
8488///
8489/// ```ignore
8490/// // Posts with at least one comment:
8491/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8492/// // Posts with no comments:
8493/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8494/// ```
8495///
8496/// As with #817, all column / table identifiers are **SQL names**
8497/// (not Rust field names) so the substrate is independent of the
8498/// outstanding multi-hop filter resolver gap. The emitted
8499/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8500/// SQL-emit time via the writer's scope stack.
8501struct ReverseHasAttr {
8502    /// Accessor name. `name = "comments"` → emits
8503    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8504    name: String,
8505    /// Child model type identifier. `child = "Comment"` — needed to
8506    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8507    /// points at the child table.
8508    child: syn::Ident,
8509    /// SQL column on the child's table that references this model's
8510    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8511    /// column is `"post_id"`.
8512    child_fk_column: String,
8513    /// SQL primary-key column on **this** model's table — the column
8514    /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8515    /// (rustango's default PK column name).
8516    self_pk_column: String,
8517}
8518
8519/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8520/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8521/// relation-existence family (issue #830). The child is a polymorphic,
8522/// content-type-discriminated model; emits a `Model::
8523/// generic_reverse_relations()` entry the queryset resolves by name.
8524struct GenericHasAttr {
8525    /// Accessor name, e.g. `name = "tags"`.
8526    name: String,
8527    /// Child model type identifier (`child = "Tag"`) — looked up for its
8528    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8529    child: syn::Ident,
8530    /// Column on the child table holding the parent's content-type id.
8531    /// Optional; defaults to `"content_type_id"`.
8532    ct_column: String,
8533    /// Column on the child table holding the parent's PK value.
8534    /// Optional; defaults to `"object_pk"`.
8535    pk_column: String,
8536    /// SQL primary-key column on **this** (parent) model's table.
8537    /// Optional; defaults to `"id"`.
8538    self_pk_column: String,
8539}
8540
8541/// Parsed form of one index declaration (field-level or container-level).
8542struct IndexAttr {
8543    /// Index name; auto-derived when `None` at parse time.
8544    name: Option<String>,
8545    /// Column names in the index.
8546    columns: Vec<String>,
8547    /// `true` for `CREATE UNIQUE INDEX`.
8548    unique: bool,
8549    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8550    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8551    /// `"btree"` when the attribute is absent — the DDL writer omits
8552    /// the `USING` clause and the backend uses its own default
8553    /// (btree on every supported dialect).
8554    method: String,
8555    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8556    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8557    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8558    where_clause: Option<String>,
8559    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8560    /// Empty `Vec` (the default) means "no covering columns".
8561    include: Vec<String>,
8562}
8563
8564/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8565struct CheckAttr {
8566    name: String,
8567    expr: String,
8568}
8569
8570/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8571/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8572/// PG-only — surfaced on every backend in the macro emit; the migration
8573/// writer skips the op on MySQL/SQLite. Issue #319.
8574struct ExcludeAttr {
8575    /// Constraint name (free-form Rust identifier).
8576    name: String,
8577    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8578    using: String,
8579    /// Comma-separated `(column, operator)` pairs, in declaration
8580    /// order. Parsed from `"col WITH op, col WITH op"`.
8581    elements: Vec<(String, String)>,
8582    /// Optional `WHERE` predicate (raw SQL).
8583    where_clause: Option<String>,
8584}
8585
8586/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8587/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8588/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8589/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8590/// the model, not the field.
8591struct CompositeFkAttr {
8592    /// Logical relation name (free-form Rust identifier).
8593    name: String,
8594    /// SQL table name of the target.
8595    to: String,
8596    /// Source-side column names, in declaration order.
8597    from: Vec<String>,
8598    /// Target-side column names, same length / order as `from`.
8599    on: Vec<String>,
8600}
8601
8602/// Parsed form of one `#[rustango(generic_fk(name = "target",
8603/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8604/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8605/// generic ("any model") FKs live on the model, not the field.
8606struct GenericFkAttr {
8607    /// Logical relation name (free-form Rust identifier).
8608    name: String,
8609    /// Source-side column carrying the `content_type_id` value.
8610    ct_column: String,
8611    /// Source-side column carrying the target row's primary key.
8612    pk_column: String,
8613}
8614
8615/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8616struct M2MAttr {
8617    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8618    name: String,
8619    /// Target table (e.g. `"app_tags"`).
8620    to: String,
8621    /// Junction table (e.g. `"post_tags"`).
8622    through: String,
8623    /// Source FK column in the junction table (e.g. `"post_id"`).
8624    src: String,
8625    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8626    dst: String,
8627    /// Whether the migration writer should auto-create the junction
8628    /// table. Default `true`. Set `auto_create = false` (#324) when
8629    /// the operator declares the through table as its own
8630    /// `#[derive(Model)]` struct with extra columns.
8631    auto_create: bool,
8632}
8633
8634/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8635/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8636/// junction carries a ContentType discriminator so unrelated models
8637/// share one pivot + related set.
8638struct GenericM2MAttr {
8639    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8640    name: String,
8641    /// Polymorphic junction table (e.g. `"taggables"`).
8642    through: String,
8643    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8644    pk_column: String,
8645    /// Junction column holding the owning model's `content_type_id`
8646    /// discriminator (e.g. `"taggable_type"`).
8647    ct_column: String,
8648    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8649    related_column: String,
8650}
8651
8652/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8653/// "user"))]`. `track` is a comma-separated list of field names whose
8654/// before/after values land in the JSONB `changes` column. `source`
8655/// is informational only — it pins a default source when the model
8656/// is written outside any `audit::with_source(...)` scope (rare).
8657#[derive(Default)]
8658struct AuditAttrs {
8659    /// Field names to capture in the `changes` JSONB. Validated
8660    /// against declared scalar fields at compile time. Empty means
8661    /// "track every scalar field".
8662    track: Option<(Vec<String>, proc_macro2::Span)>,
8663}
8664
8665/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8666/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8667/// comma-separated strings; we validate each ident against the model's
8668/// declared fields at compile time.
8669#[derive(Default)]
8670struct AdminAttrs {
8671    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8672    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8673    list_per_page: Option<usize>,
8674    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8675    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8676    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8677    /// Bulk action names. No field-validation against model fields —
8678    /// these are action handlers, not column references.
8679    actions: Option<(Vec<String>, proc_macro2::Span)>,
8680    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8681    /// sections, comma-separated fields per section, optional
8682    /// `Title:` prefix. Empty title omits the `<legend>`.
8683    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8684    /// `admin(list_display_links = "title")`. Names
8685    /// from `list_display` whose cells should link to detail/edit.
8686    /// Issue #350.
8687    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8688    /// `admin(search_help_text = "...")`. Short
8689    /// caption rendered beside the admin list view's search box.
8690    /// Issue #353.
8691    search_help_text: Option<String>,
8692    /// `admin(actions_on_top = false)`. Hides the
8693    /// action-bar above the table. Default `true`. Issue #354.
8694    actions_on_top: Option<bool>,
8695    /// `admin(actions_on_bottom = true)`. Renders an
8696    /// additional action-bar below the table. Default `false`.
8697    /// Issue #354.
8698    actions_on_bottom: Option<bool>,
8699    /// `admin(date_hierarchy = "created_at")`. Name of
8700    /// a date / datetime field whose values render as a clickable
8701    /// year / month / day drill-down strip above the list table.
8702    /// Empty / unset disables the strip. Issue #355.
8703    date_hierarchy: Option<String>,
8704    /// `admin(prepopulated_fields = "slug:title")`.
8705    /// Each entry is `target:source[+source2]`; multiple entries are
8706    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8707    /// The admin change-form emits JS that slugifies the source values
8708    /// into the target field on every keystroke. Issue #356.
8709    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8710    /// `admin(raw_id_fields = "parent, owner")`. Names
8711    /// of FK fields whose change-form widget renders a lookup link
8712    /// next to the input. Issue #357.
8713    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8714    /// `admin(autocomplete_fields = "author_id")`.
8715    /// Names of FK fields whose change-form widget renders an
8716    /// Ajax-driven typeahead populated from a `__autocomplete`
8717    /// endpoint on the target model. Issue #358.
8718    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8719    /// `admin(list_select_related = "all" | "none" | "author, …")`
8720    /// — tunes the admin list view's FK auto-JOIN
8721    /// policy. Default `"all"` matches rustango's join-everything
8722    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8723    list_select_related: Option<String>,
8724    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8725    /// Each entry is `field_name:widget_name`; multiple
8726    /// entries comma-separated. Empty / unset → no overrides. The
8727    /// list of widget names supported is documented on
8728    /// `AdminConfig::formfield_overrides`. Issue #359.
8729    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8730}
8731
8732fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8733    let mut out = ContainerAttrs {
8734        table: None,
8735        display: None,
8736        app: None,
8737        admin: None,
8738        audit: None,
8739        // Default `permissions = true` so every `#[derive(Model)]`
8740        // gets the four CRUD codenames seeded by `auto_create_permissions`
8741        // and is visible to non-superusers in the tenant admin without
8742        // manual per-model annotation. Models that intentionally don't
8743        // want permission rows (registry-internal types, framework
8744        // tables operators shouldn't manage directly) opt out via
8745        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8746        // out-of-the-box admin invisibility regression (#62).
8747        permissions: true,
8748        m2m: Vec::new(),
8749        generic_m2m: Vec::new(),
8750        indexes: Vec::new(),
8751        checks: Vec::new(),
8752        excludes: Vec::new(),
8753        composite_fks: Vec::new(),
8754        generic_fks: Vec::new(),
8755        scope: None,
8756        manager_ext: None,
8757        manager_fns: Vec::new(),
8758        default_order: Vec::new(),
8759        is_view: false,
8760        managed: true,
8761        verbose_name: None,
8762        verbose_name_plural: None,
8763        base_manager_name: None,
8764        order_with_respect_to: None,
8765        proxy: false,
8766        required_db_features: Vec::new(),
8767        required_db_vendor: None,
8768        default_related_name: None,
8769        db_table_comment: None,
8770        get_latest_by: None,
8771        extra_permissions: Vec::new(),
8772        default_permissions: Vec::new(),
8773        global_scopes: Vec::new(),
8774        through_relations: Vec::new(),
8775        reverse_has_relations: Vec::new(),
8776        generic_has_relations: Vec::new(),
8777    };
8778    for attr in &input.attrs {
8779        if !attr.path().is_ident("rustango") {
8780            continue;
8781        }
8782        attr.parse_nested_meta(|meta| {
8783            if meta.path.is_ident("table") {
8784                let s: LitStr = meta.value()?.parse()?;
8785                let name = s.value();
8786                // v0.27.3 (#65) — macro-time guard against table names
8787                // that compile but break SQL downstream. Hyphens are
8788                // the common footgun: PostgreSQL accepts a quoted
8789                // `"intermediate-region"` in CREATE TABLE, but the
8790                // FK / index name derivation in `migrate::ddl`
8791                // emits `intermediate-region_field_fkey` unquoted,
8792                // which then fails the SQL parser. Same shape rule
8793                // as Postgres regular identifiers so the safe path
8794                // is the only path.
8795                validate_table_name(&name, s.span())?;
8796                out.table = Some(name);
8797                return Ok(());
8798            }
8799            if meta.path.is_ident("display") {
8800                let s: LitStr = meta.value()?.parse()?;
8801                out.display = Some((s.value(), s.span()));
8802                return Ok(());
8803            }
8804            if meta.path.is_ident("app") {
8805                let s: LitStr = meta.value()?.parse()?;
8806                out.app = Some(s.value());
8807                return Ok(());
8808            }
8809            if meta.path.is_ident("scope") {
8810                let s: LitStr = meta.value()?.parse()?;
8811                let val = s.value();
8812                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8813                    return Err(meta.error(format!(
8814                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8815                    )));
8816                }
8817                out.scope = Some(val);
8818                return Ok(());
8819            }
8820            if meta.path.is_ident("admin") {
8821                let mut admin = AdminAttrs::default();
8822                meta.parse_nested_meta(|inner| {
8823                    if inner.path.is_ident("list_display") {
8824                        let s: LitStr = inner.value()?.parse()?;
8825                        admin.list_display =
8826                            Some((split_field_list(&s.value()), s.span()));
8827                        return Ok(());
8828                    }
8829                    if inner.path.is_ident("search_fields") {
8830                        let s: LitStr = inner.value()?.parse()?;
8831                        admin.search_fields =
8832                            Some((split_field_list(&s.value()), s.span()));
8833                        return Ok(());
8834                    }
8835                    if inner.path.is_ident("readonly_fields") {
8836                        let s: LitStr = inner.value()?.parse()?;
8837                        admin.readonly_fields =
8838                            Some((split_field_list(&s.value()), s.span()));
8839                        return Ok(());
8840                    }
8841                    if inner.path.is_ident("list_per_page") {
8842                        let lit: syn::LitInt = inner.value()?.parse()?;
8843                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8844                        return Ok(());
8845                    }
8846                    if inner.path.is_ident("ordering") {
8847                        let s: LitStr = inner.value()?.parse()?;
8848                        admin.ordering = Some((
8849                            parse_ordering_list(&s.value()),
8850                            s.span(),
8851                        ));
8852                        return Ok(());
8853                    }
8854                    if inner.path.is_ident("list_filter") {
8855                        let s: LitStr = inner.value()?.parse()?;
8856                        admin.list_filter =
8857                            Some((split_field_list(&s.value()), s.span()));
8858                        return Ok(());
8859                    }
8860                    if inner.path.is_ident("actions") {
8861                        let s: LitStr = inner.value()?.parse()?;
8862                        admin.actions =
8863                            Some((split_field_list(&s.value()), s.span()));
8864                        return Ok(());
8865                    }
8866                    if inner.path.is_ident("fieldsets") {
8867                        let s: LitStr = inner.value()?.parse()?;
8868                        admin.fieldsets =
8869                            Some((parse_fieldset_list(&s.value()), s.span()));
8870                        return Ok(());
8871                    }
8872                    if inner.path.is_ident("list_display_links") {
8873                        let s: LitStr = inner.value()?.parse()?;
8874                        admin.list_display_links =
8875                            Some((split_field_list(&s.value()), s.span()));
8876                        return Ok(());
8877                    }
8878                    if inner.path.is_ident("search_help_text") {
8879                        let s: LitStr = inner.value()?.parse()?;
8880                        admin.search_help_text = Some(s.value());
8881                        return Ok(());
8882                    }
8883                    if inner.path.is_ident("actions_on_top") {
8884                        let lit: syn::LitBool = inner.value()?.parse()?;
8885                        admin.actions_on_top = Some(lit.value);
8886                        return Ok(());
8887                    }
8888                    if inner.path.is_ident("actions_on_bottom") {
8889                        let lit: syn::LitBool = inner.value()?.parse()?;
8890                        admin.actions_on_bottom = Some(lit.value);
8891                        return Ok(());
8892                    }
8893                    if inner.path.is_ident("date_hierarchy") {
8894                        let s: LitStr = inner.value()?.parse()?;
8895                        admin.date_hierarchy = Some(s.value());
8896                        return Ok(());
8897                    }
8898                    if inner.path.is_ident("prepopulated_fields") {
8899                        let s: LitStr = inner.value()?.parse()?;
8900                        admin.prepopulated_fields =
8901                            Some((parse_prepopulated_list(&s.value()), s.span()));
8902                        return Ok(());
8903                    }
8904                    if inner.path.is_ident("raw_id_fields") {
8905                        let s: LitStr = inner.value()?.parse()?;
8906                        admin.raw_id_fields =
8907                            Some((split_field_list(&s.value()), s.span()));
8908                        return Ok(());
8909                    }
8910                    if inner.path.is_ident("autocomplete_fields") {
8911                        let s: LitStr = inner.value()?.parse()?;
8912                        admin.autocomplete_fields =
8913                            Some((split_field_list(&s.value()), s.span()));
8914                        return Ok(());
8915                    }
8916                    if inner.path.is_ident("list_select_related") {
8917                        let s: LitStr = inner.value()?.parse()?;
8918                        admin.list_select_related = Some(s.value());
8919                        return Ok(());
8920                    }
8921                    if inner.path.is_ident("formfield_overrides") {
8922                        let s: LitStr = inner.value()?.parse()?;
8923                        admin.formfield_overrides =
8924                            Some((parse_formfield_overrides(&s.value()), s.span()));
8925                        return Ok(());
8926                    }
8927                    Err(inner.error(
8928                        "unknown admin attribute (supported: \
8929                         `list_display`, `list_display_links`, \
8930                         `search_fields`, `search_help_text`, \
8931                         `readonly_fields`, \
8932                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8933                         `actions_on_top`, `actions_on_bottom`, \
8934                         `date_hierarchy`, \
8935                         `prepopulated_fields`, \
8936                         `raw_id_fields`, \
8937                         `autocomplete_fields`, \
8938                         `list_select_related`, \
8939                         `formfield_overrides`, \
8940                         `fieldsets`)",
8941                    ))
8942                })?;
8943                out.admin = Some(admin);
8944                return Ok(());
8945            }
8946            if meta.path.is_ident("manager") {
8947                // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8948                // Stretch `from_queryset = "..."` is left as a
8949                // follow-up — the issue's primary acceptance is the
8950                // `ext = ...` trait emission.
8951                meta.parse_nested_meta(|inner| {
8952                    if inner.path.is_ident("ext") {
8953                        let s: LitStr = inner.value()?.parse()?;
8954                        let name = s.value();
8955                        if name.is_empty() {
8956                            return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8957                        }
8958                        out.manager_ext =
8959                            Some(syn::Ident::new(&name, s.span()));
8960                        return Ok(());
8961                    }
8962                    Err(inner.error(
8963                        "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8964                    ))
8965                })?;
8966                return Ok(());
8967            }
8968            if meta.path.is_ident("manager_fn") {
8969                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8970                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8971                // next to the default `Self::objects()`. Multiple
8972                // attributes accumulate.
8973                let s: LitStr = meta.value()?.parse()?;
8974                let name = s.value();
8975                if name.is_empty() {
8976                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8977                }
8978                if name == "objects" {
8979                    return Err(meta.error(
8980                        "`manager_fn = \"objects\"` collides with the default \
8981                         accessor — pick a different name",
8982                    ));
8983                }
8984                let ident = syn::Ident::new(&name, s.span());
8985                if out.manager_fns.iter().any(|prev| *prev == ident) {
8986                    return Err(meta.error(format!(
8987                        "duplicate `manager_fn = \"{name}\"`"
8988                    )));
8989                }
8990                out.manager_fns.push(ident);
8991                return Ok(());
8992            }
8993            if meta.path.is_ident("default_order") {
8994                // `#[rustango(default_order = "-created_at, status")]`
8995                // — issue #291 / T2.5. Comma-separated list; `-prefix`
8996                // means descending, `+prefix` or bare name means ascending.
8997                // Per-query opt-in via `QuerySet::with_default_order()`.
8998                let s: LitStr = meta.value()?.parse()?;
8999                let raw = s.value();
9000                let span = s.span();
9001                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9002                    Vec::new();
9003                for entry in raw.split(',') {
9004                    let trimmed = entry.trim();
9005                    if trimmed.is_empty() {
9006                        return Err(syn::Error::new(
9007                            span,
9008                            "`default_order = \"...\"` has an empty entry — \
9009                             check for a stray comma",
9010                        ));
9011                    }
9012                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9013                        (true, rest.trim().to_owned())
9014                    } else if let Some(rest) = trimmed.strip_prefix('+') {
9015                        (false, rest.trim().to_owned())
9016                    } else {
9017                        (false, trimmed.to_owned())
9018                    };
9019                    if name.is_empty() {
9020                        return Err(syn::Error::new(
9021                            span,
9022                            "`default_order` entry has no column name after the prefix",
9023                        ));
9024                    }
9025                    if parsed.iter().any(|(n, _, _)| *n == name) {
9026                        return Err(syn::Error::new(
9027                            span,
9028                            format!("duplicate column `{name}` in `default_order`"),
9029                        ));
9030                    }
9031                    parsed.push((name, desc, span));
9032                }
9033                if parsed.is_empty() {
9034                    return Err(syn::Error::new(
9035                        span,
9036                        "`default_order = \"...\"` cannot be empty",
9037                    ));
9038                }
9039                out.default_order = parsed;
9040                return Ok(());
9041            }
9042            if meta.path.is_ident("global_scope") {
9043                // `#[rustango(global_scope(name = "active", apply =
9044                //  path::to::fn))]` — issue #820. The apply function
9045                // path resolves at macro-expand time in the consumer's
9046                // scope; the macro re-emits it verbatim into the
9047                // `ModelSchema::global_scopes` slice literal.
9048                let span = meta.path.span();
9049                let mut scope_name: Option<String> = None;
9050                let mut apply_path: Option<syn::Path> = None;
9051                meta.parse_nested_meta(|inner| {
9052                    if inner.path.is_ident("name") {
9053                        let s: LitStr = inner.value()?.parse()?;
9054                        let raw = s.value();
9055                        if raw.trim().is_empty() {
9056                            return Err(syn::Error::new(
9057                                s.span(),
9058                                "`global_scope(name = \"...\")` must not be empty",
9059                            ));
9060                        }
9061                        scope_name = Some(raw);
9062                        return Ok(());
9063                    }
9064                    if inner.path.is_ident("apply") {
9065                        let p: syn::Path = inner.value()?.parse()?;
9066                        apply_path = Some(p);
9067                        return Ok(());
9068                    }
9069                    Err(inner.error(
9070                        "unknown `global_scope` attribute (supported: \
9071                         `name`, `apply`)",
9072                    ))
9073                })?;
9074                let Some(name) = scope_name else {
9075                    return Err(syn::Error::new(
9076                        span,
9077                        "`global_scope` requires `name = \"...\"`",
9078                    ));
9079                };
9080                let Some(apply) = apply_path else {
9081                    return Err(syn::Error::new(
9082                        span,
9083                        "`global_scope` requires `apply = fn_path`",
9084                    ));
9085                };
9086                if out.global_scopes.iter().any(|s| s.name == name) {
9087                    return Err(syn::Error::new(
9088                        span,
9089                        format!(
9090                            "duplicate global scope name `{name}` — \
9091                             pick a unique identifier so \
9092                             `QuerySet::without_global_scope(\"{name}\")` \
9093                             is unambiguous"
9094                        ),
9095                    ));
9096                }
9097                out.global_scopes.push(GlobalScopeAttr { name, apply });
9098                return Ok(());
9099            }
9100            if meta.path.is_ident("through") {
9101                // `#[rustango(through(name = "posts", far = "Post",
9102                //  far_fk_column = "author_id", intermediate = "User",
9103                //  intermediate_fk_column = "country_id"
9104                //  [, intermediate_pk_column = "id"]))]` — issue #817.
9105                let span = meta.path.span();
9106                let mut accessor_name: Option<String> = None;
9107                let mut far_ident: Option<syn::Ident> = None;
9108                let mut far_fk_column: Option<String> = None;
9109                let mut intermediate_ident: Option<syn::Ident> = None;
9110                let mut intermediate_fk_column: Option<String> = None;
9111                let mut intermediate_pk_column: Option<String> = None;
9112                fn parse_nonempty_string(
9113                    inner: &syn::meta::ParseNestedMeta<'_>,
9114                    field: &str,
9115                ) -> syn::Result<String> {
9116                    let s: LitStr = inner.value()?.parse()?;
9117                    let raw = s.value();
9118                    let trimmed = raw.trim();
9119                    if trimmed.is_empty() {
9120                        return Err(syn::Error::new(
9121                            s.span(),
9122                            format!("`through({field} = \"...\")` must not be empty"),
9123                        ));
9124                    }
9125                    Ok(trimmed.to_owned())
9126                }
9127                meta.parse_nested_meta(|inner| {
9128                    if inner.path.is_ident("name") {
9129                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9130                        return Ok(());
9131                    }
9132                    if inner.path.is_ident("far") {
9133                        // Far model name accepted as a string literal
9134                        // so the attribute fits inside the existing
9135                        // `parse_nested_meta` shape; parsed back into a
9136                        // `syn::Ident` so the emitted accessor can
9137                        // reference the type directly.
9138                        let s: LitStr = inner.value()?.parse()?;
9139                        let raw = s.value();
9140                        let trimmed = raw.trim();
9141                        if trimmed.is_empty() {
9142                            return Err(syn::Error::new(
9143                                s.span(),
9144                                "`through(far = \"...\")` must not be empty",
9145                            ));
9146                        }
9147                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9148                        return Ok(());
9149                    }
9150                    if inner.path.is_ident("far_fk_column") {
9151                        far_fk_column =
9152                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9153                        return Ok(());
9154                    }
9155                    if inner.path.is_ident("intermediate") {
9156                        let s: LitStr = inner.value()?.parse()?;
9157                        let raw = s.value();
9158                        let trimmed = raw.trim();
9159                        if trimmed.is_empty() {
9160                            return Err(syn::Error::new(
9161                                s.span(),
9162                                "`through(intermediate = \"...\")` must not be empty",
9163                            ));
9164                        }
9165                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9166                        return Ok(());
9167                    }
9168                    if inner.path.is_ident("intermediate_fk_column") {
9169                        intermediate_fk_column =
9170                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9171                        return Ok(());
9172                    }
9173                    if inner.path.is_ident("intermediate_pk_column") {
9174                        intermediate_pk_column =
9175                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9176                        return Ok(());
9177                    }
9178                    Err(inner.error(
9179                        "unknown `through` attribute (supported: \
9180                         `name`, `far`, `far_fk_column`, \
9181                         `intermediate`, `intermediate_fk_column`, \
9182                         `intermediate_pk_column`)",
9183                    ))
9184                })?;
9185                let Some(name) = accessor_name else {
9186                    return Err(syn::Error::new(
9187                        span,
9188                        "`through` requires `name = \"...\"`",
9189                    ));
9190                };
9191                let Some(far) = far_ident else {
9192                    return Err(syn::Error::new(
9193                        span,
9194                        "`through` requires `far = \"FarModelType\"`",
9195                    ));
9196                };
9197                let Some(far_fk_column) = far_fk_column else {
9198                    return Err(syn::Error::new(
9199                        span,
9200                        "`through` requires `far_fk_column = \"<column>\"`",
9201                    ));
9202                };
9203                let Some(intermediate) = intermediate_ident else {
9204                    return Err(syn::Error::new(
9205                        span,
9206                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9207                    ));
9208                };
9209                let Some(intermediate_fk_column) = intermediate_fk_column else {
9210                    return Err(syn::Error::new(
9211                        span,
9212                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9213                    ));
9214                };
9215                let intermediate_pk_column =
9216                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9217                if out.through_relations.iter().any(|t| t.name == name) {
9218                    return Err(syn::Error::new(
9219                        span,
9220                        format!(
9221                            "duplicate `through(name = \"{name}\")` — \
9222                             pick a unique accessor name"
9223                        ),
9224                    ));
9225                }
9226                out.through_relations.push(ThroughAttr {
9227                    name,
9228                    far,
9229                    far_fk_column,
9230                    intermediate,
9231                    intermediate_fk_column,
9232                    intermediate_pk_column,
9233                });
9234                return Ok(());
9235            }
9236            if meta.path.is_ident("reverse_has") {
9237                // `#[rustango(reverse_has(name = "comments",
9238                //  child = "Comment", child_fk_column = "post_id"
9239                //  [, self_pk_column = "id"]))]` — issue #830.
9240                let span = meta.path.span();
9241                let mut accessor_name: Option<String> = None;
9242                let mut child_ident: Option<syn::Ident> = None;
9243                let mut child_fk_column: Option<String> = None;
9244                let mut self_pk_column: Option<String> = None;
9245                meta.parse_nested_meta(|inner| {
9246                    if inner.path.is_ident("name") {
9247                        let s: LitStr = inner.value()?.parse()?;
9248                        let raw = s.value();
9249                        if raw.trim().is_empty() {
9250                            return Err(syn::Error::new(
9251                                s.span(),
9252                                "`reverse_has(name = \"...\")` must not be empty",
9253                            ));
9254                        }
9255                        accessor_name = Some(raw);
9256                        return Ok(());
9257                    }
9258                    if inner.path.is_ident("child") {
9259                        let s: LitStr = inner.value()?.parse()?;
9260                        let raw = s.value();
9261                        let trimmed = raw.trim();
9262                        if trimmed.is_empty() {
9263                            return Err(syn::Error::new(
9264                                s.span(),
9265                                "`reverse_has(child = \"...\")` must not be empty",
9266                            ));
9267                        }
9268                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9269                        return Ok(());
9270                    }
9271                    if inner.path.is_ident("child_fk_column") {
9272                        let s: LitStr = inner.value()?.parse()?;
9273                        let raw = s.value();
9274                        let trimmed = raw.trim();
9275                        if trimmed.is_empty() {
9276                            return Err(syn::Error::new(
9277                                s.span(),
9278                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9279                            ));
9280                        }
9281                        child_fk_column = Some(trimmed.to_owned());
9282                        return Ok(());
9283                    }
9284                    if inner.path.is_ident("self_pk_column") {
9285                        let s: LitStr = inner.value()?.parse()?;
9286                        let raw = s.value();
9287                        let trimmed = raw.trim();
9288                        if trimmed.is_empty() {
9289                            return Err(syn::Error::new(
9290                                s.span(),
9291                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9292                            ));
9293                        }
9294                        self_pk_column = Some(trimmed.to_owned());
9295                        return Ok(());
9296                    }
9297                    Err(inner.error(
9298                        "unknown `reverse_has` attribute (supported: \
9299                         `name`, `child`, `child_fk_column`, \
9300                         `self_pk_column`)",
9301                    ))
9302                })?;
9303                let Some(name) = accessor_name else {
9304                    return Err(syn::Error::new(
9305                        span,
9306                        "`reverse_has` requires `name = \"...\"`",
9307                    ));
9308                };
9309                let Some(child) = child_ident else {
9310                    return Err(syn::Error::new(
9311                        span,
9312                        "`reverse_has` requires `child = \"ChildModelType\"`",
9313                    ));
9314                };
9315                let Some(child_fk_column) = child_fk_column else {
9316                    return Err(syn::Error::new(
9317                        span,
9318                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9319                    ));
9320                };
9321                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9322                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9323                    return Err(syn::Error::new(
9324                        span,
9325                        format!(
9326                            "duplicate `reverse_has(name = \"{name}\")` — \
9327                             pick a unique accessor name"
9328                        ),
9329                    ));
9330                }
9331                out.reverse_has_relations.push(ReverseHasAttr {
9332                    name,
9333                    child,
9334                    child_fk_column,
9335                    self_pk_column,
9336                });
9337                return Ok(());
9338            }
9339            if meta.path.is_ident("generic_has") {
9340                // `#[rustango(generic_has(name = "tags",
9341                //  child = "Tag", ct_column = "content_type_id",
9342                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9343                //  — issue #830, the reverse generic-FK (GFK) arm of the
9344                //  relation-existence family.
9345                let span = meta.path.span();
9346                let mut accessor_name: Option<String> = None;
9347                let mut child_ident: Option<syn::Ident> = None;
9348                let mut ct_column: Option<String> = None;
9349                let mut pk_column: Option<String> = None;
9350                let mut self_pk_column: Option<String> = None;
9351                meta.parse_nested_meta(|inner| {
9352                    if inner.path.is_ident("name") {
9353                        let s: LitStr = inner.value()?.parse()?;
9354                        let raw = s.value();
9355                        if raw.trim().is_empty() {
9356                            return Err(syn::Error::new(
9357                                s.span(),
9358                                "`generic_has(name = \"...\")` must not be empty",
9359                            ));
9360                        }
9361                        accessor_name = Some(raw);
9362                        return Ok(());
9363                    }
9364                    if inner.path.is_ident("child") {
9365                        let s: LitStr = inner.value()?.parse()?;
9366                        let trimmed = s.value().trim().to_owned();
9367                        if trimmed.is_empty() {
9368                            return Err(syn::Error::new(
9369                                s.span(),
9370                                "`generic_has(child = \"...\")` must not be empty",
9371                            ));
9372                        }
9373                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9374                        return Ok(());
9375                    }
9376                    if inner.path.is_ident("ct_column") {
9377                        let s: LitStr = inner.value()?.parse()?;
9378                        let trimmed = s.value().trim().to_owned();
9379                        if trimmed.is_empty() {
9380                            return Err(syn::Error::new(
9381                                s.span(),
9382                                "`generic_has(ct_column = \"...\")` must not be empty",
9383                            ));
9384                        }
9385                        ct_column = Some(trimmed);
9386                        return Ok(());
9387                    }
9388                    if inner.path.is_ident("pk_column") {
9389                        let s: LitStr = inner.value()?.parse()?;
9390                        let trimmed = s.value().trim().to_owned();
9391                        if trimmed.is_empty() {
9392                            return Err(syn::Error::new(
9393                                s.span(),
9394                                "`generic_has(pk_column = \"...\")` must not be empty",
9395                            ));
9396                        }
9397                        pk_column = Some(trimmed);
9398                        return Ok(());
9399                    }
9400                    if inner.path.is_ident("self_pk_column") {
9401                        let s: LitStr = inner.value()?.parse()?;
9402                        let trimmed = s.value().trim().to_owned();
9403                        if trimmed.is_empty() {
9404                            return Err(syn::Error::new(
9405                                s.span(),
9406                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9407                            ));
9408                        }
9409                        self_pk_column = Some(trimmed);
9410                        return Ok(());
9411                    }
9412                    Err(inner.error(
9413                        "unknown `generic_has` attribute (supported: \
9414                         `name`, `child`, `ct_column`, `pk_column`, \
9415                         `self_pk_column`)",
9416                    ))
9417                })?;
9418                let Some(name) = accessor_name else {
9419                    return Err(syn::Error::new(
9420                        span,
9421                        "`generic_has` requires `name = \"...\"`",
9422                    ));
9423                };
9424                let Some(child) = child_ident else {
9425                    return Err(syn::Error::new(
9426                        span,
9427                        "`generic_has` requires `child = \"ChildModelType\"`",
9428                    ));
9429                };
9430                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9431                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9432                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9433                if out.generic_has_relations.iter().any(|r| r.name == name) {
9434                    return Err(syn::Error::new(
9435                        span,
9436                        format!(
9437                            "duplicate `generic_has(name = \"{name}\")` — \
9438                             pick a unique accessor name"
9439                        ),
9440                    ));
9441                }
9442                out.generic_has_relations.push(GenericHasAttr {
9443                    name,
9444                    child,
9445                    ct_column,
9446                    pk_column,
9447                    self_pk_column,
9448                });
9449                return Ok(());
9450            }
9451            if meta.path.is_ident("audit") {
9452                let mut audit = AuditAttrs::default();
9453                meta.parse_nested_meta(|inner| {
9454                    if inner.path.is_ident("track") {
9455                        let s: LitStr = inner.value()?.parse()?;
9456                        audit.track =
9457                            Some((split_field_list(&s.value()), s.span()));
9458                        return Ok(());
9459                    }
9460                    Err(inner.error(
9461                        "unknown audit attribute (supported: `track`)",
9462                    ))
9463                })?;
9464                out.audit = Some(audit);
9465                return Ok(());
9466            }
9467            if meta.path.is_ident("permissions") {
9468                // Two forms accepted:
9469                //   #[rustango(permissions)]          — flag form, true
9470                //   #[rustango(permissions = false)]  — explicit opt-out
9471                //   #[rustango(permissions = true)]   — explicit opt-in
9472                if let Ok(v) = meta.value() {
9473                    let lit: syn::LitBool = v.parse()?;
9474                    out.permissions = lit.value;
9475                } else {
9476                    out.permissions = true;
9477                }
9478                return Ok(());
9479            }
9480            if meta.path.is_ident("view") {
9481                // Issue #293 / T2.10. Two forms accepted, matching
9482                // the `permissions` flag pattern:
9483                //   #[rustango(view)]          — flag form, true
9484                //   #[rustango(view = false)]  — explicit opt-out
9485                //   #[rustango(view = true)]   — explicit opt-in
9486                if let Ok(v) = meta.value() {
9487                    let lit: syn::LitBool = v.parse()?;
9488                    out.is_view = lit.value;
9489                } else {
9490                    out.is_view = true;
9491                }
9492                return Ok(());
9493            }
9494            if meta.path.is_ident("managed") {
9495                // Who owns the table. Issue #321.
9496                //   #[rustango(managed = false)]  — operator-managed table
9497                //   #[rustango(managed = true)]   — rustango-managed (the default)
9498                // Bare-flag form is intentionally not accepted: writing
9499                // `#[rustango(managed)]` reads as "yes please manage it"
9500                // which is already the default. The opt-out is the only
9501                // useful state, so it must be explicit.
9502                let v = meta.value()?;
9503                let lit: syn::LitBool = v.parse()?;
9504                out.managed = lit.value;
9505                return Ok(());
9506            }
9507            if meta.path.is_ident("verbose_name") {
9508                let s: LitStr = meta.value()?.parse()?;
9509                out.verbose_name = Some(s.value());
9510                return Ok(());
9511            }
9512            if meta.path.is_ident("verbose_name_plural") {
9513                let s: LitStr = meta.value()?.parse()?;
9514                out.verbose_name_plural = Some(s.value());
9515                return Ok(());
9516            }
9517            if meta.path.is_ident("db_table_comment") {
9518                // Free-form table-level comment attached to the DB
9519                // catalog.
9520                let s: LitStr = meta.value()?.parse()?;
9521                out.db_table_comment = Some(s.value());
9522                return Ok(());
9523            }
9524            if meta.path.is_ident("proxy") {
9525                // Declarative flag
9526                // marking the struct as a proxy of another model that
9527                // shares its DB table. Stored on `ModelSchema::proxy`
9528                // so future codegen can skip `CreateTable` emission
9529                // for proxies (parent owns the table) and route
9530                // per-instance method resolution to the proxy class.
9531                //
9532                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9533                let value = if meta.input.peek(syn::Token![=]) {
9534                    meta.value()?.parse::<syn::LitBool>()?.value
9535                } else {
9536                    true
9537                };
9538                out.proxy = value;
9539                return Ok(());
9540            }
9541            if meta.path.is_ident("order_with_respect_to") {
9542                // `order_with_respect_to = "parent_fk"` — the model's
9543                // instances are intrinsically ordered relative to
9544                // their parent FK.
9545                //
9546                // rustango stores the FK field name on
9547                // `ModelSchema::order_with_respect_to`. Declarative-only
9548                // today: the migration writer + admin surfaces still
9549                // treat every model identically. Future codegen will
9550                // key off the metadata to auto-emit the `_order`
9551                // column and reorder helpers.
9552                //
9553                // Validated as a Rust-shape identifier so the macro
9554                // can reject typos at derive time.
9555                let s: LitStr = meta.value()?.parse()?;
9556                let raw = s.value();
9557                if raw.is_empty() {
9558                    return Err(syn::Error::new(
9559                        s.span(),
9560                        "`order_with_respect_to` must be a non-empty FK field name",
9561                    ));
9562                }
9563                let valid = raw
9564                    .chars()
9565                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9566                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9567                if !valid {
9568                    return Err(syn::Error::new(
9569                        s.span(),
9570                        format!(
9571                            "`order_with_respect_to` must be a valid Rust \
9572                             identifier (letters / digits / underscores, \
9573                             not starting with a digit); got `{raw}`"
9574                        ),
9575                    ));
9576                }
9577                out.order_with_respect_to = Some(raw);
9578                return Ok(());
9579            }
9580            if meta.path.is_ident("required_db_features") {
9581                // Capability tokens the model needs (e.g.
9582                // `"json_extract"`, `"window_functions"`,
9583                // `"row_security"`). Comma-separated.
9584                // `manage check --deploy` walks every model and warns
9585                // when the active backend doesn't advertise the
9586                // capability.
9587                //
9588                // rustango ships a small registry of capability tokens
9589                // each dialect supports — see `Dialect::supports`.
9590                // Unknown tokens still parse (they end up on the
9591                // schema and show up in the warning) so projects can
9592                // declare aspirational capabilities and the check
9593                // verb will keep nagging until the dialect implements
9594                // them.
9595                let s: LitStr = meta.value()?.parse()?;
9596                let raw = s.value();
9597                let features: Vec<String> = raw
9598                    .split(',')
9599                    .map(str::trim)
9600                    .filter(|s| !s.is_empty())
9601                    .map(str::to_owned)
9602                    .collect();
9603                if features.is_empty() {
9604                    return Err(syn::Error::new(
9605                        s.span(),
9606                        "`required_db_features` must list at least one \
9607                         comma-separated capability token",
9608                    ));
9609                }
9610                out.required_db_features = features;
9611                return Ok(());
9612            }
9613            if meta.path.is_ident("required_db_vendor") {
9614                // The model is only meant to run against the named DB
9615                // backend. `manage check --deploy` flags a mismatch so
9616                // ops catches "I forgot to switch DATABASE_URL" at
9617                // deploy time rather than runtime.
9618                //
9619                // Restricted to the three backends rustango ships
9620                // dialects for so the check verb can compare reliably.
9621                let s: LitStr = meta.value()?.parse()?;
9622                let raw = s.value().to_ascii_lowercase();
9623                match raw.as_str() {
9624                    "postgresql" | "postgres" | "pg" => {
9625                        out.required_db_vendor = Some("postgres".to_owned());
9626                    }
9627                    "mysql" | "mariadb" => {
9628                        out.required_db_vendor = Some("mysql".to_owned());
9629                    }
9630                    "sqlite" | "sqlite3" => {
9631                        out.required_db_vendor = Some("sqlite".to_owned());
9632                    }
9633                    _ => {
9634                        return Err(syn::Error::new(
9635                            s.span(),
9636                            format!(
9637                                "unknown required_db_vendor `{raw}` — \
9638                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9639                                 `mysql` (alias: `mariadb`), or `sqlite` \
9640                                 (alias: `sqlite3`)"
9641                            ),
9642                        ));
9643                    }
9644                }
9645                return Ok(());
9646            }
9647            if meta.path.is_ident("base_manager_name") {
9648                // Name of the manager that `<instance>.<relation>_set`
9649                // uses when resolving reverse-relation managers.
9650                // Distinct from `default_manager_name` (what
9651                // `Self::objects()` returns). Stored on
9652                // `ModelSchema::base_manager_name`.
9653                //
9654                // Validated as a Rust identifier so it stays safe to
9655                // re-emit as code in future reverse-manager codegen.
9656                let s: LitStr = meta.value()?.parse()?;
9657                let raw = s.value();
9658                if raw.is_empty() {
9659                    return Err(syn::Error::new(
9660                        s.span(),
9661                        "`base_manager_name` must be a non-empty string",
9662                    ));
9663                }
9664                let valid = raw
9665                    .chars()
9666                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9667                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9668                if !valid {
9669                    return Err(syn::Error::new(
9670                        s.span(),
9671                        format!(
9672                            "`base_manager_name` must be a valid Rust \
9673                             identifier (letters / digits / underscores, \
9674                             not starting with a digit); got `{raw}`"
9675                        ),
9676                    ));
9677                }
9678                out.base_manager_name = Some(raw);
9679                return Ok(());
9680            }
9681            if meta.path.is_ident("default_related_name") {
9682                // The name reverse-relation accessors use when callers
9683                // don't override `related_name = "..."` on the FK / M2M
9684                // field. Stored on `ModelSchema::default_related_name`
9685                // so future reverse-manager codegen / schema emit /
9686                // admin templates can pick the right accessor name
9687                // (today rustango doesn't auto-emit reverse managers;
9688                // the metadata is the foundation for that work).
9689                //
9690                // Enforced non-empty + ASCII identifier-shape so the
9691                // string is safe to use as a Rust ident later.
9692                let s: LitStr = meta.value()?.parse()?;
9693                let raw = s.value();
9694                if raw.is_empty() {
9695                    return Err(syn::Error::new(
9696                        s.span(),
9697                        "`default_related_name` must be a non-empty string",
9698                    ));
9699                }
9700                let valid = raw
9701                    .chars()
9702                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9703                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9704                if !valid {
9705                    return Err(syn::Error::new(
9706                        s.span(),
9707                        format!(
9708                            "`default_related_name` must be snake_case ASCII \
9709                             (lowercase letters / digits / underscores, not \
9710                             starting with a digit); got `{raw}`"
9711                        ),
9712                    ));
9713                }
9714                out.default_related_name = Some(raw);
9715                return Ok(());
9716            }
9717            if meta.path.is_ident("extra_permissions") {
9718                // Comma-separated `codename:label` pairs.
9719                let s: LitStr = meta.value()?.parse()?;
9720                let raw = s.value();
9721                let mut pairs = Vec::new();
9722                for entry in raw.split(',') {
9723                    let entry = entry.trim();
9724                    if entry.is_empty() {
9725                        continue;
9726                    }
9727                    let (codename, label) = match entry.split_once(':') {
9728                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9729                        None => (entry.to_owned(), entry.to_owned()),
9730                    };
9731                    if codename.is_empty() {
9732                        return Err(meta.error(
9733                            "`extra_permissions` entries must be `codename:label` pairs",
9734                        ));
9735                    }
9736                    pairs.push((codename, label));
9737                }
9738                if pairs.is_empty() {
9739                    return Err(meta
9740                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9741                }
9742                out.extra_permissions = pairs;
9743                return Ok(());
9744            }
9745            if meta.path.is_ident("default_permissions") {
9746                // Comma-separated subset of the CRUD action set.
9747                // Empty means all four (the framework default).
9748                let s: LitStr = meta.value()?.parse()?;
9749                let raw = s.value();
9750                let mut actions: Vec<String> = Vec::new();
9751                for entry in raw.split(',') {
9752                    let action = entry.trim().to_ascii_lowercase();
9753                    if action.is_empty() {
9754                        continue;
9755                    }
9756                    match action.as_str() {
9757                        "add" | "change" | "delete" | "view" => {}
9758                        other => {
9759                            return Err(syn::Error::new(
9760                                s.span(),
9761                                format!(
9762                                    "unknown default_permissions action `{other}` — \
9763                                     expected one of `add`, `change`, `delete`, `view`"
9764                                ),
9765                            ));
9766                        }
9767                    }
9768                    if !actions.contains(&action) {
9769                        actions.push(action);
9770                    }
9771                }
9772                if actions.is_empty() {
9773                    return Err(syn::Error::new(
9774                        s.span(),
9775                        "`default_permissions = \"…\"` must list at least one action; \
9776                         use `permissions = false` on the container if you want NO \
9777                         permissions seeded for this model.",
9778                    ));
9779                }
9780                out.default_permissions = actions;
9781                return Ok(());
9782            }
9783            if meta.path.is_ident("get_latest_by") {
9784                // The `-` prefix selects descending order.
9785                let s: LitStr = meta.value()?.parse()?;
9786                let raw = s.value();
9787                let trimmed = raw.trim();
9788                if trimmed.is_empty() {
9789                    return Err(meta.error("`get_latest_by` must name a column"));
9790                }
9791                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9792                    (stripped.to_owned(), true)
9793                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9794                    (stripped.to_owned(), false)
9795                } else {
9796                    (trimmed.to_owned(), false)
9797                };
9798                if col.is_empty() {
9799                    return Err(meta.error("`get_latest_by` must name a column"));
9800                }
9801                out.get_latest_by = Some((col, desc));
9802                return Ok(());
9803            }
9804            if meta.path.is_ident("unique_together") {
9805                // Composite UNIQUE index. Two syntaxes:
9806                //
9807                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9808                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9809                //
9810                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9811                // (col1, col2)`, where <name> defaults to
9812                // `<table>_<col1>_<col2>_uq` when not supplied.
9813                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9814                out.indexes.push(IndexAttr {
9815                    name,
9816                    columns,
9817                    unique: true,
9818                    method: "btree".to_owned(),
9819                    where_clause: None,
9820                include: Vec::new(),
9821                });
9822                return Ok(());
9823            }
9824            if meta.path.is_ident("index_together") {
9825                // Composite (non-unique) index. Two syntaxes
9826                // mirroring `unique_together`.
9827                //
9828                //   #[rustango(index_together = "created_at, status")]
9829                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9830                let (columns, name) = parse_together_attr(&meta, "index_together")?;
9831                out.indexes.push(IndexAttr {
9832                    name,
9833                    columns,
9834                    unique: false,
9835                    method: "btree".to_owned(),
9836                    where_clause: None,
9837                include: Vec::new(),
9838                });
9839                return Ok(());
9840            }
9841            if meta.path.is_ident("unique_when") {
9842                // Partial unique index. Issue #265 / T1.3.
9843                //
9844                //   #[rustango(unique_when(
9845                //       columns   = "email",
9846                //       condition = "deleted_at IS NULL",
9847                //       name      = "unique_active_email"
9848                //   ))]
9849                //
9850                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9851                // on PG / SQLite (both ship partial indexes natively).
9852                // MySQL falls back to a plain UNIQUE index — the
9853                // condition is lost; document the limitation in the
9854                // generated migration.
9855                let mut columns: Option<Vec<String>> = None;
9856                let mut condition: Option<String> = None;
9857                let mut name: Option<String> = None;
9858                let mut include: Vec<String> = Vec::new();
9859                meta.parse_nested_meta(|inner| {
9860                    if inner.path.is_ident("columns") {
9861                        let s: LitStr = inner.value()?.parse()?;
9862                        columns = Some(split_field_list(&s.value()));
9863                        return Ok(());
9864                    }
9865                    if inner.path.is_ident("condition") {
9866                        let s: LitStr = inner.value()?.parse()?;
9867                        condition = Some(s.value());
9868                        return Ok(());
9869                    }
9870                    if inner.path.is_ident("name") {
9871                        let s: LitStr = inner.value()?.parse()?;
9872                        name = Some(s.value());
9873                        return Ok(());
9874                    }
9875                    if inner.path.is_ident("include") {
9876                        // PG 11+ covering-index columns. Non-key columns
9877                        // travel with the index leaf for index-only
9878                        // scans. Dropped on MySQL/SQLite by the writer.
9879                        let s: LitStr = inner.value()?.parse()?;
9880                        include = split_field_list(&s.value());
9881                        return Ok(());
9882                    }
9883                    Err(inner.error(
9884                        "unknown unique_when attribute (supported: \
9885                         `columns = \"...\"`, `condition = \"...\"`, \
9886                         `name = \"...\"`, `include = \"...\"`)",
9887                    ))
9888                })?;
9889                let columns = columns.ok_or_else(|| {
9890                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9891                })?;
9892                let condition = condition.ok_or_else(|| {
9893                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9894                })?;
9895                if columns.is_empty() {
9896                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9897                }
9898                out.indexes.push(IndexAttr {
9899                    name,
9900                    columns,
9901                    unique: true,
9902                    method: "btree".to_owned(),
9903                    where_clause: Some(condition),
9904                    include,
9905                });
9906                return Ok(());
9907            }
9908            if meta.path.is_ident("index_when") {
9909                // Non-unique partial index. Sibling of `unique_when`
9910                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9911                //
9912                //   #[rustango(index_when(
9913                //       columns   = "status, created_at",
9914                //       condition = "deleted_at IS NULL",
9915                //       name      = "active_status_created_idx"
9916                //   ))]
9917                //
9918                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9919                // on PG / SQLite (both ship partial indexes natively).
9920                // MySQL has no native partial-index support — the writer
9921                // emits a plain CREATE INDEX and the condition is lost;
9922                // operators wanting that selectivity on MySQL should
9923                // declare a covering index plus an application-level
9924                // filter.
9925                let mut columns: Option<Vec<String>> = None;
9926                let mut condition: Option<String> = None;
9927                let mut name: Option<String> = None;
9928                let mut method: String = "btree".to_owned();
9929                let mut include: Vec<String> = Vec::new();
9930                meta.parse_nested_meta(|inner| {
9931                    if inner.path.is_ident("columns") {
9932                        let s: LitStr = inner.value()?.parse()?;
9933                        columns = Some(split_field_list(&s.value()));
9934                        return Ok(());
9935                    }
9936                    if inner.path.is_ident("condition") {
9937                        let s: LitStr = inner.value()?.parse()?;
9938                        condition = Some(s.value());
9939                        return Ok(());
9940                    }
9941                    if inner.path.is_ident("name") {
9942                        let s: LitStr = inner.value()?.parse()?;
9943                        name = Some(s.value());
9944                        return Ok(());
9945                    }
9946                    if inner.path.is_ident("method") {
9947                        let s: LitStr = inner.value()?.parse()?;
9948                        method = s.value();
9949                        return Ok(());
9950                    }
9951                    if inner.path.is_ident("include") {
9952                        // PG 11+ covering-index columns; non-key
9953                        // columns travel with the index leaf. Dropped
9954                        // on MySQL/SQLite.
9955                        let s: LitStr = inner.value()?.parse()?;
9956                        include = split_field_list(&s.value());
9957                        return Ok(());
9958                    }
9959                    Err(inner.error(
9960                        "unknown index_when attribute (supported: \
9961                         `columns = \"...\"`, `condition = \"...\"`, \
9962                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9963                         `include = \"...\"`)",
9964                    ))
9965                })?;
9966                let columns = columns
9967                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9968                let condition = condition.ok_or_else(|| {
9969                    meta.error("`index_when(...)` requires `condition = \"...\"`")
9970                })?;
9971                if columns.is_empty() {
9972                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
9973                }
9974                out.indexes.push(IndexAttr {
9975                    name,
9976                    columns,
9977                    unique: false,
9978                    method,
9979                    where_clause: Some(condition),
9980                    include,
9981                });
9982                return Ok(());
9983            }
9984            if meta.path.is_ident("index") {
9985                // Container-level composite index. Two syntaxes:
9986                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
9987                //   #[rustango(index("col1, col2"))]    — call form (same result)
9988                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9989                // The call form takes the column list as a leading string
9990                // literal, then optional `unique` / `name = "..."` /
9991                // `method = "..."` flags (a leading literal can't compose
9992                // under `parse_nested_meta`, so the paren body is parsed by
9993                // hand). `unique_together` / `index_together` remain
9994                // aliases for the same feature.
9995                let cols_lit: LitStr;
9996                let mut unique = false;
9997                let mut name: Option<String> = None;
9998                let mut method = "btree".to_owned();
9999                if meta.input.peek(syn::token::Paren) {
10000                    let content;
10001                    syn::parenthesized!(content in meta.input);
10002                    cols_lit = content.parse()?;
10003                    while content.peek(syn::Token![,]) {
10004                        content.parse::<syn::Token![,]>()?;
10005                        if content.is_empty() {
10006                            break;
10007                        }
10008                        let flag: syn::Ident = content.parse()?;
10009                        if flag == "unique" {
10010                            unique = true;
10011                        } else if flag == "name" {
10012                            content.parse::<syn::Token![=]>()?;
10013                            let s: LitStr = content.parse()?;
10014                            name = Some(s.value());
10015                        } else if flag == "method" {
10016                            content.parse::<syn::Token![=]>()?;
10017                            let s: LitStr = content.parse()?;
10018                            let v = s.value();
10019                            match v.as_str() {
10020                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10021                                | "bloom" => method = v,
10022                                other => {
10023                                    return Err(syn::Error::new(
10024                                        s.span(),
10025                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10026                                    ));
10027                                }
10028                            }
10029                        } else {
10030                            return Err(syn::Error::new(
10031                                flag.span(),
10032                                "unknown index flag (supported: `unique`, `name`, `method`)",
10033                            ));
10034                        }
10035                    }
10036                } else {
10037                    cols_lit = meta.value()?.parse()?;
10038                }
10039                let columns = split_field_list(&cols_lit.value());
10040                out.indexes.push(IndexAttr {
10041                    name,
10042                    columns,
10043                    unique,
10044                    method,
10045                    where_clause: None,
10046                    include: Vec::new(),
10047                });
10048                return Ok(());
10049            }
10050            if meta.path.is_ident("check") {
10051                // #[rustango(check(name = "…", expr = "…"))]
10052                let mut name: Option<String> = None;
10053                let mut expr: Option<String> = None;
10054                meta.parse_nested_meta(|inner| {
10055                    if inner.path.is_ident("name") {
10056                        let s: LitStr = inner.value()?.parse()?;
10057                        name = Some(s.value());
10058                        return Ok(());
10059                    }
10060                    if inner.path.is_ident("expr") {
10061                        let s: LitStr = inner.value()?.parse()?;
10062                        expr = Some(s.value());
10063                        return Ok(());
10064                    }
10065                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10066                })?;
10067                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10068                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10069                out.checks.push(CheckAttr { name, expr });
10070                return Ok(());
10071            }
10072            if meta.path.is_ident("exclude") {
10073                // #[rustango(exclude(name = "…", using = "gist",
10074                //                    elements = "col WITH op, col WITH op",
10075                //                    where = "…"))]
10076                let mut name: Option<String> = None;
10077                let mut using: Option<String> = None;
10078                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10079                let mut where_clause: Option<String> = None;
10080                meta.parse_nested_meta(|inner| {
10081                    if inner.path.is_ident("name") {
10082                        let s: LitStr = inner.value()?.parse()?;
10083                        name = Some(s.value());
10084                        return Ok(());
10085                    }
10086                    if inner.path.is_ident("using") {
10087                        let s: LitStr = inner.value()?.parse()?;
10088                        using = Some(s.value());
10089                        return Ok(());
10090                    }
10091                    if inner.path.is_ident("elements") {
10092                        let s: LitStr = inner.value()?.parse()?;
10093                        elements_raw = Some((s.value(), s.span()));
10094                        return Ok(());
10095                    }
10096                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10097                        let s: LitStr = inner.value()?.parse()?;
10098                        where_clause = Some(s.value());
10099                        return Ok(());
10100                    }
10101                    Err(inner.error(
10102                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10103                    ))
10104                })?;
10105                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10106                let using = using.unwrap_or_else(|| "gist".to_owned());
10107                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10108                    meta.error(
10109                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
10110                    )
10111                })?;
10112                // Parse `col WITH op` pairs separated by commas.
10113                let mut elements: Vec<(String, String)> = Vec::new();
10114                for pair in elements_str.split(',') {
10115                    let pair = pair.trim();
10116                    if pair.is_empty() {
10117                        continue;
10118                    }
10119                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10120                    let col = split.next().unwrap_or("").trim();
10121                    let rest = split.next().unwrap_or("").trim();
10122                    // `WITH op` — case-insensitive on `WITH`, then op.
10123                    let rest_lc = rest.to_ascii_lowercase();
10124                    let op = rest_lc
10125                        .strip_prefix("with")
10126                        .map(|r| r.trim_start())
10127                        .filter(|r| !r.is_empty())
10128                        .map(|_| {
10129                            // Pull the original-case op from `rest` after the
10130                            // `WITH ` token (5 chars).
10131                            rest[4..].trim_start().to_owned()
10132                        });
10133                    let Some(op) = op else {
10134                        return Err(syn::Error::new(
10135                            elements_span,
10136                            format!(
10137                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10138                                 (e.g. `room_id WITH =` or `during WITH &&`)"
10139                            ),
10140                        ));
10141                    };
10142                    if col.is_empty() || op.is_empty() {
10143                        return Err(syn::Error::new(
10144                            elements_span,
10145                            format!(
10146                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10147                                 (both sides non-empty)"
10148                            ),
10149                        ));
10150                    }
10151                    elements.push((col.to_owned(), op));
10152                }
10153                if elements.is_empty() {
10154                    return Err(syn::Error::new(
10155                        elements_span,
10156                        "exclude requires at least one `col WITH op` element",
10157                    ));
10158                }
10159                out.excludes.push(ExcludeAttr {
10160                    name,
10161                    using,
10162                    elements,
10163                    where_clause,
10164                });
10165                return Ok(());
10166            }
10167            if meta.path.is_ident("generic_fk") {
10168                let mut gfk = GenericFkAttr {
10169                    name: String::new(),
10170                    ct_column: String::new(),
10171                    pk_column: String::new(),
10172                };
10173                meta.parse_nested_meta(|inner| {
10174                    if inner.path.is_ident("name") {
10175                        let s: LitStr = inner.value()?.parse()?;
10176                        gfk.name = s.value();
10177                        return Ok(());
10178                    }
10179                    if inner.path.is_ident("ct_column") {
10180                        let s: LitStr = inner.value()?.parse()?;
10181                        gfk.ct_column = s.value();
10182                        return Ok(());
10183                    }
10184                    if inner.path.is_ident("pk_column") {
10185                        let s: LitStr = inner.value()?.parse()?;
10186                        gfk.pk_column = s.value();
10187                        return Ok(());
10188                    }
10189                    Err(inner.error(
10190                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10191                    ))
10192                })?;
10193                if gfk.name.is_empty() {
10194                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10195                }
10196                if gfk.ct_column.is_empty() {
10197                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10198                }
10199                if gfk.pk_column.is_empty() {
10200                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10201                }
10202                out.generic_fks.push(gfk);
10203                return Ok(());
10204            }
10205            if meta.path.is_ident("fk_composite") {
10206                let mut fk = CompositeFkAttr {
10207                    name: String::new(),
10208                    to: String::new(),
10209                    from: Vec::new(),
10210                    on: Vec::new(),
10211                };
10212                meta.parse_nested_meta(|inner| {
10213                    if inner.path.is_ident("name") {
10214                        let s: LitStr = inner.value()?.parse()?;
10215                        fk.name = s.value();
10216                        return Ok(());
10217                    }
10218                    if inner.path.is_ident("to") {
10219                        let s: LitStr = inner.value()?.parse()?;
10220                        fk.to = s.value();
10221                        return Ok(());
10222                    }
10223                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10224                    // comma-list of string literals.
10225                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10226                        let value = inner.value()?;
10227                        let content;
10228                        syn::parenthesized!(content in value);
10229                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10230                            content.parse_terminated(
10231                                |p| p.parse::<syn::LitStr>(),
10232                                syn::Token![,],
10233                            )?;
10234                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10235                        if inner.path.is_ident("on") {
10236                            fk.on = cols;
10237                        } else {
10238                            fk.from = cols;
10239                        }
10240                        return Ok(());
10241                    }
10242                    Err(inner.error(
10243                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10244                    ))
10245                })?;
10246                if fk.name.is_empty() {
10247                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10248                }
10249                if fk.to.is_empty() {
10250                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10251                }
10252                if fk.from.is_empty() || fk.on.is_empty() {
10253                    return Err(meta.error(
10254                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10255                    ));
10256                }
10257                if fk.from.len() != fk.on.len() {
10258                    return Err(meta.error(format!(
10259                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10260                        fk.from.len(),
10261                        fk.on.len(),
10262                    )));
10263                }
10264                out.composite_fks.push(fk);
10265                return Ok(());
10266            }
10267            if meta.path.is_ident("m2m") {
10268                let mut m2m = M2MAttr {
10269                    name: String::new(),
10270                    to: String::new(),
10271                    through: String::new(),
10272                    src: String::new(),
10273                    dst: String::new(),
10274                    auto_create: true,
10275                };
10276                meta.parse_nested_meta(|inner| {
10277                    if inner.path.is_ident("name") {
10278                        let s: LitStr = inner.value()?.parse()?;
10279                        m2m.name = s.value();
10280                        return Ok(());
10281                    }
10282                    if inner.path.is_ident("to") {
10283                        let s: LitStr = inner.value()?.parse()?;
10284                        m2m.to = s.value();
10285                        return Ok(());
10286                    }
10287                    if inner.path.is_ident("through") {
10288                        let s: LitStr = inner.value()?.parse()?;
10289                        m2m.through = s.value();
10290                        return Ok(());
10291                    }
10292                    if inner.path.is_ident("src") {
10293                        let s: LitStr = inner.value()?.parse()?;
10294                        m2m.src = s.value();
10295                        return Ok(());
10296                    }
10297                    if inner.path.is_ident("dst") {
10298                        let s: LitStr = inner.value()?.parse()?;
10299                        m2m.dst = s.value();
10300                        return Ok(());
10301                    }
10302                    if inner.path.is_ident("auto_create") {
10303                        let lit: syn::LitBool = inner.value()?.parse()?;
10304                        m2m.auto_create = lit.value;
10305                        return Ok(());
10306                    }
10307                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10308                })?;
10309                if m2m.name.is_empty() {
10310                    return Err(meta.error("m2m requires `name = \"...\"`"));
10311                }
10312                if m2m.to.is_empty() {
10313                    return Err(meta.error("m2m requires `to = \"...\"`"));
10314                }
10315                if m2m.through.is_empty() {
10316                    return Err(meta.error("m2m requires `through = \"...\"`"));
10317                }
10318                if m2m.src.is_empty() {
10319                    return Err(meta.error("m2m requires `src = \"...\"`"));
10320                }
10321                if m2m.dst.is_empty() {
10322                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10323                }
10324                out.m2m.push(m2m);
10325                return Ok(());
10326            }
10327            if meta.path.is_ident("generic_m2m") {
10328                let mut gm = GenericM2MAttr {
10329                    name: String::new(),
10330                    through: String::new(),
10331                    pk_column: String::new(),
10332                    ct_column: String::new(),
10333                    related_column: String::new(),
10334                };
10335                meta.parse_nested_meta(|inner| {
10336                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10337                        let s: LitStr = inner.value()?.parse()?;
10338                        Ok(s.value())
10339                    };
10340                    if inner.path.is_ident("name") {
10341                        gm.name = field(&inner)?;
10342                        return Ok(());
10343                    }
10344                    if inner.path.is_ident("through") {
10345                        gm.through = field(&inner)?;
10346                        return Ok(());
10347                    }
10348                    if inner.path.is_ident("pk_column") {
10349                        gm.pk_column = field(&inner)?;
10350                        return Ok(());
10351                    }
10352                    if inner.path.is_ident("ct_column") {
10353                        gm.ct_column = field(&inner)?;
10354                        return Ok(());
10355                    }
10356                    if inner.path.is_ident("related_column") {
10357                        gm.related_column = field(&inner)?;
10358                        return Ok(());
10359                    }
10360                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10361                })?;
10362                for (val, label) in [
10363                    (&gm.name, "name"),
10364                    (&gm.through, "through"),
10365                    (&gm.pk_column, "pk_column"),
10366                    (&gm.ct_column, "ct_column"),
10367                    (&gm.related_column, "related_column"),
10368                ] {
10369                    if val.is_empty() {
10370                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10371                    }
10372                }
10373                out.generic_m2m.push(gm);
10374                return Ok(());
10375            }
10376            Err(meta.error("unknown rustango container attribute"))
10377        })?;
10378    }
10379    Ok(out)
10380}
10381
10382/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10383/// owned field names, trimming whitespace and skipping empty entries.
10384/// Field-name validation against the model is done by the caller.
10385fn split_field_list(raw: &str) -> Vec<String> {
10386    raw.split(',')
10387        .map(str::trim)
10388        .filter(|s| !s.is_empty())
10389        .map(str::to_owned)
10390        .collect()
10391}
10392
10393/// Shared parser for `unique_together` and `index_together` container
10394/// attrs. Accepts both shapes:
10395///
10396///   * `attr = "col1, col2"`              — auto-derived index name.
10397///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10398///
10399/// Returns `(columns, name)`.
10400fn parse_together_attr(
10401    meta: &syn::meta::ParseNestedMeta<'_>,
10402    attr: &str,
10403) -> syn::Result<(Vec<String>, Option<String>)> {
10404    // Disambiguate by whether the next token is `=` (key-value) or
10405    // `(` (parenthesized).
10406    if meta.input.peek(syn::Token![=]) {
10407        let cols_lit: LitStr = meta.value()?.parse()?;
10408        let columns = split_field_list(&cols_lit.value());
10409        check_together_columns(meta, attr, &columns)?;
10410        return Ok((columns, None));
10411    }
10412    let mut columns: Option<Vec<String>> = None;
10413    let mut name: Option<String> = None;
10414    meta.parse_nested_meta(|inner| {
10415        if inner.path.is_ident("columns") {
10416            let s: LitStr = inner.value()?.parse()?;
10417            columns = Some(split_field_list(&s.value()));
10418            return Ok(());
10419        }
10420        if inner.path.is_ident("name") {
10421            let s: LitStr = inner.value()?.parse()?;
10422            name = Some(s.value());
10423            return Ok(());
10424        }
10425        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10426    })?;
10427    let columns = columns.ok_or_else(|| {
10428        meta.error(format!(
10429            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10430        ))
10431    })?;
10432    check_together_columns(meta, attr, &columns)?;
10433    Ok((columns, name))
10434}
10435
10436fn check_together_columns(
10437    meta: &syn::meta::ParseNestedMeta<'_>,
10438    attr: &str,
10439    columns: &[String],
10440) -> syn::Result<()> {
10441    if columns.len() < 2 {
10442        let single = if attr == "unique_together" {
10443            "#[rustango(unique)] on the field"
10444        } else {
10445            "#[rustango(index)] on the field"
10446        };
10447        return Err(meta.error(format!(
10448            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10449        )));
10450    }
10451    Ok(())
10452}
10453
10454/// Parse the fieldsets DSL: pipe-separated sections, optional
10455/// `"Title:"` prefix on each, comma-separated field names after.
10456/// Examples:
10457/// * `"name, office"` → one untitled section with two fields
10458/// * `"Identity: name, office | Metadata: created_at"` → two titled
10459///   sections
10460///
10461/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10462fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10463    raw.split('|')
10464        .map(str::trim)
10465        .filter(|s| !s.is_empty())
10466        .map(|section| {
10467            // Split off an optional `Title:` prefix (first colon).
10468            let (title, rest) = match section.split_once(':') {
10469                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10470                _ => (String::new(), section),
10471            };
10472            let fields = split_field_list(rest);
10473            (title, fields)
10474        })
10475        .collect()
10476}
10477
10478/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10479/// comma-separated entry maps a target field to one or more source
10480/// fields joined with `+`. Whitespace around tokens is trimmed.
10481/// Entries missing `:` or with empty target/source lists are dropped.
10482fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10483    raw.split(',')
10484        .map(str::trim)
10485        .filter(|s| !s.is_empty())
10486        .filter_map(|entry| {
10487            let (target, sources_raw) = entry.split_once(':')?;
10488            let target = target.trim().to_owned();
10489            if target.is_empty() {
10490                return None;
10491            }
10492            let sources: Vec<String> = sources_raw
10493                .split('+')
10494                .map(|s| s.trim().to_owned())
10495                .filter(|s| !s.is_empty())
10496                .collect();
10497            if sources.is_empty() {
10498                return None;
10499            }
10500            Some((target, sources))
10501        })
10502        .collect()
10503}
10504
10505/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10506/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10507/// and empty halves drop silently — the macro layer only enforces shape,
10508/// not field-name vs. widget-name validity (those checks happen at
10509/// `AdminConfig` consumption time). Issue #359.
10510fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10511    raw.split(',')
10512        .map(str::trim)
10513        .filter(|s| !s.is_empty())
10514        .filter_map(|entry| {
10515            let (field, widget) = entry.split_once(':')?;
10516            let field = field.trim().to_owned();
10517            let widget = widget.trim().to_owned();
10518            if field.is_empty() || widget.is_empty() {
10519                return None;
10520            }
10521            Some((field, widget))
10522        })
10523        .collect()
10524}
10525
10526/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10527/// Returns `(field_name, desc)` pairs in the same order as the input.
10528fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10529    raw.split(',')
10530        .map(str::trim)
10531        .filter(|s| !s.is_empty())
10532        .map(|spec| {
10533            spec.strip_prefix('-')
10534                .map_or((spec.to_owned(), false), |rest| {
10535                    (rest.trim().to_owned(), true)
10536                })
10537        })
10538        .collect()
10539}
10540
10541struct FieldAttrs {
10542    column: Option<String>,
10543    primary_key: bool,
10544    fk: Option<String>,
10545    o2o: Option<String>,
10546    on: Option<String>,
10547    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10548    /// "set_default" | "no_action")]` — what happens to this row when
10549    /// the row it points at is deleted.
10550    /// Only meaningful when `fk` / `o2o` is
10551    /// also set; the macro errors at compile time if applied to a
10552    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10553    /// DDL writer renders `ON DELETE <action>` after the constraint
10554    /// clause when this is `Some`; `None` falls back to the database
10555    /// default (NO ACTION on every backend rustango supports).
10556    on_delete: Option<String>,
10557    /// `#[rustango(related_name = "...")]` — per-FK
10558    /// reverse-accessor override. When set, the derive emits
10559    /// `Parent::<related_name>[_pool]` instead of the container-level
10560    /// `default_related_name` or the `<child_snake>_set[_pool]`
10561    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10562    /// silently ignored on non-FK fields. Follow-up to #816.
10563    related_name: Option<String>,
10564    max_length: Option<u32>,
10565    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10566    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10567    /// unconstrained `vector` column.
10568    vector_dims: Option<u32>,
10569    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10570    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10571    /// unconstrained `geometry(Point)` column (SRID 0).
10572    geometry_srid: Option<u32>,
10573    min: Option<i64>,
10574    max: Option<i64>,
10575    default: Option<String>,
10576    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10577    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10578    /// "gen_random_uuid()"`. The Rust field type must be
10579    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10580    /// INSERTs so the DB DEFAULT fires.
10581    auto_uuid: bool,
10582    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10583    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10584    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10585    /// the field is `Auto::Unset`, then bound as a normal parameter
10586    /// rather than relying on a per-dialect DB function. Issue #823
10587    /// (Eloquent `HasUuids`).
10588    ///
10589    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10590    /// Composes with every backend (PG / MySQL / SQLite) — no
10591    /// `gen_random_uuid()` requirement on the database.
10592    default_uuid_v7: bool,
10593    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10594    /// Server-set on insert, immutable from app code afterwards.
10595    /// Implies `auto + default = "now()"`. Field type must be
10596    /// `DateTime<Utc>`.
10597    auto_now_add: bool,
10598    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10599    /// every insert AND every update. Implies `auto + default =
10600    /// "now()"`; the macro additionally rewrites `update_on` /
10601    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10602    /// field value.
10603    auto_now: bool,
10604    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10605    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10606    /// `soft_delete_on(executor)` and `restore_on(executor)`
10607    /// methods on the model.
10608    soft_delete: bool,
10609    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10610    /// the column in the generated DDL.
10611    unique: bool,
10612    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10613    /// generates a `CREATE INDEX` for this column. `unique` here means
10614    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10615    index: bool,
10616    index_unique: bool,
10617    index_name: Option<String>,
10618    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10619    /// `"btree"`. Issue #34.
10620    index_method: String,
10621    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10622    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10623    /// the macro skips this column from every INSERT and UPDATE
10624    /// path, so the database always recomputes the value from
10625    /// `EXPR`. Backlog item #35.
10626    generated_as: Option<String>,
10627    /// `#[rustango(help_text = "…")]` — help text
10628    /// rendered below the admin form's input. Threaded into
10629    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10630    /// layers can read it.
10631    help_text: Option<String>,
10632    /// `#[rustango(choices = "value:Label, value:Label")]` — the
10633    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10634    /// as a `&'static [(&'static str, &'static str)]` slice. When
10635    /// present, the admin form renders a `<select>` instead of `<input>`
10636    /// and the validator rejects values not in the list. Only meaningful
10637    /// for `FieldType::String`; the macro errors at compile time if
10638    /// applied to a non-string field.
10639    choices: Option<Vec<(String, String)>>,
10640    /// `#[rustango(db_comment = "…")]` — DB-side column
10641    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10642    /// the comment in CREATE TABLE; Postgres emits a separate
10643    /// `COMMENT ON COLUMN` statement after the table is created;
10644    /// SQLite silently drops the value (no native column comments).
10645    db_comment: Option<String>,
10646    /// `#[rustango(verbose_name = "…")]` — human-readable
10647    /// label for the field. Threaded into `FieldSchema::verbose_name`
10648    /// so admin column headers, form labels, and other display
10649    /// surfaces can prefer the friendly caption over the Rust
10650    /// identifier. `None` means renderers fall back to the field name.
10651    verbose_name: Option<String>,
10652    /// `#[rustango(editable = false)]` — opt out of
10653    /// auto-generated form rendering. Defaults to `true` so existing
10654    /// fields keep their current admin / form behavior; setting
10655    /// `false` removes the field from the admin change-form entirely
10656    /// (the value is still visible on detail / list views, just not
10657    /// editable).
10658    editable: bool,
10659    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10660    /// "form may submit empty even when DB is NOT NULL". Threaded into
10661    /// `FieldSchema::blank`. Defaults to `false`.
10662    blank: bool,
10663    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10664    /// case-insensitive text column. Threaded into
10665    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10666    /// fields; the macro errors at derive time if applied elsewhere.
10667    case_insensitive: bool,
10668    /// `#[rustango(validators = "email,url")]` — the
10669    /// model-level validator chain. Comma-separated names that
10670    /// dispatch to the `validators::*` family in `validate_value`.
10671    /// Empty by default; fires on every typed INSERT/UPDATE.
10672    validators: Vec<String>,
10673}
10674
10675fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10676    let mut out = FieldAttrs {
10677        column: None,
10678        primary_key: false,
10679        fk: None,
10680        o2o: None,
10681        on: None,
10682        on_delete: None,
10683        related_name: None,
10684        max_length: None,
10685        vector_dims: None,
10686        geometry_srid: None,
10687        min: None,
10688        max: None,
10689        default: None,
10690        auto_uuid: false,
10691        default_uuid_v7: false,
10692        auto_now_add: false,
10693        auto_now: false,
10694        soft_delete: false,
10695        unique: false,
10696        index: false,
10697        index_unique: false,
10698        index_name: None,
10699        index_method: "btree".to_owned(),
10700        generated_as: None,
10701        help_text: None,
10702        choices: None,
10703        db_comment: None,
10704        verbose_name: None,
10705        editable: true,
10706        blank: false,
10707        case_insensitive: false,
10708        validators: Vec::new(),
10709    };
10710    for attr in &field.attrs {
10711        if !attr.path().is_ident("rustango") {
10712            continue;
10713        }
10714        attr.parse_nested_meta(|meta| {
10715            if meta.path.is_ident("column") {
10716                let s: LitStr = meta.value()?.parse()?;
10717                let name = s.value();
10718                validate_sql_identifier(&name, "column", s.span())?;
10719                out.column = Some(name);
10720                return Ok(());
10721            }
10722            if meta.path.is_ident("primary_key") {
10723                out.primary_key = true;
10724                return Ok(());
10725            }
10726            if meta.path.is_ident("fk") {
10727                let s: LitStr = meta.value()?.parse()?;
10728                out.fk = Some(s.value());
10729                return Ok(());
10730            }
10731            if meta.path.is_ident("o2o") {
10732                let s: LitStr = meta.value()?.parse()?;
10733                out.o2o = Some(s.value());
10734                return Ok(());
10735            }
10736            if meta.path.is_ident("on") {
10737                let s: LitStr = meta.value()?.parse()?;
10738                out.on = Some(s.value());
10739                return Ok(());
10740            }
10741            if meta.path.is_ident("on_delete") {
10742                let s: LitStr = meta.value()?.parse()?;
10743                let raw = s.value();
10744                let normalized = raw.trim().to_ascii_lowercase();
10745                // Validate at parse time so the user gets a clear span
10746                // rather than a downstream compile error in the emit.
10747                match normalized.as_str() {
10748                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10749                    _ => {
10750                        return Err(syn::Error::new(
10751                            s.span(),
10752                            format!(
10753                                "unknown on_delete action `{raw}`; expected one of \
10754                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10755                            ),
10756                        ));
10757                    }
10758                }
10759                out.on_delete = Some(normalized);
10760                return Ok(());
10761            }
10762            if meta.path.is_ident("related_name") {
10763                let s: LitStr = meta.value()?.parse()?;
10764                let raw = s.value();
10765                if raw.trim().is_empty() {
10766                    return Err(syn::Error::new(
10767                        s.span(),
10768                        "`related_name` must be a non-empty identifier",
10769                    ));
10770                }
10771                // Validate as a Rust-ident shape — the value becomes
10772                // a method name on the parent type. Same rule as
10773                // `default_related_name` so the two surfaces match.
10774                if !raw
10775                    .chars()
10776                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10777                    || raw.starts_with(char::is_numeric)
10778                {
10779                    return Err(syn::Error::new(
10780                        s.span(),
10781                        "`related_name` must be snake_case ASCII (lowercase letters, \
10782                         digits, underscores; no leading digit)",
10783                    ));
10784                }
10785                out.related_name = Some(raw);
10786                return Ok(());
10787            }
10788            if meta.path.is_ident("max_length") {
10789                let lit: syn::LitInt = meta.value()?.parse()?;
10790                out.max_length = Some(lit.base10_parse::<u32>()?);
10791                return Ok(());
10792            }
10793            // `#[rustango(vector(dims = N))]` — pgvector column
10794            // dimension (#824). Nested-meta form so it reads like the
10795            // other typed-column attrs.
10796            if meta.path.is_ident("vector") {
10797                meta.parse_nested_meta(|inner| {
10798                    if inner.path.is_ident("dims") {
10799                        let lit: syn::LitInt = inner.value()?.parse()?;
10800                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10801                        return Ok(());
10802                    }
10803                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10804                })?;
10805                return Ok(());
10806            }
10807            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10808            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10809            if meta.path.is_ident("geometry") {
10810                meta.parse_nested_meta(|inner| {
10811                    if inner.path.is_ident("srid") {
10812                        let lit: syn::LitInt = inner.value()?.parse()?;
10813                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10814                        return Ok(());
10815                    }
10816                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10817                })?;
10818                return Ok(());
10819            }
10820            if meta.path.is_ident("min") {
10821                out.min = Some(parse_signed_i64(&meta)?);
10822                return Ok(());
10823            }
10824            if meta.path.is_ident("max") {
10825                out.max = Some(parse_signed_i64(&meta)?);
10826                return Ok(());
10827            }
10828            if meta.path.is_ident("default") {
10829                let s: LitStr = meta.value()?.parse()?;
10830                out.default = Some(s.value());
10831                return Ok(());
10832            }
10833            if meta.path.is_ident("generated_as") {
10834                let s: LitStr = meta.value()?.parse()?;
10835                out.generated_as = Some(s.value());
10836                return Ok(());
10837            }
10838            if meta.path.is_ident("help_text") {
10839                let s: LitStr = meta.value()?.parse()?;
10840                out.help_text = Some(s.value());
10841                return Ok(());
10842            }
10843            if meta.path.is_ident("choices") {
10844                let s: LitStr = meta.value()?.parse()?;
10845                let raw = s.value();
10846                let mut pairs: Vec<(String, String)> = Vec::new();
10847                for chunk in raw.split(',') {
10848                    let chunk = chunk.trim();
10849                    if chunk.is_empty() {
10850                        continue;
10851                    }
10852                    let (value, label) = match chunk.split_once(':') {
10853                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10854                        None => (chunk.to_owned(), chunk.to_owned()),
10855                    };
10856                    if value.is_empty() {
10857                        return Err(syn::Error::new(
10858                            s.span(),
10859                            "`choices` entry has empty value before `:`",
10860                        ));
10861                    }
10862                    pairs.push((value, label));
10863                }
10864                if pairs.is_empty() {
10865                    return Err(syn::Error::new(
10866                        s.span(),
10867                        "`choices = \"…\"` must contain at least one value",
10868                    ));
10869                }
10870                out.choices = Some(pairs);
10871                return Ok(());
10872            }
10873            if meta.path.is_ident("db_comment") {
10874                let s: LitStr = meta.value()?.parse()?;
10875                out.db_comment = Some(s.value());
10876                return Ok(());
10877            }
10878            if meta.path.is_ident("verbose_name") {
10879                let s: LitStr = meta.value()?.parse()?;
10880                out.verbose_name = Some(s.value());
10881                return Ok(());
10882            }
10883            if meta.path.is_ident("editable") {
10884                // Two forms accepted:
10885                //   #[rustango(editable = false)] / true — explicit
10886                //   #[rustango(editable)] — flag form (= true, the
10887                //   default, so harmless; included for symmetry)
10888                if let Ok(v) = meta.value() {
10889                    let lit: syn::LitBool = v.parse()?;
10890                    out.editable = lit.value;
10891                } else {
10892                    out.editable = true;
10893                }
10894                return Ok(());
10895            }
10896            if meta.path.is_ident("blank") {
10897                // Two forms accepted:
10898                //   #[rustango(blank)] — flag form, true
10899                //   #[rustango(blank = false)] / true — explicit
10900                if let Ok(v) = meta.value() {
10901                    let lit: syn::LitBool = v.parse()?;
10902                    out.blank = lit.value;
10903                } else {
10904                    out.blank = true;
10905                }
10906                return Ok(());
10907            }
10908            if meta.path.is_ident("citext") {
10909                // Case-insensitive text column (#344). Two forms:
10910                //   #[rustango(citext)]          — flag form, true
10911                //   #[rustango(citext = true)]   — explicit
10912                //   #[rustango(citext = false)]  — explicit opt-out
10913                // String-only validation lives in the field-type
10914                // emitter (the FieldType discriminant is computed in
10915                // `detect_type`); the macro records the flag and the
10916                // DDL writer emits dialect-specific COLLATE / CITEXT.
10917                if let Ok(v) = meta.value() {
10918                    let lit: syn::LitBool = v.parse()?;
10919                    out.case_insensitive = lit.value;
10920                } else {
10921                    out.case_insensitive = true;
10922                }
10923                return Ok(());
10924            }
10925            if meta.path.is_ident("validators") {
10926                let s: LitStr = meta.value()?.parse()?;
10927                let raw = s.value();
10928                out.validators = raw
10929                    .split(',')
10930                    .map(str::trim)
10931                    .filter(|s| !s.is_empty())
10932                    .map(str::to_owned)
10933                    .collect();
10934                if out.validators.is_empty() {
10935                    return Err(syn::Error::new(
10936                        s.span(),
10937                        "`validators = \"…\"` must list at least one name",
10938                    ));
10939                }
10940                return Ok(());
10941            }
10942            if meta.path.is_ident("auto_uuid") {
10943                out.auto_uuid = true;
10944                // Implied: PK + auto + DEFAULT gen_random_uuid().
10945                // Each is also explicitly settable; the explicit
10946                // value wins if conflicting.
10947                out.primary_key = true;
10948                if out.default.is_none() {
10949                    out.default = Some("gen_random_uuid()".into());
10950                }
10951                return Ok(());
10952            }
10953            if meta.path.is_ident("default_uuid_v7") {
10954                // Backend-neutral counterpart of `auto_uuid` — issue #823.
10955                // No SQL DEFAULT (the macro fills the value Rust-side
10956                // before binding); just mark the field as PK + Auto
10957                // so the insert path is the `Auto::Unset → generate`
10958                // branch.
10959                out.default_uuid_v7 = true;
10960                out.primary_key = true;
10961                return Ok(());
10962            }
10963            if meta.path.is_ident("auto_now_add") {
10964                out.auto_now_add = true;
10965                if out.default.is_none() {
10966                    out.default = Some("now()".into());
10967                }
10968                return Ok(());
10969            }
10970            if meta.path.is_ident("auto_now") {
10971                out.auto_now = true;
10972                if out.default.is_none() {
10973                    out.default = Some("now()".into());
10974                }
10975                return Ok(());
10976            }
10977            if meta.path.is_ident("soft_delete") {
10978                out.soft_delete = true;
10979                return Ok(());
10980            }
10981            if meta.path.is_ident("unique") {
10982                out.unique = true;
10983                return Ok(());
10984            }
10985            if meta.path.is_ident("index") {
10986                out.index = true;
10987                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10988                if meta.input.peek(syn::token::Paren) {
10989                    meta.parse_nested_meta(|inner| {
10990                        if inner.path.is_ident("unique") {
10991                            out.index_unique = true;
10992                            return Ok(());
10993                        }
10994                        if inner.path.is_ident("name") {
10995                            let s: LitStr = inner.value()?.parse()?;
10996                            out.index_name = Some(s.value());
10997                            return Ok(());
10998                        }
10999                        if inner.path.is_ident("method") {
11000                            let s: LitStr = inner.value()?.parse()?;
11001                            let v = s.value();
11002                            match v.as_str() {
11003                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11004                                    out.index_method = v;
11005                                }
11006                                other => {
11007                                    return Err(inner.error(format!(
11008                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11009                                    )));
11010                                }
11011                            }
11012                            return Ok(());
11013                        }
11014                        Err(inner.error(
11015                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11016                        ))
11017                    })?;
11018                }
11019                return Ok(());
11020            }
11021            Err(meta.error("unknown rustango field attribute"))
11022        })?;
11023    }
11024    Ok(out)
11025}
11026
11027/// Parse a signed integer literal, accepting optional leading `-`.
11028fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11029    let expr: syn::Expr = meta.value()?.parse()?;
11030    match expr {
11031        syn::Expr::Lit(syn::ExprLit {
11032            lit: syn::Lit::Int(lit),
11033            ..
11034        }) => lit.base10_parse::<i64>(),
11035        syn::Expr::Unary(syn::ExprUnary {
11036            op: syn::UnOp::Neg(_),
11037            expr,
11038            ..
11039        }) => {
11040            if let syn::Expr::Lit(syn::ExprLit {
11041                lit: syn::Lit::Int(lit),
11042                ..
11043            }) = *expr
11044            {
11045                let v: i64 = lit.base10_parse()?;
11046                Ok(-v)
11047            } else {
11048                Err(syn::Error::new_spanned(expr, "expected integer literal"))
11049            }
11050        }
11051        other => Err(syn::Error::new_spanned(
11052            other,
11053            "expected integer literal (signed)",
11054        )),
11055    }
11056}
11057
11058struct FieldInfo<'a> {
11059    ident: &'a syn::Ident,
11060    column: String,
11061    primary_key: bool,
11062    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11063    /// skip this column when `Auto::Unset` and emit it under
11064    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11065    auto: bool,
11066    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11067    /// the `Column::Value` associated type for typed-column tokens.
11068    value_ty: &'a Type,
11069    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11070    field_type_tokens: TokenStream2,
11071    schema: TokenStream2,
11072    from_row_init: TokenStream2,
11073    /// Variant of [`Self::from_row_init`] that reads the column via
11074    /// `format!("{prefix}__{col}")` so a model can be decoded out of
11075    /// the aliased columns of a JOINed row. Drives slice 9.0d's
11076    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11077    /// helper that `select_related` calls when stitching loaded FKs.
11078    from_aliased_row_init: TokenStream2,
11079    /// MySQL twins of the two inits above. They differ only for a UUID
11080    /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11081    from_row_init_my: TokenStream2,
11082    from_aliased_row_init_my: TokenStream2,
11083    /// A bare or optional `Uuid` field.
11084    uuid_column: bool,
11085    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11086    /// relation helper emit (`Author::<child>_set`) needs to know `T`
11087    /// to point the generated method at the right child model.
11088    fk_inner: Option<Type>,
11089    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11090    /// `kind` (since ForeignKey detection sets `kind` to K's
11091    /// underlying type) but stored separately for clarity at the
11092    /// `FkRelation` construction site, which only sees the FK's
11093    /// surface fields.
11094    fk_pk_kind: DetectedKind,
11095    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11096    /// the bare `ForeignKey<T, K>`. Routes the load_related and
11097    /// fk_pk_access emitters to wrap assignments / accessors in
11098    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11099    /// FK column compiles end-to-end. The DDL writer reads this off
11100    /// the field schema (`nullable` flag); the macro just needs to
11101    /// keep the Rust-side codegen consistent.
11102    nullable: bool,
11103    /// `true` when this column was marked `#[rustango(auto_now)]` —
11104    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11105    /// column instead of the user-supplied value, so `updated_at`
11106    /// always reflects the latest write without the caller having
11107    /// to remember to set it.
11108    auto_now: bool,
11109    /// `true` when this column was marked `#[rustango(auto_now_add)]`
11110    /// — the column is server-set on INSERT (DB DEFAULT) and
11111    /// **immutable** afterwards. `update_on` / `save_on` skip the
11112    /// column entirely so a stale `created_at` value in memory never
11113    /// rewrites the persisted timestamp.
11114    auto_now_add: bool,
11115    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11116    /// Triggers emission of `soft_delete_on(executor)` and
11117    /// `restore_on(executor)` on the model's inherent impl. There is
11118    /// at most one such column per model — emission asserts this.
11119    soft_delete: bool,
11120    /// `Some` when this column was marked
11121    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11122    /// every INSERT and UPDATE path; the database recomputes the
11123    /// value from `EXPR`. Backlog item #35.
11124    generated_as: Option<String>,
11125    /// `true` when this column was marked
11126    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11127    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11128    /// with `Uuid::now_v7()` before binding, so the PK is generated
11129    /// Rust-side and the column is always present in the INSERT
11130    /// statement (no DB DEFAULT requirement). Issue #823.
11131    default_uuid_v7: bool,
11132    /// `Some` when this FK field carried `#[rustango(related_name =
11133    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11134    /// consumed by [`reverse_helper_tokens`] to override the default
11135    /// `<child_snake>_set` accessor name. Follow-up to #816.
11136    related_name: Option<String>,
11137}
11138
11139/// Reject table names that won't survive SQL identifier
11140/// derivation downstream. Postgres' regular-identifier rule
11141/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11142/// through the framework's unquoted FK / index / constraint name
11143/// emission without surprises. We also disallow leading-digit and
11144/// the empty string for clarity.
11145///
11146/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11147/// here — those produce a runtime error from the SQL parser,
11148/// which is loud enough; statically enumerating reserved words
11149/// across the three supported dialects is more friction than help.
11150///
11151/// Backlog item #65.
11152fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11153    validate_sql_identifier(name, "table", span)
11154}
11155
11156/// Reject SQL identifiers that compile but break downstream SQL
11157/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11158/// `kind` is "table" / "column" — used for the error message so users
11159/// see which attribute caused the failure.
11160fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11161    if name.is_empty() {
11162        return Err(syn::Error::new(
11163            span,
11164            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11165        ));
11166    }
11167    let mut chars = name.chars();
11168    let first = chars.next().unwrap();
11169    if !(first.is_ascii_alphabetic() || first == '_') {
11170        return Err(syn::Error::new(
11171            span,
11172            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11173        ));
11174    }
11175    for c in chars {
11176        if !(c.is_ascii_alphanumeric() || c == '_') {
11177            return Err(syn::Error::new(
11178                span,
11179                format!(
11180                    "{kind} name `{name}` contains invalid character {c:?} — \
11181                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11182                     Hyphens in particular break FK / index name derivation \
11183                     downstream; use underscores instead (e.g. `{}`)",
11184                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11185                ),
11186            ));
11187        }
11188    }
11189    Ok(())
11190}
11191
11192fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11193    let root = rustango_root();
11194    let attrs = parse_field_attrs(field)?;
11195    let ident = field
11196        .ident
11197        .as_ref()
11198        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11199    let name = ident_name(ident);
11200    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11201    let primary_key = attrs.primary_key;
11202    let DetectedType {
11203        kind,
11204        nullable,
11205        auto: detected_auto,
11206        fk_inner,
11207    } = detect_type(&field.ty)?;
11208    check_bound_compatibility(field, &attrs, kind)?;
11209    let auto = detected_auto;
11210    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11211    // INSERT path: the user must wrap the field in `Auto<T>`, which
11212    // marks the column as DB-default-supplied. The mixin attrs then
11213    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11214    // for `auto_now`, force the value on UPDATE too.
11215    if attrs.auto_uuid {
11216        if kind != DetectedKind::Uuid {
11217            return Err(syn::Error::new_spanned(
11218                field,
11219                "`#[rustango(auto_uuid)]` requires the field type to be \
11220                 `Auto<uuid::Uuid>`",
11221            ));
11222        }
11223        if !detected_auto {
11224            return Err(syn::Error::new_spanned(
11225                field,
11226                "`#[rustango(auto_uuid)]` requires the field type to be \
11227                 wrapped in `Auto<...>` so the macro skips the column on \
11228                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11229            ));
11230        }
11231    }
11232    if attrs.default_uuid_v7 {
11233        if kind != DetectedKind::Uuid {
11234            return Err(syn::Error::new_spanned(
11235                field,
11236                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11237                 `Auto<uuid::Uuid>`",
11238            ));
11239        }
11240        if !detected_auto {
11241            return Err(syn::Error::new_spanned(
11242                field,
11243                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11244                 wrapped in `Auto<...>` so the macro can detect the \
11245                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11246            ));
11247        }
11248        if attrs.auto_uuid {
11249            return Err(syn::Error::new_spanned(
11250                field,
11251                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11252                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11253                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11254                 Pick one.",
11255            ));
11256        }
11257    }
11258    if attrs.auto_now_add || attrs.auto_now {
11259        if kind != DetectedKind::DateTime {
11260            return Err(syn::Error::new_spanned(
11261                field,
11262                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11263                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11264            ));
11265        }
11266        if !detected_auto {
11267            return Err(syn::Error::new_spanned(
11268                field,
11269                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11270                 the field type to be wrapped in `Auto<...>` so the macro skips \
11271                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11272            ));
11273        }
11274    }
11275    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11276        return Err(syn::Error::new_spanned(
11277            field,
11278            "`#[rustango(soft_delete)]` requires the field type to be \
11279             `Option<chrono::DateTime<chrono::Utc>>`",
11280        ));
11281    }
11282    let is_mixin_auto =
11283        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11284    if detected_auto && !primary_key && !is_mixin_auto {
11285        return Err(syn::Error::new_spanned(
11286            field,
11287            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11288             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11289             `auto_now`",
11290        ));
11291    }
11292    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11293        return Err(syn::Error::new_spanned(
11294            field,
11295            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11296             SERIAL / BIGSERIAL already supplies a default sequence.",
11297        ));
11298    }
11299    if fk_inner.is_some() && primary_key {
11300        return Err(syn::Error::new_spanned(
11301            field,
11302            "`ForeignKey<T>` is not allowed on a primary-key field — \
11303             a row's PK is its own identity, not a reference to a parent.",
11304        ));
11305    }
11306    if attrs.generated_as.is_some() {
11307        if primary_key {
11308            return Err(syn::Error::new_spanned(
11309                field,
11310                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11311                 primary-key field — a PK must be writable so the row \
11312                 has an identity at INSERT time.",
11313            ));
11314        }
11315        if attrs.default.is_some() {
11316            return Err(syn::Error::new_spanned(
11317                field,
11318                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11319                 `default = \"…\"` — Postgres rejects DEFAULT on \
11320                 generated columns. The expression IS the default.",
11321            ));
11322        }
11323        if detected_auto {
11324            return Err(syn::Error::new_spanned(
11325                field,
11326                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11327                 an `Auto<T>` field — generated columns are computed \
11328                 by the DB, not server-assigned via a sequence. Use a \
11329                 plain Rust type (e.g. `f64`).",
11330            ));
11331        }
11332        if fk_inner.is_some() {
11333            return Err(syn::Error::new_spanned(
11334                field,
11335                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11336                 ForeignKey field.",
11337            ));
11338        }
11339    }
11340    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11341    let column_lit = column.as_str();
11342    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11343    // dimension that the bare `Vector` Rust type can't carry, so emit
11344    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11345    // fallback of `Vector(0)`.
11346    let field_type_tokens = if kind == DetectedKind::Vector {
11347        let root = rustango_root();
11348        let dims = attrs.vector_dims.unwrap_or(0);
11349        quote!(#root::core::FieldType::Vector(#dims))
11350    } else if kind == DetectedKind::Geometry {
11351        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11352        // SRID that the bare `Point` Rust type can't carry, so emit
11353        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11354        let root = rustango_root();
11355        let srid = attrs.geometry_srid.unwrap_or(0);
11356        quote!(#root::core::FieldType::Geometry(#srid))
11357    } else {
11358        kind.variant_tokens()
11359    };
11360    let max_length = optional_u32(attrs.max_length);
11361    let min = optional_i64(attrs.min);
11362    let max = optional_i64(attrs.max);
11363    let default = optional_str(attrs.default.as_deref());
11364
11365    let unique = attrs.unique;
11366    let generated_as = optional_str(attrs.generated_as.as_deref());
11367    let help_text = optional_str(attrs.help_text.as_deref());
11368    let choices = optional_choices(attrs.choices.as_deref());
11369    let db_comment = optional_str(attrs.db_comment.as_deref());
11370    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11371    let editable = attrs.editable;
11372    let blank = attrs.blank;
11373    let case_insensitive = attrs.case_insensitive;
11374    let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11375    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11376        return Err(syn::Error::new_spanned(
11377            field,
11378            "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11379             or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11380             non-FK column.",
11381        ));
11382    }
11383    let fk_on_delete = match attrs.on_delete.as_deref() {
11384        None => quote!(::core::option::Option::None),
11385        Some(action) => {
11386            let variant = match action {
11387                "cascade" => quote!(Cascade),
11388                "restrict" => quote!(Restrict),
11389                "set_null" => quote!(SetNull),
11390                "set_default" => quote!(SetDefault),
11391                "no_action" => quote!(NoAction),
11392                // parse_field_attrs already validated this — guard against future drift.
11393                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11394            };
11395            quote!(::core::option::Option::Some(
11396                #root::core::OnDeleteAction::#variant
11397            ))
11398        }
11399    };
11400    let auto_now_flag = attrs.auto_now;
11401    let schema = quote! {{
11402        #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11403            name: #name,
11404            column: #column_lit,
11405            ty: #field_type_tokens,
11406            nullable: #nullable,
11407            primary_key: #primary_key,
11408            relation: #relation,
11409            max_length: #max_length,
11410            min: #min,
11411            max: #max,
11412            default: #default,
11413            auto: #auto,
11414            auto_now: #auto_now_flag,
11415            unique: #unique,
11416            generated_as: #generated_as,
11417            help_text: #help_text,
11418            choices: #choices,
11419            db_comment: #db_comment,
11420            verbose_name: #verbose_name,
11421            editable: #editable,
11422            blank: #blank,
11423            case_insensitive: #case_insensitive,
11424            fk_on_delete: #fk_on_delete,
11425            validators: &[ #(#validators_lits),* ],
11426        })
11427    }};
11428
11429    let from_row_init = quote! {
11430        #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11431    };
11432    let from_aliased_row_init = quote! {
11433        #ident: #root::sql::sqlx::Row::try_get(
11434            row,
11435            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11436        )?
11437    };
11438    // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11439    // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11440    let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11441    let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11442        (
11443            quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11444            quote! {
11445                #ident: #root::sql::try_get_flat_my(
11446                    row,
11447                    ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11448                )?
11449            },
11450        )
11451    } else {
11452        (from_row_init.clone(), from_aliased_row_init.clone())
11453    };
11454
11455    Ok(FieldInfo {
11456        ident,
11457        column,
11458        primary_key,
11459        auto,
11460        value_ty: &field.ty,
11461        field_type_tokens,
11462        schema,
11463        from_row_init,
11464        from_aliased_row_init,
11465        from_row_init_my,
11466        from_aliased_row_init_my,
11467        uuid_column,
11468        fk_inner: fk_inner.cloned(),
11469        fk_pk_kind: kind,
11470        nullable,
11471        auto_now: attrs.auto_now,
11472        auto_now_add: attrs.auto_now_add,
11473        soft_delete: attrs.soft_delete,
11474        generated_as: attrs.generated_as.clone(),
11475        default_uuid_v7: attrs.default_uuid_v7,
11476        related_name: attrs.related_name.clone(),
11477    })
11478}
11479
11480fn check_bound_compatibility(
11481    field: &syn::Field,
11482    attrs: &FieldAttrs,
11483    kind: DetectedKind,
11484) -> syn::Result<()> {
11485    if attrs.max_length.is_some() && kind != DetectedKind::String {
11486        return Err(syn::Error::new_spanned(
11487            field,
11488            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11489        ));
11490    }
11491    // These were silently dropped on the wrong type (#1937).
11492    if attrs.case_insensitive && kind != DetectedKind::String {
11493        return Err(syn::Error::new_spanned(
11494            field,
11495            "`citext` is only valid on `String` fields (or `Option<String>`)",
11496        ));
11497    }
11498    if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11499        return Err(syn::Error::new_spanned(
11500            field,
11501            "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11502        ));
11503    }
11504    if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11505        return Err(syn::Error::new_spanned(
11506            field,
11507            "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11508        ));
11509    }
11510    if attrs.choices.is_some() && kind != DetectedKind::String {
11511        return Err(syn::Error::new_spanned(
11512            field,
11513            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11514             integer-valued enumerations should be modeled with a Rust enum and \
11515             custom (de)serializer for now",
11516        ));
11517    }
11518    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11519        return Err(syn::Error::new_spanned(
11520            field,
11521            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11522        ));
11523    }
11524    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11525        if min > max {
11526            return Err(syn::Error::new_spanned(
11527                field,
11528                format!("`min` ({min}) is greater than `max` ({max})"),
11529            ));
11530        }
11531    }
11532    Ok(())
11533}
11534
11535/// A `&'static [ty]` of `const fn` results. The slice needs its own
11536/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11537fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11538    quote!({
11539        const ITEMS: &[#ty] = &[ #(#items),* ];
11540        ITEMS
11541    })
11542}
11543
11544fn optional_u32(value: Option<u32>) -> TokenStream2 {
11545    if let Some(v) = value {
11546        quote!(::core::option::Option::Some(#v))
11547    } else {
11548        quote!(::core::option::Option::None)
11549    }
11550}
11551
11552fn optional_i64(value: Option<i64>) -> TokenStream2 {
11553    if let Some(v) = value {
11554        quote!(::core::option::Option::Some(#v))
11555    } else {
11556        quote!(::core::option::Option::None)
11557    }
11558}
11559
11560fn optional_str(value: Option<&str>) -> TokenStream2 {
11561    if let Some(v) = value {
11562        quote!(::core::option::Option::Some(#v))
11563    } else {
11564        quote!(::core::option::Option::None)
11565    }
11566}
11567
11568fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11569    let Some(pairs) = pairs else {
11570        return quote!(::core::option::Option::None);
11571    };
11572    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11573    quote!(::core::option::Option::Some(&[#(#entries),*]))
11574}
11575
11576fn relation_tokens(
11577    field: &syn::Field,
11578    attrs: &FieldAttrs,
11579    fk_inner: Option<&syn::Type>,
11580    table: &str,
11581) -> syn::Result<TokenStream2> {
11582    let root = rustango_root();
11583    if let Some(inner) = fk_inner {
11584        if attrs.fk.is_some() || attrs.o2o.is_some() {
11585            return Err(syn::Error::new_spanned(
11586                field,
11587                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11588                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11589            ));
11590        }
11591        let on = attrs.on.as_deref().unwrap_or("id");
11592        return Ok(quote! {
11593            ::core::option::Option::Some(#root::core::Relation::fk(
11594                <#inner as #root::core::Model>::SCHEMA.table,
11595                #on,
11596            ))
11597        });
11598    }
11599    match (&attrs.fk, &attrs.o2o) {
11600        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11601            field,
11602            "`fk` and `o2o` are mutually exclusive",
11603        )),
11604        (Some(to), None) => {
11605            let on = attrs.on.as_deref().unwrap_or("id");
11606            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11607            // the model's own table. Threaded as a literal string at
11608            // macro-expansion time to sidestep the const-eval cycle
11609            // that `Self::SCHEMA.table` would create when referenced
11610            // inside Self::SCHEMA's own initializer.
11611            let resolved = if to == "self" { table } else { to };
11612            Ok(quote! {
11613                ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11614            })
11615        }
11616        (None, Some(to)) => {
11617            let on = attrs.on.as_deref().unwrap_or("id");
11618            let resolved = if to == "self" { table } else { to };
11619            Ok(quote! {
11620                ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11621            })
11622        }
11623        (None, None) => {
11624            if attrs.on.is_some() {
11625                return Err(syn::Error::new_spanned(
11626                    field,
11627                    "`on` requires `fk` or `o2o`",
11628                ));
11629            }
11630            Ok(quote!(::core::option::Option::None))
11631        }
11632    }
11633}
11634
11635/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11636/// about kinds without depending on `rustango-core` (which would require a
11637/// proc-macro/normal split it doesn't have today).
11638#[derive(Clone, Copy, PartialEq, Eq)]
11639enum DetectedKind {
11640    I16,
11641    I32,
11642    I64,
11643    F32,
11644    F64,
11645    Bool,
11646    String,
11647    DateTime,
11648    Date,
11649    Time,
11650    Uuid,
11651    Json,
11652    Decimal,
11653    Binary,
11654    /// `Array<String>` → PG `text[]` (#341).
11655    ArrayText,
11656    /// `Array<i32>` → PG `integer[]` (#341).
11657    ArrayInt,
11658    /// `Array<i64>` → PG `bigint[]` (#341).
11659    ArrayBigInt,
11660    /// `Range<i32>` → PG `int4range` (#343).
11661    RangeInt,
11662    /// `Range<i64>` → PG `int8range` (#343).
11663    RangeBigInt,
11664    /// `Range<Decimal>` → PG `numrange` (#343).
11665    RangeNumeric,
11666    /// `Range<NaiveDate>` → PG `daterange` (#343).
11667    RangeDate,
11668    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11669    RangeDateTime,
11670    /// `HStore` → PG `hstore` (#342).
11671    HStore,
11672    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11673    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11674    /// in at the `FieldType` emission site (not carried on this enum).
11675    Vector,
11676    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11677    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11678    /// threaded in at the `FieldType` emission site.
11679    Geometry,
11680}
11681
11682impl DetectedKind {
11683    fn variant_tokens(self) -> TokenStream2 {
11684        let root = rustango_root();
11685        match self {
11686            Self::I16 => quote!(#root::core::FieldType::I16),
11687            Self::I32 => quote!(#root::core::FieldType::I32),
11688            Self::I64 => quote!(#root::core::FieldType::I64),
11689            Self::F32 => quote!(#root::core::FieldType::F32),
11690            Self::F64 => quote!(#root::core::FieldType::F64),
11691            Self::Bool => quote!(#root::core::FieldType::Bool),
11692            Self::String => quote!(#root::core::FieldType::String),
11693            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11694            Self::Date => quote!(#root::core::FieldType::Date),
11695            Self::Time => quote!(#root::core::FieldType::Time),
11696            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11697            Self::Json => quote!(#root::core::FieldType::Json),
11698            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11699            Self::Binary => quote!(#root::core::FieldType::Binary),
11700            Self::ArrayText => {
11701                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11702            }
11703            Self::ArrayInt => {
11704                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11705            }
11706            Self::ArrayBigInt => {
11707                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11708            }
11709            Self::RangeInt => {
11710                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11711            }
11712            Self::RangeBigInt => {
11713                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11714            }
11715            Self::RangeNumeric => {
11716                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11717            }
11718            Self::RangeDate => {
11719                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11720            }
11721            Self::RangeDateTime => {
11722                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11723            }
11724            Self::HStore => quote!(#root::core::FieldType::HStore),
11725            // Dimension comes from the `vector(dims = N)` attribute,
11726            // applied at the emission site; `0` here is just a fallback.
11727            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11728            // SRID comes from the `geometry(srid = N)` attribute, applied
11729            // at the emission site; `0` here is just a fallback.
11730            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11731        }
11732    }
11733
11734    fn is_integer(self) -> bool {
11735        matches!(self, Self::I16 | Self::I32 | Self::I64)
11736    }
11737
11738    /// `(SqlValue::<Variant>, default expr)` for emitting the
11739    /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11740    /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11741    /// fires only when the parent's `__rustango_pk_value` returns a
11742    /// different variant than expected, which is a compile-time bug —
11743    /// but we still need a value-typed fallback to keep the match
11744    /// total.
11745    fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11746        let root = rustango_root();
11747        match self {
11748            Self::I16 => (quote!(I16), quote!(0i16)),
11749            Self::I32 => (quote!(I32), quote!(0i32)),
11750            Self::I64 => (quote!(I64), quote!(0i64)),
11751            Self::F32 => (quote!(F32), quote!(0f32)),
11752            Self::F64 => (quote!(F64), quote!(0f64)),
11753            Self::Bool => (quote!(Bool), quote!(false)),
11754            Self::String => (quote!(String), quote!(::std::string::String::new())),
11755            Self::DateTime => (
11756                quote!(DateTime),
11757                quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11758            ),
11759            Self::Date => (
11760                quote!(Date),
11761                quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11762            ),
11763            Self::Time => (
11764                quote!(Time),
11765                quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11766            ),
11767            Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11768            Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11769            Self::Decimal => (
11770                quote!(Decimal),
11771                quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11772            ),
11773            Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11774            // Arrays (#341) can never be a foreign-key primary key, so
11775            // this arm is never reached at runtime — but the match must
11776            // stay total. A bare empty `SqlValue::Array` is the dummy.
11777            Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11778                (quote!(Array), quote!(::std::vec::Vec::new()))
11779            }
11780            // Ranges (#343) likewise can't be a FK PK — never reached.
11781            Self::RangeInt
11782            | Self::RangeBigInt
11783            | Self::RangeNumeric
11784            | Self::RangeDate
11785            | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11786            // HStore (#342) can't be a FK PK — never reached.
11787            Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11788            // Vector (#824) can't be a FK PK — never reached.
11789            Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11790            // Geometry (#443) can't be a FK PK — never reached (arm is
11791            // exhaustiveness-only; never interpolated into emitted code).
11792            Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11793        }
11794    }
11795}
11796
11797/// Result of walking a field's Rust type. `kind` is the underlying
11798/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11799/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11800/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11801/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11802#[derive(Clone, Copy)]
11803struct DetectedType<'a> {
11804    kind: DetectedKind,
11805    nullable: bool,
11806    auto: bool,
11807    fk_inner: Option<&'a syn::Type>,
11808}
11809
11810/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11811/// non-`Auto` types — the caller should already have routed Auto-only
11812/// codegen through this helper, so a `None` indicates a macro-internal
11813/// invariant break.
11814fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11815    let Type::Path(TypePath { path, qself: None }) = ty else {
11816        return None;
11817    };
11818    let last = path.segments.last()?;
11819    if last.ident != "Auto" {
11820        return None;
11821    }
11822    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11823        return None;
11824    };
11825    args.args.iter().find_map(|a| match a {
11826        syn::GenericArgument::Type(t) => Some(t),
11827        _ => None,
11828    })
11829}
11830
11831fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11832    let Type::Path(TypePath { path, qself: None }) = ty else {
11833        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11834    };
11835    let last = path
11836        .segments
11837        .last()
11838        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11839
11840    if last.ident == "Option" {
11841        let inner = generic_inner(ty, &last.arguments, "Option")?;
11842        let inner_det = detect_type(inner)?;
11843        if inner_det.nullable {
11844            return Err(syn::Error::new_spanned(
11845                ty,
11846                "nested Option is not supported",
11847            ));
11848        }
11849        if inner_det.auto {
11850            return Err(syn::Error::new_spanned(
11851                ty,
11852                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11853            ));
11854        }
11855        return Ok(DetectedType {
11856            nullable: true,
11857            ..inner_det
11858        });
11859    }
11860
11861    if last.ident == "Auto" {
11862        let inner = generic_inner(ty, &last.arguments, "Auto")?;
11863        let inner_det = detect_type(inner)?;
11864        if inner_det.auto {
11865            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11866        }
11867        if inner_det.nullable {
11868            return Err(syn::Error::new_spanned(
11869                ty,
11870                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11871            ));
11872        }
11873        if inner_det.fk_inner.is_some() {
11874            return Err(syn::Error::new_spanned(
11875                ty,
11876                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11877            ));
11878        }
11879        if !matches!(
11880            inner_det.kind,
11881            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11882        ) {
11883            return Err(syn::Error::new_spanned(
11884                ty,
11885                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11886                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11887                 (DEFAULT now())",
11888            ));
11889        }
11890        return Ok(DetectedType {
11891            auto: true,
11892            ..inner_det
11893        });
11894    }
11895
11896    if last.ident == "ForeignKey" {
11897        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11898        // Resolve the FK column's underlying SQL type from `K`. When the
11899        // user wrote `ForeignKey<T>` without a key parameter, the type
11900        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11901        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11902        // recurse into K so the column DDL emits the right SQL type
11903        // (VARCHAR for String, UUID for Uuid, …) and the load_related
11904        // emitter knows which `SqlValue` variant to match.
11905        let kind = match key_ty {
11906            Some(k) => detect_type(k)?.kind,
11907            None => DetectedKind::I64,
11908        };
11909        return Ok(DetectedType {
11910            kind,
11911            nullable: false,
11912            auto: false,
11913            fk_inner: Some(inner),
11914        });
11915    }
11916
11917    let kind = match last.ident.to_string().as_str() {
11918        "i16" => DetectedKind::I16,
11919        "i32" => DetectedKind::I32,
11920        "i64" => DetectedKind::I64,
11921        "f32" => DetectedKind::F32,
11922        "f64" => DetectedKind::F64,
11923        "bool" => DetectedKind::Bool,
11924        "String" => DetectedKind::String,
11925        "DateTime" => DetectedKind::DateTime,
11926        "NaiveDate" => DetectedKind::Date,
11927        "NaiveTime" => DetectedKind::Time,
11928        "Uuid" => DetectedKind::Uuid,
11929        "Value" => DetectedKind::Json,
11930        "Decimal" => DetectedKind::Decimal,
11931        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11932        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11933        // — that would emit Binary DDL and decode-fail at runtime.
11934        "Vec" => {
11935            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11936            if let Type::Path(TypePath { path, qself: None }) = inner {
11937                if let Some(seg) = path.segments.last() {
11938                    if seg.ident == "u8" && seg.arguments.is_empty() {
11939                        return Ok(DetectedType {
11940                            kind: DetectedKind::Binary,
11941                            nullable: false,
11942                            auto: false,
11943                            fk_inner: None,
11944                        });
11945                    }
11946                }
11947            }
11948            return Err(syn::Error::new_spanned(
11949                ty,
11950                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11951                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11952            ));
11953        }
11954        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11955        // `integer[]` / `bigint[]` (#341).
11956        "Array" => {
11957            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11958            let elem = match inner {
11959                Type::Path(TypePath { path, qself: None }) => {
11960                    path.segments.last().map(|s| s.ident.to_string())
11961                }
11962                _ => None,
11963            };
11964            let kind = match elem.as_deref() {
11965                Some("String") => DetectedKind::ArrayText,
11966                Some("i32") => DetectedKind::ArrayInt,
11967                Some("i64") => DetectedKind::ArrayBigInt,
11968                _ => {
11969                    return Err(syn::Error::new_spanned(
11970                        ty,
11971                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11972                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11973                    ));
11974                }
11975            };
11976            return Ok(DetectedType {
11977                kind,
11978                nullable: false,
11979                auto: false,
11980                fk_inner: None,
11981            });
11982        }
11983        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11984        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11985        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
11986        "Range" => {
11987            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11988            let elem = match inner {
11989                Type::Path(TypePath { path, qself: None }) => {
11990                    path.segments.last().map(|s| s.ident.to_string())
11991                }
11992                _ => None,
11993            };
11994            let kind = match elem.as_deref() {
11995                Some("i32") => DetectedKind::RangeInt,
11996                Some("i64") => DetectedKind::RangeBigInt,
11997                Some("Decimal") => DetectedKind::RangeNumeric,
11998                Some("NaiveDate") => DetectedKind::RangeDate,
11999                Some("DateTime") => DetectedKind::RangeDateTime,
12000                _ => {
12001                    return Err(syn::Error::new_spanned(
12002                        ty,
12003                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12004                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12005                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12006                         (→ tstzrange) are supported (#343)",
12007                    ));
12008                }
12009            };
12010            return Ok(DetectedType {
12011                kind,
12012                nullable: false,
12013                auto: false,
12014                fk_inner: None,
12015            });
12016        }
12017        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12018        // (the `CastValue` impl bridges logical↔stored); the field's own
12019        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12020        // schema just needs `FieldType::String`.
12021        "Cast" => {
12022            return Ok(DetectedType {
12023                kind: DetectedKind::String,
12024                nullable: false,
12025                auto: false,
12026                fk_inner: None,
12027            });
12028        }
12029        // `HStore` → PG `hstore` (#342). No generic
12030        // parameter — always a string→string map.
12031        "HStore" => {
12032            return Ok(DetectedType {
12033                kind: DetectedKind::HStore,
12034                nullable: false,
12035                auto: false,
12036                fk_inner: None,
12037            });
12038        }
12039        // `Vector` → pgvector `vector(N)` (#824). The dimension is
12040        // supplied by `#[rustango(vector(dims = N))]`, not the type.
12041        "Vector" => {
12042            return Ok(DetectedType {
12043                kind: DetectedKind::Vector,
12044                nullable: false,
12045                auto: false,
12046                fk_inner: None,
12047            });
12048        }
12049        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12050        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12051        "Point" => {
12052            return Ok(DetectedType {
12053                kind: DetectedKind::Geometry,
12054                nullable: false,
12055                auto: false,
12056                fk_inner: None,
12057            });
12058        }
12059        other => {
12060            return Err(syn::Error::new_spanned(
12061                ty,
12062                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)"),
12063            ));
12064        }
12065    };
12066    Ok(DetectedType {
12067        kind,
12068        nullable: false,
12069        auto: false,
12070        fk_inner: None,
12071    })
12072}
12073
12074fn generic_inner<'a>(
12075    ty: &'a Type,
12076    arguments: &'a PathArguments,
12077    wrapper: &str,
12078) -> syn::Result<&'a Type> {
12079    let PathArguments::AngleBracketed(args) = arguments else {
12080        return Err(syn::Error::new_spanned(
12081            ty,
12082            format!("{wrapper} requires a generic argument"),
12083        ));
12084    };
12085    args.args
12086        .iter()
12087        .find_map(|a| match a {
12088            GenericArgument::Type(t) => Some(t),
12089            _ => None,
12090        })
12091        .ok_or_else(|| {
12092            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12093        })
12094}
12095
12096/// Like [`generic_inner`] but pulls *two* type args — the first is
12097/// required, the second is optional. Used by the `ForeignKey<T, K>`
12098/// detection where K defaults to `i64` when omitted.
12099fn generic_pair<'a>(
12100    ty: &'a Type,
12101    arguments: &'a PathArguments,
12102    wrapper: &str,
12103) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12104    let PathArguments::AngleBracketed(args) = arguments else {
12105        return Err(syn::Error::new_spanned(
12106            ty,
12107            format!("{wrapper} requires a generic argument"),
12108        ));
12109    };
12110    let mut types = args.args.iter().filter_map(|a| match a {
12111        GenericArgument::Type(t) => Some(t),
12112        _ => None,
12113    });
12114    let first = types.next().ok_or_else(|| {
12115        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12116    })?;
12117    let second = types.next();
12118    Ok((first, second))
12119}
12120
12121fn to_snake_case(s: &str) -> String {
12122    let mut out = String::with_capacity(s.len() + 4);
12123    for (i, ch) in s.chars().enumerate() {
12124        if ch.is_ascii_uppercase() {
12125            if i > 0 {
12126                out.push('_');
12127            }
12128            out.push(ch.to_ascii_lowercase());
12129        } else {
12130            out.push(ch);
12131        }
12132    }
12133    out
12134}
12135
12136// ============================================================
12137//  #[derive(Form)]  —  slice 8.4B
12138// ============================================================
12139
12140/// Per-field `#[form(...)]` attributes recognised by the derive.
12141#[derive(Default)]
12142struct FormFieldAttrs {
12143    min: Option<i64>,
12144    max: Option<i64>,
12145    min_length: Option<u32>,
12146    max_length: Option<u32>,
12147    /// `#[form(clean = "fn_name")]` — per-field clean hook.
12148    /// The named static method on the form struct is called after the
12149    /// field's typed parse + length/range checks; it gets the parsed
12150    /// value by reference and returns `Result<<FieldType>, String>`.
12151    /// On Ok, the returned value replaces the parsed one; on Err, the
12152    /// message is attached to the field error list. Issue #372.
12153    clean: Option<syn::Ident>,
12154}
12155
12156/// Container-level `#[form(...)]` attributes. Currently only the
12157/// cross-field `validate` hook (issue #373).
12158#[derive(Default)]
12159struct FormContainerAttrs {
12160    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12161    /// After every per-field parse succeeds, the named method on the
12162    /// form struct is called with `&self` and may return
12163    /// `Result<(), FormErrors>`. Errors merge into the field error
12164    /// list. Issue #373.
12165    validate: Option<syn::Ident>,
12166}
12167
12168/// Detected shape of a form field's Rust type.
12169#[derive(Clone, Copy)]
12170enum FormFieldKind {
12171    String,
12172    I16,
12173    I32,
12174    I64,
12175    F32,
12176    F64,
12177    Bool,
12178}
12179
12180impl FormFieldKind {
12181    fn parse_method(self) -> &'static str {
12182        match self {
12183            Self::I16 => "i16",
12184            Self::I32 => "i32",
12185            Self::I64 => "i64",
12186            Self::F32 => "f32",
12187            Self::F64 => "f64",
12188            // String + Bool don't go through `str::parse`; the codegen
12189            // handles them inline.
12190            Self::String | Self::Bool => "",
12191        }
12192    }
12193}
12194
12195fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12196    let root = rustango_root();
12197    let struct_name = &input.ident;
12198
12199    let Data::Struct(data) = &input.data else {
12200        return Err(syn::Error::new_spanned(
12201            struct_name,
12202            "Form can only be derived on structs",
12203        ));
12204    };
12205    let Fields::Named(named) = &data.fields else {
12206        return Err(syn::Error::new_spanned(
12207            struct_name,
12208            "Form requires a struct with named fields",
12209        ));
12210    };
12211
12212    // #373 — container-level `#[form(validate = "fn")]` hook.
12213    let container = parse_form_container_attrs(input)?;
12214    let post_field_clean: Vec<TokenStream2> = Vec::new();
12215    let _ = post_field_clean;
12216
12217    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12218    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12219
12220    for field in &named.named {
12221        let ident = field
12222            .ident
12223            .as_ref()
12224            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12225        let attrs = parse_form_field_attrs(field)?;
12226        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12227
12228        let name_lit = ident_name(ident);
12229        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12230        // #372 — append the per-field `clean_<field>` call right after
12231        // the parse block when the attribute is set. The clean fn
12232        // takes &T and returns Result<T, String>; on Err we attach
12233        // the message to the field error list without aborting, so
12234        // the caller sees every field error at once.
12235        let clean_block = if let Some(clean_fn) = &attrs.clean {
12236            quote! {
12237                if __errors.fields().get(#name_lit).is_none() {
12238                    match Self::#clean_fn(&#ident) {
12239                        ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12240                        ::core::result::Result::Err(__msg) => {
12241                            __errors.add(#name_lit, __msg);
12242                        }
12243                    }
12244                }
12245            }
12246        } else {
12247            quote! {}
12248        };
12249        field_blocks.push(quote! {
12250            #parse_block
12251            #clean_block
12252        });
12253        field_idents.push(ident);
12254    }
12255
12256    // #373 — after every per-field parse + clean succeeds, call the
12257    // cross-field validator if declared. Errors merge into the
12258    // outgoing FormErrors via the existing `FormErrors::merge` helper
12259    // (same primitive the serializer cross-field hook uses).
12260    let cross_field_call = if let Some(validate_fn) = &container.validate {
12261        quote! {
12262            if __errors.is_empty() {
12263                let __candidate = Self { #( #field_idents ),* };
12264                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12265                    __errors.merge(__other);
12266                }
12267                if !__errors.is_empty() {
12268                    return ::core::result::Result::Err(__errors);
12269                }
12270                return ::core::result::Result::Ok(__candidate);
12271            }
12272        }
12273    } else {
12274        quote! {}
12275    };
12276
12277    Ok(quote! {
12278        impl #root::forms::Form for #struct_name {
12279            fn parse(
12280                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12281            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12282                let mut __errors = #root::forms::FormErrors::default();
12283                #( #field_blocks )*
12284                #cross_field_call
12285                if !__errors.is_empty() {
12286                    return ::core::result::Result::Err(__errors);
12287                }
12288                ::core::result::Result::Ok(Self {
12289                    #( #field_idents ),*
12290                })
12291            }
12292        }
12293    })
12294}
12295
12296fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12297    let mut out = FormContainerAttrs::default();
12298    for attr in &input.attrs {
12299        if !attr.path().is_ident("form") {
12300            continue;
12301        }
12302        attr.parse_nested_meta(|meta| {
12303            if meta.path.is_ident("validate") {
12304                let s: LitStr = meta.value()?.parse()?;
12305                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12306                return Ok(());
12307            }
12308            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12309        })?;
12310    }
12311    Ok(out)
12312}
12313
12314fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12315    let mut out = FormFieldAttrs::default();
12316    for attr in &field.attrs {
12317        if !attr.path().is_ident("form") {
12318            continue;
12319        }
12320        attr.parse_nested_meta(|meta| {
12321            if meta.path.is_ident("min") {
12322                let lit: syn::LitInt = meta.value()?.parse()?;
12323                out.min = Some(lit.base10_parse::<i64>()?);
12324                return Ok(());
12325            }
12326            if meta.path.is_ident("max") {
12327                let lit: syn::LitInt = meta.value()?.parse()?;
12328                out.max = Some(lit.base10_parse::<i64>()?);
12329                return Ok(());
12330            }
12331            if meta.path.is_ident("min_length") {
12332                let lit: syn::LitInt = meta.value()?.parse()?;
12333                out.min_length = Some(lit.base10_parse::<u32>()?);
12334                return Ok(());
12335            }
12336            if meta.path.is_ident("max_length") {
12337                let lit: syn::LitInt = meta.value()?.parse()?;
12338                out.max_length = Some(lit.base10_parse::<u32>()?);
12339                return Ok(());
12340            }
12341            if meta.path.is_ident("clean") {
12342                let s: LitStr = meta.value()?.parse()?;
12343                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12344                return Ok(());
12345            }
12346            Err(meta.error(
12347                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12348            ))
12349        })?;
12350    }
12351    Ok(out)
12352}
12353
12354fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12355    let Type::Path(TypePath { path, qself: None }) = ty else {
12356        return Err(syn::Error::new(
12357            span,
12358            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12359        ));
12360    };
12361    let last = path
12362        .segments
12363        .last()
12364        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12365
12366    if last.ident == "Option" {
12367        let inner = generic_inner(ty, &last.arguments, "Option")?;
12368        let (kind, nested) = detect_form_field(inner, span)?;
12369        if nested {
12370            return Err(syn::Error::new(
12371                span,
12372                "nested Option in Form fields is not supported",
12373            ));
12374        }
12375        return Ok((kind, true));
12376    }
12377
12378    let kind = match last.ident.to_string().as_str() {
12379        "String" => FormFieldKind::String,
12380        "i16" => FormFieldKind::I16,
12381        "i32" => FormFieldKind::I32,
12382        "i64" => FormFieldKind::I64,
12383        "f32" => FormFieldKind::F32,
12384        "f64" => FormFieldKind::F64,
12385        "bool" => FormFieldKind::Bool,
12386        other => {
12387            return Err(syn::Error::new(
12388                span,
12389                format!(
12390                    "Form field type `{other}` is not supported in v0.8 — use String / \
12391                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12392                ),
12393            ));
12394        }
12395    };
12396    Ok((kind, false))
12397}
12398
12399#[allow(clippy::too_many_lines)]
12400fn render_form_field_parse(
12401    ident: &syn::Ident,
12402    name_lit: &str,
12403    kind: FormFieldKind,
12404    nullable: bool,
12405    attrs: &FormFieldAttrs,
12406) -> TokenStream2 {
12407    // Pull the raw &str from the payload. Uses variable name `data` to
12408    // match the new `Form::parse(data: &HashMap<…>)` signature.
12409    let lookup = quote! {
12410        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12411    };
12412
12413    let parsed_value = match kind {
12414        FormFieldKind::Bool => quote! {
12415            let __v: bool = match __raw {
12416                ::core::option::Option::None => false,
12417                ::core::option::Option::Some(__s) => !matches!(
12418                    __s.to_ascii_lowercase().as_str(),
12419                    "" | "false" | "0" | "off" | "no"
12420                ),
12421            };
12422        },
12423        FormFieldKind::String => {
12424            if nullable {
12425                quote! {
12426                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12427                        ::core::option::Option::None => ::core::option::Option::None,
12428                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12429                            ::core::option::Option::None
12430                        }
12431                        ::core::option::Option::Some(__s) => {
12432                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12433                        }
12434                    };
12435                }
12436            } else {
12437                quote! {
12438                    let __v: ::std::string::String = match __raw {
12439                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12440                            ::core::clone::Clone::clone(__s)
12441                        }
12442                        _ => {
12443                            __errors.add(#name_lit, "This field is required.");
12444                            ::std::string::String::new()
12445                        }
12446                    };
12447                }
12448            }
12449        }
12450        FormFieldKind::I16
12451        | FormFieldKind::I32
12452        | FormFieldKind::I64
12453        | FormFieldKind::F32
12454        | FormFieldKind::F64 => {
12455            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12456            let ty_lit = kind.parse_method();
12457            let default_val = match kind {
12458                FormFieldKind::I16 => quote! { 0i16 },
12459                FormFieldKind::I32 => quote! { 0i32 },
12460                FormFieldKind::I64 => quote! { 0i64 },
12461                FormFieldKind::F32 => quote! { 0f32 },
12462                FormFieldKind::F64 => quote! { 0f64 },
12463                _ => quote! { Default::default() },
12464            };
12465            if nullable {
12466                quote! {
12467                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12468                        ::core::option::Option::None => ::core::option::Option::None,
12469                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12470                            ::core::option::Option::None
12471                        }
12472                        ::core::option::Option::Some(__s) => {
12473                            match __s.parse::<#parse_ty>() {
12474                                ::core::result::Result::Ok(__n) => {
12475                                    ::core::option::Option::Some(__n)
12476                                }
12477                                ::core::result::Result::Err(__e) => {
12478                                    __errors.add(
12479                                        #name_lit,
12480                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12481                                    );
12482                                    ::core::option::Option::None
12483                                }
12484                            }
12485                        }
12486                    };
12487                }
12488            } else {
12489                quote! {
12490                    let __v: #parse_ty = match __raw {
12491                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12492                            match __s.parse::<#parse_ty>() {
12493                                ::core::result::Result::Ok(__n) => __n,
12494                                ::core::result::Result::Err(__e) => {
12495                                    __errors.add(
12496                                        #name_lit,
12497                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12498                                    );
12499                                    #default_val
12500                                }
12501                            }
12502                        }
12503                        _ => {
12504                            __errors.add(#name_lit, "This field is required.");
12505                            #default_val
12506                        }
12507                    };
12508                }
12509            }
12510        }
12511    };
12512
12513    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12514
12515    quote! {
12516        // `mut` so the per-field `clean` hook (#372) can rewrite the
12517        // parsed value in-place when it returns Ok with a normalized
12518        // form (e.g. trim / lowercase).
12519        let mut #ident = {
12520            #lookup
12521            #parsed_value
12522            #validators
12523            __v
12524        };
12525    }
12526}
12527
12528fn render_form_validators(
12529    name_lit: &str,
12530    kind: FormFieldKind,
12531    nullable: bool,
12532    attrs: &FormFieldAttrs,
12533) -> TokenStream2 {
12534    let mut checks: Vec<TokenStream2> = Vec::new();
12535
12536    let val_ref = if nullable {
12537        quote! { __v.as_ref() }
12538    } else {
12539        quote! { ::core::option::Option::Some(&__v) }
12540    };
12541
12542    let is_string = matches!(kind, FormFieldKind::String);
12543    let is_numeric = matches!(
12544        kind,
12545        FormFieldKind::I16
12546            | FormFieldKind::I32
12547            | FormFieldKind::I64
12548            | FormFieldKind::F32
12549            | FormFieldKind::F64
12550    );
12551
12552    if is_string {
12553        if let Some(min_len) = attrs.min_length {
12554            let min_len_usize = min_len as usize;
12555            checks.push(quote! {
12556                if let ::core::option::Option::Some(__s) = #val_ref {
12557                    if __s.chars().count() < #min_len_usize {
12558                        __errors.add(
12559                            #name_lit,
12560                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12561                        );
12562                    }
12563                }
12564            });
12565        }
12566        if let Some(max_len) = attrs.max_length {
12567            let max_len_usize = max_len as usize;
12568            checks.push(quote! {
12569                if let ::core::option::Option::Some(__s) = #val_ref {
12570                    if __s.chars().count() > #max_len_usize {
12571                        __errors.add(
12572                            #name_lit,
12573                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12574                        );
12575                    }
12576                }
12577            });
12578        }
12579    }
12580
12581    if is_numeric {
12582        if let Some(min) = attrs.min {
12583            checks.push(quote! {
12584                if let ::core::option::Option::Some(__n) = #val_ref {
12585                    if (*__n as f64) < (#min as f64) {
12586                        __errors.add(
12587                            #name_lit,
12588                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12589                        );
12590                    }
12591                }
12592            });
12593        }
12594        if let Some(max) = attrs.max {
12595            checks.push(quote! {
12596                if let ::core::option::Option::Some(__n) = #val_ref {
12597                    if (*__n as f64) > (#max as f64) {
12598                        __errors.add(
12599                            #name_lit,
12600                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12601                        );
12602                    }
12603                }
12604            });
12605        }
12606    }
12607
12608    quote! { #( #checks )* }
12609}
12610
12611// ============================================================
12612//  #[derive(ViewSet)]
12613// ============================================================
12614
12615struct ViewSetAttrs {
12616    model: syn::Path,
12617    fields: Option<Vec<String>>,
12618    filter_fields: Vec<String>,
12619    search_fields: Vec<String>,
12620    /// (field_name, desc)
12621    ordering: Vec<(String, bool)>,
12622    page_size: Option<usize>,
12623    read_only: bool,
12624    allow_anonymous: bool,
12625    perms: ViewSetPermsAttrs,
12626    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12627    /// retrieve / create responses through this `#[derive(Serializer)]`
12628    /// type instead of the default field-level projection (requires the
12629    /// `serializer` feature). Tri-dialect.
12630    serializer: Option<syn::Path>,
12631}
12632
12633#[derive(Default)]
12634struct ViewSetPermsAttrs {
12635    list: Vec<String>,
12636    retrieve: Vec<String>,
12637    create: Vec<String>,
12638    update: Vec<String>,
12639    destroy: Vec<String>,
12640}
12641
12642fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12643    let root = rustango_root();
12644    let struct_name = &input.ident;
12645
12646    // Must be a unit struct or an empty named struct.
12647    match &input.data {
12648        Data::Struct(s) => match &s.fields {
12649            Fields::Unit | Fields::Named(_) => {}
12650            Fields::Unnamed(_) => {
12651                return Err(syn::Error::new_spanned(
12652                    struct_name,
12653                    "ViewSet can only be derived on a unit struct or an empty named struct",
12654                ));
12655            }
12656        },
12657        _ => {
12658            return Err(syn::Error::new_spanned(
12659                struct_name,
12660                "ViewSet can only be derived on a struct",
12661            ));
12662        }
12663    }
12664
12665    let attrs = parse_viewset_attrs(input)?;
12666    let model_path = &attrs.model;
12667
12668    // `.fields(&[...])` call — None means skip (use all scalar fields).
12669    let fields_call = if let Some(ref fields) = attrs.fields {
12670        let lits = fields.iter().map(|f| f.as_str());
12671        quote!(.fields(&[ #(#lits),* ]))
12672    } else {
12673        quote!()
12674    };
12675
12676    let filter_fields_call = if attrs.filter_fields.is_empty() {
12677        quote!()
12678    } else {
12679        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12680        quote!(.filter_fields(&[ #(#lits),* ]))
12681    };
12682
12683    let search_fields_call = if attrs.search_fields.is_empty() {
12684        quote!()
12685    } else {
12686        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12687        quote!(.search_fields(&[ #(#lits),* ]))
12688    };
12689
12690    let ordering_call = if attrs.ordering.is_empty() {
12691        quote!()
12692    } else {
12693        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12694            let f = f.as_str();
12695            quote!((#f, #desc))
12696        });
12697        quote!(.ordering(&[ #(#pairs),* ]))
12698    };
12699
12700    let page_size_call = if let Some(n) = attrs.page_size {
12701        quote!(.page_size(#n))
12702    } else {
12703        quote!()
12704    };
12705
12706    let read_only_call = if attrs.read_only {
12707        quote!(.read_only())
12708    } else {
12709        quote!()
12710    };
12711    let allow_anonymous_call = if attrs.allow_anonymous {
12712        quote!(.allow_anonymous())
12713    } else {
12714        quote!()
12715    };
12716
12717    // `.serializer::<S>()` — reshape responses through a derived
12718    // serializer. Requires the downstream crate to enable the
12719    // `serializer` feature (the method is gated on it).
12720    let serializer_call = if let Some(ref ser) = attrs.serializer {
12721        quote!(.serializer::<#ser>())
12722    } else {
12723        quote!()
12724    };
12725
12726    let perms = &attrs.perms;
12727    let perms_call = if perms.list.is_empty()
12728        && perms.retrieve.is_empty()
12729        && perms.create.is_empty()
12730        && perms.update.is_empty()
12731        && perms.destroy.is_empty()
12732    {
12733        quote!()
12734    } else {
12735        let list_lits = perms.list.iter().map(|s| s.as_str());
12736        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12737        let create_lits = perms.create.iter().map(|s| s.as_str());
12738        let update_lits = perms.update.iter().map(|s| s.as_str());
12739        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12740        quote! {
12741            .permissions(#root::viewset::ViewSetPerms {
12742                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
12743                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12744                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
12745                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
12746                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
12747            })
12748        }
12749    };
12750
12751    Ok(quote! {
12752        impl #struct_name {
12753            /// Build an `axum::Router` with the six standard REST endpoints
12754            /// for this ViewSet, mounted at `prefix`.
12755            ///
12756            /// Accepts any backend's pool — a `sqlx::PgPool`,
12757            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12758            /// enum — because they all `Into<Pool>` (#1273). This is why
12759            /// the derive no longer names `PgPool`, which would not
12760            /// exist on a project built without the `postgres` feature.
12761            pub fn router(
12762                prefix: &str,
12763                pool: impl ::core::convert::Into<#root::sql::Pool>,
12764            ) -> #root::__axum::Router {
12765                #root::viewset::ViewSet::for_model(
12766                    <#model_path as #root::core::Model>::SCHEMA
12767                )
12768                    #fields_call
12769                    #filter_fields_call
12770                    #search_fields_call
12771                    #ordering_call
12772                    #page_size_call
12773                    #perms_call
12774                    #read_only_call
12775                    #allow_anonymous_call
12776                    #serializer_call
12777                    .router_pool(prefix, pool.into())
12778            }
12779        }
12780    })
12781}
12782
12783fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12784    let mut model: Option<syn::Path> = None;
12785    let mut fields: Option<Vec<String>> = None;
12786    let mut filter_fields: Vec<String> = Vec::new();
12787    let mut search_fields: Vec<String> = Vec::new();
12788    let mut ordering: Vec<(String, bool)> = Vec::new();
12789    let mut page_size: Option<usize> = None;
12790    let mut read_only = false;
12791    let mut allow_anonymous = false;
12792    let mut perms = ViewSetPermsAttrs::default();
12793    let mut serializer: Option<syn::Path> = None;
12794
12795    for attr in &input.attrs {
12796        if !attr.path().is_ident("viewset") {
12797            continue;
12798        }
12799        attr.parse_nested_meta(|meta| {
12800            if meta.path.is_ident("model") {
12801                let path: syn::Path = meta.value()?.parse()?;
12802                model = Some(path);
12803                return Ok(());
12804            }
12805            if meta.path.is_ident("serializer") {
12806                let path: syn::Path = meta.value()?.parse()?;
12807                serializer = Some(path);
12808                return Ok(());
12809            }
12810            if meta.path.is_ident("fields") {
12811                let s: LitStr = meta.value()?.parse()?;
12812                fields = Some(split_field_list(&s.value()));
12813                return Ok(());
12814            }
12815            if meta.path.is_ident("filter_fields") {
12816                let s: LitStr = meta.value()?.parse()?;
12817                filter_fields = split_field_list(&s.value());
12818                return Ok(());
12819            }
12820            if meta.path.is_ident("search_fields") {
12821                let s: LitStr = meta.value()?.parse()?;
12822                search_fields = split_field_list(&s.value());
12823                return Ok(());
12824            }
12825            if meta.path.is_ident("ordering") {
12826                let s: LitStr = meta.value()?.parse()?;
12827                ordering = parse_ordering_list(&s.value());
12828                return Ok(());
12829            }
12830            if meta.path.is_ident("page_size") {
12831                let lit: syn::LitInt = meta.value()?.parse()?;
12832                page_size = Some(lit.base10_parse::<usize>()?);
12833                return Ok(());
12834            }
12835            if meta.path.is_ident("read_only") {
12836                read_only = true;
12837                return Ok(());
12838            }
12839            if meta.path.is_ident("allow_anonymous") {
12840                allow_anonymous = true;
12841                return Ok(());
12842            }
12843            if meta.path.is_ident("permissions") {
12844                meta.parse_nested_meta(|inner| {
12845                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12846                        let s: LitStr = inner.value()?.parse()?;
12847                        Ok(split_field_list(&s.value()))
12848                    };
12849                    if inner.path.is_ident("list") {
12850                        perms.list = parse_codenames(&inner)?;
12851                    } else if inner.path.is_ident("retrieve") {
12852                        perms.retrieve = parse_codenames(&inner)?;
12853                    } else if inner.path.is_ident("create") {
12854                        perms.create = parse_codenames(&inner)?;
12855                    } else if inner.path.is_ident("update") {
12856                        perms.update = parse_codenames(&inner)?;
12857                    } else if inner.path.is_ident("destroy") {
12858                        perms.destroy = parse_codenames(&inner)?;
12859                    } else {
12860                        return Err(inner.error(
12861                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12862                        ));
12863                    }
12864                    Ok(())
12865                })?;
12866                return Ok(());
12867            }
12868            Err(meta.error(
12869                "unknown viewset attribute (supported: model, fields, filter_fields, \
12870                 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
12871                 permissions(...))",
12872            ))
12873        })?;
12874    }
12875
12876    let model = model.ok_or_else(|| {
12877        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12878    })?;
12879
12880    Ok(ViewSetAttrs {
12881        model,
12882        fields,
12883        filter_fields,
12884        search_fields,
12885        ordering,
12886        page_size,
12887        read_only,
12888        allow_anonymous,
12889        perms,
12890        serializer,
12891    })
12892}
12893
12894// ============================================================ #[derive(Serializer)]
12895
12896struct SerializerContainerAttrs {
12897    model: syn::Path,
12898    /// `#[serializer(validate = "fn_name")]` on the struct — the
12899    /// cross-field validation hook (#436). The named inherent method
12900    /// must take `&self` and return
12901    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12902    /// `validate()` runs every per-field validator first; then calls
12903    /// the cross-field method if declared; aggregates all errors into
12904    /// one `FormErrors`.
12905    cross_validate: Option<syn::Ident>,
12906}
12907
12908#[derive(Default)]
12909struct SerializerFieldAttrs {
12910    read_only: bool,
12911    write_only: bool,
12912    source: Option<String>,
12913    skip: bool,
12914    /// `#[serializer(method = "fn_name")]` — computed field. The macro
12915    /// emits a `from_model` initializer that calls
12916    /// `Self::fn_name(&model)` and stores the return value.
12917    method: Option<String>,
12918    /// `#[serializer(validate = "fn_name")]` — per-field validator
12919    /// callable run by `Self::validate(&self)`. Must return
12920    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12921    /// the field name.
12922    validate: Option<String>,
12923    /// `#[serializer(nested)]` on a field whose type is another
12924    /// `Serializer` — the macro emits `from_model` initializer that
12925    /// reads the parent via `model.<source>.value()` then calls the
12926    /// child serializer's `from_model(parent)`. When the FK is
12927    /// unloaded the field falls back to `Default::default()` (does
12928    /// NOT panic) so a missing prefetch in prod degrades gracefully.
12929    /// Source field on the model defaults to the field name; override
12930    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12931    /// panic-on-unloaded behavior for tests.
12932    nested: bool,
12933    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12934    /// strict behavior: panic when the FK isn't loaded. Useful in
12935    /// test code where forgetting select_related must trip a hard
12936    /// failure rather than render a blank nested object.
12937    nested_strict: bool,
12938    /// `#[serializer(many = TagSerializer)]` — declare the field as
12939    /// a list of nested serializers. Field type must be `Vec<S>`
12940    /// where `S` is the inner serializer. The macro initializes the
12941    /// field to `Vec::new()` in `from_model` and emits a typed
12942    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12943    /// maps each model row through `S::from_model`. Auto-load isn't
12944    /// possible (the M2M / one-to-many accessor is async); callers
12945    /// fetch the children + call the setter post-from_model.
12946    many: Option<syn::Type>,
12947    /// `#[serializer(slug = "name")]` — render a relation as one of
12948    /// its fields.
12949    /// Source field on the model must be a `ForeignKey<T>`; the
12950    /// macro emits `from_model` glue that walks
12951    /// `model.<source>.value()?.<slug>` and clones it. Field type on
12952    /// the serializer is typically `String` (whatever type the slug
12953    /// column has). When the FK is unloaded the field falls back to
12954    /// `Default::default()`, same graceful-degrade contract as
12955    /// `nested`. Source defaults to the field name; override with
12956    /// `source = "..."`. v0.44.
12957    slug: Option<String>,
12958    /// `#[serializer(max_length = N)]` — caps
12959    /// the character count of a string field on write. Overrides the
12960    /// model's `max_length`; when absent the model value is inherited.
12961    max_length: Option<u64>,
12962    /// `#[serializer(min_length = N)]` — minimum character count.
12963    /// Serializer-only (the model has no `min_length` column).
12964    min_length: Option<u64>,
12965    /// `#[serializer(min = N)]` — inclusive
12966    /// integer lower bound; overrides the model's `min` when given.
12967    min: Option<i64>,
12968    /// `#[serializer(max = N)]` — inclusive
12969    /// integer upper bound; overrides the model's `max` when given.
12970    max: Option<i64>,
12971}
12972
12973fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12974    let mut model: Option<syn::Path> = None;
12975    let mut cross_validate: Option<syn::Ident> = None;
12976    for attr in &input.attrs {
12977        if !attr.path().is_ident("serializer") {
12978            continue;
12979        }
12980        attr.parse_nested_meta(|meta| {
12981            if meta.path.is_ident("model") {
12982                let _eq: syn::Token![=] = meta.input.parse()?;
12983                model = Some(meta.input.parse()?);
12984                return Ok(());
12985            }
12986            if meta.path.is_ident("validate") {
12987                // #436 — container-level `validate = "fn_name"` for
12988                // cross-field validation. Field-level
12989                // `#[serializer(validate = "...")]` on a field is
12990                // parsed separately in `parse_serializer_field_attrs`.
12991                let s: LitStr = meta.value()?.parse()?;
12992                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12993                return Ok(());
12994            }
12995            Err(meta.error(
12996                "unknown serializer container attribute \
12997                 (supported: `model`, `validate`)",
12998            ))
12999        })?;
13000    }
13001    let model = model.ok_or_else(|| {
13002        syn::Error::new_spanned(
13003            &input.ident,
13004            "`#[serializer(model = SomeModel)]` is required",
13005        )
13006    })?;
13007    Ok(SerializerContainerAttrs {
13008        model,
13009        cross_validate,
13010    })
13011}
13012
13013fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13014    let mut out = SerializerFieldAttrs::default();
13015    for attr in &field.attrs {
13016        if !attr.path().is_ident("serializer") {
13017            continue;
13018        }
13019        attr.parse_nested_meta(|meta| {
13020            if meta.path.is_ident("read_only") {
13021                out.read_only = true;
13022                return Ok(());
13023            }
13024            if meta.path.is_ident("write_only") {
13025                out.write_only = true;
13026                return Ok(());
13027            }
13028            if meta.path.is_ident("skip") {
13029                out.skip = true;
13030                return Ok(());
13031            }
13032            if meta.path.is_ident("source") {
13033                let s: LitStr = meta.value()?.parse()?;
13034                out.source = Some(s.value());
13035                return Ok(());
13036            }
13037            if meta.path.is_ident("method") {
13038                let s: LitStr = meta.value()?.parse()?;
13039                out.method = Some(s.value());
13040                return Ok(());
13041            }
13042            if meta.path.is_ident("validate") {
13043                let s: LitStr = meta.value()?.parse()?;
13044                out.validate = Some(s.value());
13045                return Ok(());
13046            }
13047            if meta.path.is_ident("many") {
13048                let _eq: syn::Token![=] = meta.input.parse()?;
13049                out.many = Some(meta.input.parse()?);
13050                return Ok(());
13051            }
13052            if meta.path.is_ident("nested") {
13053                out.nested = true;
13054                // Optional strict flag inside parentheses:
13055                //   #[serializer(nested(strict))]
13056                if meta.input.peek(syn::token::Paren) {
13057                    meta.parse_nested_meta(|inner| {
13058                        if inner.path.is_ident("strict") {
13059                            out.nested_strict = true;
13060                            return Ok(());
13061                        }
13062                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13063                    })?;
13064                }
13065                return Ok(());
13066            }
13067            if meta.path.is_ident("slug") {
13068                let s: LitStr = meta.value()?.parse()?;
13069                out.slug = Some(s.value());
13070                return Ok(());
13071            }
13072            if meta.path.is_ident("max_length") {
13073                let lit: syn::LitInt = meta.value()?.parse()?;
13074                out.max_length = Some(lit.base10_parse::<u64>()?);
13075                return Ok(());
13076            }
13077            if meta.path.is_ident("min_length") {
13078                let lit: syn::LitInt = meta.value()?.parse()?;
13079                out.min_length = Some(lit.base10_parse::<u64>()?);
13080                return Ok(());
13081            }
13082            if meta.path.is_ident("min") {
13083                let lit: syn::LitInt = meta.value()?.parse()?;
13084                out.min = Some(lit.base10_parse::<i64>()?);
13085                return Ok(());
13086            }
13087            if meta.path.is_ident("max") {
13088                let lit: syn::LitInt = meta.value()?.parse()?;
13089                out.max = Some(lit.base10_parse::<i64>()?);
13090                return Ok(());
13091            }
13092            Err(meta.error(
13093                "unknown serializer field attribute (supported: \
13094                 `read_only`, `write_only`, `source`, `skip`, `method`, \
13095                 `validate`, `nested`, `many`, `slug`, `max_length`, \
13096                 `min_length`, `min`, `max`)",
13097            ))
13098        })?;
13099    }
13100    // Validate: read_only + write_only is nonsensical
13101    if out.read_only && out.write_only {
13102        return Err(syn::Error::new_spanned(
13103            field,
13104            "a field cannot be both `read_only` and `write_only`",
13105        ));
13106    }
13107    if out.method.is_some() && out.source.is_some() {
13108        return Err(syn::Error::new_spanned(
13109            field,
13110            "`method` and `source` are mutually exclusive — `method` computes \
13111             the value from a method, `source` reads it from a different model field",
13112        ));
13113    }
13114    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13115        return Err(syn::Error::new_spanned(
13116            field,
13117            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13118             — pick one strategy for populating the field",
13119        ));
13120    }
13121    Ok(out)
13122}
13123
13124fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13125    let root = rustango_root();
13126    let struct_name = &input.ident;
13127    let struct_name_lit = struct_name.to_string();
13128
13129    let Data::Struct(data) = &input.data else {
13130        return Err(syn::Error::new_spanned(
13131            struct_name,
13132            "Serializer can only be derived on structs",
13133        ));
13134    };
13135    let Fields::Named(named) = &data.fields else {
13136        return Err(syn::Error::new_spanned(
13137            struct_name,
13138            "Serializer requires a struct with named fields",
13139        ));
13140    };
13141
13142    let container = parse_serializer_container_attrs(input)?;
13143    let model_path = &container.model;
13144
13145    // Classify each field. `ty` is only consumed by the
13146    // `#[cfg(feature = "openapi")]` block below, but we always
13147    // capture it to keep the field-info build a single pass.
13148    #[allow(dead_code)]
13149    struct FieldInfo {
13150        ident: syn::Ident,
13151        ty: syn::Type,
13152        attrs: SerializerFieldAttrs,
13153    }
13154    let mut fields_info: Vec<FieldInfo> = Vec::new();
13155    for field in &named.named {
13156        let ident = field.ident.clone().expect("named field has ident");
13157        let attrs = parse_serializer_field_attrs(field)?;
13158        fields_info.push(FieldInfo {
13159            ident,
13160            ty: field.ty.clone(),
13161            attrs,
13162        });
13163    }
13164
13165    // Generate from_model body: struct literal with each field assigned.
13166    let from_model_fields = fields_info.iter().map(|fi| {
13167        let ident = &fi.ident;
13168        let ty = &fi.ty;
13169        if let Some(_inner) = &fi.attrs.many {
13170            // Many — collection field. Initialize empty; caller
13171            // populates via the macro-emitted set_<field> helper
13172            // after fetching the M2M children.
13173            quote! { #ident: ::std::vec::Vec::new() }
13174        } else if let Some(method) = &fi.attrs.method {
13175            // SerializerMethodField: call Self::<method>(&model) to
13176            // compute the value. Method signature must be
13177            // `fn <method>(model: &T) -> <field type>`.
13178            let method_ident = syn::Ident::new(method, ident.span());
13179            quote! { #ident: Self::#method_ident(model) }
13180        } else if let Some(slug_field) = &fi.attrs.slug {
13181            // v0.44 — SlugRelatedField. Source defaults to the field
13182            // name on this struct; override via `source = "..."`. The
13183            // source field on the model is expected to be a
13184            // `ForeignKey<T>`; the slug field on the parent is named
13185            // by the attribute value. When the FK is unloaded the
13186            // field falls back to `Default::default()` — same
13187            // graceful-degrade contract as `nested`.
13188            let src_name = fi
13189                .attrs
13190                .source
13191                .as_deref()
13192                .unwrap_or(&ident_name(&fi.ident))
13193                .to_owned();
13194            let src_ident = ident_from_name(&src_name, ident.span());
13195            let slug_ident = syn::Ident::new(slug_field, ident.span());
13196            quote! {
13197                #ident: match model.#src_ident.value() {
13198                    ::core::option::Option::Some(__loaded) =>
13199                        ::core::clone::Clone::clone(&__loaded.#slug_ident),
13200                    ::core::option::Option::None =>
13201                        ::core::default::Default::default(),
13202                }
13203            }
13204        } else if fi.attrs.nested {
13205            // Nested serializer. Source defaults to the field name on
13206            // this struct; override via `source = "..."`. The source
13207            // field on the model is expected to be a `ForeignKey<T>`
13208            // whose `.value()` returns `Option<&T>` after lazy-load.
13209            //
13210            // Behavior matrix (tweakable per-field):
13211            //   * FK loaded   → nested object materializes via
13212            //                   ChildSerializer::from_model(parent).
13213            //   * FK unloaded → fall back to ChildSerializer::default()
13214            //                   (so prod doesn't crash on a missing
13215            //                   prefetch — just renders a blank nested
13216            //                   object). Add `#[serializer(nested,
13217            //                   strict)]` to keep the v0.18.1
13218            //                   panic-on-unloaded behavior for tests
13219            //                   that want hard guardrails.
13220            let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13221            let src_ident = ident_from_name(&src_name, ident.span());
13222            if fi.attrs.nested_strict {
13223                let panic_msg = format!(
13224                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13225                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13226                );
13227                quote! {
13228                    #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13229                        model.#src_ident.value().expect(#panic_msg),
13230                    )
13231                }
13232            } else {
13233                quote! {
13234                    #ident: match model.#src_ident.value() {
13235                        ::core::option::Option::Some(__loaded) =>
13236                            <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13237                        ::core::option::Option::None =>
13238                            ::core::default::Default::default(),
13239                    }
13240                }
13241            }
13242        } else if fi.attrs.write_only || fi.attrs.skip {
13243            // Not read from model — use default
13244            quote! { #ident: ::core::default::Default::default() }
13245        } else if let Some(src) = &fi.attrs.source {
13246            let src_ident = ident_from_name(src, ident.span());
13247            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13248        } else {
13249            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13250        }
13251    });
13252
13253    // is_writable predicate (also used by writable_lits /
13254    // writable_source_lits below).
13255    let is_writable = |fi: &&FieldInfo| {
13256        !fi.attrs.read_only
13257            && !fi.attrs.skip
13258            && fi.attrs.method.is_none()
13259            && !fi.attrs.nested
13260            && fi.attrs.many.is_none()
13261            && fi.attrs.slug.is_none()
13262    };
13263
13264    // Declarative field constraints: one block
13265    // per writable field, run inside `validate()`. Each resolves its
13266    // bounds as the serializer attr when given (`#[serializer(max_length
13267    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13268    // `max` / `choices`), then dispatches on the field's JSON value via
13269    // `forms::validators::check_value` (string → length/choices, integer
13270    // → min/max).
13271    let opt_usize = |v: Option<u64>| match v {
13272        Some(n) => {
13273            let n = n as usize;
13274            quote!(::core::option::Option::Some(#n))
13275        }
13276        None => quote!(::core::option::Option::None),
13277    };
13278    let opt_i64 = |v: Option<i64>| match v {
13279        Some(n) => quote!(::core::option::Option::Some(#n)),
13280        None => quote!(::core::option::Option::None),
13281    };
13282    let constraint_blocks: Vec<_> = fields_info
13283        .iter()
13284        .filter(is_writable)
13285        .map(|fi| {
13286            let ident = &fi.ident;
13287            let fname = ident_name(ident);
13288            let mname = fi
13289                .attrs
13290                .source
13291                .clone()
13292                .unwrap_or_else(|| ident_name(&fi.ident));
13293            let attr_max_len = opt_usize(fi.attrs.max_length);
13294            let attr_min_len = opt_usize(fi.attrs.min_length);
13295            let attr_min = opt_i64(fi.attrs.min);
13296            let attr_max = opt_i64(fi.attrs.max);
13297            quote! {
13298                if __sent(#fname) {
13299                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13300                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13301                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13302                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13303                    let __min: ::core::option::Option<i64> =
13304                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13305                    let __max: ::core::option::Option<i64> =
13306                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13307                    let __choices = __sf.and_then(|__f| __f.choices);
13308                    let __v = #root::__serde_json::to_value(&self.#ident)
13309                        .unwrap_or(#root::__serde_json::Value::Null);
13310                    #root::forms::validators::check_value(
13311                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13312                        &mut __errors,
13313                    );
13314                }
13315            }
13316        })
13317        .collect();
13318    let has_constraints = !constraint_blocks.is_empty();
13319
13320    // Per-field validators. Emit a
13321    // `validate(&self)` method that runs each user-defined validator
13322    // and aggregates errors into `FormErrors`.
13323    let validator_calls: Vec<_> = fields_info
13324        .iter()
13325        .filter_map(|fi| {
13326            let ident = &fi.ident;
13327            let name_lit = ident_name(ident);
13328            let method = fi.attrs.validate.as_ref()?;
13329            let method_ident = syn::Ident::new(method, ident.span());
13330            Some(quote! {
13331                if __sent(#name_lit) {
13332                    if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13333                        __errors.add(#name_lit.to_owned(), __e);
13334                    }
13335                }
13336            })
13337        })
13338        .collect();
13339    // #436 — cross-field `validate(&self)`. If the
13340    // container declared `#[serializer(validate = "fn_name")]`,
13341    // the macro-generated `validate(&self)` runs every per-field
13342    // validator first, then calls the user's cross-field method,
13343    // merging its `FormErrors` into the per-field errors. Either
13344    // alone is enough to emit the wrapper.
13345    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13346        quote! {
13347            // Merge cross-field errors into the per-field bucket so
13348            // a single .validate() call surfaces both layers.
13349            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13350                __errors.merge(__cross);
13351            }
13352        }
13353    });
13354    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13355    // Anything to run? Declarative constraints (for any writable field) +
13356    // per-field validators + cross-field hook.
13357    let has_run_validations = has_validators || has_constraints;
13358    // Shared body: declarative field constraints first (length / range /
13359    // choices, serializer-attr-or-model), then per-field validators, then
13360    // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13361    let checks = quote! {
13362        let mut __errors = #root::forms::FormErrors::default();
13363        #( #constraint_blocks )*
13364        #( #validator_calls )*
13365        #cross_validate_call
13366        if __errors.is_empty() {
13367            ::core::result::Result::Ok(())
13368        } else {
13369            ::core::result::Result::Err(__errors)
13370        }
13371    };
13372    let validate_body = quote! {
13373        let __sent = |_: &str| true;
13374        #checks
13375    };
13376    // PATCH: only the keys the body carries.
13377    let validate_patch_body = quote! {
13378        let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13379        #checks
13380    };
13381    // Inherent `validate(&self)` — kept for back-compat with direct
13382    // `serializer.validate()` calls that don't import `ModelSerializer`.
13383    // Emitted only when the serializer declares per-field/cross-field
13384    // validators (unchanged rule) so it never collides with a
13385    // hand-written inherent `validate`.
13386    let validate_method = if has_validators {
13387        quote! {
13388            impl #struct_name {
13389                /// Run the declarative field constraints, every
13390                /// `#[serializer(validate = "...")]` per-field validator,
13391                /// and (when declared) the container-level cross-field
13392                /// validator. Aggregates errors into `FormErrors` keyed by
13393                /// the field name. Returns `Ok(())` when all pass.
13394                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13395                    #validate_body
13396                }
13397            }
13398        }
13399    } else {
13400        quote! {}
13401    };
13402    // Trait-method override so the type-erased ViewSet write path can
13403    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13404    // there's anything to run (declarative constraints inherited from the
13405    // model count); otherwise the trait's default no-op is used.
13406    let trait_validate_override = if has_run_validations {
13407        quote! {
13408            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13409                #validate_body
13410            }
13411        }
13412    } else {
13413        quote! {}
13414    };
13415
13416    // For every `#[serializer(many = S)]` field, emit a
13417    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13418    // helper that maps the parents through `S::from_model`.
13419    let many_setters: Vec<_> = fields_info
13420        .iter()
13421        .filter_map(|fi| {
13422            let many_ty = fi.attrs.many.as_ref()?;
13423            let ident = &fi.ident;
13424            let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13425            Some(quote! {
13426                /// Populate this `many` field by mapping each parent model
13427                /// through the inner serializer's `from_model`. Call after
13428                /// fetching the M2M / one-to-many children since
13429                /// `from_model` itself can't await an SQL query.
13430                pub fn #setter(
13431                    &mut self,
13432                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13433                ) -> &mut Self {
13434                    self.#ident = models.iter()
13435                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13436                        .collect();
13437                    self
13438                }
13439            })
13440        })
13441        .collect();
13442    let many_setters_impl = if many_setters.is_empty() {
13443        quote! {}
13444    } else {
13445        quote! {
13446            impl #struct_name {
13447                #( #many_setters )*
13448            }
13449        }
13450    };
13451
13452    // Generate custom Serialize: skip write_only fields
13453    let output_fields: Vec<_> = fields_info
13454        .iter()
13455        .filter(|fi| !fi.attrs.write_only)
13456        .collect();
13457    let output_field_count = output_fields.len();
13458    let serialize_fields = output_fields.iter().map(|fi| {
13459        let ident = &fi.ident;
13460        let name_lit = ident_name(ident);
13461        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13462    });
13463
13464    // writable_fields: normal + write_only.
13465    // Exclude:
13466    //   - `read_only` — server-computed.
13467    //   - `skip` — caller sets manually post-from_model.
13468    //   - `method` — computed from a Self::fn(&model) call; accepting
13469    //     it on write is meaningless.
13470    //   - `nested` / `many` — populated from related-model data, not
13471    //     from a field on the wire body.
13472    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13473    // `many` in `writable_fields()`, which made the ViewSet write
13474    // path accept those fields from the JSON body and try to bind
13475    // them to the SQL UPDATE — a silent no-op at best, a type
13476    // mismatch at worst.
13477    // (`is_writable` is defined above, near the constraint codegen.)
13478    let writable_lits: Vec<_> = fields_info
13479        .iter()
13480        .filter(is_writable)
13481        .map(|fi| ident_name(&fi.ident))
13482        .collect();
13483
13484    // `writable_source_fields`: the MODEL field names of writable
13485    // serializer fields (source-resolved). The ViewSet write path skips
13486    // every model column NOT in this set, so `read_only` / `method` /
13487    // computed fields a client posts are ignored instead of written.
13488    let writable_source_lits: Vec<String> = fields_info
13489        .iter()
13490        .filter(is_writable)
13491        .map(|fi| {
13492            fi.attrs
13493                .source
13494                .clone()
13495                .unwrap_or_else(|| ident_name(&fi.ident))
13496        })
13497        .collect();
13498
13499    // `readable_source_fields`: model columns the JSON output copies
13500    // straight from the model, so `?ordering=` cannot sort on a column
13501    // the API never shows (#1845).
13502    let readable_source_lits: Vec<String> = fields_info
13503        .iter()
13504        .filter(|fi| {
13505            !fi.attrs.write_only
13506                && !fi.attrs.skip
13507                && fi.attrs.method.is_none()
13508                && !fi.attrs.nested
13509                && fi.attrs.many.is_none()
13510                && fi.attrs.slug.is_none()
13511        })
13512        .map(|fi| {
13513            fi.attrs
13514                .source
13515                .clone()
13516                .unwrap_or_else(|| ident_name(&fi.ident))
13517        })
13518        .collect();
13519
13520    // `from_writable_json`: build a partial instance for input
13521    // validation. Writable fields are parsed from the JSON body (keyed
13522    // by serializer field name); every other field defaults. Per-field
13523    // type errors collect into `FormErrors` keyed by the field name.
13524    // Construction is field-by-field (not `Self::default()`) so it works
13525    // for serializers regardless of a struct-level `Default`.
13526    let from_writable_json_inits: Vec<_> = fields_info
13527        .iter()
13528        .map(|fi| {
13529            let ident = &fi.ident;
13530            let fname = ident_name(ident);
13531            let ty = &fi.ty;
13532            if is_writable(&fi) {
13533                quote! {
13534                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13535                        ::core::option::Option::Some(__v) => {
13536                            match #root::__serde_json::from_value::<#ty>(
13537                                ::core::clone::Clone::clone(__v),
13538                            ) {
13539                                ::core::result::Result::Ok(__x) => __x,
13540                                ::core::result::Result::Err(__e) => {
13541                                    __errors.add(#fname.to_owned(), __e.to_string());
13542                                    ::core::default::Default::default()
13543                                }
13544                            }
13545                        }
13546                        ::core::option::Option::None => ::core::default::Default::default(),
13547                    }
13548                }
13549            } else {
13550                quote! { #ident: ::core::default::Default::default() }
13551            }
13552        })
13553        .collect();
13554    // `validate_patch`: overlay each sent writable field on the stored row.
13555    let patch_overlays: Vec<_> = fields_info
13556        .iter()
13557        .filter(is_writable)
13558        .map(|fi| {
13559            let ident = &fi.ident;
13560            let fname = ident_name(ident);
13561            let ty = &fi.ty;
13562            quote! {
13563                if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
13564                    match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
13565                        ::core::result::Result::Ok(__x) => __merged.#ident = __x,
13566                        ::core::result::Result::Err(__e) => {
13567                            __errors.add(#fname.to_owned(), __e.to_string());
13568                        }
13569                    }
13570                }
13571            }
13572        })
13573        .collect();
13574    let patch_validate_call = if has_run_validations {
13575        quote! { __merged.__rustango_validate_patch(__obj) }
13576    } else {
13577        quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
13578    };
13579    let patch_validate_helper = if has_run_validations {
13580        quote! {
13581            impl #struct_name {
13582                #[doc(hidden)]
13583                fn __rustango_validate_patch(
13584                    &self,
13585                    __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
13586                ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13587                    #validate_patch_body
13588                }
13589            }
13590        }
13591    } else {
13592        quote! {}
13593    };
13594
13595    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13596    // Only includes fields shown in JSON output (skips write_only). For each
13597    // `Option<T>` field, omit from `required` and add `.nullable()`.
13598    let openapi_impl = {
13599        #[cfg(feature = "openapi")]
13600        {
13601            let property_calls = output_fields.iter().map(|fi| {
13602                let ident = &fi.ident;
13603                let name_lit = ident_name(ident);
13604                let ty = &fi.ty;
13605                let nullable_call = if is_option(ty) {
13606                    quote! { .nullable() }
13607                } else {
13608                    quote! {}
13609                };
13610                quote! {
13611                    .property(
13612                        #name_lit,
13613                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13614                            #nullable_call,
13615                    )
13616                }
13617            });
13618            let required_lits: Vec<_> = output_fields
13619                .iter()
13620                .filter(|fi| !is_option(&fi.ty))
13621                .map(|fi| ident_name(&fi.ident))
13622                .collect();
13623            quote! {
13624                impl #root::openapi::OpenApiSchema for #struct_name {
13625                    fn openapi_schema() -> #root::openapi::Schema {
13626                        #root::openapi::Schema::object()
13627                            #( #property_calls )*
13628                            .required([ #( #required_lits ),* ])
13629                    }
13630                }
13631            }
13632        }
13633        #[cfg(not(feature = "openapi"))]
13634        {
13635            quote! {}
13636        }
13637    };
13638
13639    Ok(quote! {
13640        impl #root::serializer::ModelSerializer for #struct_name {
13641            type Model = #model_path;
13642
13643            fn from_model(model: &Self::Model) -> Self {
13644                Self {
13645                    #( #from_model_fields ),*
13646                }
13647            }
13648
13649            fn writable_fields() -> &'static [&'static str] {
13650                &[ #( #writable_lits ),* ]
13651            }
13652
13653            fn writable_source_fields() -> &'static [&'static str] {
13654                &[ #( #writable_source_lits ),* ]
13655            }
13656
13657            fn readable_source_fields() -> &'static [&'static str] {
13658                &[ #( #readable_source_lits ),* ]
13659            }
13660
13661            fn from_writable_json(
13662                __body: &#root::__serde_json::Value,
13663            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13664                let mut __errors = #root::forms::FormErrors::default();
13665                let __obj = __body.as_object();
13666                let __out = Self {
13667                    #( #from_writable_json_inits ),*
13668                };
13669                if __errors.is_empty() {
13670                    ::core::result::Result::Ok(__out)
13671                } else {
13672                    ::core::result::Result::Err(__errors)
13673                }
13674            }
13675
13676            fn validate_patch(
13677                __stored: &Self::Model,
13678                __body: &#root::__serde_json::Value,
13679            ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13680                let mut __errors = #root::forms::FormErrors::default();
13681                let __obj = __body.as_object();
13682                #[allow(unused_mut)]
13683                let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
13684                #( #patch_overlays )*
13685                if !__errors.is_empty() {
13686                    return ::core::result::Result::Err(__errors);
13687                }
13688                #patch_validate_call
13689            }
13690
13691            #trait_validate_override
13692        }
13693
13694        #patch_validate_helper
13695
13696        impl #root::__serde::Serialize for #struct_name {
13697            fn serialize<S>(&self, serializer: S)
13698                -> ::core::result::Result<S::Ok, S::Error>
13699            where
13700                S: #root::__serde::Serializer,
13701            {
13702                use #root::__serde::ser::SerializeStruct;
13703                let mut __state = serializer.serialize_struct(
13704                    #struct_name_lit,
13705                    #output_field_count,
13706                )?;
13707                #( #serialize_fields )*
13708                __state.end()
13709            }
13710        }
13711
13712        #openapi_impl
13713
13714        #validate_method
13715
13716        #many_setters_impl
13717    })
13718}
13719
13720/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13721/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13722/// when the feature is off.
13723#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13724fn is_option(ty: &syn::Type) -> bool {
13725    if let syn::Type::Path(p) = ty {
13726        if let Some(last) = p.path.segments.last() {
13727            return last.ident == "Option";
13728        }
13729    }
13730    false
13731}
13732
13733#[cfg(test)]
13734mod main_attr_tests {
13735    use super::*;
13736
13737    fn expand(args: &str) -> String {
13738        let body = quote! { async fn main() { run().await } };
13739        expand_main(args.parse().unwrap(), body)
13740            .expect("expansion failed")
13741            .to_string()
13742    }
13743
13744    /// Asserts on the expansion rather than on `parse_logging`,
13745    /// because the defect was a subscriber reaching the output, not
13746    /// an argument being misread (#1465).
13747    #[test]
13748    fn logging_false_emits_no_subscriber() {
13749        let out = expand("logging = false");
13750        assert!(
13751            !out.contains("tracing_subscriber"),
13752            "`logging = false` must not install a subscriber:\n{out}",
13753        );
13754        assert!(
13755            out.contains("block_on"),
13756            "the runtime must still be built:\n{out}",
13757        );
13758    }
13759
13760    #[test]
13761    fn default_still_installs_a_subscriber() {
13762        // The control. Without it, an `expand_main` that dropped the
13763        // prologue unconditionally would pass the test above while
13764        // leaving every default project with no logging at all.
13765        let out = expand("");
13766        assert!(
13767            out.contains("tracing_subscriber"),
13768            "the default entrypoint must still install one:\n{out}",
13769        );
13770    }
13771
13772    #[test]
13773    fn logging_false_composes_with_flavor() {
13774        let out = expand("flavor = \"current_thread\", logging = false");
13775        assert!(!out.contains("tracing_subscriber"), "{out}");
13776        assert!(out.contains("new_current_thread"), "{out}");
13777    }
13778
13779    #[test]
13780    fn logging_true_is_the_default_shape() {
13781        // `logging = true` is accepted and means the default, so the
13782        // match must not fire on the substring `logging`.
13783        assert!(parse_logging(&"logging = true".parse().unwrap()));
13784        assert!(!parse_logging(&"logging=false".parse().unwrap()));
13785    }
13786}
13787
13788#[cfg(test)]
13789mod field_attr_tests {
13790    use super::*;
13791
13792    fn error_for(field: syn::Field) -> Option<String> {
13793        let attrs = parse_field_attrs(&field).unwrap();
13794        let kind = detect_type(&field.ty).unwrap().kind;
13795        check_bound_compatibility(&field, &attrs, kind)
13796            .err()
13797            .map(|e| e.to_string())
13798    }
13799
13800    /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
13801    #[test]
13802    fn type_specific_attrs_reject_other_types() {
13803        let f: syn::FieldsNamed = syn::parse_quote!({
13804            #[rustango(citext)] a: i64,
13805            #[rustango(vector(dims = 3))] b: String,
13806            #[rustango(geometry(srid = 4326))] c: String,
13807            #[rustango(citext)] d: String,
13808        });
13809        let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
13810        assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
13811        assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
13812        assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
13813        assert_eq!(errs[3], None);
13814    }
13815
13816    /// A raw ident derives names without `r#`; this used to panic (#1937).
13817    #[test]
13818    fn raw_idents_derive_plain_names() {
13819        let ident: syn::Ident = syn::parse_quote!(r#type);
13820        assert_eq!(column_type_ident(&ident).to_string(), "type_col");
13821        assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
13822    }
13823}