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. `RUST_LOG` and
272/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
273/// environment, else from `./.env` (only those keys are read):
274///
275/// ```ignore
276/// #[rustango::main]
277/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
278///     rustango::server::Builder::from_env().await?
279///         .migrate("migrations").await?
280///         .api(my_app::urls::api())
281///         .seed_with(my_app::seed::run).await?
282///         .serve("0.0.0.0:8080").await
283/// }
284/// ```
285///
286/// Optional `flavor = "current_thread"` passes through to
287/// `#[tokio::main]`; default is the multi-threaded runtime.
288///
289/// `logging = false` suppresses the default subscriber, for apps
290/// that install their own. Needed whenever the body reaches
291/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
292/// macro installs gets there first, and `tracing` keeps the first
293/// one, so the `[logging]` section would otherwise be read and
294/// discarded (#1465).
295///
296/// ```ignore
297/// #[rustango::main(logging = false)]
298/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
299///     rustango::manage::Cli::new()
300///         .with_settings_from_env()
301///         .with_logging()          // now the first installer
302///         .run().await
303/// }
304/// ```
305///
306/// Pulls `tracing-subscriber` into the rustango crate behind the
307/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
308/// out get plain `#[tokio::main]` ergonomics without the dependency.
309#[proc_macro_attribute]
310pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
311    expand_main(args.into(), item.into())
312        .unwrap_or_else(syn::Error::into_compile_error)
313        .into()
314}
315
316fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
317    let mut input: syn::ItemFn = syn::parse2(item)?;
318    if input.sig.asyncness.is_none() {
319        return Err(syn::Error::new(
320            input.sig.ident.span(),
321            "`#[rustango::main]` must wrap an `async fn`",
322        ));
323    }
324
325    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
326    // to `#[tokio::main]`. Tokio's proc-macro internally emits
327    // `::tokio::*` paths that resolve against the user crate's deps,
328    // so calling it through the rustango re-export still requires the
329    // user to add tokio to their own Cargo.toml. Building the
330    // runtime ourselves keeps the dep transitive through the
331    // `runtime` feature on rustango.
332    //
333    // Parse optional `flavor = "current_thread"` / `flavor =
334    // "multi_thread"` from the attribute args. Unknown args are
335    // tolerated (forward-compat with tokio's own arg surface).
336    let root = rustango_root();
337    let flavor = parse_flavor(&args);
338    let builder_call = match flavor {
339        Flavor::CurrentThread => quote! {
340            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
341        },
342        Flavor::MultiThread => quote! {
343            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
344        },
345    };
346
347    // Detach the user body and rewrite `main` as a sync fn that
348    // builds the runtime and blocks on the async body.
349    let user_body = input.block.clone();
350    input.sig.asyncness = None;
351
352    // `logging = false` drops the block entirely, so a later
353    // `Cli::with_logging()` is the first installer and its
354    // `[logging]` settings actually take effect (#1465).
355    let logging_prologue = if parse_logging(&args) {
356        quote! {
357            // `Setup`'s defaults, not a copy of them: filter, format and
358            // the `Color::Auto` rule live in one place (#1493).
359            // `logging` and this macro share the `runtime` gate.
360            // The real `RUST_LOG` still wins over `./.env`'s (#2204).
361            let _ = #root::logging::Setup::new()
362                .with_default_env_filter(#root::__private_runtime::main_default_filter())
363                .with_format(#root::__private_runtime::main_format())
364                .install();
365        }
366    } else {
367        quote! {}
368    };
369
370    input.block = syn::parse2(quote! {{
371        { #logging_prologue }
372        let __rt = #builder_call
373            .enable_all()
374            .build()
375            .expect("failed to build tokio runtime");
376        __rt.block_on(async move #user_body)
377    }})?;
378
379    Ok(quote! {
380        #input
381    })
382}
383
384enum Flavor {
385    MultiThread,
386    CurrentThread,
387}
388
389fn parse_flavor(args: &TokenStream2) -> Flavor {
390    // Cheap parser: look for the literal token sequence
391    // `flavor = "current_thread"`. Everything else (including
392    // bare `multi_thread` or no args) defaults to multi-thread.
393    let s = args.to_string();
394    if s.contains("current_thread") {
395        Flavor::CurrentThread
396    } else {
397        Flavor::MultiThread
398    }
399}
400
401/// `false` when the attribute carries `logging = false` — the caller
402/// installs their own subscriber, usually via `Cli::with_logging`
403/// (#1465). Same cheap token match as `parse_flavor`.
404fn parse_logging(args: &TokenStream2) -> bool {
405    !args.to_string().replace(' ', "").contains("logging=false")
406}
407
408/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
409struct QInput {
410    base_path: syn::Path,
411    field: syn::Ident,
412    value: syn::Expr,
413}
414
415impl syn::parse::Parse for QInput {
416    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
417        let base_path: syn::Path = input.parse()?;
418        input.parse::<syn::Token![.]>()?;
419        let field: syn::Ident = input.parse()?;
420        input.parse::<syn::Token![=]>()?;
421        let value: syn::Expr = input.parse()?;
422        Ok(QInput {
423            base_path,
424            field,
425            value,
426        })
427    }
428}
429
430fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
431    let q: QInput = syn::parse2(input)?;
432    let root = rustango_root();
433    let field_str = q.field.to_string();
434    let field_span = q.field.span();
435    let (base, suffix) = match field_str.find("__") {
436        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
437        None => (field_str.as_str(), ""),
438    };
439    if base.is_empty() {
440        return Err(syn::Error::new(
441            field_span,
442            "Q!(): field name is empty before `__` suffix",
443        ));
444    }
445    let base_ident = syn::Ident::new(base, field_span);
446    let value = &q.value;
447    let path = &q.base_path;
448
449    // Most suffixes map directly to a Column method with the value
450    // forwarded unchanged. Some need value-shape massaging (wildcards
451    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
452    // ISNULL). Unknown suffixes fail the build.
453    let expanded = match suffix {
454        "" | "exact" => quote! {
455            #root::core::Column::eq(#path::#base_ident, #value)
456        },
457        "ne" => quote! {
458            #root::core::Column::ne(#path::#base_ident, #value)
459        },
460        "gt" => quote! {
461            #root::core::Column::gt(#path::#base_ident, #value)
462        },
463        "gte" => quote! {
464            #root::core::Column::gte(#path::#base_ident, #value)
465        },
466        "lt" => quote! {
467            #root::core::Column::lt(#path::#base_ident, #value)
468        },
469        "lte" => quote! {
470            #root::core::Column::lte(#path::#base_ident, #value)
471        },
472        // The escaped-LIKE lookups delegate to the typed Column
473        // methods, which escape the value and pair it with the
474        // *Escaped ops so `%` / `_` match literally on every dialect
475        // (#1257) — the macro no longer owns any pattern wrapping.
476        "iexact" => quote! {
477            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
478        },
479        "contains" => quote! {
480            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
481        },
482        "icontains" => quote! {
483            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
484        },
485        "startswith" => quote! {
486            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
487        },
488        "istartswith" => quote! {
489            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
490        },
491        "endswith" => quote! {
492            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
493        },
494        "iendswith" => quote! {
495            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
496        },
497        "in" => quote! {
498            #root::core::Column::is_in(#path::#base_ident, #value)
499        },
500        "not_in" => quote! {
501            #root::core::Column::not_in(#path::#base_ident, #value)
502        },
503        "isnull" => {
504            // Must be a bool literal at macro time so we can route to
505            // is_null vs is_not_null without a runtime branch.
506            let b = match value {
507                syn::Expr::Lit(syn::ExprLit {
508                    lit: syn::Lit::Bool(b),
509                    ..
510                }) => b.value(),
511                _ => {
512                    return Err(syn::Error::new_spanned(
513                        value,
514                        "Q!(): `__isnull` requires a `true` or `false` literal",
515                    ));
516                }
517            };
518            if b {
519                quote! { #root::core::Column::is_null(#path::#base_ident) }
520            } else {
521                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
522            }
523        }
524        "between" => {
525            // Accept a tuple literal `(lo, hi)`.
526            let tuple = match value {
527                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
528                _ => {
529                    return Err(syn::Error::new_spanned(
530                        value,
531                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
532                    ));
533                }
534            };
535            let lo = &tuple.elems[0];
536            let hi = &tuple.elems[1];
537            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
538        }
539        "regex" => quote! {
540            #root::core::Column::regex(#path::#base_ident, #value)
541        },
542        "iregex" => quote! {
543            #root::core::Column::iregex(#path::#base_ident, #value)
544        },
545        _ => {
546            return Err(syn::Error::new(
547                field_span,
548                format!(
549                    "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",
550                    suffix
551                ),
552            ));
553        }
554    };
555    Ok(expanded)
556}
557
558fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
559    // Default to "./migrations" if invoked without args.
560    let path_str = if input.is_empty() {
561        "./migrations".to_string()
562    } else {
563        let lit: LitStr = syn::parse2(input)?;
564        lit.value()
565    };
566
567    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
568        syn::Error::new(
569            proc_macro2::Span::call_site(),
570            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
571        )
572    })?;
573    let abs = std::path::Path::new(&manifest).join(&path_str);
574
575    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
576    if abs.is_dir() {
577        let read = std::fs::read_dir(&abs).map_err(|e| {
578            syn::Error::new(
579                proc_macro2::Span::call_site(),
580                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
581            )
582        })?;
583        for entry in read.flatten() {
584            let path = entry.path();
585            if !path.is_file() {
586                continue;
587            }
588            if path.extension().and_then(|s| s.to_str()) != Some("json") {
589                continue;
590            }
591            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
592                continue;
593            };
594            entries.push((stem.to_owned(), path));
595        }
596    }
597    entries.sort_by(|a, b| a.0.cmp(&b.0));
598
599    // Compile-time chain validation: read each migration's JSON,
600    // pull `name` and `prev` (file-stem-keyed for the chain check),
601    // and verify every `prev` points to another migration in the
602    // slice. Mismatches between the file stem and the embedded
603    // `name` field — and broken `prev` chains — fail at MACRO
604    // EXPANSION time so a misshapen migration set never compiles.
605    //
606    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
607    // migrations + a Rust proc-macro that reads them is a combo no
608    // other Rust web framework matches (Cot's are imperative Rust
609    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
610    // a static chain to validate).
611    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
612    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
613    for (stem, path) in &entries {
614        let raw = std::fs::read_to_string(path).map_err(|e| {
615            syn::Error::new(
616                proc_macro2::Span::call_site(),
617                format!(
618                    "embed_migrations!: cannot read {} for chain validation: {e}",
619                    path.display()
620                ),
621            )
622        })?;
623        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
624            syn::Error::new(
625                proc_macro2::Span::call_site(),
626                format!(
627                    "embed_migrations!: {} is not valid JSON: {e}",
628                    path.display()
629                ),
630            )
631        })?;
632        let name = json
633            .get("name")
634            .and_then(|v| v.as_str())
635            .ok_or_else(|| {
636                syn::Error::new(
637                    proc_macro2::Span::call_site(),
638                    format!(
639                        "embed_migrations!: {} is missing the `name` field",
640                        path.display()
641                    ),
642                )
643            })?
644            .to_owned();
645        if name != *stem {
646            return Err(syn::Error::new(
647                proc_macro2::Span::call_site(),
648                format!(
649                    "embed_migrations!: file stem `{stem}` does not match the migration's \
650                     `name` field `{name}` — rename the file or fix the JSON",
651                ),
652            ));
653        }
654        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
655        chain_names.push(name.clone());
656        prev_refs.push((name, prev));
657    }
658
659    let name_set: std::collections::HashSet<&str> =
660        chain_names.iter().map(String::as_str).collect();
661    for (name, prev) in &prev_refs {
662        if let Some(p) = prev {
663            if !name_set.contains(p.as_str()) {
664                return Err(syn::Error::new(
665                    proc_macro2::Span::call_site(),
666                    format!(
667                        "embed_migrations!: broken migration chain — `{name}` declares \
668                         prev=`{p}` but no migration with that name exists in {}",
669                        abs.display()
670                    ),
671                ));
672            }
673        }
674    }
675
676    let pairs: Vec<TokenStream2> = entries
677        .iter()
678        .map(|(name, path)| {
679            let path_lit = path.display().to_string();
680            quote! { (#name, ::core::include_str!(#path_lit)) }
681        })
682        .collect();
683
684    Ok(quote! {
685        {
686            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
687            __RUSTANGO_EMBEDDED
688        }
689    })
690}
691
692fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
693    let root = rustango_root();
694    let struct_name = &input.ident;
695
696    let Data::Struct(data) = &input.data else {
697        return Err(syn::Error::new_spanned(
698            struct_name,
699            "Model can only be derived on structs",
700        ));
701    };
702    let Fields::Named(named) = &data.fields else {
703        return Err(syn::Error::new_spanned(
704            struct_name,
705            "Model requires a struct with named fields",
706        ));
707    };
708
709    let container = parse_container_attrs(input)?;
710    let table = container
711        .table
712        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
713    let model_name = struct_name.to_string();
714
715    let collected = collect_fields(named, &table)?;
716
717    // Validate that #[rustango(display = "…")] names a real field.
718    if let Some((ref display, span)) = container.display {
719        if !collected.field_names.iter().any(|n| n == display) {
720            return Err(syn::Error::new(
721                span,
722                format!("`display = \"{display}\"` does not match any field on this struct"),
723            ));
724        }
725    }
726    let display = container.display.map(|(name, _)| name);
727    let app_label = container.app.clone();
728
729    // Validate admin field-name lists against declared field names.
730    // Note: `list_display` is intentionally NOT validated here. As of
731    // v0.32 it may also reference inventory-registered computed
732    // fields (via `register_admin_computed!`) whose existence the
733    // macro can't see at compile time — they're submitted from any
734    // crate that depends on rustango. The runtime list-view resolves
735    // unknown names against the inventory + silently drops the
736    // truly-bogus ones, which is the cheaper trade-off versus
737    // forcing a per-Model attr to opt out.
738    if let Some(admin) = &container.admin {
739        for (label, list) in [
740            ("search_fields", &admin.search_fields),
741            ("readonly_fields", &admin.readonly_fields),
742            ("list_filter", &admin.list_filter),
743        ] {
744            if let Some((names, span)) = list {
745                for name in names {
746                    if !collected.field_names.iter().any(|n| n == name) {
747                        return Err(syn::Error::new(
748                            *span,
749                            format!(
750                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
751                            ),
752                        ));
753                    }
754                }
755            }
756        }
757        if let Some((pairs, span)) = &admin.ordering {
758            for (name, _) in pairs {
759                if !collected.field_names.iter().any(|n| n == name) {
760                    return Err(syn::Error::new(
761                        *span,
762                        format!(
763                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
764                        ),
765                    ));
766                }
767            }
768        }
769        if let Some((groups, span)) = &admin.fieldsets {
770            for (_, fields) in groups {
771                for name in fields {
772                    if !collected.field_names.iter().any(|n| n == name) {
773                        return Err(syn::Error::new(
774                            *span,
775                            format!(
776                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
777                            ),
778                        ));
779                    }
780                }
781            }
782        }
783    }
784    if let Some(audit) = &container.audit {
785        if let Some((names, span)) = &audit.track {
786            for name in names {
787                if !collected.field_names.iter().any(|n| n == name) {
788                    return Err(syn::Error::new(
789                        *span,
790                        format!(
791                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
792                        ),
793                    ));
794                }
795            }
796        }
797    }
798
799    // Issue #291 / T2.5 — validate each `default_order` column name
800    // against the model's collected fields. Typos fail at macro-expand
801    // time, not at the database.
802    for (col, _desc, span) in &container.default_order {
803        if !collected.field_names.iter().any(|n| n == col) {
804            return Err(syn::Error::new(
805                *span,
806                format!(
807                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
808                ),
809            ));
810        }
811    }
812
813    // Build the audit_track list for ModelSchema: None when no audit attr,
814    // Some(empty) when audit present without track, Some(names) when explicit.
815    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
816        audit
817            .track
818            .as_ref()
819            .map(|(names, _)| names.clone())
820            .unwrap_or_default()
821    });
822
823    // The upsert conflict target: a container-level composite unique,
824    // never a field `index(unique)` or a partial index PG can't target.
825    let upsert_unique: Option<Vec<String>> = container
826        .indexes
827        .iter()
828        .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
829        .map(|i| i.columns.clone());
830
831    // Merge field-level indexes into the container's index list.
832    let mut all_indexes: Vec<IndexAttr> = container.indexes;
833    for field in &named.named {
834        let ident = field.ident.as_ref().expect("named");
835        // The same default column as `process_field` (#1937).
836        let col = ident_name(ident);
837        // Re-parse field attrs to check for index flag
838        if let Ok(fa) = parse_field_attrs(field) {
839            if fa.index {
840                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
841                let auto_name = if fa.index_unique {
842                    format!("{table}_{col_name}_uq_idx")
843                } else {
844                    format!("{table}_{col_name}_idx")
845                };
846                all_indexes.push(IndexAttr {
847                    name: fa.index_name.or(Some(auto_name)),
848                    columns: vec![col_name],
849                    unique: fa.index_unique,
850                    method: fa.index_method,
851                    where_clause: None,
852                    include: Vec::new(),
853                });
854            }
855        }
856    }
857    // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
858    for field in &named.named {
859        let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
860            continue;
861        };
862        if det.kind != DetectedKind::String || fa.max_length.is_some() {
863            continue;
864        }
865        let col = fa
866            .column
867            .clone()
868            .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
869        // MySQL indexes every foreign-key column too.
870        let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
871        if fa.primary_key
872            || fa.unique
873            || relation
874            || all_indexes.iter().any(|i| i.columns.contains(&col))
875        {
876            return Err(syn::Error::new_spanned(
877                field,
878                "a primary-key, unique, indexed or foreign-key `String` needs \
879                 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
880            ));
881        }
882    }
883
884    let model_impl = model_impl_tokens(
885        struct_name,
886        &model_name,
887        &table,
888        display.as_deref(),
889        app_label.as_deref(),
890        container.admin.as_ref(),
891        &container.default_order,
892        &collected.field_schemas,
893        collected.soft_delete_column.as_deref(),
894        container.permissions,
895        audit_track_names.as_deref(),
896        &container.m2m,
897        &all_indexes,
898        &container.checks,
899        &container.excludes,
900        &container.composite_fks,
901        &container.generic_fks,
902        container.scope.as_deref(),
903        container.is_view,
904        container.verbose_name.as_deref(),
905        container.verbose_name_plural.as_deref(),
906        container.managed,
907        container.base_manager_name.as_deref(),
908        container.order_with_respect_to.as_deref(),
909        container.proxy,
910        &container.required_db_features,
911        container.required_db_vendor.as_deref(),
912        container.default_related_name.as_deref(),
913        container.db_table_comment.as_deref(),
914        container
915            .get_latest_by
916            .as_ref()
917            .map(|(c, d)| (c.as_str(), *d)),
918        &container.extra_permissions,
919        &container.default_permissions,
920        &container.global_scopes,
921        &container.reverse_has_relations,
922        &container.generic_has_relations,
923    );
924    let module_ident = column_module_ident(struct_name);
925    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
926    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
927        let track_set: Option<std::collections::HashSet<&str>> = audit
928            .track
929            .as_ref()
930            .map(|(names, _)| names.iter().map(String::as_str).collect());
931        collected
932            .column_entries
933            .iter()
934            .filter(|c| {
935                track_set
936                    .as_ref()
937                    .map_or(true, |s| s.contains(c.name.as_str()))
938            })
939            .collect()
940    });
941    let inherent_impl = inherent_impl_tokens(
942        struct_name,
943        &collected,
944        collected.primary_key.as_ref(),
945        &column_consts,
946        audited_fields.as_deref(),
947        upsert_unique.as_deref(),
948        &container.manager_fns,
949    );
950    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
951    let from_row_impl = from_row_impl_tokens(
952        struct_name,
953        &collected.from_row_inits,
954        &collected.from_row_inits_my,
955    );
956    let reverse_helpers = reverse_helper_tokens(
957        struct_name,
958        &collected.fk_relations,
959        container.default_related_name.as_deref(),
960    );
961    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
962    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
963    // Issue #817 — `#[rustango(through(...))]` accessors.
964    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
965    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
966    let reverse_has_accessors =
967        reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
968    let generic_fk_accessors = generic_fk_accessor_tokens(
969        struct_name,
970        &container.generic_fks,
971        &collected.column_entries,
972    );
973
974    Ok(quote! {
975        #model_impl
976        #inherent_impl
977        #from_row_impl
978        #column_module
979        #reverse_helpers
980        #m2m_accessors
981        #generic_m2m_accessors
982        #through_accessors
983        #reverse_has_accessors
984        #generic_fk_accessors
985
986        #root::core::inventory::submit! {
987            // `module_path!()` evaluates at the registration site,
988            // so a Model declared in `crate::blog::models` records
989            // `"<crate>::blog::models"` and `resolved_app_label()`
990            // can infer "blog" without an explicit attribute.
991            #root::core::ModelEntry::new(
992                <#struct_name as #root::core::Model>::SCHEMA,
993                ::core::module_path!(),
994            )
995            .with_audited(
996                <#struct_name as #root::core::Model>::__rustango_audited_update,
997                <#struct_name as #root::core::Model>::__rustango_audited_delete,
998            )
999            .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1000            .with_audited_update_record(
1001                <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1002            )
1003        }
1004    })
1005}
1006
1007/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1008/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1009fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1010    let root = rustango_root();
1011    let parent_ty = &rel.parent_type;
1012    let field_ident = &rel.field_ident;
1013    let null_fk = if rel.nullable {
1014        quote! {
1015            if #receiver.#field_ident.is_none() {
1016                return ::core::result::Result::Ok(false);
1017            }
1018        }
1019    } else {
1020        quote! {}
1021    };
1022    quote! {
1023        #null_fk
1024        #root::sql::__rustango_require_join(
1025            row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1026        )?;
1027    }
1028}
1029
1030/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1031/// reused, and the deeper chain stitched into it; re-decoding it would
1032/// drop what that chain loaded.
1033fn loaded_reuse_tokens(
1034    rel: &FkRelation,
1035    receiver: &TokenStream2,
1036    load: &TokenStream2,
1037) -> TokenStream2 {
1038    let root = rustango_root();
1039    let field_ident = &rel.field_ident;
1040    let pattern = if rel.nullable {
1041        quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1042    } else {
1043        quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1044    };
1045    quote! {
1046        if let #pattern = &mut #receiver.#field_ident {
1047            if let ::core::option::Option::Some(__r) = __rest {
1048                let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1049            }
1050            return ::core::result::Result::Ok(true);
1051        }
1052    }
1053}
1054
1055/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1056/// matches `field_name` against the model's FK fields and, for a
1057/// match, decodes the FK target via the parent's macro-generated
1058/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
1059/// `ForeignKey::Loaded` on `self`.
1060///
1061/// Always emitted (with empty arms for FK-less models, which
1062/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1063/// trait bound on `fetch_on` is universally satisfied — users
1064/// never have to think about implementing it.
1065fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1066    let root = rustango_root();
1067    let arms = fk_relations.iter().map(|rel| {
1068        let parent_ty = &rel.parent_type;
1069        let field_ident = &rel.field_ident;
1070        let fk_name = ident_name(field_ident);
1071        let fk_col = fk_name.as_str();
1072        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1073        let guard = join_guard_tokens(rel, &quote!(self));
1074        let reuse = loaded_reuse_tokens(
1075            rel,
1076            &quote!(self),
1077            &quote!(#root::sql::LoadRelated::__rustango_load_related),
1078        );
1079        let assign = if rel.nullable {
1080            quote! {
1081                self.#field_ident = ::core::option::Option::Some(
1082                    #root::sql::ForeignKey::loaded(_pk, _parent),
1083                );
1084            }
1085        } else {
1086            quote! {
1087                self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1088            }
1089        };
1090        quote! {
1091            #fk_col => {
1092                #guard
1093                #reuse
1094                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1095                // Audit #451 — multi-hop `select_related("a__b__c")`:
1096                // stitch the deeper relation onto this parent first,
1097                // decoding it at the accumulated `__next_alias`. The
1098                // parent type also impls `LoadRelated`, so this recurses
1099                // the FK chain to arbitrary depth.
1100                if let ::core::option::Option::Some(__r) = __rest {
1101                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1102                        &mut _parent, row, __r, &__next_alias,
1103                    )?;
1104                }
1105                // Loud-in-debug, default-in-release: a divergence
1106                // between the FK field's declared `K` (drives the
1107                // expected `SqlValue::<Variant>`) and the parent's
1108                // `__rustango_pk_value` output is a macro-internal
1109                // invariant break — surfacing the panic in dev
1110                // catches it before users hit silent PK=0 corruption.
1111                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1112                    #root::core::SqlValue::#variant_ident(v) => v,
1113                    _other => {
1114                        ::core::debug_assert!(
1115                            false,
1116                            "rustango macro bug: load_related on FK `{}` expected \
1117                             SqlValue::{} from parent's __rustango_pk_value but got \
1118                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1119                            #fk_col,
1120                            ::core::stringify!(#variant_ident),
1121                            _other,
1122                        );
1123                        #default_expr
1124                    }
1125                };
1126                #assign
1127                ::core::result::Result::Ok(true)
1128            }
1129        }
1130    });
1131    quote! {
1132        #[cfg(feature = "postgres")]
1133        impl #root::sql::LoadRelated for #struct_name {
1134            #[allow(unused_variables)]
1135            fn __rustango_load_related(
1136                &mut self,
1137                row: &#root::sql::sqlx::postgres::PgRow,
1138                field_name: &str,
1139                alias: &str,
1140            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1141                // Audit #451 — split the multi-hop path: `base` is the FK
1142                // on THIS model, `__rest` (if any) is the remaining chain
1143                // to stitch onto the loaded parent. `__next_alias` is the
1144                // accumulated join alias the parent's columns live under
1145                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1146                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1147                    match field_name.split_once("__") {
1148                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1149                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1150                    };
1151                let __next_alias: ::std::string::String = match __rest {
1152                    ::core::option::Option::Some(__r) => {
1153                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1154                        ::std::format!("{}__{}", alias, __rb)
1155                    }
1156                    ::core::option::Option::None => ::std::string::String::new(),
1157                };
1158                match __base {
1159                    #( #arms )*
1160                    _ => ::core::result::Result::Ok(false),
1161                }
1162            }
1163        }
1164    }
1165}
1166
1167/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1168/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1169/// which expands to a `LoadRelatedMy` impl when rustango is built with
1170/// the `mysql` feature, and to nothing otherwise. The decoded parent
1171/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1172/// decoder, also batch8) so the dual emission is symmetric across
1173/// backends.
1174fn load_related_impl_my_tokens(
1175    struct_name: &syn::Ident,
1176    fk_relations: &[FkRelation],
1177) -> TokenStream2 {
1178    let root = rustango_root();
1179    let arms = fk_relations.iter().map(|rel| {
1180        let parent_ty = &rel.parent_type;
1181        let field_ident = &rel.field_ident;
1182        let fk_name = ident_name(field_ident);
1183        let fk_col = fk_name.as_str();
1184        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1185        let guard = join_guard_tokens(rel, &quote!(__self));
1186        let reuse = loaded_reuse_tokens(
1187            rel,
1188            &quote!(__self),
1189            &quote!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1190        );
1191        let assign = if rel.nullable {
1192            quote! {
1193                __self.#field_ident = ::core::option::Option::Some(
1194                    #root::sql::ForeignKey::loaded(_pk, _parent),
1195                );
1196            }
1197        } else {
1198            quote! {
1199                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1200            }
1201        };
1202        // `self` IS hygiene-tracked through macro_rules — emitted from
1203        // a different context than the `&mut self` parameter inside
1204        // the macro_rules-expanded fn. Pass it through as `__self`
1205        // and let the macro_rules rebind it to the receiver.
1206        quote! {
1207            #fk_col => {
1208                #guard
1209                #reuse
1210                let mut _parent: #parent_ty =
1211                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1212                // Audit #451 — multi-hop: stitch the deeper relation onto
1213                // the parent at the accumulated alias (see PG twin).
1214                if let ::core::option::Option::Some(__r) = __rest {
1215                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1216                        &mut _parent, row, __r, &__next_alias,
1217                    )?;
1218                }
1219                // See note in `load_related_impl_tokens` (PG twin) —
1220                // the same loud-in-debug invariant guard.
1221                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1222                    #root::core::SqlValue::#variant_ident(v) => v,
1223                    _other => {
1224                        ::core::debug_assert!(
1225                            false,
1226                            "rustango macro bug: load_related on FK `{}` expected \
1227                             SqlValue::{} from parent's __rustango_pk_value but got \
1228                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1229                            #fk_col,
1230                            ::core::stringify!(#variant_ident),
1231                            _other,
1232                        );
1233                        #default_expr
1234                    }
1235                };
1236                #assign
1237                ::core::result::Result::Ok(true)
1238            }
1239        }
1240    });
1241    quote! {
1242        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1243            #( #arms )*
1244        });
1245    }
1246}
1247
1248/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1249/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1250/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1251fn load_related_impl_sqlite_tokens(
1252    struct_name: &syn::Ident,
1253    fk_relations: &[FkRelation],
1254) -> TokenStream2 {
1255    let root = rustango_root();
1256    let arms = fk_relations.iter().map(|rel| {
1257        let parent_ty = &rel.parent_type;
1258        let field_ident = &rel.field_ident;
1259        let fk_name = ident_name(field_ident);
1260        let fk_col = fk_name.as_str();
1261        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1262        let guard = join_guard_tokens(rel, &quote!(__self));
1263        let reuse = loaded_reuse_tokens(
1264            rel,
1265            &quote!(__self),
1266            &quote!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1267        );
1268        let assign = if rel.nullable {
1269            quote! {
1270                __self.#field_ident = ::core::option::Option::Some(
1271                    #root::sql::ForeignKey::loaded(_pk, _parent),
1272                );
1273            }
1274        } else {
1275            quote! {
1276                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1277            }
1278        };
1279        quote! {
1280            #fk_col => {
1281                #guard
1282                #reuse
1283                let mut _parent: #parent_ty =
1284                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1285                // Audit #451 — multi-hop: stitch the deeper relation onto
1286                // the parent at the accumulated alias (see PG twin).
1287                if let ::core::option::Option::Some(__r) = __rest {
1288                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1289                        &mut _parent, row, __r, &__next_alias,
1290                    )?;
1291                }
1292                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1293                    #root::core::SqlValue::#variant_ident(v) => v,
1294                    _other => {
1295                        ::core::debug_assert!(
1296                            false,
1297                            "rustango macro bug: load_related on FK `{}` expected \
1298                             SqlValue::{} from parent's __rustango_pk_value but got \
1299                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1300                            #fk_col,
1301                            ::core::stringify!(#variant_ident),
1302                            _other,
1303                        );
1304                        #default_expr
1305                    }
1306                };
1307                #assign
1308                ::core::result::Result::Ok(true)
1309            }
1310        }
1311    });
1312    quote! {
1313        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1314            #( #arms )*
1315        });
1316    }
1317}
1318
1319/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1320/// matches `field_name` against the model's FK fields and returns
1321/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1322/// group children by parent PK.
1323///
1324/// Always emitted (with `_ => None` for FK-less models) so the
1325/// trait bound on `fetch_with_prefetch` is universally satisfied.
1326fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1327    let root = rustango_root();
1328    let arms = fk_relations.iter().map(|rel| {
1329        let field_ident = &rel.field_ident;
1330        let fk_name = ident_name(field_ident);
1331        let fk_col = fk_name.as_str();
1332        if rel.pk_kind == DetectedKind::I64 {
1333            // i64 FK — return the stored PK so prefetch_related can
1334            // group children by it. Nullable variant unwraps via
1335            // `as_ref().map(...)`: an unset (NULL) FK column yields
1336            // `None` and that child sits out of the grouping (correct
1337            // semantics — it has no parent to attach to).
1338            if rel.nullable {
1339                quote! {
1340                    #fk_col => self.#field_ident
1341                        .as_ref()
1342                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1343                }
1344            } else {
1345                quote! {
1346                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1347                }
1348            }
1349        } else {
1350            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1351            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1352            // i64-keyed grouping path — the trait signature is
1353            // `Option<i64>` and a non-i64 PK can't lower into it.
1354            // The FK still works for everything else (CRUD, lazy
1355            // load via `.get()`, select_related JOINs); only the
1356            // bulk prefetch grouper needs the integer key.
1357            quote! {
1358                #fk_col => ::core::option::Option::None,
1359            }
1360        }
1361    });
1362    // PK-type-agnostic version: every FK arm emits an
1363    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1364    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1365    // opt OUT of the legacy i64 method (it returns None) but opt IN
1366    // here.
1367    let value_arms = fk_relations.iter().map(|rel| {
1368        let field_ident = &rel.field_ident;
1369        let fk_name = ident_name(field_ident);
1370        let fk_col = fk_name.as_str();
1371        if rel.nullable {
1372            quote! {
1373                #fk_col => self.#field_ident
1374                    .as_ref()
1375                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1376                        #root::sql::ForeignKey::pk(fk)
1377                    )),
1378            }
1379        } else {
1380            quote! {
1381                #fk_col => ::core::option::Option::Some(
1382                    ::core::convert::Into::<#root::core::SqlValue>::into(
1383                        self.#field_ident.pk()
1384                    )
1385                ),
1386            }
1387        }
1388    });
1389    quote! {
1390        impl #root::sql::FkPkAccess for #struct_name {
1391            #[allow(unused_variables)]
1392            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1393                match field_name {
1394                    #( #arms )*
1395                    _ => ::core::option::Option::None,
1396                }
1397            }
1398            #[allow(unused_variables)]
1399            fn __rustango_fk_pk_value(
1400                &self,
1401                field_name: &str,
1402            ) -> ::core::option::Option<#root::core::SqlValue> {
1403                match field_name {
1404                    #( #value_arms )*
1405                    _ => ::core::option::Option::None,
1406                }
1407            }
1408        }
1409    }
1410}
1411
1412/// For every `ForeignKey<Parent>` field on `Child`, emit
1413/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1414/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1415/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1416/// — the canonical reverse-FK fetch. One round trip, no N+1.
1417///
1418/// **PG-only emission**: the accessor is bounded on
1419/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1420/// both of which are gated behind the `postgres` cargo feature. The
1421/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1422/// model derive itself compiles on tri-dialect / sqlite-only
1423/// downstream builds — the accessor just isn't materialised. A tri-
1424/// dialect `_set_pool` variant is a separate follow-up.
1425fn reverse_helper_tokens(
1426    child_ident: &syn::Ident,
1427    fk_relations: &[FkRelation],
1428    default_related_name: Option<&str>,
1429) -> TokenStream2 {
1430    let root = rustango_root();
1431    if fk_relations.is_empty() {
1432        return TokenStream2::new();
1433    }
1434    // Method-name resolution per FK (issue #816 + follow-up):
1435    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1436    //      itself — wins over everything else.
1437    //   2. Container-level `default_related_name = "..."` on the
1438    //      child. Applies to every FK on this model that didn't
1439    //      override.
1440    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1441    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1442    //      edge cases.
1443    //
1444    // The PG-on-executor variant keeps the resolved name; the
1445    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1446    // framework's convention for the `&Pool` flavor of every helper).
1447    let default_pg_suffix = default_related_name
1448        .map(str::to_owned)
1449        .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1450    let impls = fk_relations.iter().map(|rel| {
1451        let pg_suffix = rel
1452            .related_name
1453            .clone()
1454            .unwrap_or_else(|| default_pg_suffix.clone());
1455        let pool_suffix = format!("{}_pool", pg_suffix);
1456        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1457        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1458        let parent_ty = &rel.parent_type;
1459        let fk_name = ident_name(&rel.field_ident);
1460        let fk_col = fk_name.as_str();
1461        let doc = format!(
1462            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1463             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1464             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1465             further `{child_ident}::objects()` filters via direct queryset use."
1466        );
1467        let pool_doc = format!(
1468            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1469             [`#root::sql::Pool`] and dispatches per backend so the \
1470             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1471             Use this from framework code that holds a `&Pool` (admin, \
1472             tenancy resolver, viewset handlers); reach for the executor- \
1473             bound variant when you already have a typed `sqlx::Executor`."
1474        );
1475        quote! {
1476            #[cfg(feature = "postgres")]
1477            impl #parent_ty {
1478                #[doc = #doc]
1479                ///
1480                /// # Errors
1481                /// Returns [`#root::sql::ExecError`] for SQL-writing
1482                /// or driver failures.
1483                pub async fn #pg_method_ident<'_c, _E>(
1484                    &self,
1485                    _executor: _E,
1486                ) -> ::core::result::Result<
1487                    ::std::vec::Vec<#child_ident>,
1488                    #root::sql::ExecError,
1489                >
1490                where
1491                    _E: #root::sql::sqlx::Executor<
1492                        '_c,
1493                        Database = #root::sql::sqlx::Postgres,
1494                    >,
1495                {
1496                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1497                    #root::query::QuerySet::<#child_ident>::new()
1498                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1499                        .fetch_on(_executor)
1500                        .await
1501                }
1502            }
1503
1504            impl #parent_ty {
1505                #[doc = #pool_doc]
1506                ///
1507                /// # Errors
1508                /// Returns [`#root::sql::ExecError`] for SQL-writing
1509                /// or driver failures.
1510                pub async fn #pool_method_ident(
1511                    &self,
1512                    pool: &#root::sql::Pool,
1513                ) -> ::core::result::Result<
1514                    ::std::vec::Vec<#child_ident>,
1515                    #root::sql::ExecError,
1516                > {
1517                    use #root::sql::FetcherPool as _;
1518                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1519                    #root::query::QuerySet::<#child_ident>::new()
1520                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1521                        .fetch(pool)
1522                        .await
1523                }
1524            }
1525        }
1526    });
1527    quote! { #( #impls )* }
1528}
1529
1530/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1531/// relation declared on the model.
1532/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1533/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1534///
1535/// Closes #239 + #240 — `comment.content_object` /
1536/// `comment.content_object = post` ergonomics on top of the existing
1537/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1538///
1539/// `column_entries` is passed so we can resolve each `ct_column` /
1540/// `pk_column` SQL name back to its Rust field ident — the macro
1541/// only sees the column-side strings in the attribute, but the
1542/// emitted accessor needs to read the actual struct field.
1543fn generic_fk_accessor_tokens(
1544    struct_name: &syn::Ident,
1545    generic_fks: &[GenericFkAttr],
1546    column_entries: &[ColumnEntry],
1547) -> TokenStream2 {
1548    let root = rustango_root();
1549    if generic_fks.is_empty() {
1550        return TokenStream2::new();
1551    }
1552    let methods = generic_fks.iter().filter_map(|gfk| {
1553        // Resolve `ct_column` + `pk_column` to the struct's Rust
1554        // field idents. A typo (column name doesn't match any field)
1555        // emits no method for that registration — the user will see
1556        // the compiler reject the SCHEMA literal anyway, so there's
1557        // a clear error path without us double-reporting.
1558        let ct_ident = column_entries
1559            .iter()
1560            .find(|c| c.column == gfk.ct_column)
1561            .map(|c| c.ident.clone())?;
1562        let pk_ident = column_entries
1563            .iter()
1564            .find(|c| c.column == gfk.pk_column)
1565            .map(|c| c.ident.clone())?;
1566
1567        let accessor_ident =
1568            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1569        let setter_ident =
1570            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1571        let name_literal = gfk.name.as_str();
1572
1573        Some(quote! {
1574            #[doc = concat!(
1575                "Resolve the polymorphic `",
1576                #name_literal,
1577                "` relation. Reads `self.",
1578                stringify!(#ct_ident),
1579                "` + `self.",
1580                stringify!(#pk_ident),
1581                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1582                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1583                #name_literal,
1584                "\", ...))]`."
1585            )]
1586            pub async fn #accessor_ident(
1587                &self,
1588                pool: &#root::sql::Pool,
1589            ) -> ::core::result::Result<
1590                ::core::option::Option<#root::__serde_json::Value>,
1591                #root::sql::ExecError,
1592            > {
1593                let gfk = #root::contenttypes::GenericForeignKey::new(
1594                    self.#ct_ident as i64,
1595                    self.#pk_ident as i64,
1596                );
1597                gfk.get_object(pool).await
1598            }
1599
1600            #[doc = concat!(
1601                "Set the polymorphic `",
1602                #name_literal,
1603                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1604                stringify!(#ct_ident),
1605                "` and `self.",
1606                stringify!(#pk_ident),
1607                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1608                #name_literal,
1609                "\", ...))]`."
1610            )]
1611            pub async fn #setter_ident<T: #root::core::Model>(
1612                &mut self,
1613                pool: &#root::sql::Pool,
1614                target_pk: i64,
1615            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1616                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1617                    pool,
1618                    target_pk,
1619                ).await?;
1620                self.#ct_ident = gfk.content_type_id as _;
1621                self.#pk_ident = gfk.object_pk as _;
1622                ::core::result::Result::Ok(())
1623            }
1624        })
1625    });
1626    quote! {
1627        impl #struct_name {
1628            #( #methods )*
1629        }
1630    }
1631}
1632
1633fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1634    let root = rustango_root();
1635    if m2m_relations.is_empty() {
1636        return TokenStream2::new();
1637    }
1638    let methods = m2m_relations.iter().map(|rel| {
1639        let method_name = format!("{}_m2m", rel.name);
1640        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1641        let through = rel.through.as_str();
1642        let src_col = rel.src.as_str();
1643        let dst_col = rel.dst.as_str();
1644        quote! {
1645            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1646                #root::sql::M2MManager {
1647                    src_pk: self.__rustango_pk_value(),
1648                    through: #through,
1649                    src_col: #src_col,
1650                    dst_col: #dst_col,
1651                }
1652            }
1653        }
1654    });
1655    quote! {
1656        impl #struct_name {
1657            #( #methods )*
1658        }
1659    }
1660}
1661
1662/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1663/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1664fn generic_m2m_accessor_tokens(
1665    struct_name: &syn::Ident,
1666    relations: &[GenericM2MAttr],
1667) -> TokenStream2 {
1668    let root = rustango_root();
1669    if relations.is_empty() {
1670        return TokenStream2::new();
1671    }
1672    let methods = relations.iter().map(|rel| {
1673        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1674        let through = rel.through.as_str();
1675        let pk_col = rel.pk_column.as_str();
1676        let ct_col = rel.ct_column.as_str();
1677        let related_col = rel.related_column.as_str();
1678        quote! {
1679            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1680                #root::sql::GenericM2MManager {
1681                    src_pk: self.__rustango_pk_value(),
1682                    src_schema: <Self as #root::core::Model>::SCHEMA,
1683                    through: #through,
1684                    pk_col: #pk_col,
1685                    ct_col: #ct_col,
1686                    dst_col: #related_col,
1687                }
1688            }
1689        }
1690    });
1691    quote! {
1692        impl #struct_name {
1693            #( #methods )*
1694        }
1695    }
1696}
1697
1698/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1699/// associated functions for each `#[rustango(reverse_has(...))]`
1700/// attribute. Issue #830.
1701///
1702/// The two emitted functions return ready-to-use `WhereExpr` nodes
1703/// that downstream callers drop into
1704/// `QuerySet::<Self>::where_raw(...)`:
1705///
1706/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1707///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1708///   `whereHas` parity (without the closure-style sub-predicate
1709///   refinement — that's a follow-up; users can layer additional
1710///   predicates by constructing the SelectQuery themselves and
1711///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1712/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1713///   `whereDoesntHave` parity.
1714///
1715/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1716/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1717/// machinery threads the outer-table reference through automatically
1718/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1719fn reverse_has_accessor_tokens(
1720    struct_name: &syn::Ident,
1721    reverse_has_relations: &[ReverseHasAttr],
1722) -> TokenStream2 {
1723    let root = rustango_root();
1724    if reverse_has_relations.is_empty() {
1725        return TokenStream2::new();
1726    }
1727    let methods = reverse_has_relations.iter().map(|rel| {
1728        let exists_name = format!("{}_exists_expr", rel.name);
1729        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1730        let count_name = format!("{}_count", rel.name);
1731        let fetch_name = format!("{}_fetch", rel.name);
1732        let first_name = format!("{}_first", rel.name);
1733        let pluck_name = format!("{}_pluck", rel.name);
1734        let accessor_name = rel.name.as_str();
1735        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1736        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1737        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1738        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1739        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1740        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1741        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1742        let child = &rel.child;
1743        let child_fk_column = rel.child_fk_column.as_str();
1744        let self_pk_column = rel.self_pk_column.as_str();
1745        let exists_doc = format!(
1746            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1747             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1748             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1749             filter to {struct_name}s with at least one matching child.",
1750        );
1751        let not_exists_doc = format!(
1752            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1753             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1754             <outer>.{self_pk_column})`. Drop into \
1755             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1756             {struct_name}s with **no** matching child.",
1757        );
1758        let count_doc = format!(
1759            "Eloquent `$model->{name}->count()` analog — returns \
1760             the number of `{child}` rows whose `{child_fk_column}` \
1761             matches this `{struct_name}` instance's primary key. \
1762             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1763             {child_fk_column} = <self.pk>`.",
1764            name = rel.name,
1765        );
1766        let accessor_doc = format!(
1767            "Eloquent `$model->{name}` accessor — returns a \
1768             `QuerySet<{child}>` filtered to rows whose \
1769             `{child_fk_column}` matches this `{struct_name}` \
1770             instance's primary key. **Chainable**: compose `.filter()` \
1771             / `.order_by()` / `.limit()` etc. on top, then call \
1772             `.fetch(&pool)` (the QuerySet trait method) when \
1773             done. For the simple \"fetch all\" hot path with no \
1774             further composition, prefer the bare-name \
1775             `{name}_fetch(&pool)` companion.",
1776            name = rel.name,
1777        );
1778        let fetch_doc = format!(
1779            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1780             over `{name}(&self).fetch(&pool)`. Use this when \
1781             you don't need further `.filter()` / `.order_by()` \
1782             composition; falls back to the chainable accessor when \
1783             you do. Avoids the `_pool` suffix on the most common \
1784             call-site shape.",
1785            name = rel.name,
1786        );
1787        quote! {
1788            #[doc = #accessor_doc]
1789            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1790                #root::query::QuerySet::<#child>::new()
1791                    .filter(#child_fk_column, self.__rustango_pk_value())
1792            }
1793
1794            #[doc = #fetch_doc]
1795            pub async fn #fetch_ident(
1796                &self,
1797                pool: &#root::sql::Pool,
1798            ) -> ::core::result::Result<
1799                ::std::vec::Vec<#child>,
1800                #root::sql::ExecError,
1801            > {
1802                use #root::sql::FetcherPool as _;
1803                self.#accessor_ident().fetch(pool).await
1804            }
1805
1806            /// Eloquent `$model->relation->first()` / `hasOne`
1807            /// semantics — bare-name shortcut over
1808            /// `self.<name>().first(&pool)`. Returns `None` when no
1809            /// child rows match. Useful when the relation is
1810            /// nominally many-to-one in shape but at most one row is
1811            /// expected (latest comment, primary tag, etc.).
1812            pub async fn #first_ident(
1813                &self,
1814                pool: &#root::sql::Pool,
1815            ) -> ::core::result::Result<
1816                ::core::option::Option<#child>,
1817                #root::sql::ExecError,
1818            > {
1819                self.#accessor_ident().first(pool).await
1820            }
1821
1822            /// Eloquent `$model->relation->pluck($col)` — project a
1823            /// single column from the child rows into a `Vec<U>`.
1824            /// Skips the typed `Child` decode, which is cheaper when
1825            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1826            pub async fn #pluck_ident<U>(
1827                &self,
1828                col: &'static str,
1829                pool: &#root::sql::Pool,
1830            ) -> ::core::result::Result<
1831                ::std::vec::Vec<U>,
1832                #root::sql::ExecError,
1833            >
1834            where
1835                U: #root::sql::FlatScalar,
1836            {
1837                self.#accessor_ident()
1838                    .values_list_flat(col)
1839                    .fetch::<U>(pool)
1840                    .await
1841            }
1842
1843            #[doc = #exists_doc]
1844            pub fn #exists_ident() -> #root::core::WhereExpr {
1845                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1846                let child_schema =
1847                    <#child as #root::core::Model>::SCHEMA;
1848                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1849                    lhs: Expr::Column(#child_fk_column),
1850                    op: Op::Eq,
1851                    rhs: Expr::OuterRef(#self_pk_column),
1852                });
1853                WhereExpr::Exists(::std::boxed::Box::new(inner))
1854            }
1855
1856            #[doc = #not_exists_doc]
1857            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1858                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1859                let child_schema =
1860                    <#child as #root::core::Model>::SCHEMA;
1861                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1862                    lhs: Expr::Column(#child_fk_column),
1863                    op: Op::Eq,
1864                    rhs: Expr::OuterRef(#self_pk_column),
1865                });
1866                WhereExpr::NotExists(::std::boxed::Box::new(inner))
1867            }
1868
1869            #[doc = #count_doc]
1870            pub async fn #count_ident(
1871                &self,
1872                pool: &#root::sql::Pool,
1873            ) -> ::core::result::Result<
1874                i64,
1875                #root::sql::ExecError,
1876            > {
1877                use #root::sql::CounterPool as _;
1878                #root::query::QuerySet::<#child>::new()
1879                    .filter(#child_fk_column, self.__rustango_pk_value())
1880                    .count(pool)
1881                    .await
1882            }
1883        }
1884    });
1885    quote! {
1886        impl #struct_name {
1887            #( #methods )*
1888        }
1889    }
1890}
1891
1892/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1893/// `#[rustango(through(...))]` attribute. Issue #817.
1894///
1895/// Each accessor builds a correlated subquery via
1896/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1897/// intermediate table, filters on the FK column pointing at this
1898/// model, and projects the intermediate PK. The outer queryset
1899/// filters the far model by its FK-to-intermediate column being in
1900/// that set.
1901///
1902/// The returned `QuerySet<Far>` is **chainable** — the subquery
1903/// lives inside a `where_raw` clause so the user's later
1904/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1905/// disturb it.
1906///
1907/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1908/// SQLite — no LATERAL or backend-specific syntax involved.
1909fn through_accessor_tokens(
1910    struct_name: &syn::Ident,
1911    through_relations: &[ThroughAttr],
1912) -> TokenStream2 {
1913    let root = rustango_root();
1914    if through_relations.is_empty() {
1915        return TokenStream2::new();
1916    }
1917    let methods = through_relations.iter().map(|rel| {
1918        let method_name = format!("{}_through", rel.name);
1919        let count_name = format!("{}_through_count", rel.name);
1920        let fetch_name = format!("{}_through_fetch", rel.name);
1921        let first_name = format!("{}_through_first", rel.name);
1922        let pluck_name = format!("{}_through_pluck", rel.name);
1923        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1924        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1925        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1926        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1927        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1928        let far = &rel.far;
1929        let intermediate = &rel.intermediate;
1930        let far_fk_column = rel.far_fk_column.as_str();
1931        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1932        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1933        let doc = format!(
1934            "Eloquent `hasManyThrough` accessor — returns a \
1935             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1936             instance through the intermediate `{intermediate}` table. \
1937             Generated SQL shape: \
1938             `… WHERE {far_fk_column} IN (SELECT \
1939             {intermediate_pk_column} FROM <{intermediate}> WHERE \
1940             {intermediate_fk_column} = self.pk)`. Chainable like any \
1941             other QuerySet — compose `.filter()` / `.order_by()` / \
1942             `.limit()` etc. on top.",
1943        );
1944        let count_doc = format!(
1945            "Eloquent `$model->{name}->count()` analog for the \
1946             through-relation — returns the number of `{far}` rows \
1947             reachable through `{intermediate}`. Equivalent to \
1948             `self.{name}_through().count(pool)` but spelled \
1949             as a bare instance method for parity with the \
1950             `reverse_has` `<name>_count` shape.",
1951            name = rel.name,
1952        );
1953        quote! {
1954            #[doc = #doc]
1955            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1956                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1957                let intermediate_schema =
1958                    <#intermediate as #root::core::Model>::SCHEMA;
1959                let sub = SelectQuery::new(intermediate_schema)
1960                    .where_clause(WhereExpr::Predicate(Filter::new(
1961                        #intermediate_fk_column,
1962                        Op::Eq,
1963                        self.__rustango_pk_value(),
1964                    )))
1965                    .projection(::std::vec![#intermediate_pk_column]);
1966                #root::query::QuerySet::<#far>::new().where_raw(
1967                    WhereExpr::InSubquery {
1968                        column: #far_fk_column,
1969                        negated: false,
1970                        subquery: ::std::boxed::Box::new(sub),
1971                    },
1972                )
1973            }
1974
1975            #[doc = #count_doc]
1976            pub async fn #count_ident(
1977                &self,
1978                pool: &#root::sql::Pool,
1979            ) -> ::core::result::Result<
1980                i64,
1981                #root::sql::ExecError,
1982            > {
1983                use #root::sql::CounterPool as _;
1984                self.#method_ident().count(pool).await
1985            }
1986
1987            /// Eloquent `$model->relation->get()` for the
1988            /// through-relation — bare-name hot-path over
1989            /// `self.<name>_through().fetch(&pool)`. Use this
1990            /// when you don't need further `.filter()` /
1991            /// `.order_by()` composition; falls back to the
1992            /// chainable accessor when you do. Avoids the `_pool`
1993            /// suffix on the most common call-site shape.
1994            pub async fn #fetch_ident(
1995                &self,
1996                pool: &#root::sql::Pool,
1997            ) -> ::core::result::Result<
1998                ::std::vec::Vec<#far>,
1999                #root::sql::ExecError,
2000            > {
2001                use #root::sql::FetcherPool as _;
2002                self.#method_ident().fetch(pool).await
2003            }
2004
2005            /// Eloquent `hasOneThrough` analog — bare-name shortcut
2006            /// over `self.<name>_through().first(&pool)`. Returns
2007            /// `None` when no far rows are reachable through the
2008            /// intermediate. Useful when at most one row is
2009            /// expected (latest comment by country, primary tag,
2010            /// etc.).
2011            pub async fn #first_ident(
2012                &self,
2013                pool: &#root::sql::Pool,
2014            ) -> ::core::result::Result<
2015                ::core::option::Option<#far>,
2016                #root::sql::ExecError,
2017            > {
2018                self.#method_ident().first(pool).await
2019            }
2020
2021            /// Pluck a single column from the far rows into `Vec<U>`
2022            /// — cheaper than the typed `<Far>` decode when only one
2023            /// scalar column is needed.
2024            pub async fn #pluck_ident<U>(
2025                &self,
2026                col: &'static str,
2027                pool: &#root::sql::Pool,
2028            ) -> ::core::result::Result<
2029                ::std::vec::Vec<U>,
2030                #root::sql::ExecError,
2031            >
2032            where
2033                U: #root::sql::FlatScalar,
2034            {
2035                self.#method_ident()
2036                    .values_list_flat(col)
2037                    .fetch::<U>(pool)
2038                    .await
2039            }
2040        }
2041    });
2042    quote! {
2043        impl #struct_name {
2044            #( #methods )*
2045        }
2046    }
2047}
2048
2049struct ColumnEntry {
2050    /// The struct field ident, used both for the inherent const name on
2051    /// the model and for the inner column type's name.
2052    ident: syn::Ident,
2053    /// The struct's field type, used as `Column::Value`.
2054    value_ty: Type,
2055    /// Rust-side field name (e.g. `"id"`).
2056    name: String,
2057    /// SQL-side column name (e.g. `"user_id"`).
2058    column: String,
2059    /// `#root::core::FieldType::I64` etc.
2060    field_type_tokens: TokenStream2,
2061    /// A bare or optional `Uuid` field; MySQL decodes it from text.
2062    uuid_column: bool,
2063}
2064
2065struct CollectedFields {
2066    field_schemas: Vec<TokenStream2>,
2067    from_row_inits: Vec<TokenStream2>,
2068    /// Aliased counterparts of `from_row_inits` — read columns via
2069    /// `format!("{prefix}__{col}")` aliases so a Model can be
2070    /// decoded from a JOINed row's projected target columns.
2071    from_aliased_row_inits: Vec<TokenStream2>,
2072    /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2073    from_row_inits_my: Vec<TokenStream2>,
2074    from_aliased_row_inits_my: Vec<TokenStream2>,
2075    /// Static column-name list — used by the simple insert path
2076    /// (no `Auto<T>` fields). Aligned with `insert_values`.
2077    insert_columns: Vec<TokenStream2>,
2078    /// Static `Into<SqlValue>` expressions, one per field. Aligned
2079    /// with `insert_columns`. Used by the simple insert path only.
2080    insert_values: Vec<TokenStream2>,
2081    /// Per-field push expressions for the dynamic (Auto-aware)
2082    /// insert path. Each statement either unconditionally pushes
2083    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2084    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2085    insert_pushes: Vec<TokenStream2>,
2086    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2087    /// when `has_auto == false`.
2088    returning_cols: Vec<TokenStream2>,
2089    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2090    /// field. Run after `insert_returning` to populate the model.
2091    auto_assigns: Vec<TokenStream2>,
2092    /// `(ident, column_literal)` pairs for every Auto field. Used by
2093    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2094    /// instead of `self`.
2095    auto_field_idents: Vec<(syn::Ident, String)>,
2096    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2097    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2098    /// back into the struct after insert (MySQL has no RETURNING → the
2099    /// field stays at its placeholder until the next read).
2100    generated_field_idents: Vec<(syn::Ident, String)>,
2101    /// First `Auto<T>` field the database fills, with its inner `T`: the
2102    /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2103    first_db_auto: Option<(syn::Ident, Type)>,
2104    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2105    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2106    /// `columns`).
2107    bulk_pushes_no_auto: Vec<TokenStream2>,
2108    /// Bulk-insert per-row pushes for **all fields including Auto**.
2109    /// Used by the all-Auto-Set bulk path (Auto col included with the
2110    /// caller-supplied value).
2111    bulk_pushes_all: Vec<TokenStream2>,
2112    /// Column-name literals for non-Auto fields only (paired with
2113    /// `bulk_pushes_no_auto`).
2114    bulk_columns_no_auto: Vec<TokenStream2>,
2115    /// Column-name literals for every field including Auto (paired
2116    /// with `bulk_pushes_all`).
2117    bulk_columns_all: Vec<TokenStream2>,
2118    /// Fills each `default_uuid_v7` field of `_row` in place, so a
2119    /// mutable bulk insert hands the PKs back (#1934).
2120    bulk_fill_in_place: Vec<TokenStream2>,
2121    /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2122    rust_filled_idents: Vec<syn::Ident>,
2123    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2124    /// + the loop that asserts every row matches. One pair per Auto
2125    /// field. Empty when `has_auto == false`.
2126    bulk_auto_uniformity: Vec<TokenStream2>,
2127    /// Identifier of the first Auto field, used as the witness for
2128    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2129    first_auto_ident: Option<syn::Ident>,
2130    /// `true` if any field on the struct is `Auto<T>`.
2131    has_auto: bool,
2132    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2133    /// Gates `save()` codegen — only Auto PKs let us infer
2134    /// insert-vs-update from the in-memory value.
2135    pk_is_auto: bool,
2136    /// `Assignment` constructors for every non-PK column. Drives the
2137    /// UPDATE branch of `save()`.
2138    update_assignments: Vec<TokenStream2>,
2139    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2140    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2141    upsert_update_columns: Vec<TokenStream2>,
2142    primary_key: Option<(syn::Ident, String)>,
2143    column_entries: Vec<ColumnEntry>,
2144    /// Rust-side field names, in declaration order. Used to validate
2145    /// container attributes like `display = "…"`.
2146    field_names: Vec<String>,
2147    /// FK fields on this child model. Drives the reverse-relation
2148    /// helper emit — for each FK, the macro adds an inherent
2149    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2150    /// method on the parent type.
2151    fk_relations: Vec<FkRelation>,
2152    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2153    /// the model has one. Drives emission of the `soft_delete_on` /
2154    /// `restore_on` inherent methods. At most one such column per
2155    /// model is allowed; collect_fields rejects duplicates.
2156    soft_delete_column: Option<String>,
2157    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2158    /// companion to `soft_delete_column` for emitting predicates
2159    /// that need to read the field off `&self` (e.g. `trashed()`).
2160    soft_delete_field_ident: Option<syn::Ident>,
2161}
2162
2163#[derive(Clone)]
2164struct FkRelation {
2165    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2166    /// helper is emitted as `impl <ParentType> { … }`.
2167    parent_type: Type,
2168    /// Rust field ident of the FK. Its name is the key for select_related,
2169    /// prefetch and filters; the SQL column may differ (#1936).
2170    field_ident: syn::Ident,
2171    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2172    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2173    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2174    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2175    pk_kind: DetectedKind,
2176    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2177    /// FK column). Drives the `Some(...)` wrapping in load_related
2178    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2179    /// the codegen matches the field's declared shape.
2180    nullable: bool,
2181    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2182    /// override. When set, the reverse helper picks this name instead
2183    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2184    /// #816 (issue's "Related" note re: per-FK override).
2185    related_name: Option<String>,
2186}
2187
2188fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2189    let root = rustango_root();
2190    let cap = named.named.len();
2191    let mut out = CollectedFields {
2192        field_schemas: Vec::with_capacity(cap),
2193        from_row_inits: Vec::with_capacity(cap),
2194        from_aliased_row_inits: Vec::with_capacity(cap),
2195        from_row_inits_my: Vec::with_capacity(cap),
2196        from_aliased_row_inits_my: Vec::with_capacity(cap),
2197        insert_columns: Vec::with_capacity(cap),
2198        insert_values: Vec::with_capacity(cap),
2199        insert_pushes: Vec::with_capacity(cap),
2200        returning_cols: Vec::new(),
2201        auto_assigns: Vec::new(),
2202        auto_field_idents: Vec::new(),
2203        generated_field_idents: Vec::new(),
2204        first_db_auto: None,
2205        bulk_pushes_no_auto: Vec::with_capacity(cap),
2206        bulk_pushes_all: Vec::with_capacity(cap),
2207        bulk_columns_no_auto: Vec::with_capacity(cap),
2208        bulk_columns_all: Vec::with_capacity(cap),
2209        bulk_fill_in_place: Vec::new(),
2210        rust_filled_idents: Vec::new(),
2211        bulk_auto_uniformity: Vec::new(),
2212        first_auto_ident: None,
2213        has_auto: false,
2214        pk_is_auto: false,
2215        update_assignments: Vec::with_capacity(cap),
2216        upsert_update_columns: Vec::with_capacity(cap),
2217        primary_key: None,
2218        column_entries: Vec::with_capacity(cap),
2219        field_names: Vec::with_capacity(cap),
2220        fk_relations: Vec::new(),
2221        soft_delete_column: None,
2222        soft_delete_field_ident: None,
2223    };
2224
2225    for field in &named.named {
2226        let info = process_field(field, table)?;
2227        out.field_names.push(ident_name(info.ident));
2228        out.field_schemas.push(info.schema);
2229        out.from_row_inits.push(info.from_row_init);
2230        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2231        out.from_row_inits_my.push(info.from_row_init_my);
2232        out.from_aliased_row_inits_my
2233            .push(info.from_aliased_row_init_my);
2234        if let Some(parent_ty) = info.fk_inner.clone() {
2235            out.fk_relations.push(FkRelation {
2236                parent_type: parent_ty,
2237                field_ident: info.ident.clone(),
2238                pk_kind: info.fk_pk_kind,
2239                nullable: info.nullable,
2240                related_name: info.related_name.clone(),
2241            });
2242        }
2243        if info.soft_delete {
2244            if out.soft_delete_column.is_some() {
2245                return Err(syn::Error::new_spanned(
2246                    field,
2247                    "only one field may be marked `#[rustango(soft_delete)]`",
2248                ));
2249            }
2250            out.soft_delete_column = Some(info.column.clone());
2251            out.soft_delete_field_ident = Some(info.ident.clone());
2252        }
2253        let column = info.column.as_str();
2254        let ident = info.ident;
2255        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2256        // skip every write path — Postgres recomputes the value
2257        // from EXPR. Push only the column-entry record (so typed
2258        // column constants still exist for filtering / projection)
2259        // and the schema literal (already pushed above) and move
2260        // on. No insert_columns/values, no insert_pushes, no
2261        // bulk_*, no update_assignments, no upsert_update_columns,
2262        // no returning_cols.
2263        if info.generated_as.is_some() {
2264            out.column_entries.push(ColumnEntry {
2265                ident: ident.clone(),
2266                value_ty: info.value_ty.clone(),
2267                name: ident_name(ident),
2268                column: info.column.clone(),
2269                field_type_tokens: info.field_type_tokens,
2270                uuid_column: info.uuid_column,
2271            });
2272            // #1028 — refresh the DB-computed value after insert on
2273            // RETURNING-capable backends. The column joins `returning_cols`
2274            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2275            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2276            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2277            // it keeps the placeholder until the next read.
2278            out.returning_cols.push(quote!(#column));
2279            out.generated_field_idents
2280                .push((ident.clone(), info.column.clone()));
2281            continue;
2282        }
2283        out.insert_columns.push(quote!(#column));
2284        out.insert_values.push(quote! {
2285            ::core::convert::Into::<#root::core::SqlValue>::into(
2286                ::core::clone::Clone::clone(&self.#ident)
2287            )
2288        });
2289        if info.auto {
2290            out.has_auto = true;
2291            if out.first_auto_ident.is_none() {
2292                out.first_auto_ident = Some(ident.clone());
2293            }
2294            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2295            // before binding, so the value is already in
2296            // `self.#ident` after the insert_push — RETURNING is
2297            // redundant. Skip adding this column to returning_cols /
2298            // auto_assigns / auto_field_idents to avoid (a) an
2299            // unnecessary RETURNING column on every dialect, and (b)
2300            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2301            // integer PK.
2302            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2303            // for the same reason: they are filled Rust-side below and
2304            // bound, so the value is already in `self` and RETURNING
2305            // would be a redundant column on every dialect.
2306            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2307                if out.first_db_auto.is_none() {
2308                    out.first_db_auto = auto_inner_type(info.value_ty)
2309                        .cloned()
2310                        .map(|ty| (ident.clone(), ty));
2311                }
2312                out.returning_cols.push(quote!(#column));
2313                out.auto_field_idents
2314                    .push((ident.clone(), info.column.clone()));
2315                out.auto_assigns.push(quote! {
2316                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2317                });
2318            }
2319            if info.default_uuid_v7 {
2320                // Rust-side UUIDv7 generation (issue #823, Eloquent
2321                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2322                // and bind; Auto::Set → bind the user's value. The
2323                // column is ALWAYS present in the INSERT statement —
2324                // no RETURNING / no DB DEFAULT needed.
2325                out.insert_pushes.push(quote! {
2326                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2327                        self.#ident = #root::sql::Auto::Set(
2328                            #root::__uuid::Uuid::now_v7(),
2329                        );
2330                    }
2331                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2332                        _columns.push(#column);
2333                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2334                            ::core::clone::Clone::clone(_v)
2335                        ));
2336                    }
2337                });
2338            } else if info.auto_now_add || info.auto_now {
2339                // Rust-side write timestamp (#1464), mirroring
2340                // `default_uuid_v7` above. `auto_now` has always bound
2341                // `Utc::now()` on UPDATE but not on INSERT, so the
2342                // first write of an `updated_at` took the DB default
2343                // while every later one took the clock — the same
2344                // column, filled two different ways.
2345                //
2346                // The default is the problem. `CREATE TABLE IF NOT
2347                // EXISTS` leaves an upgraded table's
2348                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2349                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2350                // statement that changes it — so every insert relying
2351                // on that default wrote the legacy shape forever, while
2352                // the migrate sweep converted the rows around it. The
2353                // column went permanently mixed and `ORDER BY` inverted
2354                // the admin audit log (#1616 rework review,
2355                // correctness-001 / dialects-005 / security-002).
2356                //
2357                // Binding it here makes the stale default unreachable
2358                // rather than trying to migrate it: the timestamp comes
2359                // from the application, not the database.
2360                out.insert_pushes.push(quote! {
2361                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2362                        self.#ident = #root::sql::Auto::Set(
2363                            #root::__chrono::Utc::now(),
2364                        );
2365                    }
2366                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2367                        _columns.push(#column);
2368                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2369                            ::core::clone::Clone::clone(_v)
2370                        ));
2371                    }
2372                });
2373            } else {
2374                out.insert_pushes.push(quote! {
2375                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2376                        _columns.push(#column);
2377                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2378                            ::core::clone::Clone::clone(_v)
2379                        ));
2380                    }
2381                });
2382            }
2383            // …except the timestamp columns, which must appear in BOTH
2384            // paths (#1464). The path is chosen by the *first* Auto
2385            // field — in practice the PK — so a bulk insert of rows
2386            // with an unset PK took the no-auto branch and dropped the
2387            // timestamp column, falling back to the DB default that
2388            // this change exists to stop depending on.
2389            //
2390            // `rows` is borrowed immutably here, so this fills per row
2391            // at push time rather than writing back into the struct. An
2392            // explicitly-Set value is still honoured; only `Unset`
2393            // takes the clock.
2394            // `default_uuid_v7` likewise: its column has no DB default (#1934).
2395            if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2396                out.rust_filled_idents.push(ident.clone());
2397            }
2398            let rust_fill = if info.default_uuid_v7 {
2399                Some(quote!(#root::__uuid::Uuid::now_v7()))
2400            } else if info.auto_now_add || info.auto_now {
2401                Some(quote!(#root::__chrono::Utc::now()))
2402            } else {
2403                None
2404            };
2405            // Bulk: Auto fields appear in the all-Set path. An unset filled
2406            // field takes its fill there too, not NULL (#1950).
2407            out.bulk_columns_all.push(quote!(#column));
2408            if let Some(fill) = &rust_fill {
2409                let push = quote! {
2410                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2411                        match &_row.#ident {
2412                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2413                            #root::sql::Auto::Unset => #fill,
2414                        }
2415                    ));
2416                };
2417                out.bulk_columns_no_auto.push(quote!(#column));
2418                out.bulk_pushes_no_auto.push(push.clone());
2419                out.bulk_pushes_all.push(push);
2420            } else {
2421                out.bulk_pushes_all.push(quote! {
2422                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2423                        ::core::clone::Clone::clone(&_row.#ident)
2424                    ));
2425                });
2426            }
2427            if info.default_uuid_v7 {
2428                out.bulk_fill_in_place.push(quote! {
2429                    if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2430                        _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2431                    }
2432                });
2433            }
2434            // Uniformity check: every row's Auto state must match the
2435            // first row's. Mixed Set/Unset within one bulk_insert is
2436            // rejected here so the column list stays consistent.
2437            // A filled field may be unset on any row, so it is exempt.
2438            let ident_clone = ident.clone();
2439            if rust_fill.is_none() {
2440                out.bulk_auto_uniformity.push(quote! {
2441                    for _r in rows.iter().skip(1) {
2442                        if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2443                            return ::core::result::Result::Err(
2444                                #root::sql::ExecError::Sql(
2445                                    #root::sql::SqlError::BulkAutoMixed
2446                                )
2447                            );
2448                        }
2449                    }
2450                });
2451            }
2452        } else {
2453            out.insert_pushes.push(quote! {
2454                _columns.push(#column);
2455                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2456                    ::core::clone::Clone::clone(&self.#ident)
2457                ));
2458            });
2459            // Bulk: non-Auto fields appear in BOTH paths.
2460            out.bulk_columns_no_auto.push(quote!(#column));
2461            out.bulk_columns_all.push(quote!(#column));
2462            let push_expr = quote! {
2463                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2464                    ::core::clone::Clone::clone(&_row.#ident)
2465                ));
2466            };
2467            out.bulk_pushes_no_auto.push(push_expr.clone());
2468            out.bulk_pushes_all.push(push_expr);
2469        }
2470        if info.primary_key {
2471            if out.primary_key.is_some() {
2472                return Err(syn::Error::new_spanned(
2473                    field,
2474                    "only one field may be marked `#[rustango(primary_key)]`",
2475                ));
2476            }
2477            out.primary_key = Some((ident.clone(), info.column.clone()));
2478            if info.auto {
2479                out.pk_is_auto = true;
2480            }
2481        } else if info.auto_now_add {
2482            // Immutable post-insert: skip from UPDATE entirely.
2483        } else if info.auto_now {
2484            // `auto_now` columns: bind `chrono::Utc::now()` on every
2485            // UPDATE so the column is always overridden with the
2486            // wall-clock at write time, regardless of what value the
2487            // user left in the struct field.
2488            out.update_assignments.push(quote! {
2489                #root::core::Assignment::new(
2490                    #column,
2491                    ::core::convert::Into::<#root::core::SqlValue>::into(
2492                        #root::__chrono::Utc::now()
2493                    ),
2494                )
2495            });
2496            out.upsert_update_columns.push(quote!(#column));
2497        } else {
2498            out.update_assignments.push(quote! {
2499                #root::core::Assignment::new(
2500                    #column,
2501                    ::core::convert::Into::<#root::core::SqlValue>::into(
2502                        ::core::clone::Clone::clone(&self.#ident)
2503                    ),
2504                )
2505            });
2506            out.upsert_update_columns.push(quote!(#column));
2507        }
2508        out.column_entries.push(ColumnEntry {
2509            ident: ident.clone(),
2510            value_ty: info.value_ty.clone(),
2511            name: ident_name(ident),
2512            column: info.column.clone(),
2513            field_type_tokens: info.field_type_tokens,
2514            uuid_column: info.uuid_column,
2515        });
2516    }
2517    Ok(out)
2518}
2519
2520fn model_impl_tokens(
2521    struct_name: &syn::Ident,
2522    model_name: &str,
2523    table: &str,
2524    display: Option<&str>,
2525    app_label: Option<&str>,
2526    admin: Option<&AdminAttrs>,
2527    default_order: &[(String, bool, proc_macro2::Span)],
2528    field_schemas: &[TokenStream2],
2529    soft_delete_column: Option<&str>,
2530    permissions: bool,
2531    audit_track: Option<&[String]>,
2532    m2m_relations: &[M2MAttr],
2533    indexes: &[IndexAttr],
2534    checks: &[CheckAttr],
2535    excludes: &[ExcludeAttr],
2536    composite_fks: &[CompositeFkAttr],
2537    generic_fks: &[GenericFkAttr],
2538    scope: Option<&str>,
2539    is_view: bool,
2540    verbose_name: Option<&str>,
2541    verbose_name_plural: Option<&str>,
2542    managed: bool,
2543    base_manager_name: Option<&str>,
2544    order_with_respect_to: Option<&str>,
2545    proxy: bool,
2546    required_db_features: &[String],
2547    required_db_vendor: Option<&str>,
2548    default_related_name: Option<&str>,
2549    db_table_comment: Option<&str>,
2550    get_latest_by: Option<(&str, bool)>,
2551    extra_permissions: &[(String, String)],
2552    default_permissions: &[String],
2553    global_scopes: &[GlobalScopeAttr],
2554    reverse_has_relations: &[ReverseHasAttr],
2555    generic_has_relations: &[GenericHasAttr],
2556) -> TokenStream2 {
2557    let root = rustango_root();
2558    let display_tokens = if let Some(name) = display {
2559        quote!(::core::option::Option::Some(#name))
2560    } else {
2561        quote!(::core::option::Option::None)
2562    };
2563    let app_label_tokens = if let Some(name) = app_label {
2564        quote!(::core::option::Option::Some(#name))
2565    } else {
2566        quote!(::core::option::Option::None)
2567    };
2568    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2569        quote!(::core::option::Option::Some(#col))
2570    } else {
2571        quote!(::core::option::Option::None)
2572    };
2573    let audit_track_tokens = match audit_track {
2574        None => quote!(::core::option::Option::None),
2575        Some(names) => {
2576            let lits = names.iter().map(|n| n.as_str());
2577            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2578        }
2579    };
2580    let admin_tokens = admin_config_tokens(admin);
2581    // Default `tenant` so single-tenant projects (no `scope` attr
2582    // anywhere) keep the v0.24.x behavior. Container-attr parser
2583    // already validated the value is "registry" or "tenant".
2584    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2585        Some("registry") => quote!(#root::core::ModelScope::Registry),
2586        _ => quote!(#root::core::ModelScope::Tenant),
2587    };
2588    let verbose_name_tokens = optional_str(verbose_name);
2589    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2590    let base_manager_name_tokens = optional_str(base_manager_name);
2591    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2592    let required_db_features_lits: Vec<&str> =
2593        required_db_features.iter().map(String::as_str).collect();
2594    let required_db_vendor_tokens = optional_str(required_db_vendor);
2595    let default_related_name_tokens = optional_str(default_related_name);
2596    let db_table_comment_tokens = optional_str(db_table_comment);
2597    let get_latest_by_tokens = match get_latest_by {
2598        Some((col, desc)) => {
2599            quote!(::core::option::Option::Some((#col, #desc)))
2600        }
2601        None => quote!(::core::option::Option::None),
2602    };
2603    let extra_permission_tokens: Vec<_> = extra_permissions
2604        .iter()
2605        .map(|(c, l)| quote!((#c, #l)))
2606        .collect();
2607    let default_permission_tokens: Vec<_> = default_permissions
2608        .iter()
2609        .map(|action| quote!(#action))
2610        .collect();
2611    let indexes_tokens = indexes.iter().map(|idx| {
2612        // When no explicit `name = "..."` was given, derive a stable,
2613        // collision-free name from the table + columns
2614        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2615        // would clash the moment a model declares two unnamed indexes.
2616        // Capped at Postgres's 63-char identifier limit.
2617        let derived_name = idx.name.clone().unwrap_or_else(|| {
2618            let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2619            if n.len() > 63 {
2620                n.truncate(63);
2621            }
2622            n
2623        });
2624        let name = derived_name.as_str();
2625        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2626        let unique = idx.unique;
2627        // Map the parsed method string onto the IndexMethod enum
2628        // variant — kept at the codegen layer so the IR doesn't
2629        // carry the string form.
2630        let method_variant = match idx.method.as_str() {
2631            "gin" => quote!(#root::core::IndexMethod::Gin),
2632            "gist" => quote!(#root::core::IndexMethod::Gist),
2633            "brin" => quote!(#root::core::IndexMethod::Brin),
2634            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2635            "hash" => quote!(#root::core::IndexMethod::Hash),
2636            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2637            _ => quote!(#root::core::IndexMethod::BTree),
2638        };
2639        let where_clause = match &idx.where_clause {
2640            Some(s) => quote!(::core::option::Option::Some(#s)),
2641            None => quote!(::core::option::Option::None),
2642        };
2643        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2644        quote! {{
2645            let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2646            i.unique = #unique;
2647            i.method = #method_variant;
2648            i.where_clause = #where_clause;
2649            i.include = &[ #(#include_lits),* ];
2650            i
2651        }}
2652    });
2653    let checks_tokens = checks.iter().map(|c| {
2654        let name = c.name.as_str();
2655        let expr = c.expr.as_str();
2656        quote!(#root::core::CheckConstraint::new(#name, #expr))
2657    });
2658    let excludes_tokens = excludes.iter().map(|e| {
2659        let name = e.name.as_str();
2660        let using = e.using.as_str();
2661        let element_tokens = e.elements.iter().map(|(col, op)| {
2662            let col_s = col.as_str();
2663            let op_s = op.as_str();
2664            quote!((#col_s, #op_s))
2665        });
2666        let where_tokens = match e.where_clause.as_deref() {
2667            Some(w) => quote!(::core::option::Option::Some(#w)),
2668            None => quote!(::core::option::Option::None),
2669        };
2670        quote! {{
2671            let mut e = #root::core::ExclusionConstraint::new(
2672                #name,
2673                #using,
2674                &[ #(#element_tokens),* ],
2675            );
2676            e.where_clause = #where_tokens;
2677            e
2678        }}
2679    });
2680    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2681        let name = rel.name.as_str();
2682        let to = rel.to.as_str();
2683        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2684        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2685        quote! {
2686            #root::core::CompositeFkRelation::new(
2687                #name,
2688                #to,
2689                &[ #(#from_cols),* ],
2690                &[ #(#on_cols),* ],
2691            )
2692        }
2693    });
2694    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2695        let name = rel.name.as_str();
2696        let ct_col = rel.ct_column.as_str();
2697        let pk_col = rel.pk_column.as_str();
2698        quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2699    });
2700    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2701    // no `#[rustango(default_order = "...")]` attribute was supplied.
2702    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2703        let col_lit = col.as_str();
2704        quote! { (#col_lit, #desc) }
2705    });
2706
2707    // Issue #820 — `global_scopes` slice literal. Empty when no
2708    // `#[rustango(global_scope(...))]` attribute was supplied. The
2709    // `apply` path is re-emitted verbatim so it resolves in the
2710    // consumer's scope; the name is stored as a string literal.
2711    let global_scope_tokens = global_scopes.iter().map(|s| {
2712        let name = s.name.as_str();
2713        // The slice sits in a nested `const`, where `Self` does not resolve.
2714        let mut apply = s.apply.clone();
2715        if let Some(first) = apply.segments.first_mut() {
2716            if first.ident == "Self" {
2717                first.ident = struct_name.clone();
2718            }
2719        }
2720        quote!(#root::core::GlobalScope::new(#name, #apply))
2721    });
2722
2723    let m2m_tokens = m2m_relations.iter().map(|rel| {
2724        let name = rel.name.as_str();
2725        let to = rel.to.as_str();
2726        let through = rel.through.as_str();
2727        let src = rel.src.as_str();
2728        let dst = rel.dst.as_str();
2729        let auto_create = rel.auto_create;
2730        quote! {{
2731            let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2732            m.auto_create = #auto_create;
2733            m
2734        }}
2735    });
2736    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2737    // when the model declares `#[rustango(reverse_has(...))]`. Each
2738    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2739    // a const expression. Models without reverse_has fall through to
2740    // the trait's empty default — no override emitted.
2741    let reverse_relations_override = if reverse_has_relations.is_empty() {
2742        quote!()
2743    } else {
2744        let entries = reverse_has_relations.iter().map(|rel| {
2745            let name = rel.name.as_str();
2746            let child = &rel.child;
2747            let child_fk_column = rel.child_fk_column.as_str();
2748            let self_pk_column = rel.self_pk_column.as_str();
2749            quote! {
2750                #root::core::ReverseRelation::new(
2751                    #name,
2752                    <#child as #root::core::Model>::SCHEMA,
2753                    #child_fk_column,
2754                    #self_pk_column,
2755                )
2756            }
2757        });
2758        quote! {
2759            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2760                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2761                RELS
2762            }
2763        }
2764    };
2765    // Issue #830 — `Model::generic_reverse_relations()` override for
2766    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2767    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2768        quote!()
2769    } else {
2770        let entries = generic_has_relations.iter().map(|rel| {
2771            let name = rel.name.as_str();
2772            let child = &rel.child;
2773            let ct_column = rel.ct_column.as_str();
2774            let pk_column = rel.pk_column.as_str();
2775            let self_pk_column = rel.self_pk_column.as_str();
2776            quote! {
2777                #root::core::GenericReverseRelation::new(
2778                    #name,
2779                    <#child as #root::core::Model>::SCHEMA,
2780                    #ct_column,
2781                    #pk_column,
2782                    #self_pk_column,
2783                )
2784            }
2785        });
2786        quote! {
2787            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2788                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2789                RELS
2790            }
2791        }
2792    };
2793    let fields_slice = const_slice(
2794        &quote!(#root::core::FieldSchema),
2795        field_schemas.iter().cloned(),
2796    );
2797    let m2m_slice = const_slice(&quote!(#root::core::M2MRelation), m2m_tokens);
2798    let indexes_slice = const_slice(&quote!(#root::core::IndexSchema), indexes_tokens);
2799    let checks_slice = const_slice(&quote!(#root::core::CheckConstraint), checks_tokens);
2800    let excludes_slice = const_slice(&quote!(#root::core::ExclusionConstraint), excludes_tokens);
2801    let composite_fk_slice = const_slice(
2802        &quote!(#root::core::CompositeFkRelation),
2803        composite_fk_tokens,
2804    );
2805    let generic_fk_slice = const_slice(&quote!(#root::core::GenericRelation), generic_fk_tokens);
2806    let audited_update_override = if audit_track.is_some() {
2807        quote! {
2808            fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2809                ::core::option::Option::Some(Self::__rustango_update_audited)
2810            }
2811            fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2812                ::core::option::Option::Some(Self::__rustango_delete_audited)
2813            }
2814            fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2815                ::core::option::Option::Some(Self::__rustango_create_audited)
2816            }
2817            fn __rustango_audited_update_record(
2818            ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
2819                ::core::option::Option::Some(Self::__rustango_update_record_audited)
2820            }
2821        }
2822    } else {
2823        quote!()
2824    };
2825    let global_scope_slice = const_slice(&quote!(#root::core::GlobalScope), global_scope_tokens);
2826    quote! {
2827        impl #root::core::Model for #struct_name {
2828            const SCHEMA: &'static #root::core::ModelSchema = &{
2829                #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2830                    name: #model_name,
2831                    table: #table,
2832                    fields: #fields_slice,
2833                    display: #display_tokens,
2834                    app_label: #app_label_tokens,
2835                    admin: #admin_tokens,
2836                    soft_delete_column: #soft_delete_tokens,
2837                    permissions: #permissions,
2838                    audit_track: #audit_track_tokens,
2839                    m2m: #m2m_slice,
2840                    indexes: #indexes_slice,
2841                    check_constraints: #checks_slice,
2842                    exclusion_constraints: #excludes_slice,
2843                    composite_relations: #composite_fk_slice,
2844                    generic_relations: #generic_fk_slice,
2845                    scope: #scope_tokens,
2846                    default_order: &[ #(#default_order_tokens),* ],
2847                    is_view: #is_view,
2848                    verbose_name: #verbose_name_tokens,
2849                    verbose_name_plural: #verbose_name_plural_tokens,
2850                    managed: #managed,
2851                    base_manager_name: #base_manager_name_tokens,
2852                    order_with_respect_to: #order_with_respect_to_tokens,
2853                    proxy: #proxy,
2854                    required_db_features: &[ #(#required_db_features_lits),* ],
2855                    required_db_vendor: #required_db_vendor_tokens,
2856                    default_related_name: #default_related_name_tokens,
2857                    db_table_comment: #db_table_comment_tokens,
2858                    get_latest_by: #get_latest_by_tokens,
2859                    extra_permissions: &[ #(#extra_permission_tokens),* ],
2860                    default_permissions: &[ #(#default_permission_tokens),* ],
2861                    global_scopes: #global_scope_slice,
2862                })
2863            };
2864
2865            #reverse_relations_override
2866            #generic_reverse_relations_override
2867            #audited_update_override
2868        }
2869    }
2870}
2871
2872/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2873/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2874/// otherwise a static reference to a populated `AdminConfig`.
2875fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2876    let root = rustango_root();
2877    let Some(admin) = admin else {
2878        return quote!(::core::option::Option::None);
2879    };
2880
2881    let list_display = admin
2882        .list_display
2883        .as_ref()
2884        .map(|(v, _)| v.as_slice())
2885        .unwrap_or(&[]);
2886    let list_display_lits = list_display.iter().map(|s| s.as_str());
2887
2888    let search_fields = admin
2889        .search_fields
2890        .as_ref()
2891        .map(|(v, _)| v.as_slice())
2892        .unwrap_or(&[]);
2893    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2894
2895    let readonly_fields = admin
2896        .readonly_fields
2897        .as_ref()
2898        .map(|(v, _)| v.as_slice())
2899        .unwrap_or(&[]);
2900    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2901
2902    let list_filter = admin
2903        .list_filter
2904        .as_ref()
2905        .map(|(v, _)| v.as_slice())
2906        .unwrap_or(&[]);
2907    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2908
2909    let actions = admin
2910        .actions
2911        .as_ref()
2912        .map(|(v, _)| v.as_slice())
2913        .unwrap_or(&[]);
2914    let actions_lits = actions.iter().map(|s| s.as_str());
2915
2916    let fieldsets = admin
2917        .fieldsets
2918        .as_ref()
2919        .map(|(v, _)| v.as_slice())
2920        .unwrap_or(&[]);
2921    let fieldset_tokens = const_slice(
2922        &quote!(#root::core::Fieldset),
2923        fieldsets.iter().map(|(title, fields)| {
2924            let title = title.as_str();
2925            let field_lits = fields.iter().map(String::as_str);
2926            quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
2927        }),
2928    );
2929
2930    let list_display_links = admin
2931        .list_display_links
2932        .as_ref()
2933        .map(|(v, _)| v.as_slice())
2934        .unwrap_or(&[]);
2935    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2936
2937    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2938    let actions_on_top = admin.actions_on_top.unwrap_or(true);
2939    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2940    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2941
2942    let prepopulated = admin
2943        .prepopulated_fields
2944        .as_ref()
2945        .map(|(v, _)| v.as_slice())
2946        .unwrap_or(&[]);
2947    let prepopulated_tokens = const_slice(
2948        &quote!(#root::core::PrepopulatedField),
2949        prepopulated.iter().map(|(target, sources)| {
2950            let target = target.as_str();
2951            let source_lits = sources.iter().map(String::as_str);
2952            quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
2953        }),
2954    );
2955
2956    let raw_id_fields = admin
2957        .raw_id_fields
2958        .as_ref()
2959        .map(|(v, _)| v.as_slice())
2960        .unwrap_or(&[]);
2961    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2962
2963    let autocomplete_fields = admin
2964        .autocomplete_fields
2965        .as_ref()
2966        .map(|(v, _)| v.as_slice())
2967        .unwrap_or(&[]);
2968    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2969
2970    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2971    let list_select_related_tokens = match admin.list_select_related.as_deref() {
2972        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2973        Some("none") => quote!(#root::core::ListSelectRelated::None),
2974        Some(raw) => {
2975            let names: Vec<&str> = raw
2976                .split(',')
2977                .map(str::trim)
2978                .filter(|s| !s.is_empty())
2979                .collect();
2980            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2981        }
2982    };
2983
2984    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2985    // a Vec<(String, String)>. Empty / unset → no overrides.
2986    let formfield_pairs: Vec<(&str, &str)> = admin
2987        .formfield_overrides
2988        .as_ref()
2989        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2990        .unwrap_or_default();
2991    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2992        let field = *field;
2993        let widget = *widget;
2994        quote!((#field, #widget))
2995    });
2996
2997    let list_per_page = admin.list_per_page.unwrap_or(0);
2998
2999    let ordering_pairs = admin
3000        .ordering
3001        .as_ref()
3002        .map(|(v, _)| v.as_slice())
3003        .unwrap_or(&[]);
3004    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3005        let name = name.as_str();
3006        let desc = *desc;
3007        quote!((#name, #desc))
3008    });
3009
3010    quote! {
3011        ::core::option::Option::Some({
3012            const ADMIN: &#root::core::AdminConfig = &{
3013                #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3014                    list_display: &[ #( #list_display_lits ),* ],
3015                    search_fields: &[ #( #search_fields_lits ),* ],
3016                    list_per_page: #list_per_page,
3017                    ordering: &[ #( #ordering_tokens ),* ],
3018                    readonly_fields: &[ #( #readonly_fields_lits ),* ],
3019                    list_filter: &[ #( #list_filter_lits ),* ],
3020                    actions: &[ #( #actions_lits ),* ],
3021                    fieldsets: #fieldset_tokens,
3022                    list_display_links: &[ #( #list_display_links_lits ),* ],
3023                    search_help_text: #search_help_text,
3024                    actions_on_top: #actions_on_top,
3025                    actions_on_bottom: #actions_on_bottom,
3026                    date_hierarchy: #date_hierarchy,
3027                    prepopulated_fields: #prepopulated_tokens,
3028                    raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3029                    autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3030                    list_select_related: #list_select_related_tokens,
3031                    formfield_overrides: &[ #( #formfield_tokens ),* ],
3032                })
3033            };
3034            ADMIN
3035        })
3036    }
3037}
3038
3039fn inherent_impl_tokens(
3040    struct_name: &syn::Ident,
3041    fields: &CollectedFields,
3042    primary_key: Option<&(syn::Ident, String)>,
3043    column_consts: &TokenStream2,
3044    audited_fields: Option<&[&ColumnEntry]>,
3045    upsert_unique: Option<&[String]>,
3046    manager_fns: &[syn::Ident],
3047) -> TokenStream2 {
3048    let root = rustango_root();
3049    // Audit-emit fragments threaded into write paths. Non-empty only
3050    // when the model carries `#[rustango(audit(...))]`. They reborrow
3051    // `_executor` (a `&mut PgConnection` for audited models — the
3052    // macro switches the signature below) so the data write and the
3053    // audit INSERT both run on the same caller-supplied connection.
3054    let executor_passes_to_data_write = if audited_fields.is_some() {
3055        quote!(&mut *_executor)
3056    } else {
3057        quote!(_executor)
3058    };
3059    let executor_param = if audited_fields.is_some() {
3060        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
3061    } else {
3062        quote!(_executor: _E)
3063    };
3064    let executor_generics = if audited_fields.is_some() {
3065        quote!()
3066    } else {
3067        quote!(<'_c, _E>)
3068    };
3069    let executor_where = if audited_fields.is_some() {
3070        quote!()
3071    } else {
3072        quote! {
3073            where
3074                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
3075        }
3076    };
3077    // For audited models the `_on` methods take `&mut PgConnection`, so
3078    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
3079    // must acquire a connection first. Non-audited models keep the
3080    // direct delegation since `&PgPool` IS an Executor.
3081    let pool_to_save_on = if audited_fields.is_some() {
3082        quote! {
3083            let mut _conn = pool.acquire().await?;
3084            self.save_on(&mut *_conn).await
3085        }
3086    } else {
3087        quote!(self.save_on(pool).await)
3088    };
3089    let pool_to_insert_on = if audited_fields.is_some() {
3090        quote! {
3091            let mut _conn = pool.acquire().await?;
3092            self.insert_on(&mut *_conn).await
3093        }
3094    } else {
3095        quote!(self.insert_on(pool).await)
3096    };
3097    let pool_to_delete_on = if audited_fields.is_some() {
3098        quote! {
3099            let mut _conn = pool.acquire().await?;
3100            self.delete_on(&mut *_conn).await
3101        }
3102    } else {
3103        quote!(self.delete_on(pool).await)
3104    };
3105    let pool_to_bulk_insert_on = if audited_fields.is_some() {
3106        quote! {
3107            let mut _tx = pool.begin().await?;
3108            Self::bulk_insert_on(rows, &mut *_tx).await?;
3109            _tx.commit().await?;
3110            ::core::result::Result::Ok(())
3111        }
3112    } else {
3113        quote!(Self::bulk_insert_on(rows, pool).await)
3114    };
3115    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
3116    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
3117    // directly, but `upsert_on` for audited models takes
3118    // `&mut PgConnection` (the audit emit needs a real connection).
3119    // Add the missing acquire shim to keep audited Auto-PK upsert
3120    // compiling.
3121    let pool_to_upsert_on = if audited_fields.is_some() {
3122        quote! {
3123            let mut _tx = pool.begin().await?;
3124            self.upsert_on(&mut *_tx).await?;
3125            _tx.commit().await?;
3126            ::core::result::Result::Ok(())
3127        }
3128    } else {
3129        quote!(self.upsert_on(pool).await)
3130    };
3131
3132    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3133    // (audit-on-connection over &Pool needs a bi-dialect transaction
3134    // helper, deferred). Two body shapes:
3135    // - has_auto: build InsertQuery skipping Auto::Unset columns,
3136    //   request Auto cols in `returning`, dispatch via
3137    //   `insert_returning_pool`, then on the returned `PgRow` /
3138    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
3139    //   row OR drop the single i64 into the first Auto field on MySQL
3140    //   (multi-Auto models on MySQL error at runtime since
3141    //   `LAST_INSERT_ID()` only reports one)
3142    // - non-Auto: build InsertQuery with explicit columns/values and
3143    //   call `insert_pool` (no returning needed)
3144    // pool_insert_method body for the audited Auto-PK case is moved
3145    // to after audit_pair_tokens / audit_pk_to_string (they live
3146    // ~150 lines below). This block keeps the non-audited and
3147    // non-Auto branches in place — the audited Auto-PK arm is
3148    // computed below and merged via the dispatch helper variable.
3149    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3150        // Audited models with explicit (non-Auto) PKs go through
3151        // the non-Auto insert path below — the audit emit is one
3152        // round-trip after the INSERT inside the same tx via
3153        // audit::save_one_with_audit? No, INSERT semantics
3154        // differ. For non-Auto PK + audited, route through a
3155        // dedicated insert + audit emit on the same tx, but defer
3156        // the macro emission to the audit-bundle-aware block below
3157        // — this `quote!()` placeholder gets overwritten there.
3158        quote!()
3159    } else if audited_fields.is_some() && fields.has_auto {
3160        // Audited Auto-PK insert_pool — assembled after the audit
3161        // bundles. Placeholder; real emission below.
3162        quote!()
3163    } else if fields.has_auto {
3164        let pushes = &fields.insert_pushes;
3165        let returning_cols = &fields.returning_cols;
3166        // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3167        let filled = &fields.rust_filled_idents;
3168        let was_unset: Vec<syn::Ident> = filled
3169            .iter()
3170            .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3171            .collect();
3172        let insert_or_ignore_call = quote! {
3173            #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3174            let mut _columns: ::std::vec::Vec<&'static str> =
3175                ::std::vec::Vec::new();
3176            let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3177                ::std::vec::Vec::new();
3178            #( #pushes )*
3179            let _query = #root::core::InsertQuery::new(
3180                <Self as #root::core::Model>::SCHEMA,
3181                _columns,
3182                _values,
3183            );
3184            let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3185            if !matches!(_result, ::core::result::Result::Ok(true)) {
3186                #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3187            }
3188            _result
3189        };
3190        // When every `Auto<T>` field is filled Rust-side
3191        // (`default_uuid_v7`, issue #823), there is no column to read
3192        // back from the database — `returning_cols` is empty. Route
3193        // through plain `insert_pool` instead of
3194        // `insert_returning_pool` to skip the redundant RETURNING /
3195        // LAST_INSERT_ID round-trip.
3196        if fields.returning_cols.is_empty() {
3197            quote! {
3198                /// Insert this row against either backend. Every
3199                /// `Auto<T>` PK on this model is filled Rust-side
3200                /// (e.g. `default_uuid_v7`) before binding, so no
3201                /// RETURNING round-trip is needed.
3202                ///
3203                /// # Errors
3204                /// As [`Self::insert`].
3205                pub async fn insert_pool(
3206                    &mut self,
3207                    pool: &#root::sql::Pool,
3208                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3209                    let mut _columns: ::std::vec::Vec<&'static str> =
3210                        ::std::vec::Vec::new();
3211                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3212                        ::std::vec::Vec::new();
3213                    #( #pushes )*
3214                    let _query = #root::core::InsertQuery::new(
3215                        <Self as #root::core::Model>::SCHEMA,
3216                        _columns,
3217                        _values,
3218                    );
3219                    #root::sql::insert_pool(pool, &_query).await
3220                }
3221
3222                /// Eloquent `Model::insertOrIgnore()` — same shape
3223                /// as the auto-PK branch above. Returns `Ok(true)`
3224                /// when inserted, `Ok(false)` when a conflict caused
3225                /// the INSERT to silently skip.
3226                ///
3227                /// # Errors
3228                /// As [`Self::insert`].
3229                pub async fn insert_or_ignore(
3230                    &mut self,
3231                    pool: &#root::sql::Pool,
3232                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3233                    #insert_or_ignore_call
3234                }
3235            }
3236        } else {
3237            quote! {
3238                /// Insert this row against either backend, populating any
3239                /// `Auto<T>` PK from the auto-assigned value.
3240                ///
3241                /// # Errors
3242                /// As [`Self::insert`].
3243                pub async fn insert_pool(
3244                    &mut self,
3245                    pool: &#root::sql::Pool,
3246                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3247                    let mut _columns: ::std::vec::Vec<&'static str> =
3248                        ::std::vec::Vec::new();
3249                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3250                        ::std::vec::Vec::new();
3251                    #( #pushes )*
3252                    let _query = #root::core::InsertQuery::new(
3253                        <Self as #root::core::Model>::SCHEMA,
3254                        _columns,
3255                        _values,
3256                    )
3257                    .returning(::std::vec![ #( #returning_cols ),* ]);
3258                    let _result = #root::sql::insert_returning_pool(
3259                        pool, &_query,
3260                    ).await?;
3261                    #root::sql::apply_auto_pk(_result, self)
3262                }
3263
3264                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3265                /// row or silently skip on unique-constraint
3266                /// violation. Returns `Ok(true)` when a row was
3267                /// inserted, `Ok(false)` when a conflict caused the
3268                /// INSERT to silently skip.
3269                ///
3270                /// **Caveat on auto-PK models**: when the row is
3271                /// skipped (conflict), this instance's `Auto<T>`
3272                /// fields stay `Unset` — no PK is back-populated
3273                /// because the server didn't auto-assign one. For
3274                /// "insert then read back the PK or the existing
3275                /// row's PK", use the `upsert` family or
3276                /// `get_or_create`.
3277                ///
3278                /// # Errors
3279                /// As [`Self::insert`].
3280                pub async fn insert_or_ignore(
3281                    &mut self,
3282                    pool: &#root::sql::Pool,
3283                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3284                    #insert_or_ignore_call
3285                }
3286            }
3287        }
3288    } else {
3289        let insert_columns = &fields.insert_columns;
3290        let insert_values = &fields.insert_values;
3291        quote! {
3292            /// Insert this row into its table against either backend.
3293            /// Equivalent to [`Self::insert`] but takes
3294            /// [`#root::sql::Pool`].
3295            ///
3296            /// # Errors
3297            /// As [`Self::insert`].
3298            pub async fn insert_pool(
3299                &self,
3300                pool: &#root::sql::Pool,
3301            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3302                let _query = #root::core::InsertQuery::new(
3303                    <Self as #root::core::Model>::SCHEMA,
3304                    ::std::vec![ #( #insert_columns ),* ],
3305                    ::std::vec![ #( #insert_values ),* ],
3306                );
3307                #root::sql::insert_pool(pool, &_query).await
3308            }
3309
3310            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3311            /// or silently skip on unique-constraint violation. Maps
3312            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3313            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3314            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3315            /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3316            ///
3317            /// Returns `Ok(true)` when a row was inserted,
3318            /// `Ok(false)` when a conflict caused the INSERT to
3319            /// silently skip.
3320            ///
3321            /// Use for "create-if-absent" patterns where you don't
3322            /// need the row back. For "find-or-create with the row
3323            /// returned", use the queryset-level
3324            /// `crate::sql::get_or_create` free function.
3325            ///
3326            /// # Errors
3327            /// As [`Self::insert`], plus any dialect-specific
3328            /// translation error from the ConflictClause writer.
3329            pub async fn insert_or_ignore(
3330                &self,
3331                pool: &#root::sql::Pool,
3332            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3333                let _query = #root::core::InsertQuery::new(
3334                    <Self as #root::core::Model>::SCHEMA,
3335                    ::std::vec![ #( #insert_columns ),* ],
3336                    ::std::vec![ #( #insert_values ),* ],
3337                );
3338                #root::sql::insert_or_ignore(pool, &_query).await
3339            }
3340        }
3341    };
3342
3343    // The tracked (column, JSON value) pairs, spliced only into
3344    // `__rustango_audit_pairs`. Empty when the model isn't audited.
3345    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3346        .map(|tracked| {
3347            tracked
3348                .iter()
3349                .map(|c| {
3350                    let column_lit = c.column.as_str();
3351                    let ident = &c.ident;
3352                    quote! {
3353                        (
3354                            #column_lit,
3355                            #root::__serde_json::to_value(&self.#ident)
3356                                .unwrap_or(#root::__serde_json::Value::Null),
3357                        )
3358                    }
3359                })
3360                .collect()
3361        })
3362        .unwrap_or_default();
3363    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3364        if fields.pk_is_auto {
3365            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3366        } else {
3367            quote!(::std::format!("{}", &self.#pk_ident))
3368        }
3369    } else {
3370        quote!(::std::string::String::new())
3371    };
3372    // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3373    // is built, so every audited write path records the same shape.
3374    let delete_audited_body = if primary_key.is_some() {
3375        quote! {
3376            ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3377                pool,
3378                query,
3379                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3380            ))
3381        }
3382    } else {
3383        quote! {
3384            let _ = (pool, query);
3385            ::std::boxed::Box::pin(async {
3386                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3387                    table: <Self as #root::core::Model>::SCHEMA.table,
3388                })
3389            })
3390        }
3391    };
3392    let update_audited_body = if primary_key.is_some() {
3393        quote! {
3394            ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3395                pool,
3396                query,
3397                move |_r: &Self| _r.__rustango_audit_entry(op),
3398            ))
3399        }
3400    } else {
3401        quote! {
3402            let _ = (pool, query, op);
3403            ::std::boxed::Box::pin(async {
3404                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3405                    table: <Self as #root::core::Model>::SCHEMA.table,
3406                })
3407            })
3408        }
3409    };
3410    let audit_entry_method = if audited_fields.is_some() {
3411        let pairs = audit_pair_tokens.iter();
3412        let pk_str = audit_pk_to_string.clone();
3413        quote! {
3414            /// This row's tracked `(column, value)` pairs.
3415            #[doc(hidden)]
3416            pub fn __rustango_audit_pairs(
3417                &self,
3418            ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3419                ::std::vec![ #( #pairs ),* ]
3420            }
3421
3422            /// This row's audit PK.
3423            #[doc(hidden)]
3424            pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3425                #pk_str
3426            }
3427
3428            /// Snapshot audit entry for `operation` on this row.
3429            #[doc(hidden)]
3430            pub fn __rustango_audit_entry(
3431                &self,
3432                operation: #root::audit::AuditOp,
3433            ) -> #root::audit::PendingEntry {
3434                #root::audit::PendingEntry {
3435                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3436                    entity_pk: self.__rustango_audit_pk(),
3437                    operation,
3438                    source: #root::audit::current_source(),
3439                    changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3440                }
3441            }
3442
3443            /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3444            #[doc(hidden)]
3445            pub fn __rustango_update_audited<'a>(
3446                pool: &'a #root::sql::Pool,
3447                query: &'a #root::core::UpdateQuery,
3448                op: #root::audit::AuditOp,
3449            ) -> ::std::pin::Pin<::std::boxed::Box<
3450                dyn ::core::future::Future<
3451                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3452                > + ::core::marker::Send + 'a,
3453            >> {
3454                #update_audited_body
3455            }
3456
3457            /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3458            #[doc(hidden)]
3459            pub fn __rustango_create_audited<'a, 't>(
3460                tx: &'a mut #root::sql::PoolTx<'t>,
3461                pk: #root::core::SqlValue,
3462            ) -> ::std::pin::Pin<::std::boxed::Box<
3463                dyn ::core::future::Future<
3464                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3465                > + ::core::marker::Send + 'a,
3466            >> {
3467                ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3468                    tx,
3469                    <Self as #root::core::Model>::SCHEMA,
3470                    pk,
3471                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3472                ))
3473            }
3474
3475            /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3476            #[doc(hidden)]
3477            pub fn __rustango_update_record_audited<'a, 't>(
3478                tx: &'a mut #root::sql::PoolTx<'t>,
3479                pk: #root::core::SqlValue,
3480            ) -> ::std::pin::Pin<::std::boxed::Box<
3481                dyn ::core::future::Future<
3482                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3483                > + ::core::marker::Send + 'a,
3484            >> {
3485                ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3486                    tx,
3487                    <Self as #root::core::Model>::SCHEMA,
3488                    pk,
3489                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3490                ))
3491            }
3492
3493            /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3494            #[doc(hidden)]
3495            pub fn __rustango_delete_audited<'a>(
3496                pool: &'a #root::sql::Pool,
3497                query: &'a #root::core::DeleteQuery,
3498            ) -> ::std::pin::Pin<::std::boxed::Box<
3499                dyn ::core::future::Future<
3500                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3501                > + ::core::marker::Send + 'a,
3502            >> {
3503                #delete_audited_body
3504            }
3505        }
3506    } else {
3507        quote!()
3508    };
3509    // Builds `_audit_entry`; `written` is the soft-delete column's new
3510    // value, recorded in place of the stale `self` field.
3511    let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3512        if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3513            quote! {
3514                let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3515                _audit_entry.set_tracked(#col, #v);
3516            }
3517        } else {
3518            quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3519        }
3520    };
3521    let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3522        .unwrap_or(#root::__serde_json::Value::Null));
3523    let restored_json = quote!(#root::__serde_json::Value::Null);
3524    // `_on` emit; `affected` paths skip the row when nothing changed.
3525    let make_op_emit =
3526        |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3527            if audited_fields.is_none() {
3528                return quote!();
3529            }
3530            let entry = make_entry(op_path, written);
3531            let emit = quote! {
3532                #entry
3533                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3534            };
3535            if affected {
3536                quote!(if _affected > 0 { #emit })
3537            } else {
3538                emit
3539            }
3540        };
3541    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3542    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3543    let audit_softdelete_emit = make_op_emit(
3544        quote!(#root::audit::AuditOp::SoftDelete),
3545        Some(soft_deleted_json.clone()),
3546        true,
3547    );
3548    let audit_restore_emit = make_op_emit(
3549        quote!(#root::audit::AuditOp::Restore),
3550        Some(restored_json.clone()),
3551        true,
3552    );
3553    // `&Pool` update runner for soft_delete / restore: one tx with the
3554    // audit row when audited, a plain UPDATE otherwise.
3555    let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3556        if audited_fields.is_some() {
3557            let entry = make_entry(op_path, Some(written));
3558            quote! {
3559                #entry
3560                #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3561            }
3562        } else {
3563            quote!(#root::sql::update_pool(pool, &_query).await)
3564        }
3565    };
3566    let pool_softdelete_run =
3567        make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3568    let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3569
3570    // `save_pool(&Pool)` — emitted for every model with a PK. The
3571    // audited `save_pool` is built below on the diff path; both share
3572    // `save_partial_methods`, which differ only in how they run `_query`.
3573    let save_partial_methods = |pk_ident: &syn::Ident,
3574                                pk_column_lit: &str,
3575                                run: TokenStream2|
3576     -> TokenStream2 {
3577        let assignments = &fields.update_assignments;
3578        quote! {
3579            /// Save (UPDATE) only the listed Rust-side fields,
3580            /// leaving every other column untouched. Issue #66.
3581            ///
3582            /// `fields` are Rust-side struct field names; the macro
3583            /// resolves each to its SQL column. Unknown field
3584            /// names return [`#root::core::QueryError::UnknownField`]
3585            /// wrapped in `ExecError::Query`. An empty list is a
3586            /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3587            /// On an audited model the audit diff covers only these fields.
3588            ///
3589            /// Use this when:
3590            /// * you only mutated a couple of fields on a wide row
3591            ///   (avoid re-writing every column on every save), or
3592            /// * two writers diverged after their initial read and
3593            ///   you want to preserve the other writer's changes to
3594            ///   columns you didn't touch.
3595            ///
3596            /// Auto-PK models with an unset PK return
3597            /// [`#root::core::QueryError::UnknownField`] with
3598            /// field name `<pk>` — `save_partial` is an
3599            /// UPDATE-only path. Call [`Self::insert_pool`]
3600            /// (or [`Self::save_pool`] which dispatches based on
3601            /// PK state) for the INSERT case.
3602            ///
3603            /// # Errors
3604            /// As [`Self::save_pool`], plus `UnknownField` for
3605            /// unknown / empty / Auto-Unset cases.
3606            pub async fn save_partial(
3607                &mut self,
3608                fields: &[&str],
3609                pool: &#root::sql::Pool,
3610            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3611                if fields.is_empty() {
3612                    #root::__tracing::warn!(
3613                        target: "rustango::save_partial",
3614                        model = <Self as #root::core::Model>::SCHEMA.name,
3615                        "save_partial called with empty field list — no-op"
3616                    );
3617                    return ::core::result::Result::Ok(0);
3618                }
3619                let _schema = <Self as #root::core::Model>::SCHEMA;
3620                // Validate field names against the schema.
3621                let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3622                    ::std::collections::HashSet::with_capacity(fields.len());
3623                for f in fields {
3624                    match _schema.field(f) {
3625                        ::core::option::Option::Some(fs) => {
3626                            _wanted_cols.insert(fs.column);
3627                        }
3628                        ::core::option::Option::None => {
3629                            return ::core::result::Result::Err(
3630                                #root::sql::ExecError::Query(
3631                                    #root::core::QueryError::UnknownField {
3632                                        model: _schema.name,
3633                                        field: (*f).to_owned(),
3634                                    }
3635                                )
3636                            );
3637                        }
3638                    }
3639                }
3640                // Build the full assignment vec, then keep only the
3641                // assignments whose column is in `_wanted_cols`.
3642                let _full: ::std::vec::Vec<#root::core::Assignment> =
3643                    ::std::vec![ #( #assignments ),* ];
3644                let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3645                    .into_iter()
3646                    .filter(|a| _wanted_cols.contains(a.column))
3647                    .collect();
3648                if _filtered.is_empty() {
3649                    // All field names valid, but they all map to
3650                    // non-assignable slots (PK column, computed/
3651                    // virtual fields, relations without an
3652                    // assignment). Same no-op as an empty list.
3653                    #root::__tracing::warn!(
3654                        target: "rustango::save_partial",
3655                        model = _schema.name,
3656                        "save_partial: every named field maps to a non-assignable column — no-op"
3657                    );
3658                    return ::core::result::Result::Ok(0);
3659                }
3660                let _query = #root::core::UpdateQuery::new(
3661                    _schema,
3662                    _filtered,
3663                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3664                        #pk_column_lit,
3665                        #root::core::Op::Eq,
3666                        ::core::convert::Into::<#root::core::SqlValue>::into(
3667                            ::core::clone::Clone::clone(&self.#pk_ident)
3668                        ),
3669                    )),
3670                );
3671                #run
3672            }
3673
3674            /// Typed-column counterpart of [`Self::save_partial`] —
3675            /// issue #67. `fields` is a tuple of [`Column`]
3676            /// constants whose `Model` matches `Self`; typos and
3677            /// model mismatches surface at *compile time*
3678            /// (`Author::name` inside a `Post::save_partial_typed`
3679            /// call is a type error, no runtime check).
3680            ///
3681            /// ```ignore
3682            /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3683            /// ```
3684            ///
3685            /// Lowers to [`Self::save_partial`] under the hood — the
3686            /// tuple is reduced to a `&[&str]` slice of Rust-side
3687            /// field names and forwarded.
3688            ///
3689            /// [`Column`]: #root::core::Column
3690            ///
3691            /// # Errors
3692            /// As [`Self::save_partial`].
3693            pub async fn save_partial_typed<
3694                L: #root::core::TypedFieldList<Self>,
3695            >(
3696                &mut self,
3697                fields: L,
3698                pool: &#root::sql::Pool,
3699            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3700                let _names = fields.rust_field_names();
3701                let _refs: ::std::vec::Vec<&str> =
3702                    _names.iter().copied().collect();
3703                self.save_partial(&_refs, pool).await
3704            }
3705        }
3706    };
3707    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3708        let pk_column_lit = pk_col.as_str();
3709        let assignments = &fields.update_assignments;
3710        if audited_fields.is_some() {
3711            quote!()
3712        } else {
3713            let partial = save_partial_methods(
3714                pk_ident,
3715                pk_column_lit,
3716                quote!(#root::sql::update_pool(pool, &_query).await),
3717            );
3718            let dispatch_unset = if fields.pk_is_auto {
3719                quote! {
3720                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3721                        return self.insert_pool(pool).await.map(|()| 1u64);
3722                    }
3723                }
3724            } else {
3725                quote!()
3726            };
3727            quote! {
3728                /// Save this row to its table against either backend.
3729                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3730                /// `UPDATE` keyed on the PK; global scopes do not apply.
3731                ///
3732                /// # Errors
3733                /// As [`Self::save`].
3734                pub async fn save_pool(
3735                    &mut self,
3736                    pool: &#root::sql::Pool,
3737                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3738                    #dispatch_unset
3739                    let _query = #root::core::UpdateQuery::new(
3740                        <Self as #root::core::Model>::SCHEMA,
3741                        ::std::vec![ #( #assignments ),* ],
3742                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3743                            #pk_column_lit,
3744                            #root::core::Op::Eq,
3745                            ::core::convert::Into::<#root::core::SqlValue>::into(
3746                                ::core::clone::Clone::clone(&self.#pk_ident)
3747                            ),
3748                        )),
3749                    );
3750                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3751                    ::core::result::Result::Ok(_affected)
3752                }
3753
3754                #partial
3755            }
3756        }
3757    } else {
3758        quote!()
3759    };
3760
3761    // Audited `insert_pool` (overrides the placeholder set higher up
3762    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3763    // audited models get insert_pool routing through
3764    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3765    // + auto-PK readback + audit emit). Snapshot-style audit (the
3766    // PendingEntry's `changes` carries post-write field values).
3767    let pool_insert_method = if audited_fields.is_some() {
3768        if let Some(_) = primary_key {
3769            let pushes = if fields.has_auto {
3770                fields.insert_pushes.clone()
3771            } else {
3772                // For non-Auto-PK models, the macro normally builds
3773                // {columns, values} from fields.insert_columns +
3774                // fields.insert_values rather than insert_pushes.
3775                // Map those into the pushes shape.
3776                fields
3777                    .insert_columns
3778                    .iter()
3779                    .zip(&fields.insert_values)
3780                    .map(|(col, val)| {
3781                        quote! {
3782                            _columns.push(#col);
3783                            _values.push(#val);
3784                        }
3785                    })
3786                    .collect()
3787            };
3788            // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
3789            let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
3790            {
3791                fields.returning_cols.clone()
3792            } else {
3793                // Non-Auto-PK: still need RETURNING something for the
3794                // audit helper's contract (it errors on empty
3795                // returning). Return the PK column so the audit row
3796                // can carry the assigned PK back. Some non-Auto PKs
3797                // are server-side-default (e.g. UUIDv4 default), so
3798                // RETURNING is genuinely useful.
3799                primary_key
3800                    .map(|(_, col)| {
3801                        let lit = col.as_str();
3802                        vec![quote!(#lit)]
3803                    })
3804                    .unwrap_or_default()
3805            };
3806            quote! {
3807                /// Insert this row against either backend with audit
3808                /// emission inside the same transaction. Bi-dialect
3809                /// counterpart of [`Self::insert`] for audited models.
3810                ///
3811                /// Snapshot-style audit (post-write field values).
3812                ///
3813                /// # Errors
3814                /// As [`Self::insert`].
3815                pub async fn insert_pool(
3816                    &mut self,
3817                    pool: &#root::sql::Pool,
3818                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3819                    let mut _columns: ::std::vec::Vec<&'static str> =
3820                        ::std::vec::Vec::new();
3821                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3822                        ::std::vec::Vec::new();
3823                    #( #pushes )*
3824                    let _query = #root::core::InsertQuery::new(
3825                        <Self as #root::core::Model>::SCHEMA,
3826                        _columns,
3827                        _values,
3828                    )
3829                    .returning(::std::vec![ #( #returning_cols ),* ]);
3830                    #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
3831                        _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
3832                    })
3833                    .await
3834                }
3835            }
3836        } else {
3837            quote!()
3838        }
3839    } else {
3840        // Keep the non-audited pool_insert_method we built earlier.
3841        pool_insert_method
3842    };
3843
3844    // Update audited save_pool: now that insert_pool is wired for
3845    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3846    // insert_pool. Non-audited save_pool already does this.
3847    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3848    // Replaces the snapshot-only emission with a per-backend transaction
3849    // body that:
3850    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3851    //     column), capturing BEFORE values
3852    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3853    //  3. builds AFTER pairs from &self
3854    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3855    //     AuditOp::Update + diff_changes(...) on the same tx connection
3856    //  5. commits
3857    //
3858    // Per-backend arms inline the SQL string + placeholder shape, then
3859    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3860    // is polymorphic over Row type — the same tokens work against
3861    // PgRow and MySqlRow as long as the field's Rust type implements
3862    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3863    // primitives + chrono/uuid/serde_json::Value all do).
3864    let pool_save_method = if let Some(tracked) = audited_fields {
3865        if let Some((pk_ident, pk_col)) = primary_key {
3866            let pk_column_lit = pk_col.as_str();
3867            let partial = save_partial_methods(
3868                pk_ident,
3869                pk_column_lit,
3870                quote! {
3871                    self.__rustango_save_with_diff(
3872                        pool,
3873                        &_query,
3874                        ::core::option::Option::Some(&_wanted_cols),
3875                    )
3876                    .await
3877                },
3878            );
3879            // Per-tracked-column BEFORE-pair token list. Each entry
3880            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3881            // The Row alias resolves to PgRow / MySqlRow per call site,
3882            // so the same template generates both the PG and MySQL bodies.
3883            let mk_before_pairs =
3884                |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
3885                    tracked
3886                        .iter()
3887                        .map(|c| {
3888                            let getter = getter(c);
3889                            let column_lit = c.column.as_str();
3890                            let value_ty = &c.value_ty;
3891                            quote! {
3892                                (
3893                                    #column_lit,
3894                                    match #getter::<#value_ty>(
3895                                        _audit_before_row, #column_lit,
3896                                    ) {
3897                                        ::core::result::Result::Ok(v) => {
3898                                            #root::__serde_json::to_value(&v)
3899                                                .unwrap_or(#root::__serde_json::Value::Null)
3900                                        }
3901                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3902                                    },
3903                                )
3904                            }
3905                        })
3906                        .collect()
3907                };
3908            let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
3909            let before_pairs_my = mk_before_pairs(&|c| {
3910                if c.uuid_column {
3911                    quote!(#root::sql::try_get_flat_my)
3912                } else {
3913                    quote!(#root::sql::try_get_returning_my)
3914                }
3915            });
3916            let before_pairs_sqlite =
3917                mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
3918            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
3919            let pk_value_for_bind = if fields.pk_is_auto {
3920                quote!(self.#pk_ident.get().copied().unwrap_or_default())
3921            } else {
3922                quote!(::core::clone::Clone::clone(&self.#pk_ident))
3923            };
3924            let assignments = &fields.update_assignments;
3925            let unset_dispatch = if fields.has_auto {
3926                quote! {
3927                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3928                        return self.insert_pool(pool).await.map(|()| 1u64);
3929                    }
3930                }
3931            } else {
3932                quote!()
3933            };
3934            quote! {
3935                /// Save this row against either backend with audit
3936                /// emission (diff-style: BEFORE+AFTER) inside the
3937                /// same transaction. Auto::Unset PK routes to
3938                /// insert_pool. Bi-dialect counterpart of
3939                /// [`Self::save`] for audited models.
3940                ///
3941                /// The audit row's `changes` JSON contains one
3942                /// `{ "field": { "before": …, "after": … } }` entry
3943                /// per tracked column whose value actually changed
3944                /// — same shape as the existing &PgPool save() emits.
3945                /// Keyed on the PK; global scopes do not apply.
3946                ///
3947                /// # Errors
3948                /// As [`Self::save`].
3949                pub async fn save_pool(
3950                    &mut self,
3951                    pool: &#root::sql::Pool,
3952                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3953                    #unset_dispatch
3954                    let _query = #root::core::UpdateQuery::new(
3955                        <Self as #root::core::Model>::SCHEMA,
3956                        ::std::vec![ #( #assignments ),* ],
3957                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3958                            #pk_column_lit,
3959                            #root::core::Op::Eq,
3960                            ::core::convert::Into::<#root::core::SqlValue>::into(
3961                                ::core::clone::Clone::clone(&self.#pk_ident)
3962                            ),
3963                        )),
3964                    );
3965                    self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
3966                        .await
3967                }
3968
3969                /// Run `query` and write its diff entry, limited to `only` columns.
3970                #[doc(hidden)]
3971                async fn __rustango_save_with_diff(
3972                    &self,
3973                    pool: &#root::sql::Pool,
3974                    query: &#root::core::UpdateQuery,
3975                    only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
3976                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3977                    let mut _after_pairs = self.__rustango_audit_pairs();
3978                    if let ::core::option::Option::Some(only) = only {
3979                        _after_pairs.retain(|(col, _)| only.contains(col));
3980                    }
3981                    let _before_query = #root::audit::before_image_query(
3982                        <Self as #root::core::Model>::SCHEMA,
3983                        #pk_column_lit,
3984                        ::core::convert::Into::<#root::core::SqlValue>::into(
3985                            #pk_value_for_bind,
3986                        ),
3987                        &[ #( #tracked_cols ),* ],
3988                    );
3989                    #root::audit::save_one_with_diff(
3990                        pool,
3991                        query,
3992                        &_before_query,
3993                        <Self as #root::core::Model>::SCHEMA.table,
3994                        self.__rustango_audit_pk(),
3995                        _after_pairs,
3996                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3997                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3998                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3999                    ).await
4000                }
4001
4002                #partial
4003            }
4004        } else {
4005            quote!()
4006        }
4007    } else {
4008        pool_save_method
4009    };
4010
4011    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4012    // body shapes:
4013    // - non-audited: simple dispatch through `sql::delete_pool`
4014    // - audited: routes through `audit::delete_one_with_audit`,
4015    //   which opens a per-backend transaction wrapping DELETE +
4016    //   audit emit so the data write and audit row commit atomically.
4017    let pool_delete_method = {
4018        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4019        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4020        if let Some(pk_ident) = pk_ident_for_pool {
4021            if audited_fields.is_some() {
4022                quote! {
4023                    /// Delete this row against either backend with audit
4024                    /// emission inside the same transaction. Bi-dialect
4025                    /// counterpart of [`Self::delete`] for audited models.
4026                    /// Keyed on the PK; global scopes do not apply.
4027                    ///
4028                    /// # Errors
4029                    /// As [`Self::delete`].
4030                    pub async fn delete_pool(
4031                        &self,
4032                        pool: &#root::sql::Pool,
4033                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4034                        let _query = #root::core::DeleteQuery::by_pk(
4035                            <Self as #root::core::Model>::SCHEMA,
4036                            #pk_column_lit,
4037                            ::core::convert::Into::<#root::core::SqlValue>::into(
4038                                ::core::clone::Clone::clone(&self.#pk_ident)
4039                            ),
4040                        );
4041                        let _audit_entry =
4042                            self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4043                        #root::audit::delete_one_with_audit(
4044                            pool, &_query, &_audit_entry,
4045                        ).await
4046                    }
4047                }
4048            } else {
4049                quote! {
4050                    /// Delete the row identified by this instance's primary key
4051                    /// against either backend. Equivalent to [`Self::delete`] but
4052                    /// takes [`#root::sql::Pool`] and dispatches per backend.
4053                    /// Global scopes do not apply.
4054                    ///
4055                    /// # Errors
4056                    /// As [`Self::delete`].
4057                    pub async fn delete_pool(
4058                        &self,
4059                        pool: &#root::sql::Pool,
4060                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4061                        let _query = #root::core::DeleteQuery::by_pk(
4062                            <Self as #root::core::Model>::SCHEMA,
4063                            #pk_column_lit,
4064                            ::core::convert::Into::<#root::core::SqlValue>::into(
4065                                ::core::clone::Clone::clone(&self.#pk_ident)
4066                            ),
4067                        );
4068                        #root::sql::delete_pool(pool, &_query).await
4069                    }
4070                }
4071            }
4072        } else {
4073            quote!()
4074        }
4075    };
4076
4077    // Bulk writers on audited models write one audit row per affected
4078    // row, in the write's transaction (#1747).
4079    let (bulk_delete_run, bulk_update_run, truncate_run) = if audited_fields.is_some() {
4080        (
4081            quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4082                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4083            })
4084            .await),
4085            quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4086                _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4087            })
4088            .await),
4089            quote!(#root::audit::truncate_with_audit(pool, _table, &_sql).await),
4090        )
4091    } else {
4092        (
4093            quote!(#root::sql::delete_pool(pool, &_query).await),
4094            quote!(#root::sql::update_pool(pool, &_query).await),
4095            quote!(#root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await),
4096        )
4097    };
4098
4099    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4100    // matching this instance's PK and overwrite the in-memory state
4101    // with the freshly-fetched columns.
4102    // Issue #825. Only emitted when the model declares a PK; non-PK
4103    // models can't address a specific row.
4104    //
4105    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4106    // every field from `self`; resets the PK to `Auto::Unset` when
4107    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4108    // a fresh autoincrement value. Non-Auto PKs preserve the source
4109    // PK — the caller must overwrite before insert. Pure-Rust, no
4110    // I/O, no dialect surface.
4111    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4112        let other_field_clones: Vec<TokenStream2> = fields
4113            .column_entries
4114            .iter()
4115            .filter(|c| &c.ident != pk_ident)
4116            .map(|c| {
4117                let ident = &c.ident;
4118                quote! {
4119                    #ident: ::core::clone::Clone::clone(&self.#ident)
4120                }
4121            })
4122            .collect();
4123        let pk_clone_token = if fields.pk_is_auto {
4124            quote! { #pk_ident: #root::sql::Auto::Unset }
4125        } else {
4126            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4127        };
4128        let replicate_doc = if fields.pk_is_auto {
4129            quote! {
4130                /// Eloquent-style `replicate()` — return a clone of this
4131                /// row with the primary key reset to [`Auto::Unset`] so
4132                /// the copy is ready for `insert_pool` / `save_pool` to
4133                /// allocate a fresh autoincrement value. Every other
4134                /// field is `Clone`d verbatim. Issue #825.
4135                ///
4136                /// `auto_now_add` / `auto_now` timestamp fields are
4137                /// **not** reset (Eloquent's `replicate` doesn't reset
4138                /// them either) — pass them through the normal insert
4139                /// path if you want fresh values, or assign them
4140                /// explicitly after the call.
4141            }
4142        } else {
4143            quote! {
4144                /// Eloquent-style `replicate()` — clone this row
4145                /// verbatim. Because the primary key is **not** an
4146                /// `Auto<T>`, the clone keeps the source PK; the
4147                /// caller must overwrite `copy.<pk>` before inserting
4148                /// to avoid a unique-key violation. Issue #825.
4149            }
4150        };
4151        // 2026-06-07 — field-name / shortcut collision guard.
4152        //
4153        // The macro emits both `pub const <field>: <field>_col = ...`
4154        // (per-field typed-column const, used by the typed-builder
4155        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4156        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4157        // model has a field named e.g. `count`, both items would land
4158        // in the same inherent impl with the same name, and the
4159        // compiler rejects the derive with "duplicate definitions".
4160        //
4161        // Drop the conflicting shortcut for that model. Callers can
4162        // still reach the same behavior via
4163        // `QuerySet::<Model>::default().count(&pool)`.
4164        let column_names: ::std::collections::HashSet<String> = fields
4165            .column_entries
4166            .iter()
4167            .map(|c| ident_name(&c.ident))
4168            .collect();
4169        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4170            if column_names.contains(name) {
4171                quote! {}
4172            } else {
4173                tokens
4174            }
4175        };
4176        let count_method = emit_if_no_field_collision(
4177            "count",
4178            quote! {
4179                /// Count rows of this model — `SELECT COUNT(*) FROM
4180                /// <table>`. Eloquent `Model::count()` parity.
4181                ///
4182                /// Skipped on models that already declare a field named
4183                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4184                /// in that case.
4185                ///
4186                /// # Errors
4187                /// As [`CounterPool::count`].
4188                ///
4189                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4190                pub async fn count(
4191                    pool: &#root::sql::Pool,
4192                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4193                    use #root::sql::CounterPool as _;
4194                    #root::query::QuerySet::<Self>::default()
4195                        .count(pool)
4196                        .await
4197                }
4198            },
4199        );
4200        let value_method = emit_if_no_field_collision(
4201            "value",
4202            quote! {
4203                /// Pluck a single scalar from the first row.
4204                /// Eloquent `Model::query()->value($col)` parity.
4205                ///
4206                /// Skipped on models that already declare a field named
4207                /// `value`. Drop into
4208                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4209                ///
4210                /// # Errors
4211                /// As `ValuesFlatQuerySet::first`.
4212                pub async fn value<U>(
4213                    col: &str,
4214                    pool: &#root::sql::Pool,
4215                ) -> ::core::result::Result<
4216                    ::core::option::Option<U>,
4217                    #root::sql::ExecError,
4218                >
4219                where
4220                    U: #root::sql::FlatScalar,
4221                {
4222                    let _col_static: &'static str = Self::__resolve_col(col)?;
4223                    #root::query::QuerySet::<Self>::default()
4224                        .values_list_flat(_col_static)
4225                        .first::<U>(pool)
4226                        .await
4227                }
4228            },
4229        );
4230        let sum_method = emit_if_no_field_collision(
4231            "sum",
4232            quote! {
4233                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4234                /// Skipped on models that already declare a field named
4235                /// `sum`.
4236                ///
4237                /// # Errors
4238                /// As [`#root::sql::fetch_aggregate_pool`].
4239                pub async fn sum<U>(
4240                    col: &str,
4241                    pool: &#root::sql::Pool,
4242                ) -> ::core::result::Result<
4243                    ::core::option::Option<U>,
4244                    #root::sql::ExecError,
4245                >
4246                where
4247                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4248                        + #root::sql::MaybeMyFromRow
4249                        + #root::sql::MaybeSqliteFromRow
4250                        + ::core::marker::Send
4251                        + ::core::marker::Unpin,
4252                {
4253                    #root::query::QuerySet::<Self>::default()
4254                        .sum::<U>(col, pool)
4255                        .await
4256                }
4257            },
4258        );
4259        let avg_method = emit_if_no_field_collision(
4260            "avg",
4261            quote! {
4262                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4263                /// models that already declare a field named `avg`.
4264                ///
4265                /// # Errors
4266                /// As [`#root::sql::fetch_aggregate_pool`].
4267                pub async fn avg<U>(
4268                    col: &str,
4269                    pool: &#root::sql::Pool,
4270                ) -> ::core::result::Result<
4271                    ::core::option::Option<U>,
4272                    #root::sql::ExecError,
4273                >
4274                where
4275                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4276                        + #root::sql::MaybeMyFromRow
4277                        + #root::sql::MaybeSqliteFromRow
4278                        + ::core::marker::Send
4279                        + ::core::marker::Unpin,
4280                {
4281                    #root::query::QuerySet::<Self>::default()
4282                        .avg::<U>(col, pool)
4283                        .await
4284                }
4285            },
4286        );
4287        let min_method = emit_if_no_field_collision(
4288            "min",
4289            quote! {
4290                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4291                /// models that already declare a field named `min`.
4292                ///
4293                /// # Errors
4294                /// As [`#root::sql::fetch_aggregate_pool`].
4295                pub async fn min<U>(
4296                    col: &str,
4297                    pool: &#root::sql::Pool,
4298                ) -> ::core::result::Result<
4299                    ::core::option::Option<U>,
4300                    #root::sql::ExecError,
4301                >
4302                where
4303                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4304                        + #root::sql::MaybeMyFromRow
4305                        + #root::sql::MaybeSqliteFromRow
4306                        + ::core::marker::Send
4307                        + ::core::marker::Unpin,
4308                {
4309                    #root::query::QuerySet::<Self>::default()
4310                        .min::<U>(col, pool)
4311                        .await
4312                }
4313            },
4314        );
4315        let max_method = emit_if_no_field_collision(
4316            "max",
4317            quote! {
4318                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4319                /// models that already declare a field named `max`.
4320                ///
4321                /// # Errors
4322                /// As [`#root::sql::fetch_aggregate_pool`].
4323                pub async fn max<U>(
4324                    col: &str,
4325                    pool: &#root::sql::Pool,
4326                ) -> ::core::result::Result<
4327                    ::core::option::Option<U>,
4328                    #root::sql::ExecError,
4329                >
4330                where
4331                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4332                        + #root::sql::MaybeMyFromRow
4333                        + #root::sql::MaybeSqliteFromRow
4334                        + ::core::marker::Send
4335                        + ::core::marker::Unpin,
4336                {
4337                    #root::query::QuerySet::<Self>::default()
4338                        .max::<U>(col, pool)
4339                        .await
4340                }
4341            },
4342        );
4343        let first_method = emit_if_no_field_collision(
4344            "first",
4345            quote! {
4346                /// First row of this model. Eloquent `Model::first()`.
4347                /// Skipped on models that already declare a field named
4348                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4349                ///
4350                /// # Errors
4351                /// As `QuerySet::first`.
4352                pub async fn first(
4353                    pool: &#root::sql::Pool,
4354                ) -> ::core::result::Result<
4355                    ::core::option::Option<Self>,
4356                    #root::sql::ExecError,
4357                > {
4358                    #root::query::QuerySet::<Self>::default()
4359                        .first(pool)
4360                        .await
4361                }
4362            },
4363        );
4364        let last_method = emit_if_no_field_collision(
4365            "last",
4366            quote! {
4367                /// Last row of this model by primary-key DESC.
4368                /// Eloquent `Model::query()->latest('id')->first()`
4369                /// parity — fetches the highest-PK row without
4370                /// requiring the caller to spell the PK column.
4371                /// Returns `None` on an empty table.
4372                ///
4373                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4374                /// Skipped on models that already declare a field
4375                /// named `last`.
4376                ///
4377                /// # Errors
4378                /// As `QuerySet::last`.
4379                pub async fn last(
4380                    pool: &#root::sql::Pool,
4381                ) -> ::core::result::Result<
4382                    ::core::option::Option<Self>,
4383                    #root::sql::ExecError,
4384                > {
4385                    #root::query::QuerySet::<Self>::default()
4386                        .last(pool)
4387                        .await
4388                }
4389            },
4390        );
4391        quote! {
4392            /// Re-SELECT this row by its primary key and overwrite
4393            /// every in-memory field with the freshly-fetched value.
4394            /// Issue #825.
4395            ///
4396            /// Use this when the row may have been modified by another
4397            /// process / connection / job since you read it — e.g. after
4398            /// a queued task callback, or to re-sync stale UI state
4399            /// before re-saving.
4400            ///
4401            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4402            /// when the primary key no longer matches any row (e.g.
4403            /// the row was deleted concurrently).
4404            ///
4405            /// # Errors
4406            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4407            /// the PK no longer exists.
4408            ///
4409            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4410            pub async fn refresh_from_db(
4411                &mut self,
4412                pool: &#root::sql::Pool,
4413            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4414                use #root::sql::FetcherPool as _;
4415                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4416                    ::core::clone::Clone::clone(&self.#pk_ident),
4417                );
4418                let mut _rows: ::std::vec::Vec<Self> =
4419                    #root::query::QuerySet::<Self>::default()
4420                        .filter(::core::stringify!(#pk_ident), _pk_val)
4421                        .limit(1)
4422                        .fetch(pool)
4423                        .await?;
4424                match _rows.into_iter().next() {
4425                    ::core::option::Option::Some(_fresh) => {
4426                        *self = _fresh;
4427                        ::core::result::Result::Ok(())
4428                    }
4429                    ::core::option::Option::None => ::core::result::Result::Err(
4430                        #root::sql::ExecError::Driver(
4431                            #root::sql::sqlx::Error::RowNotFound,
4432                        ),
4433                    ),
4434                }
4435            }
4436
4437            /// Atomically increment the integer column `col` by
4438            /// `by` for this row. Equivalent to
4439            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4440            ///
4441            /// **Doesn't mutate `self`** — the in-memory copy is now
4442            /// stale; call [`Self::refresh_from_db`] /
4443            /// [`Self::fresh`] to re-sync. Returns the rows-
4444            /// affected count (0 when the PK doesn't match any row,
4445            /// 1 on success).
4446            ///
4447            /// `col` is the Rust field name as a string; unknown
4448            /// fields surface as `UnknownField` at runtime. Negative
4449            /// `by` values atomically decrement (see also
4450            /// [`Self::decrement`]).
4451            ///
4452            /// # Errors
4453            /// As [`UpdaterPool::execute_pool`].
4454            ///
4455            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4456            pub async fn increment(
4457                &self,
4458                col: &str,
4459                by: i64,
4460                pool: &#root::sql::Pool,
4461            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4462                Self::__increment_one(self, col, by, pool).await
4463            }
4464
4465            /// Sibling of [`Self::increment`] — atomically
4466            /// decrement this row's `col` by `by`. Eloquent
4467            /// `$model->decrement($col, $by)` parity. Equivalent to
4468            /// `self.increment(col, -by, &pool)`; the separate name
4469            /// keeps call sites readable.
4470            ///
4471            /// # Errors
4472            /// As [`Self::increment`].
4473            pub async fn decrement(
4474                &self,
4475                col: &str,
4476                by: i64,
4477                pool: &#root::sql::Pool,
4478            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4479                Self::__increment_one(self, col, -by, pool).await
4480            }
4481
4482            /// Bulk-increment: add `by` to `col` on every row of the
4483            /// table. Eloquent `Model::query()->increment($col, $by)`
4484            /// parity. Use for counters, score adjustments, view
4485            /// rollups.
4486            ///
4487            /// # Errors
4488            /// As [`UpdaterPool::execute_pool`].
4489            ///
4490            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4491            pub async fn increment_each(
4492                col: &str,
4493                by: i64,
4494                pool: &#root::sql::Pool,
4495            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4496                Self::__increment_all(col, by, pool).await
4497            }
4498
4499            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4500            ///
4501            /// # Errors
4502            /// As [`Self::increment_each`].
4503            pub async fn decrement_each(
4504                col: &str,
4505                by: i64,
4506                pool: &#root::sql::Pool,
4507            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4508                Self::__increment_all(col, -by, pool).await
4509            }
4510
4511            /// Internal: forward to
4512            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4513            /// One-line wrapper kept as a per-Model method so the
4514            /// macro's emitted `increment` / `decrement` instance
4515            /// calls don't have to thread `Self` through manually.
4516            #[doc(hidden)]
4517            pub async fn __increment_one(
4518                this: &Self,
4519                col: &str,
4520                by: i64,
4521                pool: &#root::sql::Pool,
4522            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4523                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4524                    ::core::clone::Clone::clone(&this.#pk_ident),
4525                );
4526                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4527                    ::core::stringify!(#pk_ident),
4528                    _pk_val,
4529                    col,
4530                    by,
4531                    pool,
4532                )
4533                .await
4534            }
4535
4536            /// Internal: run
4537            /// [`#root::sql::model_shortcuts::increment_all_query`].
4538            #[doc(hidden)]
4539            pub async fn __increment_all(
4540                col: &str,
4541                by: i64,
4542                pool: &#root::sql::Pool,
4543            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4544                let _query =
4545                    #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4546                #bulk_update_run
4547            }
4548
4549            /// Internal: forward to
4550            /// [`#root::sql::model_shortcuts::resolve_col`].
4551            #[doc(hidden)]
4552            pub fn __resolve_col(
4553                col: &str,
4554            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4555                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4556            }
4557
4558            /// Internal: forward to
4559            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4560            #[doc(hidden)]
4561            #[must_use]
4562            pub fn __add_signed_expr(
4563                col_static: &'static str,
4564                signed_by: i64,
4565            ) -> #root::core::Expr {
4566                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4567            }
4568
4569            /// Re-SELECT this row by its primary key and return a
4570            /// **new** instance with the freshly-fetched fields.
4571            /// Eloquent `Model::fresh()` parity — non-mutating
4572            /// counterpart of [`Self::refresh_from_db`].
4573            ///
4574            /// Returns `Ok(None)` when the row was deleted
4575            /// concurrently — vs [`Self::refresh_from_db`]
4576            /// which surfaces that as `RowNotFound` because
4577            /// in-place mutation has nothing to write to.
4578            ///
4579            /// Useful when you want to compare the in-memory
4580            /// instance against the persisted state (audit-style
4581            /// diffs, conflict detection) without mutating the
4582            /// reference you already hold.
4583            ///
4584            /// # Errors
4585            /// As [`FetcherPool::fetch`].
4586            ///
4587            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4588            pub async fn fresh(
4589                &self,
4590                pool: &#root::sql::Pool,
4591            ) -> ::core::result::Result<
4592                ::core::option::Option<Self>,
4593                #root::sql::ExecError,
4594            > {
4595                use #root::sql::FetcherPool as _;
4596                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4597                    ::core::clone::Clone::clone(&self.#pk_ident),
4598                );
4599                let _rows: ::std::vec::Vec<Self> =
4600                    #root::query::QuerySet::<Self>::default()
4601                        .filter(::core::stringify!(#pk_ident), _pk_val)
4602                        .limit(1)
4603                        .fetch(pool)
4604                        .await?;
4605                ::core::result::Result::Ok(_rows.into_iter().next())
4606            }
4607
4608            #replicate_doc
4609            #[must_use]
4610            pub fn replicate(&self) -> Self {
4611                Self {
4612                    #pk_clone_token,
4613                    #( #other_field_clones, )*
4614                }
4615            }
4616
4617            #first_method
4618
4619            #last_method
4620
4621            /// Throwing counterpart of [`Self::first`] —
4622            /// errors with `RowNotFound` when the table is empty.
4623            /// Eloquent `Model::firstOrFail()` parity.
4624            ///
4625            /// # Errors
4626            /// As [`Self::first`]; additionally
4627            /// [`sqlx::Error::RowNotFound`] on empty tables.
4628            ///
4629            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4630            pub async fn first_or_fail(
4631                pool: &#root::sql::Pool,
4632            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4633                // Route through the queryset rather than `Self::first`,
4634                // which is suppressed on models with a field named
4635                // `first` (field/shortcut collision guard).
4636                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4637                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4638                    ::core::option::Option::None => ::core::result::Result::Err(
4639                        #root::sql::ExecError::Driver(
4640                            #root::sql::sqlx::Error::RowNotFound,
4641                        ),
4642                    ),
4643                }
4644            }
4645
4646            /// Single-column projection — `SELECT <col> FROM
4647            /// <table>`. Returns `Vec<U>` where each element is the
4648            /// decoded value of the column. Eloquent
4649            /// `Model::pluck($column)` parity.
4650            ///
4651            /// Thin wrapper over `QuerySet::<Self>::default()
4652            /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4653            /// `FlatScalar`; use `Option<_>` for a nullable column.
4654            ///
4655            /// # Errors
4656            /// As `ValuesFlatQuerySet::fetch`.
4657            pub async fn pluck<U>(
4658                col: &'static str,
4659                pool: &#root::sql::Pool,
4660            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4661            where
4662                U: #root::sql::FlatScalar,
4663            {
4664                #root::query::QuerySet::<Self>::default()
4665                    .values_list_flat(col)
4666                    .fetch::<U>(pool)
4667                    .await
4668            }
4669
4670            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4671            /// stream every row of this model in batches of `n`,
4672            /// invoking the callback once per batch. Stable PK-ASC
4673            /// ordering so the LIMIT/OFFSET pagination is
4674            /// deterministic across drivers.
4675            ///
4676            /// The callback is async — it can do further DB work
4677            /// (writes, related-row lookups, queue dispatch) per
4678            /// batch. Return `Err(...)` from the callback to abort
4679            /// the iteration early; the error bubbles up.
4680            ///
4681            /// **When to use**: bulk processing flows that can't
4682            /// fit the whole table in memory — sending newsletters,
4683            /// running data migrations, computing summary
4684            /// statistics. For small / known-bounded tables, plain
4685            /// `Self::all(&pool)` is simpler.
4686            ///
4687            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4688            /// large tables. For multi-million-row scans prefer
4689            /// keyset-by-PK (the standard "WHERE id > last_seen"
4690            /// shape) over `chunk(...)`.
4691            ///
4692            /// Skipped on models without a primary key — chunking
4693            /// needs a stable order to avoid skipping / repeating
4694            /// rows across batches.
4695            pub async fn chunk<F, Fut>(
4696                n: i64,
4697                pool: &#root::sql::Pool,
4698                mut cb: F,
4699            ) -> ::core::result::Result<(), #root::sql::ExecError>
4700            where
4701                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4702                Fut: ::core::future::Future<
4703                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4704                >,
4705            {
4706                use #root::sql::FetcherPool as _;
4707                let pk_col = match Self::primary_key_column() {
4708                    ::core::option::Option::Some(c) => c,
4709                    ::core::option::Option::None => {
4710                        return ::core::result::Result::Ok(());
4711                    }
4712                };
4713                let mut offset: i64 = 0;
4714                loop {
4715                    let rows: ::std::vec::Vec<Self> =
4716                        #root::query::QuerySet::<Self>::default()
4717                            .order_by(&[(pk_col, false)])
4718                            .limit(n)
4719                            .offset(offset)
4720                            .fetch(pool)
4721                            .await?;
4722                    if rows.is_empty() {
4723                        return ::core::result::Result::Ok(());
4724                    }
4725                    let len = rows.len() as i64;
4726                    cb(rows).await?;
4727                    if len < n {
4728                        return ::core::result::Result::Ok(());
4729                    }
4730                    offset += n;
4731                }
4732            }
4733
4734            /// Eloquent `Model::chunkById($n, fn (...))` — same
4735            /// per-batch callback shape as [`Self::chunk`], but uses
4736            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4737            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4738            /// — the right choice for multi-million-row sweeps.
4739            ///
4740            /// Requires the primary key to be a signed integer type
4741            /// (`i64` / `i32`); the keyset comparison rides on
4742            /// `__rustango_pk_value()` lowering through
4743            /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4744            /// non-integer PKs (UUID, String) — those should use
4745            /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4746            /// keyset loop.
4747            ///
4748            /// Callback errors abort iteration; the error bubbles up
4749            /// unchanged. Empty table → callback invoked zero times.
4750            pub async fn chunk_by_id<F, Fut>(
4751                n: i64,
4752                pool: &#root::sql::Pool,
4753                mut cb: F,
4754            ) -> ::core::result::Result<(), #root::sql::ExecError>
4755            where
4756                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4757                Fut: ::core::future::Future<
4758                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4759                >,
4760            {
4761                use #root::sql::FetcherPool as _;
4762                let pk_col = match Self::primary_key_column() {
4763                    ::core::option::Option::Some(c) => c,
4764                    ::core::option::Option::None => {
4765                        return ::core::result::Result::Ok(());
4766                    }
4767                };
4768                // Track the largest PK seen so the next batch picks
4769                // up from there. `i64::MIN` as the sentinel — the
4770                // very first iteration's `> MIN` matches every row,
4771                // so the loop entry is uniform with subsequent
4772                // iterations.
4773                let mut last_seen: i64 = i64::MIN;
4774                loop {
4775                    let key = ::std::format!("{}__gt", pk_col);
4776                    let rows: ::std::vec::Vec<Self> =
4777                        #root::query::QuerySet::<Self>::default()
4778                            .filter(key.as_str(), last_seen)
4779                            .order_by(&[(pk_col, false)])
4780                            .limit(n)
4781                            .fetch(pool)
4782                            .await?;
4783                    if rows.is_empty() {
4784                        return ::core::result::Result::Ok(());
4785                    }
4786                    let len = rows.len() as i64;
4787                    // Capture the last row's PK BEFORE moving rows
4788                    // into the callback.
4789                    let max_pk = match rows
4790                        .last()
4791                        .map(|r| r.__rustango_pk_value())
4792                    {
4793                        ::core::option::Option::Some(
4794                            #root::core::SqlValue::I64(v),
4795                        ) => v,
4796                        ::core::option::Option::Some(
4797                            #root::core::SqlValue::I32(v),
4798                        ) => i64::from(v),
4799                        _ => return ::core::result::Result::Ok(()),
4800                    };
4801                    cb(rows).await?;
4802                    if len < n {
4803                        return ::core::result::Result::Ok(());
4804                    }
4805                    last_seen = max_pk;
4806                }
4807            }
4808
4809            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4810            /// per-row callback companion to [`Self::chunk`].
4811            /// Streams every row in keyset-paginated batches of
4812            /// `batch` size, calling `cb` once per row.
4813            ///
4814            /// Inherits the keyset-paginated scan of
4815            /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4816            /// non-integer PKs are silently a no-op).
4817            ///
4818            /// ```ignore
4819            /// Post::each(500, &pool, |p| async move {
4820            ///     reindex(p).await?;
4821            ///     Ok(())
4822            /// }).await?;
4823            /// ```
4824            ///
4825            /// Return `Err(...)` from the callback to abort
4826            /// iteration; the error bubbles up unchanged.
4827            pub async fn each<F, Fut>(
4828                batch: i64,
4829                pool: &#root::sql::Pool,
4830                mut cb: F,
4831            ) -> ::core::result::Result<(), #root::sql::ExecError>
4832            where
4833                F: ::core::ops::FnMut(Self) -> Fut,
4834                Fut: ::core::future::Future<
4835                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4836                >,
4837            {
4838                use #root::sql::FetcherPool as _;
4839                let pk_col = match Self::primary_key_column() {
4840                    ::core::option::Option::Some(c) => c,
4841                    ::core::option::Option::None => {
4842                        return ::core::result::Result::Ok(());
4843                    }
4844                };
4845                let mut last_seen: i64 = i64::MIN;
4846                loop {
4847                    let key = ::std::format!("{}__gt", pk_col);
4848                    let rows: ::std::vec::Vec<Self> =
4849                        #root::query::QuerySet::<Self>::default()
4850                            .filter(key.as_str(), last_seen)
4851                            .order_by(&[(pk_col, false)])
4852                            .limit(batch)
4853                            .fetch(pool)
4854                            .await?;
4855                    if rows.is_empty() {
4856                        return ::core::result::Result::Ok(());
4857                    }
4858                    let len = rows.len() as i64;
4859                    let max_pk = match rows
4860                        .last()
4861                        .map(|r| r.__rustango_pk_value())
4862                    {
4863                        ::core::option::Option::Some(
4864                            #root::core::SqlValue::I64(v),
4865                        ) => v,
4866                        ::core::option::Option::Some(
4867                            #root::core::SqlValue::I32(v),
4868                        ) => i64::from(v),
4869                        _ => return ::core::result::Result::Ok(()),
4870                    };
4871                    for row in rows {
4872                        cb(row).await?;
4873                    }
4874                    if len < batch {
4875                        return ::core::result::Result::Ok(());
4876                    }
4877                    last_seen = max_pk;
4878                }
4879            }
4880
4881            /// Delete every row of this model — `TRUNCATE TABLE
4882            /// <table> RESTART IDENTITY CASCADE` on Postgres,
4883            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4884            /// support `TRUNCATE` inside foreign-key constraints
4885            /// or — for SQLite — at all). Eloquent `Model::truncate()`
4886            /// parity.
4887            ///
4888            /// **Use only in tests / fixture-reset flows.** Production
4889            /// writes through this would silently bypass the
4890            /// `pre_delete` / `post_delete` signals (no per-row hooks
4891            /// fire on a TRUNCATE / bulk DELETE FROM). An audited model
4892            /// gets one bulk audit entry, not one per row. Global scopes
4893            /// do not apply: every row goes.
4894            ///
4895            /// # Errors
4896            /// As [`raw_execute_pool`].
4897            ///
4898            /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4899            pub async fn truncate(
4900                pool: &#root::sql::Pool,
4901            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4902                let _table = <Self as #root::core::Model>::SCHEMA.table;
4903                let _dialect = pool.dialect();
4904                let _quoted = _dialect.quote_ident(_table);
4905                let _sql = if _dialect.name() == "postgres" {
4906                    ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4907                } else {
4908                    ::std::format!("DELETE FROM {}", _quoted)
4909                };
4910                #truncate_run
4911            }
4912
4913            /// Bulk-delete every row whose primary key is in
4914            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4915            /// Returns the affected row count.
4916            ///
4917            /// Eloquent `Model::destroy([1, 2, 3])` parity.
4918            /// Empty `pks` is a no-op (returns 0).
4919            ///
4920            /// Accepts any iterable whose elements are
4921            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4922            /// `[i64; N]`, etc.
4923            ///
4924            /// # Errors
4925            /// As `delete_pool`.
4926            pub async fn destroy<V>(
4927                pks: impl ::core::iter::IntoIterator<Item = V>,
4928                pool: &#root::sql::Pool,
4929            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4930            where
4931                V: ::core::convert::Into<#root::core::SqlValue>,
4932            {
4933                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4934                    pks.into_iter().map(::core::convert::Into::into).collect();
4935                if _values.is_empty() {
4936                    return ::core::result::Result::Ok(0);
4937                }
4938                let _pk = <Self as #root::core::Model>::SCHEMA
4939                    .primary_key()
4940                    .ok_or_else(|| {
4941                        #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
4942                    })?;
4943                let _query = #root::query::QuerySet::<Self>::default()
4944                    .filter_op(
4945                        _pk.name,
4946                        #root::core::Op::In,
4947                        #root::core::SqlValue::List(_values),
4948                    )
4949                    .compile_delete()?;
4950                #bulk_delete_run
4951            }
4952
4953            /// Fetch every row where `<col> = <val>`. Eloquent
4954            /// `Model::where($col, $val)->get()` parity.
4955            ///
4956            /// Thin wrapper over `QuerySet::<Self>::default()
4957            /// .filter(col, val).fetch(pool)`. For one row,
4958            /// use [`Self::first_where`]; for a chain that
4959            /// needs further `.filter()` / `.order_by()` /
4960            /// `.limit()`, drop down to `Self::query().filter(...)`
4961            /// directly.
4962            ///
4963            /// `val` accepts any value `Into<SqlValue>` so plain
4964            /// strings, ints, UUIDs, etc. all work.
4965            ///
4966            /// # Errors
4967            /// As [`FetcherPool::fetch`].
4968            ///
4969            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4970            pub async fn where_(
4971                col: &str,
4972                val: impl ::core::convert::Into<#root::core::SqlValue>,
4973                pool: &#root::sql::Pool,
4974            ) -> ::core::result::Result<
4975                ::std::vec::Vec<Self>,
4976                #root::sql::ExecError,
4977            > {
4978                use #root::sql::FetcherPool as _;
4979                #root::query::QuerySet::<Self>::default()
4980                    .filter(col, val)
4981                    .fetch(pool)
4982                    .await
4983            }
4984
4985            /// Fetch every row where `<col> IN (vals)`. Eloquent
4986            /// `Model::whereIn($col, $vals)->get()` parity. Empty
4987            /// `vals` returns no rows (matches SQL's empty-IN
4988            /// semantics).
4989            ///
4990            /// `vals` accepts any iterable whose items are
4991            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4992            /// etc.
4993            ///
4994            /// # Errors
4995            /// As [`FetcherPool::fetch`].
4996            ///
4997            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4998            pub async fn where_in<V>(
4999                col: &str,
5000                vals: impl ::core::iter::IntoIterator<Item = V>,
5001                pool: &#root::sql::Pool,
5002            ) -> ::core::result::Result<
5003                ::std::vec::Vec<Self>,
5004                #root::sql::ExecError,
5005            >
5006            where
5007                V: ::core::convert::Into<#root::core::SqlValue>,
5008            {
5009                use #root::sql::FetcherPool as _;
5010                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5011                    vals.into_iter().map(::core::convert::Into::into).collect();
5012                if _values.is_empty() {
5013                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5014                }
5015                let _key = ::std::format!("{}__in", col);
5016                #root::query::QuerySet::<Self>::default()
5017                    .filter(&_key, #root::core::SqlValue::List(_values))
5018                    .fetch(pool)
5019                    .await
5020            }
5021
5022            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5023            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5024            /// `vals` returns every row (matches SQL's empty-NOT-IN
5025            /// semantics — vacuously true for every row).
5026            ///
5027            /// # Errors
5028            /// As [`FetcherPool::fetch`].
5029            ///
5030            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5031            pub async fn where_not_in<V>(
5032                col: &str,
5033                vals: impl ::core::iter::IntoIterator<Item = V>,
5034                pool: &#root::sql::Pool,
5035            ) -> ::core::result::Result<
5036                ::std::vec::Vec<Self>,
5037                #root::sql::ExecError,
5038            >
5039            where
5040                V: ::core::convert::Into<#root::core::SqlValue>,
5041            {
5042                use #root::sql::FetcherPool as _;
5043                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5044                    vals.into_iter().map(::core::convert::Into::into).collect();
5045                if _values.is_empty() {
5046                    return #root::query::QuerySet::<Self>::default()
5047                        .fetch(pool)
5048                        .await;
5049                }
5050                let _key = ::std::format!("{}__not_in", col);
5051                #root::query::QuerySet::<Self>::default()
5052                    .filter(&_key, #root::core::SqlValue::List(_values))
5053                    .fetch(pool)
5054                    .await
5055            }
5056
5057            /// Fetch every row where `<col> IS NULL`. Eloquent
5058            /// `Model::whereNull($col)->get()` parity.
5059            ///
5060            /// # Errors
5061            /// As [`FetcherPool::fetch`].
5062            ///
5063            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5064            pub async fn where_null(
5065                col: &str,
5066                pool: &#root::sql::Pool,
5067            ) -> ::core::result::Result<
5068                ::std::vec::Vec<Self>,
5069                #root::sql::ExecError,
5070            > {
5071                use #root::sql::FetcherPool as _;
5072                let _key = ::std::format!("{}__isnull", col);
5073                #root::query::QuerySet::<Self>::default()
5074                    .filter(&_key, #root::core::SqlValue::Bool(true))
5075                    .fetch(pool)
5076                    .await
5077            }
5078
5079            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5080            /// `Model::whereNotNull($col)->get()` parity.
5081            ///
5082            /// # Errors
5083            /// As [`FetcherPool::fetch`].
5084            ///
5085            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5086            pub async fn where_not_null(
5087                col: &str,
5088                pool: &#root::sql::Pool,
5089            ) -> ::core::result::Result<
5090                ::std::vec::Vec<Self>,
5091                #root::sql::ExecError,
5092            > {
5093                use #root::sql::FetcherPool as _;
5094                let _key = ::std::format!("{}__isnull", col);
5095                #root::query::QuerySet::<Self>::default()
5096                    .filter(&_key, #root::core::SqlValue::Bool(false))
5097                    .fetch(pool)
5098                    .await
5099            }
5100
5101            /// Fetch up to `n` rows in random order. Eloquent
5102            /// `Model::inRandomOrder()->limit($n)->get()` /
5103            /// `Model::query()->inRandomOrder()->get()->take($n)`
5104            /// parity. **Performance caveat**: random ordering
5105            /// forces a full table scan + per-row random key sort;
5106            /// the optimizer cannot use an index. Prefer a
5107            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5108            ///
5109            /// # Errors
5110            /// As [`FetcherPool::fetch`].
5111            ///
5112            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5113            pub async fn random_n(
5114                n: i64,
5115                pool: &#root::sql::Pool,
5116            ) -> ::core::result::Result<
5117                ::std::vec::Vec<Self>,
5118                #root::sql::ExecError,
5119            > {
5120                use #root::sql::FetcherPool as _;
5121                #root::query::QuerySet::<Self>::default()
5122                    .order_random()
5123                    .limit(n)
5124                    .fetch(pool)
5125                    .await
5126            }
5127
5128            /// Fetch one row in random order. Eloquent
5129            /// `Model::inRandomOrder()->first()` parity. Same
5130            /// performance caveat as [`Self::random_n`].
5131            ///
5132            /// # Errors
5133            /// As [`FetcherPool::fetch`].
5134            ///
5135            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5136            pub async fn random(
5137                pool: &#root::sql::Pool,
5138            ) -> ::core::result::Result<
5139                ::core::option::Option<Self>,
5140                #root::sql::ExecError,
5141            > {
5142                ::core::result::Result::Ok(
5143                    Self::random_n(1, pool).await?.into_iter().next(),
5144                )
5145            }
5146
5147            /// Fetch every row ordered ASC by `field`. Eloquent
5148            /// `Model::oldest($field)->get()` parity — the multi-row
5149            /// counterpart of [`Self::earliest`].
5150            ///
5151            /// # Errors
5152            /// As [`FetcherPool::fetch`].
5153            ///
5154            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5155            pub async fn oldest(
5156                field: &str,
5157                pool: &#root::sql::Pool,
5158            ) -> ::core::result::Result<
5159                ::std::vec::Vec<Self>,
5160                #root::sql::ExecError,
5161            > {
5162                use #root::sql::FetcherPool as _;
5163                #root::query::QuerySet::<Self>::default()
5164                    .order_by(&[(field, false)])
5165                    .fetch(pool)
5166                    .await
5167            }
5168
5169            /// Fetch every row ordered DESC by `field`. Eloquent
5170            /// `Model::latest($field)->get()` parity — the multi-row
5171            /// counterpart of [`Self::latest`].
5172            ///
5173            /// # Errors
5174            /// As [`FetcherPool::fetch`].
5175            ///
5176            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5177            pub async fn newest(
5178                field: &str,
5179                pool: &#root::sql::Pool,
5180            ) -> ::core::result::Result<
5181                ::std::vec::Vec<Self>,
5182                #root::sql::ExecError,
5183            > {
5184                use #root::sql::FetcherPool as _;
5185                #root::query::QuerySet::<Self>::default()
5186                    .order_by(&[(field, true)])
5187                    .fetch(pool)
5188                    .await
5189            }
5190
5191            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5192            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5193            /// Routes through the existing `__year` lookup suffix
5194            /// (issue #829).
5195            ///
5196            /// # Errors
5197            /// As [`FetcherPool::fetch`].
5198            ///
5199            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5200            pub async fn where_year(
5201                col: &str,
5202                year: i64,
5203                pool: &#root::sql::Pool,
5204            ) -> ::core::result::Result<
5205                ::std::vec::Vec<Self>,
5206                #root::sql::ExecError,
5207            > {
5208                use #root::sql::FetcherPool as _;
5209                let _key = ::std::format!("{}__year", col);
5210                #root::query::QuerySet::<Self>::default()
5211                    .filter(&_key, #root::core::SqlValue::I64(year))
5212                    .fetch(pool)
5213                    .await
5214            }
5215
5216            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5217            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5218            /// `month` is 1–12.
5219            ///
5220            /// # Errors
5221            /// As [`FetcherPool::fetch`].
5222            ///
5223            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5224            pub async fn where_month(
5225                col: &str,
5226                month: i64,
5227                pool: &#root::sql::Pool,
5228            ) -> ::core::result::Result<
5229                ::std::vec::Vec<Self>,
5230                #root::sql::ExecError,
5231            > {
5232                use #root::sql::FetcherPool as _;
5233                let _key = ::std::format!("{}__month", col);
5234                #root::query::QuerySet::<Self>::default()
5235                    .filter(&_key, #root::core::SqlValue::I64(month))
5236                    .fetch(pool)
5237                    .await
5238            }
5239
5240            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5241            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5242            /// `day` is 1–31.
5243            ///
5244            /// # Errors
5245            /// As [`FetcherPool::fetch`].
5246            ///
5247            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5248            pub async fn where_day(
5249                col: &str,
5250                day: i64,
5251                pool: &#root::sql::Pool,
5252            ) -> ::core::result::Result<
5253                ::std::vec::Vec<Self>,
5254                #root::sql::ExecError,
5255            > {
5256                use #root::sql::FetcherPool as _;
5257                let _key = ::std::format!("{}__day", col);
5258                #root::query::QuerySet::<Self>::default()
5259                    .filter(&_key, #root::core::SqlValue::I64(day))
5260                    .fetch(pool)
5261                    .await
5262            }
5263
5264            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5265            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5266            /// `hour` is 0–23.
5267            ///
5268            /// # Errors
5269            /// As [`FetcherPool::fetch`].
5270            ///
5271            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5272            pub async fn where_hour(
5273                col: &str,
5274                hour: i64,
5275                pool: &#root::sql::Pool,
5276            ) -> ::core::result::Result<
5277                ::std::vec::Vec<Self>,
5278                #root::sql::ExecError,
5279            > {
5280                use #root::sql::FetcherPool as _;
5281                let _key = ::std::format!("{}__hour", col);
5282                #root::query::QuerySet::<Self>::default()
5283                    .filter(&_key, #root::core::SqlValue::I64(hour))
5284                    .fetch(pool)
5285                    .await
5286            }
5287
5288            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5289            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5290            /// `minute` is 0–59.
5291            ///
5292            /// # Errors
5293            /// As [`FetcherPool::fetch`].
5294            ///
5295            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5296            pub async fn where_minute(
5297                col: &str,
5298                minute: i64,
5299                pool: &#root::sql::Pool,
5300            ) -> ::core::result::Result<
5301                ::std::vec::Vec<Self>,
5302                #root::sql::ExecError,
5303            > {
5304                use #root::sql::FetcherPool as _;
5305                let _key = ::std::format!("{}__minute", col);
5306                #root::query::QuerySet::<Self>::default()
5307                    .filter(&_key, #root::core::SqlValue::I64(minute))
5308                    .fetch(pool)
5309                    .await
5310            }
5311
5312            /// Fetch every row where `<col> LIKE <pattern>` —
5313            /// caller-supplied pattern (must include `%` / `_`
5314            /// wildcards manually). Eloquent `Model::whereLike`
5315            /// parity.
5316            ///
5317            /// # Errors
5318            /// As [`FetcherPool::fetch`].
5319            ///
5320            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5321            pub async fn where_like(
5322                col: &str,
5323                pattern: impl ::core::convert::Into<::std::string::String>,
5324                pool: &#root::sql::Pool,
5325            ) -> ::core::result::Result<
5326                ::std::vec::Vec<Self>,
5327                #root::sql::ExecError,
5328            > {
5329                use #root::sql::FetcherPool as _;
5330                let _key = ::std::format!("{}__like", col);
5331                #root::query::QuerySet::<Self>::default()
5332                    .filter(
5333                        &_key,
5334                        #root::core::SqlValue::String(pattern.into()),
5335                    )
5336                    .fetch(pool)
5337                    .await
5338            }
5339
5340            /// Fetch every row where `<col> ILIKE <pattern>` —
5341            /// case-insensitive LIKE (PG native, MySQL/SQLite
5342            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5343            ///
5344            /// # Errors
5345            /// As [`FetcherPool::fetch`].
5346            ///
5347            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5348            pub async fn where_ilike(
5349                col: &str,
5350                pattern: impl ::core::convert::Into<::std::string::String>,
5351                pool: &#root::sql::Pool,
5352            ) -> ::core::result::Result<
5353                ::std::vec::Vec<Self>,
5354                #root::sql::ExecError,
5355            > {
5356                use #root::sql::FetcherPool as _;
5357                let _key = ::std::format!("{}__ilike", col);
5358                #root::query::QuerySet::<Self>::default()
5359                    .filter(
5360                        &_key,
5361                        #root::core::SqlValue::String(pattern.into()),
5362                    )
5363                    .fetch(pool)
5364                    .await
5365            }
5366
5367            /// Fetch every row where `<col>` starts with `prefix`
5368            /// (auto-appends `%`). Eloquent
5369            /// `whereLike("col", "$prefix%")` parity.
5370            ///
5371            /// # Errors
5372            /// As [`FetcherPool::fetch`].
5373            ///
5374            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5375            pub async fn where_starts_with(
5376                col: &str,
5377                prefix: impl ::core::convert::Into<::std::string::String>,
5378                pool: &#root::sql::Pool,
5379            ) -> ::core::result::Result<
5380                ::std::vec::Vec<Self>,
5381                #root::sql::ExecError,
5382            > {
5383                use #root::sql::FetcherPool as _;
5384                let _key = ::std::format!("{}__startswith", col);
5385                #root::query::QuerySet::<Self>::default()
5386                    .filter(
5387                        &_key,
5388                        #root::core::SqlValue::String(prefix.into()),
5389                    )
5390                    .fetch(pool)
5391                    .await
5392            }
5393
5394            /// Fetch every row where `<col>` ends with `suffix`
5395            /// (auto-prepends `%`). Eloquent
5396            /// `whereLike("col", "%$suffix")` parity.
5397            ///
5398            /// # Errors
5399            /// As [`FetcherPool::fetch`].
5400            ///
5401            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5402            pub async fn where_ends_with(
5403                col: &str,
5404                suffix: impl ::core::convert::Into<::std::string::String>,
5405                pool: &#root::sql::Pool,
5406            ) -> ::core::result::Result<
5407                ::std::vec::Vec<Self>,
5408                #root::sql::ExecError,
5409            > {
5410                use #root::sql::FetcherPool as _;
5411                let _key = ::std::format!("{}__endswith", col);
5412                #root::query::QuerySet::<Self>::default()
5413                    .filter(
5414                        &_key,
5415                        #root::core::SqlValue::String(suffix.into()),
5416                    )
5417                    .fetch(pool)
5418                    .await
5419            }
5420
5421            /// Fetch every row where `<col>` contains `substr`
5422            /// (auto-wraps with `%`).
5423            /// Eloquent `whereLike("col", "%$substr%")` parity.
5424            ///
5425            /// # Errors
5426            /// As [`FetcherPool::fetch`].
5427            ///
5428            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5429            pub async fn where_contains(
5430                col: &str,
5431                substr: impl ::core::convert::Into<::std::string::String>,
5432                pool: &#root::sql::Pool,
5433            ) -> ::core::result::Result<
5434                ::std::vec::Vec<Self>,
5435                #root::sql::ExecError,
5436            > {
5437                use #root::sql::FetcherPool as _;
5438                let _key = ::std::format!("{}__contains", col);
5439                #root::query::QuerySet::<Self>::default()
5440                    .filter(
5441                        &_key,
5442                        #root::core::SqlValue::String(substr.into()),
5443                    )
5444                    .fetch(pool)
5445                    .await
5446            }
5447
5448            /// Fetch every row where `<col> > val`. Eloquent
5449            /// `Model::where($col, ">", $val)->get()` parity.
5450            ///
5451            /// # Errors
5452            /// As [`FetcherPool::fetch`].
5453            ///
5454            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5455            pub async fn where_gt(
5456                col: &str,
5457                val: impl ::core::convert::Into<#root::core::SqlValue>,
5458                pool: &#root::sql::Pool,
5459            ) -> ::core::result::Result<
5460                ::std::vec::Vec<Self>,
5461                #root::sql::ExecError,
5462            > {
5463                use #root::sql::FetcherPool as _;
5464                let _key = ::std::format!("{}__gt", col);
5465                #root::query::QuerySet::<Self>::default()
5466                    .filter(&_key, val)
5467                    .fetch(pool)
5468                    .await
5469            }
5470
5471            /// Fetch every row where `<col> >= val`. Eloquent
5472            /// `Model::where($col, ">=", $val)->get()` parity.
5473            ///
5474            /// # Errors
5475            /// As [`FetcherPool::fetch`].
5476            ///
5477            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5478            pub async fn where_gte(
5479                col: &str,
5480                val: impl ::core::convert::Into<#root::core::SqlValue>,
5481                pool: &#root::sql::Pool,
5482            ) -> ::core::result::Result<
5483                ::std::vec::Vec<Self>,
5484                #root::sql::ExecError,
5485            > {
5486                use #root::sql::FetcherPool as _;
5487                let _key = ::std::format!("{}__gte", col);
5488                #root::query::QuerySet::<Self>::default()
5489                    .filter(&_key, val)
5490                    .fetch(pool)
5491                    .await
5492            }
5493
5494            /// Fetch every row where `<col> < val`. Eloquent
5495            /// `Model::where($col, "<", $val)->get()` parity.
5496            ///
5497            /// # Errors
5498            /// As [`FetcherPool::fetch`].
5499            ///
5500            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5501            pub async fn where_lt(
5502                col: &str,
5503                val: impl ::core::convert::Into<#root::core::SqlValue>,
5504                pool: &#root::sql::Pool,
5505            ) -> ::core::result::Result<
5506                ::std::vec::Vec<Self>,
5507                #root::sql::ExecError,
5508            > {
5509                use #root::sql::FetcherPool as _;
5510                let _key = ::std::format!("{}__lt", col);
5511                #root::query::QuerySet::<Self>::default()
5512                    .filter(&_key, val)
5513                    .fetch(pool)
5514                    .await
5515            }
5516
5517            /// Fetch every row where `<col> <= val`. Eloquent
5518            /// `Model::where($col, "<=", $val)->get()` parity.
5519            ///
5520            /// # Errors
5521            /// As [`FetcherPool::fetch`].
5522            ///
5523            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5524            pub async fn where_lte(
5525                col: &str,
5526                val: impl ::core::convert::Into<#root::core::SqlValue>,
5527                pool: &#root::sql::Pool,
5528            ) -> ::core::result::Result<
5529                ::std::vec::Vec<Self>,
5530                #root::sql::ExecError,
5531            > {
5532                use #root::sql::FetcherPool as _;
5533                let _key = ::std::format!("{}__lte", col);
5534                #root::query::QuerySet::<Self>::default()
5535                    .filter(&_key, val)
5536                    .fetch(pool)
5537                    .await
5538            }
5539
5540            /// Fetch every row where `<col> <> val`. Eloquent
5541            /// `Model::where($col, "!=", $val)->get()` parity.
5542            ///
5543            /// # Errors
5544            /// As [`FetcherPool::fetch`].
5545            ///
5546            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5547            pub async fn where_ne(
5548                col: &str,
5549                val: impl ::core::convert::Into<#root::core::SqlValue>,
5550                pool: &#root::sql::Pool,
5551            ) -> ::core::result::Result<
5552                ::std::vec::Vec<Self>,
5553                #root::sql::ExecError,
5554            > {
5555                use #root::sql::FetcherPool as _;
5556                let _key = ::std::format!("{}__ne", col);
5557                #root::query::QuerySet::<Self>::default()
5558                    .filter(&_key, val)
5559                    .fetch(pool)
5560                    .await
5561            }
5562
5563            /// Fetch every row matching `<col> = val` for ANY of the
5564            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5565            /// parity. Empty `cols` returns no rows.
5566            ///
5567            /// Resolves each `&str` column to its SCHEMA-registered
5568            /// `&'static str` once and builds a single OR-composed
5569            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5570            ///
5571            /// # Errors
5572            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5573            /// when any column is not declared on the model.
5574            ///
5575            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5576            pub async fn where_any(
5577                cols: &[&str],
5578                val: impl ::core::convert::Into<#root::core::SqlValue>,
5579                pool: &#root::sql::Pool,
5580            ) -> ::core::result::Result<
5581                ::std::vec::Vec<Self>,
5582                #root::sql::ExecError,
5583            > {
5584                Self::__where_multi(cols, val, false, pool).await
5585            }
5586
5587            /// Fetch every row matching `<col> = val` for ALL listed
5588            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5589            /// Empty `cols` returns every row (vacuous AND).
5590            ///
5591            /// # Errors
5592            /// As [`Self::where_any`].
5593            pub async fn where_all(
5594                cols: &[&str],
5595                val: impl ::core::convert::Into<#root::core::SqlValue>,
5596                pool: &#root::sql::Pool,
5597            ) -> ::core::result::Result<
5598                ::std::vec::Vec<Self>,
5599                #root::sql::ExecError,
5600            > {
5601                Self::__where_multi(cols, val, true, pool).await
5602            }
5603
5604            /// Internal: build a Q expression composing `cols` via
5605            /// AND (`all`) or OR (`!all`), then fetch. Backs
5606            /// `where_any` / `where_all`.
5607            #[doc(hidden)]
5608            pub async fn __where_multi(
5609                cols: &[&str],
5610                val: impl ::core::convert::Into<#root::core::SqlValue>,
5611                all: bool,
5612                pool: &#root::sql::Pool,
5613            ) -> ::core::result::Result<
5614                ::std::vec::Vec<Self>,
5615                #root::sql::ExecError,
5616            > {
5617                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5618                    .await
5619            }
5620
5621            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5622            /// parity. PK-ordered
5623            /// is NOT guaranteed without an explicit `order_by` —
5624            /// drop into `Self::query()` for that.
5625            ///
5626            /// # Errors
5627            /// As [`FetcherPool::fetch`].
5628            ///
5629            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5630            pub async fn take(
5631                n: i64,
5632                pool: &#root::sql::Pool,
5633            ) -> ::core::result::Result<
5634                ::std::vec::Vec<Self>,
5635                #root::sql::ExecError,
5636            > {
5637                use #root::sql::FetcherPool as _;
5638                #root::query::QuerySet::<Self>::default()
5639                    .limit(n)
5640                    .fetch(pool)
5641                    .await
5642            }
5643
5644            /// Fetch the page-th window of `per_page` rows
5645            /// (1-indexed). Eloquent
5646            /// `Model::query()->forPage($page, $perPage)->get()`
5647            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5648            /// LIMIT per_page`; for large offsets this is O(N) —
5649            /// prefer keyset pagination via PK on hot paths.
5650            ///
5651            /// # Errors
5652            /// As [`FetcherPool::fetch`].
5653            ///
5654            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5655            pub async fn for_page(
5656                page: i64,
5657                per_page: i64,
5658                pool: &#root::sql::Pool,
5659            ) -> ::core::result::Result<
5660                ::std::vec::Vec<Self>,
5661                #root::sql::ExecError,
5662            > {
5663                use #root::sql::FetcherPool as _;
5664                let _offset = #root::list_params::page_offset(page, per_page);
5665                #root::query::QuerySet::<Self>::default()
5666                    .limit(per_page)
5667                    .offset(_offset)
5668                    .fetch(pool)
5669                    .await
5670            }
5671
5672            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5673            /// one page of rows AND the total row count in one
5674            /// call. Returns `(rows, total)`. Two queries: a
5675            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5676            /// for the total.
5677            ///
5678            /// Useful for paginated UIs that need both the visible
5679            /// rows AND a "Page X of Y" / total-count widget. For
5680            /// large tables prefer keyset pagination + cached count;
5681            /// every call to `paginate` re-counts the full table.
5682            ///
5683            /// Same 1-indexed `page` convention as [`Self::for_page`].
5684            ///
5685            /// # Errors
5686            /// As [`Self::for_page`] and [`Self::count`].
5687            pub async fn paginate(
5688                page: i64,
5689                per_page: i64,
5690                pool: &#root::sql::Pool,
5691            ) -> ::core::result::Result<
5692                (::std::vec::Vec<Self>, i64),
5693                #root::sql::ExecError,
5694            > {
5695                // Route through the queryset rather than `Self::count`:
5696                // the `count` inherent method is suppressed on models
5697                // that declare a field named `count` (field/shortcut
5698                // collision guard), and `paginate` must still compile
5699                // for those models.
5700                let total = {
5701                    use #root::sql::CounterPool as _;
5702                    #root::query::QuerySet::<Self>::default()
5703                        .count(pool)
5704                        .await?
5705                };
5706                let rows = Self::for_page(page, per_page, pool).await?;
5707                ::core::result::Result::Ok((rows, total))
5708            }
5709
5710            /// Bulk-update — set `set_col = set_val` on every row
5711            /// matching `where_col = where_val`. Returns affected row
5712            /// count. Eloquent
5713            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5714            /// parity.
5715            ///
5716            /// For multi-column updates drop into the queryset
5717            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5718            ///
5719            /// # Errors
5720            /// As [`UpdaterPool::execute_pool`].
5721            ///
5722            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5723            pub async fn update_where(
5724                where_col: &str,
5725                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5726                set_col: &str,
5727                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5728                pool: &#root::sql::Pool,
5729            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5730                let _query = #root::query::QuerySet::<Self>::default()
5731                    .filter(where_col, where_val)
5732                    .update()
5733                    .set(set_col, set_val)
5734                    .compile()?;
5735                #bulk_update_run
5736            }
5737
5738            /// Bulk-delete — remove every row matching
5739            /// `where_col = where_val`. Returns affected row count.
5740            /// Eloquent
5741            /// `Model::where($where_col, $where_val)->delete()` parity.
5742            ///
5743            /// # Errors
5744            /// As [`#root::sql::delete_pool`].
5745            pub async fn delete_where(
5746                where_col: &str,
5747                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5748                pool: &#root::sql::Pool,
5749            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5750                let _query = #root::query::QuerySet::<Self>::default()
5751                    .filter_op(where_col, #root::core::Op::Eq, where_val)
5752                    .compile_delete()?;
5753                #bulk_delete_run
5754            }
5755
5756            /// Bulk-update — set `set_col = set_val` on EVERY row of
5757            /// this model's table. **No WHERE clause** — use with
5758            /// care. Eloquent `Model::query()->update([$col => $val])`
5759            /// parity.
5760            ///
5761            /// Use for backfills, one-shot reset flows, etc. The
5762            /// returned count is rows affected.
5763            ///
5764            /// # Errors
5765            /// As [`UpdaterPool::execute_pool`].
5766            ///
5767            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5768            pub async fn update_all(
5769                set_col: &str,
5770                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5771                pool: &#root::sql::Pool,
5772            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5773                let _query = #root::query::QuerySet::<Self>::default()
5774                    .update()
5775                    .set(set_col, set_val)
5776                    .compile()?;
5777                #bulk_update_run
5778            }
5779
5780            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5781            /// Eloquent `Model::whereNotLike` parity. Pattern is
5782            /// passed verbatim — caller controls `%` / `_`.
5783            ///
5784            /// # Errors
5785            /// As [`FetcherPool::fetch`].
5786            ///
5787            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5788            pub async fn where_not_like(
5789                col: &str,
5790                pattern: impl ::core::convert::Into<::std::string::String>,
5791                pool: &#root::sql::Pool,
5792            ) -> ::core::result::Result<
5793                ::std::vec::Vec<Self>,
5794                #root::sql::ExecError,
5795            > {
5796                use #root::sql::FetcherPool as _;
5797                let _key = ::std::format!("{}__not_like", col);
5798                #root::query::QuerySet::<Self>::default()
5799                    .filter(
5800                        &_key,
5801                        #root::core::SqlValue::String(pattern.into()),
5802                    )
5803                    .fetch(pool)
5804                    .await
5805            }
5806
5807            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5808            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5809            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5810            ///
5811            /// # Errors
5812            /// As [`FetcherPool::fetch`].
5813            ///
5814            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5815            pub async fn where_not_ilike(
5816                col: &str,
5817                pattern: impl ::core::convert::Into<::std::string::String>,
5818                pool: &#root::sql::Pool,
5819            ) -> ::core::result::Result<
5820                ::std::vec::Vec<Self>,
5821                #root::sql::ExecError,
5822            > {
5823                use #root::sql::FetcherPool as _;
5824                let _key = ::std::format!("{}__not_ilike", col);
5825                #root::query::QuerySet::<Self>::default()
5826                    .filter(
5827                        &_key,
5828                        #root::core::SqlValue::String(pattern.into()),
5829                    )
5830                    .fetch(pool)
5831                    .await
5832            }
5833
5834            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5835            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5836            /// parity.
5837            ///
5838            /// # Errors
5839            /// As [`FetcherPool::fetch`].
5840            ///
5841            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5842            pub async fn where_not_between(
5843                col: &str,
5844                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5845                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5846                pool: &#root::sql::Pool,
5847            ) -> ::core::result::Result<
5848                ::std::vec::Vec<Self>,
5849                #root::sql::ExecError,
5850            > {
5851                use #root::sql::FetcherPool as _;
5852                let _key = ::std::format!("{}__not_between", col);
5853                let _vals = #root::core::SqlValue::List(::std::vec![
5854                    ::core::convert::Into::into(lo),
5855                    ::core::convert::Into::into(hi),
5856                ]);
5857                #root::query::QuerySet::<Self>::default()
5858                    .filter(&_key, _vals)
5859                    .fetch(pool)
5860                    .await
5861            }
5862
5863            /// Returns the SQL table name for this model. Eloquent
5864            /// `$model->getTable()` parity.
5865            #[must_use]
5866            pub fn table_name() -> &'static str {
5867                <Self as #root::core::Model>::SCHEMA.table
5868            }
5869
5870            /// Returns the SQL column name of this model's primary
5871            /// key, or `None` when the model has no
5872            /// `#[rustango(primary_key)]`. Eloquent
5873            /// `$model->getKeyName()` parity.
5874            #[must_use]
5875            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5876                <Self as #root::core::Model>::SCHEMA
5877                    .primary_key()
5878                    .map(|f| f.column)
5879            }
5880
5881            /// Fetch every row where `<col> BETWEEN lo AND hi`
5882            /// (inclusive on both ends — same as SQL). Eloquent
5883            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5884            ///
5885            /// # Errors
5886            /// As [`FetcherPool::fetch`].
5887            ///
5888            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5889            pub async fn where_between(
5890                col: &str,
5891                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5892                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5893                pool: &#root::sql::Pool,
5894            ) -> ::core::result::Result<
5895                ::std::vec::Vec<Self>,
5896                #root::sql::ExecError,
5897            > {
5898                use #root::sql::FetcherPool as _;
5899                let _key = ::std::format!("{}__between", col);
5900                let _vals = #root::core::SqlValue::List(::std::vec![
5901                    ::core::convert::Into::into(lo),
5902                    ::core::convert::Into::into(hi),
5903                ]);
5904                #root::query::QuerySet::<Self>::default()
5905                    .filter(&_key, _vals)
5906                    .fetch(pool)
5907                    .await
5908            }
5909
5910            /// Fetch the first row where `<col> = <val>`. Returns
5911            /// `Ok(None)` when no row matches. Eloquent
5912            /// `Model::firstWhere($col, $val)` parity.
5913            ///
5914            /// Thin wrapper over `QuerySet::<Self>::default()
5915            /// .filter(col, val).first(pool)`. Use this when you
5916            /// want one row identified by a non-PK column (e.g.
5917            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5918            ///
5919            /// `val` accepts any value `Into<SqlValue>` so plain
5920            /// strings, ints, UUIDs, etc. all work.
5921            ///
5922            /// # Errors
5923            /// As `QuerySet::first`.
5924            pub async fn first_where(
5925                col: &str,
5926                val: impl ::core::convert::Into<#root::core::SqlValue>,
5927                pool: &#root::sql::Pool,
5928            ) -> ::core::result::Result<
5929                ::core::option::Option<Self>,
5930                #root::sql::ExecError,
5931            > {
5932                #root::query::QuerySet::<Self>::default()
5933                    .filter(col, val)
5934                    .first(pool)
5935                    .await
5936            }
5937
5938            #value_method
5939
5940            /// Fetch the row with the largest `field` value —
5941            /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5942            /// `Ok(None)` for an empty table. Eloquent
5943            /// `Model::latest($field)->first()` parity.
5944            /// Thin wrapper over `QuerySet::<Self>::default()
5945            /// .latest(field, pool)`.
5946            ///
5947            /// **Field name** is the Rust field ident as a string
5948            /// (not the SQL column). It is a runtime `&str`, so an
5949            /// unknown one compiles and surfaces as
5950            /// `ExecError::Query(QueryError::UnknownField)` when the
5951            /// query runs.
5952            ///
5953            /// # Errors
5954            /// As `QuerySet::latest`.
5955            pub async fn latest(
5956                field: &str,
5957                pool: &#root::sql::Pool,
5958            ) -> ::core::result::Result<
5959                ::core::option::Option<Self>,
5960                #root::sql::ExecError,
5961            > {
5962                #root::query::QuerySet::<Self>::default()
5963                    .latest(field, pool)
5964                    .await
5965            }
5966
5967            /// Sibling of [`Self::latest`] — fetches the row
5968            /// with the smallest `field` value (`ORDER BY <field>
5969            /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5970            /// ->first()` parity.
5971            ///
5972            /// # Errors
5973            /// As [`Self::latest`].
5974            pub async fn earliest(
5975                field: &str,
5976                pool: &#root::sql::Pool,
5977            ) -> ::core::result::Result<
5978                ::core::option::Option<Self>,
5979                #root::sql::ExecError,
5980            > {
5981                #root::query::QuerySet::<Self>::default()
5982                    .earliest(field, pool)
5983                    .await
5984            }
5985
5986            #count_method
5987
5988            /// `true` when the table contains at least one row.
5989            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
5990            /// over `QuerySet::<Self>::default().exists(pool)`.
5991            ///
5992            /// # Errors
5993            /// As [`ExistsPool::exists`].
5994            ///
5995            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5996            pub async fn exists(
5997                pool: &#root::sql::Pool,
5998            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5999                use #root::sql::ExistsPool as _;
6000                #root::query::QuerySet::<Self>::default()
6001                    .exists(pool)
6002                    .await
6003            }
6004
6005            /// Inverse of [`Self::exists`] — returns `true` when
6006            /// the table has zero rows. Eloquent
6007            /// `Model::doesntExist()` parity.
6008            ///
6009            /// # Errors
6010            /// As [`Self::exists`].
6011            pub async fn doesnt_exist(
6012                pool: &#root::sql::Pool,
6013            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6014                Self::exists(pool).await.map(|e| !e)
6015            }
6016
6017            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6018            /// `true` when a row with primary key `pk` exists in
6019            /// the table. Sugar over
6020            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6021            ///
6022            /// Differs from [`Self::exists`] (which checks "any row
6023            /// in the table") by checking a specific PK existence.
6024            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6025            /// because the row is never materialized — the SQL is
6026            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6027            ///
6028            /// # Errors
6029            /// As [`#root::sql::ExistsPool::contains_pk`].
6030            pub async fn contains_pk(
6031                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6032                pool: &#root::sql::Pool,
6033            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6034                use #root::sql::ExistsPool as _;
6035                #root::query::QuerySet::<Self>::default()
6036                    .contains_pk(pool, pk)
6037                    .await
6038            }
6039
6040            #sum_method
6041            #avg_method
6042            #min_method
6043            #max_method
6044
6045            /// Fetch every row of this model from `pool`. Eloquent
6046            /// `Model::all()` parity — a thin wrapper over
6047            /// `QuerySet::<Self>::default().fetch(pool)`.
6048            ///
6049            /// **Use with care on large tables** — there's no
6050            /// pagination or limit; the entire table is materialized
6051            /// into memory. For anything beyond fixture / lookup
6052            /// tables, page through `QuerySet::<Self>::default()
6053            /// .order_by(...).limit(N).offset(M).fetch(pool)`
6054            /// or stream via `.iterator(chunk_size)`.
6055            ///
6056            /// # Errors
6057            /// As [`FetcherPool::fetch`].
6058            ///
6059            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6060            pub async fn all(
6061                pool: &#root::sql::Pool,
6062            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6063            {
6064                use #root::sql::FetcherPool as _;
6065                #root::query::QuerySet::<Self>::default()
6066                    .fetch(pool)
6067                    .await
6068            }
6069
6070            /// Look up every row whose primary key is in `pks`.
6071            /// Returns the matching rows in **inventory** order — NOT
6072            /// the order of `pks`. Empty `pks` returns an empty
6073            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6074            /// with a list) parity.
6075            ///
6076            /// Thin wrapper over `QuerySet::<Self>::default()
6077            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6078            /// Caller-supplied PKs that don't match a row are
6079            /// silently skipped (the returned vec is shorter than
6080            /// the input list). For an order-preserving / "fail
6081            /// when any missing" variant, build the queryset
6082            /// explicitly with `in_bulk` instead.
6083            ///
6084            /// Accepts any iterable whose elements are
6085            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6086            /// `Vec<Uuid>`, etc.
6087            ///
6088            /// # Errors
6089            /// As [`FetcherPool::fetch`].
6090            ///
6091            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6092            pub async fn find_many<V>(
6093                pks: impl ::core::iter::IntoIterator<Item = V>,
6094                pool: &#root::sql::Pool,
6095            ) -> ::core::result::Result<
6096                ::std::vec::Vec<Self>,
6097                #root::sql::ExecError,
6098            >
6099            where
6100                V: ::core::convert::Into<#root::core::SqlValue>,
6101            {
6102                use #root::sql::FetcherPool as _;
6103                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6104                    pks.into_iter().map(::core::convert::Into::into).collect();
6105                if _values.is_empty() {
6106                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6107                }
6108                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6109                #root::query::QuerySet::<Self>::default()
6110                    .filter(&_key, #root::core::SqlValue::List(_values))
6111                    .fetch(pool)
6112                    .await
6113            }
6114
6115            /// Look up the row whose primary key equals `pk`. Returns
6116            /// `Ok(None)` when no row matches, rather than erroring.
6117            /// Eloquent `Model::find`
6118            /// shape — accepts any value `Into<SqlValue>`.
6119            ///
6120            /// One-liner shortcut for the common
6121            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6122            /// .limit(1).fetch(pool).await?.into_iter().next()`
6123            /// dance.
6124            ///
6125            /// # Errors
6126            /// As [`FetcherPool::fetch`].
6127            ///
6128            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6129            pub async fn find(
6130                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6131                pool: &#root::sql::Pool,
6132            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6133            {
6134                use #root::sql::FetcherPool as _;
6135                let _pk_val: #root::core::SqlValue = pk.into();
6136                let mut _rows: ::std::vec::Vec<Self> =
6137                    #root::query::QuerySet::<Self>::default()
6138                        .filter(::core::stringify!(#pk_ident), _pk_val)
6139                        .limit(1)
6140                        .fetch(pool)
6141                        .await?;
6142                ::core::result::Result::Ok(_rows.into_iter().next())
6143            }
6144
6145            /// Look up the row whose primary key equals `pk`. Errors
6146            /// when no row matches — the throwing counterpart of
6147            /// [`Self::find`]. Eloquent `Model::findOrFail`
6148            /// parity.
6149            ///
6150            /// Translates the miss into
6151            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6152            /// so callers can `?`-bubble straight through the typical
6153            /// `ExecError` error chain.
6154            ///
6155            /// # Errors
6156            /// As [`Self::find`]; additionally
6157            /// [`sqlx::Error::RowNotFound`] when no row matches.
6158            ///
6159            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6160            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6161            pub async fn find_or_fail(
6162                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6163                pool: &#root::sql::Pool,
6164            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6165                match Self::find(pk, pool).await? {
6166                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6167                    ::core::option::Option::None => ::core::result::Result::Err(
6168                        #root::sql::ExecError::Driver(
6169                            #root::sql::sqlx::Error::RowNotFound,
6170                        ),
6171                    ),
6172                }
6173            }
6174
6175            /// Look up multiple rows by primary key, **failing** if
6176            /// any requested PK is missing. Eloquent
6177            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6178            /// inventory order (NOT request order — same as
6179            /// [`Self::find_many`]).
6180            ///
6181            /// Empty `pks` returns an empty `Vec` (no rows requested
6182            /// → nothing to fail on).
6183            ///
6184            /// Differs from [`Self::find_many`] in that this method
6185            /// requires every PK to resolve. If even one is missing,
6186            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6187            /// in `ExecError::Driver`). Rows are deduped at the SQL
6188            /// layer, so passing the same PK twice in `pks` counts as
6189            /// one expected row.
6190            ///
6191            /// # Errors
6192            /// As [`Self::find_many`], plus `RowNotFound` when the
6193            /// returned row count is less than the count of distinct
6194            /// requested PKs.
6195            pub async fn find_many_or_fail<V>(
6196                pks: impl ::core::iter::IntoIterator<Item = V>,
6197                pool: &#root::sql::Pool,
6198            ) -> ::core::result::Result<
6199                ::std::vec::Vec<Self>,
6200                #root::sql::ExecError,
6201            >
6202            where
6203                V: ::core::convert::Into<#root::core::SqlValue>,
6204            {
6205                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6206                    pks.into_iter().map(::core::convert::Into::into).collect();
6207                if _values.is_empty() {
6208                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6209                }
6210                // Dedup the requested PK list before counting so
6211                // duplicate-PK requests don't false-fail (the SQL
6212                // `IN (...)` clause naturally dedups too).
6213                let mut _seen: ::std::collections::HashSet<
6214                    ::std::string::String,
6215                > = ::std::collections::HashSet::new();
6216                for v in &_values {
6217                    _seen.insert(v.to_display_string());
6218                }
6219                let _expected = _seen.len();
6220                let _rows = Self::find_many(_values, pool).await?;
6221                if _rows.len() < _expected {
6222                    return ::core::result::Result::Err(
6223                        #root::sql::ExecError::Driver(
6224                            #root::sql::sqlx::Error::RowNotFound,
6225                        ),
6226                    );
6227                }
6228                ::core::result::Result::Ok(_rows)
6229            }
6230
6231            /// Find by primary key or run `fallback` to produce a
6232            /// default row to return. Eloquent
6233            /// `Model::findOr($pk, fn() => …)` parity.
6234            ///
6235            /// Unlike [`Self::find_or_fail`] (which raises on
6236            /// miss), this is the "give me something sensible"
6237            /// branch: typical use is "fetch the user's row, else
6238            /// fall back to an anonymous/guest stub".
6239            ///
6240            /// `fallback` runs only when no row matches — the DB
6241            /// round-trip happens unconditionally.
6242            ///
6243            /// # Errors
6244            /// As [`Self::find`].
6245            pub async fn find_or<F>(
6246                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6247                pool: &#root::sql::Pool,
6248                fallback: F,
6249            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6250            where
6251                F: ::core::ops::FnOnce() -> Self,
6252            {
6253                ::core::result::Result::Ok(
6254                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6255                )
6256            }
6257
6258            /// Same as [`Self::find_or`] but also returns a `bool`
6259            /// indicating whether the row was found in the DB
6260            /// (`true`) or freshly built from `fallback` (`false`).
6261            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6262            /// in PHP the returned model also exposes `->exists`;
6263            /// here that's surfaced as the second tuple element.
6264            ///
6265            /// Useful for edit-or-create form handlers where the
6266            /// caller needs to know whether to PATCH or POST when
6267            /// the user submits the form.
6268            ///
6269            /// # Errors
6270            /// As [`Self::find`].
6271            pub async fn find_or_new<F>(
6272                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6273                pool: &#root::sql::Pool,
6274                fallback: F,
6275            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6276            where
6277                F: ::core::ops::FnOnce() -> Self,
6278            {
6279                match Self::find(pk, pool).await? {
6280                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6281                    ::core::option::Option::None => {
6282                        ::core::result::Result::Ok((fallback(), false))
6283                    }
6284                }
6285            }
6286
6287            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6288            /// [`Self::find_or_new`] but **persists** the new row
6289            /// when the PK isn't found. Returns `(row, exists: bool)`
6290            /// — `exists=true` when the PK was found,
6291            /// `false` when a fresh row was inserted.
6292            ///
6293            /// ```ignore
6294            /// let (post, found) = Post::find_or_insert(
6295            ///     pk,
6296            ///     &pool,
6297            ///     || Post { id: Auto::default(), title: "new".into() },
6298            /// ).await?;
6299            /// // `found` true → returned existing row;
6300            /// // `found` false → fallback was inserted, `post.id` populated.
6301            /// ```
6302            ///
6303            /// **Caveat**: two concurrent calls that both miss the
6304            /// find can both INSERT, violating uniqueness. Wrap in a
6305            /// transaction or rely on a UNIQUE constraint + handle
6306            /// the conflict error if you need race-free semantics.
6307            ///
6308            /// # Errors
6309            /// As [`Self::find`] and [`Self::save_pool`].
6310            pub async fn find_or_insert<F>(
6311                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6312                pool: &#root::sql::Pool,
6313                fallback: F,
6314            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6315            where
6316                F: ::core::ops::FnOnce() -> Self,
6317            {
6318                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6319                    return ::core::result::Result::Ok((_row, true));
6320                }
6321                let mut _new = fallback();
6322                _new.save_pool(pool).await?;
6323                ::core::result::Result::Ok((_new, false))
6324            }
6325
6326            /// Fetch the first row of the table, or run `fallback`
6327            /// when the table is empty. Eloquent
6328            /// `Model::firstOr(fn() => …)` parity.
6329            ///
6330            /// # Errors
6331            /// As [`Self::first`].
6332            pub async fn first_or<F>(
6333                pool: &#root::sql::Pool,
6334                fallback: F,
6335            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6336            where
6337                F: ::core::ops::FnOnce() -> Self,
6338            {
6339                // Route through the queryset rather than `Self::first`,
6340                // which is suppressed on models with a field named
6341                // `first` (field/shortcut collision guard).
6342                ::core::result::Result::Ok(
6343                    #root::query::QuerySet::<Self>::default()
6344                        .first(pool)
6345                        .await?
6346                        .unwrap_or_else(fallback),
6347                )
6348            }
6349
6350            /// Fetch exactly one row matching `<col> = <val>`. Errors
6351            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6352            /// or more than one matches
6353            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6354            /// Eloquent `Model::sole($col, $val)` parity.
6355            ///
6356            /// # Errors
6357            /// As [`Self::where_`] plus the explicit
6358            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6359            pub async fn sole(
6360                col: &str,
6361                val: impl ::core::convert::Into<#root::core::SqlValue>,
6362                pool: &#root::sql::Pool,
6363            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6364                let mut _rows = Self::where_(col, val, pool).await?;
6365                match _rows.len() {
6366                    0 => ::core::result::Result::Err(
6367                        #root::sql::ExecError::Driver(
6368                            #root::sql::sqlx::Error::RowNotFound,
6369                        ),
6370                    ),
6371                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6372                    n => ::core::result::Result::Err(
6373                        #root::sql::ExecError::MultipleRowsReturned {
6374                            op: "sole",
6375                            table: <Self as #root::core::Model>::SCHEMA.name,
6376                            count: n,
6377                        },
6378                    ),
6379                }
6380            }
6381        }
6382    } else {
6383        quote!()
6384    };
6385
6386    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6387    // the non-audited `_pool` methods but execute against an open
6388    // `PoolTx` so the writes participate in the caller's transaction.
6389    // Auditing inside TX is deferred; these always use the plain
6390    // executor primitives regardless of whether the model is audited.
6391    let tx_insert_method = if fields.has_auto {
6392        let pushes = &fields.insert_pushes;
6393        let returning_cols = &fields.returning_cols;
6394        quote! {
6395            /// Insert this row inside an open transaction, populating
6396            /// any `Auto<T>` PK from the auto-assigned value. Works
6397            /// against any backend that `tx` wraps.
6398            ///
6399            /// # Errors
6400            /// As [`Self::insert_pool`].
6401            pub async fn insert_tx(
6402                &mut self,
6403                tx: &mut #root::sql::PoolTx<'_>,
6404            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6405                let mut _columns: ::std::vec::Vec<&'static str> =
6406                    ::std::vec::Vec::new();
6407                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6408                    ::std::vec::Vec::new();
6409                #( #pushes )*
6410                let _query = #root::core::InsertQuery::new(
6411                    <Self as #root::core::Model>::SCHEMA,
6412                    _columns,
6413                    _values,
6414                )
6415                .returning(::std::vec![ #( #returning_cols ),* ]);
6416                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6417                #root::sql::apply_auto_pk(_result, self)
6418            }
6419        }
6420    } else {
6421        let insert_columns = &fields.insert_columns;
6422        let insert_values = &fields.insert_values;
6423        quote! {
6424            /// Insert this row inside an open transaction.
6425            ///
6426            /// # Errors
6427            /// As [`Self::insert_pool`].
6428            pub async fn insert_tx(
6429                &self,
6430                tx: &mut #root::sql::PoolTx<'_>,
6431            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6432                let _query = #root::core::InsertQuery::new(
6433                    <Self as #root::core::Model>::SCHEMA,
6434                    ::std::vec![ #( #insert_columns ),* ],
6435                    ::std::vec![ #( #insert_values ),* ],
6436                );
6437                #root::sql::insert_tx(tx, &_query).await
6438            }
6439        }
6440    };
6441
6442    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6443        let pk_column_lit = pk_col.as_str();
6444        let assignments = &fields.update_assignments;
6445        let dispatch_unset = if fields.pk_is_auto {
6446            quote! {
6447                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6448                    return self.insert_tx(tx).await.map(|()| 1u64);
6449                }
6450            }
6451        } else {
6452            quote!()
6453        };
6454        quote! {
6455            /// Save this row inside an open transaction. `INSERT` when
6456            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6457            /// PK. Works against any backend that `tx` wraps.
6458            ///
6459            /// # Errors
6460            /// As [`Self::save_pool`].
6461            pub async fn save_tx(
6462                &mut self,
6463                tx: &mut #root::sql::PoolTx<'_>,
6464            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6465                #dispatch_unset
6466                let _query = #root::core::UpdateQuery::new(
6467                    <Self as #root::core::Model>::SCHEMA,
6468                    ::std::vec![ #( #assignments ),* ],
6469                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6470                        #pk_column_lit,
6471                        #root::core::Op::Eq,
6472                        ::core::convert::Into::<#root::core::SqlValue>::into(
6473                            ::core::clone::Clone::clone(&self.#pk_ident)
6474                        ),
6475                    )),
6476                );
6477                let _affected = #root::sql::update_tx(tx, &_query).await?;
6478                ::core::result::Result::Ok(_affected)
6479            }
6480        }
6481    } else {
6482        quote!()
6483    };
6484
6485    let tx_delete_method = {
6486        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6487        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6488        if let Some(pk_ident) = pk_ident_for_tx {
6489            quote! {
6490                /// Delete the row identified by this instance's PK
6491                /// inside an open transaction. Works against any backend
6492                /// that `tx` wraps.
6493                ///
6494                /// # Errors
6495                /// As [`Self::delete_pool`].
6496                pub async fn delete_tx(
6497                    &self,
6498                    tx: &mut #root::sql::PoolTx<'_>,
6499                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6500                    let _query = #root::core::DeleteQuery::by_pk(
6501                        <Self as #root::core::Model>::SCHEMA,
6502                        #pk_column_lit,
6503                        ::core::convert::Into::<#root::core::SqlValue>::into(
6504                            ::core::clone::Clone::clone(&self.#pk_ident)
6505                        ),
6506                    );
6507                    #root::sql::delete_tx(tx, &_query).await
6508                }
6509            }
6510        } else {
6511            quote!()
6512        }
6513    };
6514
6515    // Update emission captures both BEFORE and AFTER state — runs an
6516    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6517    // each tracked field's prior value, then after the UPDATE diffs
6518    // against the in-memory `&self`. `diff_changes` drops unchanged
6519    // columns so the JSON only contains the actual delta.
6520    //
6521    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6522    // and binds `_audit_before_pairs`; `audit_update_post` runs
6523    // after the UPDATE and emits the PendingEntry.
6524    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6525        audited_fields
6526    {
6527        if tracked.is_empty() {
6528            (quote!(), quote!())
6529        } else {
6530            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6531            let pk_column_lit_for_select =
6532                primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6533            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6534                if fields.pk_is_auto {
6535                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6536                } else {
6537                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6538                }
6539            } else {
6540                quote!(0_i64)
6541            };
6542            let before_pairs = tracked.iter().map(|c| {
6543                let column_lit = c.column.as_str();
6544                let value_ty = &c.value_ty;
6545                quote! {
6546                    (
6547                        #column_lit,
6548                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6549                            &_audit_before_row, #column_lit,
6550                        ) {
6551                            ::core::result::Result::Ok(v) => {
6552                                #root::__serde_json::to_value(&v)
6553                                    .unwrap_or(#root::__serde_json::Value::Null)
6554                            }
6555                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6556                        },
6557                    )
6558                }
6559            });
6560            let pre = quote! {
6561                let _audit_before_query = #root::audit::before_image_query(
6562                    <Self as #root::core::Model>::SCHEMA,
6563                    #pk_column_lit_for_select,
6564                    ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6565                    &[ #( #tracked_cols ),* ],
6566                );
6567                // A failed pre-read must not let the UPDATE run unaudited (#1907).
6568                let _audit_before_pairs:
6569                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6570                    #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6571                        .await?
6572                        .into_iter()
6573                        .next()
6574                        .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6575            };
6576            let post = quote! {
6577                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6578                    let _audit_after = self.__rustango_audit_pairs();
6579                    if let ::core::option::Option::Some(_audit_entry) =
6580                        #root::audit::PendingEntry::update_diff(
6581                            <Self as #root::core::Model>::SCHEMA.table,
6582                            self.__rustango_audit_pk(),
6583                            &_audit_before,
6584                            &_audit_after,
6585                        )
6586                    {
6587                        #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6588                    }
6589                }
6590            };
6591            (pre, post)
6592        }
6593    } else {
6594        (quote!(), quote!())
6595    };
6596
6597    // Bulk-insert audit: one `Create` entry per row once RETURNING has
6598    // set its PK, written in one batched INSERT.
6599    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6600        quote! {
6601            let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6602                .iter()
6603                .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6604                .collect();
6605            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6606        }
6607    } else {
6608        quote!()
6609    };
6610
6611    let save_method = if fields.pk_is_auto {
6612        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6613        let pk_column_lit = pk_column.as_str();
6614        let assignments = &fields.update_assignments;
6615        let upsert_cols = &fields.upsert_update_columns;
6616        let upsert_pushes = &fields.insert_pushes;
6617        let upsert_returning = &fields.returning_cols;
6618        let upsert_auto_assigns = &fields.auto_assigns;
6619        // Conflict target: prefer the first declared `unique_together`
6620        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6621        // `BIGSERIAL` and never collide on insert, so a PK-only target
6622        // would silently turn `upsert()` into "always-insert" for
6623        // surrogate-PK models with composite UNIQUE constraints — see
6624        // `RolePermission` / `UserRole` / `UserPermission` in the
6625        // tenancy permission engine. When no `unique_together` is
6626        // declared we keep the PK target (the original behaviour).
6627        let upsert_target_columns: Vec<String> =
6628            upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6629        let upsert_target_lits = upsert_target_columns
6630            .iter()
6631            .map(String::as_str)
6632            .collect::<Vec<_>>();
6633        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6634            quote!(#root::core::ConflictClause::DoNothing)
6635        } else {
6636            quote!(#root::core::ConflictClause::DoUpdate {
6637                target: ::std::vec![ #( #upsert_target_lits ),* ],
6638                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6639            })
6640        };
6641        // Audited: the upsert reports whether it inserted or updated (#1795).
6642        let (upsert_run, audit_upsert_emit) = if audited_fields.is_some() {
6643            (
6644                quote! {
6645                    let (_returning_row_v, _audit_op) =
6646                        #root::audit::upsert_returning_on(&mut *_executor, &query).await?;
6647                },
6648                quote! {
6649                    let _audit_entry = self.__rustango_audit_entry(_audit_op);
6650                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6651                },
6652            )
6653        } else {
6654            (
6655                quote! {
6656                    let _returning_row_v =
6657                        #root::sql::__macro_internals::insert_returning_on(_executor, &query)
6658                            .await?;
6659                },
6660                quote!(),
6661            )
6662        };
6663        Some(quote! {
6664            /// Insert this row if its `Auto<T>` primary key is
6665            /// `Unset`, otherwise update the existing row matching the
6666            /// PK. The caller doesn't need to pick `insert` vs the
6667            /// bulk-update path manually.
6668            ///
6669            /// On the insert branch, populates the PK from `RETURNING`
6670            /// (same behavior as `insert`). On the update branch,
6671            /// writes every non-PK column back; if no row matches the
6672            /// PK, returns `Ok(())` silently.
6673            ///
6674            /// Only generated when the primary key is declared as
6675            /// `Auto<T>`. Models with a manually-managed PK must use
6676            /// `insert` or the QuerySet update builder.
6677            ///
6678            /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6679            /// driver-specific `PgPool`, so it does not exist at all on a
6680            /// build without the `postgres` feature: selecting `sqlite`
6681            /// makes the method vanish rather than fail with anything that
6682            /// names the cause.
6683            ///
6684            /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6685            /// all three backends. The naming is inverted — the short name
6686            /// is the narrow one — and that is tracked in #1293.
6687            ///
6688            /// # Errors
6689            /// Returns [`#root::sql::ExecError`] for SQL-writing
6690            /// or driver failures.
6691            #[cfg(feature = "postgres")]
6692            pub async fn save(
6693                &mut self,
6694                pool: &#root::sql::sqlx::PgPool,
6695            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6696                #pool_to_save_on
6697            }
6698
6699            /// Like [`Self::save`] but accepts any sqlx executor —
6700            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6701            /// escape hatch for tenant-scoped writes: schema-mode
6702            /// tenants share the registry pool but rely on a per-
6703            /// checkout `SET search_path`, so passing `&PgPool` would
6704            /// silently hit the wrong schema. Acquire a connection
6705            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6706            ///
6707            /// # Errors
6708            /// As [`Self::save`].
6709            #[cfg(feature = "postgres")]
6710            pub async fn save_on #executor_generics (
6711                &mut self,
6712                #executor_param,
6713            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6714            #executor_where
6715            {
6716                // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6717                // the rows-affected count (0 when the PK no longer exists).
6718                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6719                    return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6720                }
6721                #audit_update_pre
6722                let _query = #root::core::UpdateQuery::new(
6723                    <Self as #root::core::Model>::SCHEMA,
6724                    ::std::vec![ #( #assignments ),* ],
6725                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6726                        #pk_column_lit,
6727                        #root::core::Op::Eq,
6728                        ::core::convert::Into::<#root::core::SqlValue>::into(
6729                            ::core::clone::Clone::clone(&self.#pk_ident)
6730                        ),
6731                    )),
6732                );
6733                let _affected = #root::sql::__macro_internals::update_on(
6734                    #executor_passes_to_data_write,
6735                    &_query,
6736                ).await?;
6737                #audit_update_post
6738                ::core::result::Result::Ok(_affected)
6739            }
6740
6741            /// Per-call override for the audit source. Runs
6742            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6743            /// scope so the resulting audit entry records `source`
6744            /// instead of the task-local default. Useful for seed
6745            /// scripts and one-off CLI tools that don't sit inside an
6746            /// admin handler. The override applies only to this call;
6747            /// no global state changes.
6748            ///
6749            /// # Errors
6750            /// As [`Self::save_on`].
6751            #[cfg(feature = "postgres")]
6752            pub async fn save_on_with #executor_generics (
6753                &mut self,
6754                #executor_param,
6755                source: #root::audit::AuditSource,
6756            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6757            #executor_where
6758            {
6759                #root::audit::with_source(source, self.save_on(_executor)).await
6760            }
6761
6762            /// Insert this row or update it in-place if the primary key already
6763            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6764            ///
6765            /// With `Auto::Unset` PK the server assigns a new key and no conflict
6766            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6767            /// inserted if absent or all non-PK columns are overwritten if present.
6768            ///
6769            /// # Errors
6770            /// As [`Self::insert_on`].
6771            #[cfg(feature = "postgres")]
6772            pub async fn upsert(
6773                &mut self,
6774                pool: &#root::sql::sqlx::PgPool,
6775            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6776                #pool_to_upsert_on
6777            }
6778
6779            /// Like [`Self::upsert`] but accepts any sqlx executor.
6780            /// See [`Self::save_on`] for tenancy-scoped rationale.
6781            ///
6782            /// # Errors
6783            /// As [`Self::upsert`].
6784            #[cfg(feature = "postgres")]
6785            pub async fn upsert_on #executor_generics (
6786                &mut self,
6787                #executor_param,
6788            ) -> ::core::result::Result<(), #root::sql::ExecError>
6789            #executor_where
6790            {
6791                let mut _columns: ::std::vec::Vec<&'static str> =
6792                    ::std::vec::Vec::new();
6793                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6794                    ::std::vec::Vec::new();
6795                #( #upsert_pushes )*
6796                let query = #root::core::InsertQuery::new(
6797                    <Self as #root::core::Model>::SCHEMA,
6798                    _columns,
6799                    _values,
6800                )
6801                .returning(::std::vec![ #( #upsert_returning ),* ])
6802                .on_conflict(#conflict_clause);
6803                #upsert_run
6804                let _returning_row = &_returning_row_v;
6805                #( #upsert_auto_assigns )*
6806                #audit_upsert_emit
6807                ::core::result::Result::Ok(())
6808            }
6809        })
6810    } else {
6811        None
6812    };
6813
6814    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6815        let pk_column_lit = pk_column.as_str();
6816        // Optional `soft_delete_on` / `restore_on` companions when the
6817        // model has a `#[rustango(soft_delete)]` column. They land
6818        // alongside the regular `delete_on` so callers have both
6819        // options — a hard delete (audit-tracked as a real DELETE) and
6820        // a logical delete (audit-tracked as an UPDATE setting the
6821        // deleted_at column to NOW()).
6822        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6823            let col_lit = col;
6824            let sd_field_ident = fields
6825                .soft_delete_field_ident
6826                .clone()
6827                .expect("soft_delete_column without ident");
6828            quote! {
6829                /// Soft-delete this row by setting its
6830                /// `#[rustango(soft_delete)]` column to `NOW()`.
6831                /// The row stays in the table; query helpers can
6832                /// filter it out by checking the column for `IS NOT
6833                /// NULL`.
6834                ///
6835                /// # Errors
6836                /// As [`Self::delete`].
6837                #[cfg(feature = "postgres")]
6838                pub async fn soft_delete_on #executor_generics (
6839                    &self,
6840                    #executor_param,
6841                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6842                #executor_where
6843                {
6844                    let _now = #root::__chrono::Utc::now();
6845                    let _query = #root::soft_delete::__mark_query(
6846                        <Self as #root::core::Model>::SCHEMA,
6847                        #col_lit,
6848                        #pk_column_lit,
6849                        ::core::convert::Into::<#root::core::SqlValue>::into(
6850                            ::core::clone::Clone::clone(&self.#pk_ident)
6851                        ),
6852                        ::core::option::Option::Some(_now),
6853                    );
6854                    let _affected = #root::sql::__macro_internals::update_on(
6855                        #executor_passes_to_data_write,
6856                        &_query,
6857                    ).await?;
6858                    #audit_softdelete_emit
6859                    ::core::result::Result::Ok(_affected)
6860                }
6861
6862                /// Inverse of [`Self::soft_delete_on`] — clears the
6863                /// soft-delete column back to NULL so the row is
6864                /// considered live again.
6865                ///
6866                /// # Errors
6867                /// As [`Self::delete`].
6868                #[cfg(feature = "postgres")]
6869                pub async fn restore_on #executor_generics (
6870                    &self,
6871                    #executor_param,
6872                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6873                #executor_where
6874                {
6875                    let _query = #root::soft_delete::__mark_query(
6876                        <Self as #root::core::Model>::SCHEMA,
6877                        #col_lit,
6878                        #pk_column_lit,
6879                        ::core::convert::Into::<#root::core::SqlValue>::into(
6880                            ::core::clone::Clone::clone(&self.#pk_ident)
6881                        ),
6882                        ::core::option::Option::None,
6883                    );
6884                    let _affected = #root::sql::__macro_internals::update_on(
6885                        #executor_passes_to_data_write,
6886                        &_query,
6887                    ).await?;
6888                    #audit_restore_emit
6889                    ::core::result::Result::Ok(_affected)
6890                }
6891
6892                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6893                /// — takes [`#root::sql::Pool`] and dispatches per
6894                /// backend. Eloquent `Model::delete()` semantics on
6895                /// soft-delete-enabled models (closes #821 partial).
6896                ///
6897                /// Sets the `#[rustango(soft_delete)]` column to
6898                /// `NOW()` on every backend. Query helpers
6899                /// (`QuerySet::active()` / `only_trashed()`,
6900                /// `soft_delete::active_filter` /
6901                /// `compose_with_active`) filter trashed rows out by
6902                /// reading `IS NULL` on the same column. Keyed on the PK;
6903                /// global scopes do not apply.
6904                ///
6905                /// # Errors
6906                /// As [`#root::sql::update_pool`].
6907                pub async fn soft_delete(
6908                    &self,
6909                    pool: &#root::sql::Pool,
6910                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6911                    let _now = #root::__chrono::Utc::now();
6912                    let _query = #root::soft_delete::__mark_query(
6913                        <Self as #root::core::Model>::SCHEMA,
6914                        #col_lit,
6915                        #pk_column_lit,
6916                        ::core::convert::Into::<#root::core::SqlValue>::into(
6917                            ::core::clone::Clone::clone(&self.#pk_ident)
6918                        ),
6919                        ::core::option::Option::Some(_now),
6920                    );
6921                    #pool_softdelete_run
6922                }
6923
6924                /// Tri-dialect counterpart of [`Self::restore_on`].
6925                /// Clears the `#[rustango(soft_delete)]` column back
6926                /// to `NULL`, marking the row live again. Eloquent
6927                /// `Model::restore()` parity. Keyed on the PK; global
6928                /// scopes do not apply.
6929                ///
6930                /// # Errors
6931                /// As [`#root::sql::update_pool`].
6932                pub async fn restore(
6933                    &self,
6934                    pool: &#root::sql::Pool,
6935                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6936                    let _query = #root::soft_delete::__mark_query(
6937                        <Self as #root::core::Model>::SCHEMA,
6938                        #col_lit,
6939                        #pk_column_lit,
6940                        ::core::convert::Into::<#root::core::SqlValue>::into(
6941                            ::core::clone::Clone::clone(&self.#pk_ident)
6942                        ),
6943                        ::core::option::Option::None,
6944                    );
6945                    #pool_restore_run
6946                }
6947
6948                /// Hard-delete this row, ignoring the soft-delete
6949                /// column. Eloquent `Model::forceDelete()` parity —
6950                /// the escape hatch when you need to actually purge
6951                /// data (GDPR, fixture cleanup, etc.).
6952                ///
6953                /// Equivalent to [`Self::delete_pool`] (the framework's
6954                /// non-soft delete) — exposed under the Eloquent name
6955                /// for muscle-memory + so soft-delete-enabled models
6956                /// have all three operations (soft / restore / force)
6957                /// in one place.
6958                ///
6959                /// # Errors
6960                /// As [`Self::delete_pool`].
6961                pub async fn force_delete(
6962                    &self,
6963                    pool: &#root::sql::Pool,
6964                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6965                    Self::delete_pool(self, pool).await
6966                }
6967
6968                /// Returns `true` when this row is soft-deleted (its
6969                /// `#[rustango(soft_delete)]` column is currently
6970                /// set — Eloquent `$model->trashed()` parity).
6971                ///
6972                /// Pure in-memory predicate over `&self`; does not
6973                /// hit the database. Useful in admin/template code
6974                /// like `{% if post.trashed() %}…{% endif %}` and in
6975                /// guard clauses on restore/force-delete flows.
6976                pub fn trashed(&self) -> bool {
6977                    ::core::option::Option::is_some(&self.#sd_field_ident)
6978                }
6979
6980                /// Fetch every row whose `#[rustango(soft_delete)]`
6981                /// column is `NULL` (a.k.a. the live, non-trashed
6982                /// rows). Eloquent's default `Model::all()` behavior on
6983                /// a soft-delete model (Eloquent auto-scopes trashed
6984                /// rows out; rustango doesn't have auto-scoping yet —
6985                /// see issue #820 — so this is the explicit shortcut).
6986                ///
6987                /// One-liner over `QuerySet::<Self>::default()
6988                /// .active().fetch(pool)`. Closes #821 partial.
6989                ///
6990                /// # Errors
6991                /// As [`#root::sql::FetcherPool::fetch`].
6992                pub async fn active(
6993                    pool: &#root::sql::Pool,
6994                ) -> ::core::result::Result<
6995                    ::std::vec::Vec<Self>,
6996                    #root::sql::ExecError,
6997                > {
6998                    use #root::sql::FetcherPool as _;
6999                    #root::query::QuerySet::<Self>::default()
7000                        .active()
7001                        .fetch(pool)
7002                        .await
7003                }
7004
7005                /// Fetch ONLY soft-deleted rows. Eloquent
7006                /// `Model::onlyTrashed()->get()` parity — drives the
7007                /// admin "Trash" page, restore flows, GDPR purge
7008                /// scans, etc.
7009                ///
7010                /// One-liner over `QuerySet::<Self>::default()
7011                /// .only_trashed().fetch(pool)`. Closes #821
7012                /// partial.
7013                ///
7014                /// # Errors
7015                /// As [`#root::sql::FetcherPool::fetch`].
7016                pub async fn only_trashed(
7017                    pool: &#root::sql::Pool,
7018                ) -> ::core::result::Result<
7019                    ::std::vec::Vec<Self>,
7020                    #root::sql::ExecError,
7021                > {
7022                    use #root::sql::FetcherPool as _;
7023                    #root::query::QuerySet::<Self>::default()
7024                        .only_trashed()
7025                        .fetch(pool)
7026                        .await
7027                }
7028
7029                /// Fetch every row, both live and soft-deleted.
7030                /// Eloquent `Model::withTrashed()->get()` parity.
7031                ///
7032                /// Today every queryset already includes trashed rows
7033                /// (rustango has no global-scope tracking yet — issue
7034                /// #820), so this is functionally equivalent to
7035                /// [`Self::all`]. Exposed as a named shortcut so
7036                /// soft-delete-aware code reads `Model::with_trashed_pool`
7037                /// rather than `Model::all_pool` — keeps intent visible
7038                /// in callers and stays correct when auto-scoping lands.
7039                ///
7040                /// Closes #821 partial.
7041                ///
7042                /// # Errors
7043                /// As [`#root::sql::FetcherPool::fetch`].
7044                pub async fn with_trashed(
7045                    pool: &#root::sql::Pool,
7046                ) -> ::core::result::Result<
7047                    ::std::vec::Vec<Self>,
7048                    #root::sql::ExecError,
7049                > {
7050                    use #root::sql::FetcherPool as _;
7051                    #root::query::QuerySet::<Self>::default()
7052                        .with_trashed()
7053                        .fetch(pool)
7054                        .await
7055                }
7056
7057            }
7058        } else {
7059            quote!()
7060        };
7061        quote! {
7062            /// Delete the row identified by this instance's primary key.
7063            ///
7064            /// Returns the number of rows affected (0 or 1).
7065            ///
7066            /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7067            /// a driver-specific `PgPool`, so it does not exist on a build
7068            /// without the `postgres` feature. [`Self::delete_pool`] takes
7069            /// `rustango::sql::Pool` and works on all three. The naming is
7070            /// inverted; tracked in #1293.
7071            ///
7072            /// # Errors
7073            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7074            /// driver failures.
7075            #[cfg(feature = "postgres")]
7076            pub async fn delete(
7077                &self,
7078                pool: &#root::sql::sqlx::PgPool,
7079            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7080                #pool_to_delete_on
7081            }
7082
7083            /// Like [`Self::delete`] but accepts any sqlx executor —
7084            /// for tenant-scoped deletes against an explicitly-acquired
7085            /// connection. See [`Self::save_on`] for the rationale.
7086            ///
7087            /// # Errors
7088            /// As [`Self::delete`].
7089            #[cfg(feature = "postgres")]
7090            pub async fn delete_on #executor_generics (
7091                &self,
7092                #executor_param,
7093            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7094            #executor_where
7095            {
7096                let query = #root::core::DeleteQuery::by_pk(
7097                    <Self as #root::core::Model>::SCHEMA,
7098                    #pk_column_lit,
7099                    ::core::convert::Into::<#root::core::SqlValue>::into(
7100                        ::core::clone::Clone::clone(&self.#pk_ident)
7101                    ),
7102                );
7103                let _affected = #root::sql::__macro_internals::delete_on(
7104                    #executor_passes_to_data_write,
7105                    &query,
7106                ).await?;
7107                #audit_delete_emit
7108                ::core::result::Result::Ok(_affected)
7109            }
7110
7111            /// Per-call audit-source override for [`Self::delete_on`].
7112            /// See [`Self::save_on_with`] for shape rationale.
7113            ///
7114            /// # Errors
7115            /// As [`Self::delete_on`].
7116            #[cfg(feature = "postgres")]
7117            pub async fn delete_on_with #executor_generics (
7118                &self,
7119                #executor_param,
7120                source: #root::audit::AuditSource,
7121            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7122            #executor_where
7123            {
7124                #root::audit::with_source(source, self.delete_on(_executor)).await
7125            }
7126            #pool_delete_method
7127            #pool_insert_method
7128            #audit_entry_method
7129            #pool_save_method
7130            #refresh_replicate_methods
7131            #tx_delete_method
7132            #tx_insert_method
7133            #tx_save_method
7134            #soft_delete_methods
7135
7136            /// Returns `true` when `other` represents the same DB
7137            /// row as `self` — i.e. their primary keys compare
7138            /// equal. Eloquent `$model->is($other)` parity.
7139            ///
7140            /// Because both arguments are typed `&Self`, the
7141            /// model/table check is automatic — `Post::is` cannot
7142            /// be invoked against a `Comment` at compile time. Only
7143            /// the PK has to be compared at runtime.
7144            pub fn is(&self, other: &Self) -> bool {
7145                self.#pk_ident == other.#pk_ident
7146            }
7147
7148            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7149            /// parity.
7150            pub fn is_not(&self, other: &Self) -> bool {
7151                self.#pk_ident != other.#pk_ident
7152            }
7153
7154            /// Returns this row's primary-key value as an
7155            /// [`#root::core::SqlValue`]. Eloquent
7156            /// `$model->getKey()` parity.
7157            ///
7158            /// Useful when you need to thread the PK through a
7159            /// generic `Into<SqlValue>`-bound API without knowing the
7160            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7161            #[must_use]
7162            pub fn get_key(&self) -> #root::core::SqlValue {
7163                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7164            }
7165
7166        }
7167    });
7168
7169    let insert_method = if fields.has_auto {
7170        let pushes = &fields.insert_pushes;
7171        let returning_cols = &fields.returning_cols;
7172        let auto_assigns = &fields.auto_assigns;
7173        quote! {
7174            /// Insert this row into its table. Skips columns whose
7175            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7176            /// sequence fills them in, then reads each `Auto` column
7177            /// back via `RETURNING` and stores it on `self`.
7178            ///
7179            /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7180            /// a driver-specific `PgPool`, so it does not exist on a build
7181            /// without the `postgres` feature. [`Self::insert_pool`] takes
7182            /// `rustango::sql::Pool` and works on all three. The naming is
7183            /// inverted; tracked in #1293.
7184            ///
7185            /// # Errors
7186            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7187            /// driver failures.
7188            #[cfg(feature = "postgres")]
7189            pub async fn insert(
7190                &mut self,
7191                pool: &#root::sql::sqlx::PgPool,
7192            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7193                #pool_to_insert_on
7194            }
7195
7196            /// Like [`Self::insert`] but accepts any sqlx executor.
7197            /// See [`Self::save_on`] for tenancy-scoped rationale.
7198            ///
7199            /// # Errors
7200            /// As [`Self::insert`].
7201            #[cfg(feature = "postgres")]
7202            pub async fn insert_on #executor_generics (
7203                &mut self,
7204                #executor_param,
7205            ) -> ::core::result::Result<(), #root::sql::ExecError>
7206            #executor_where
7207            {
7208                let mut _columns: ::std::vec::Vec<&'static str> =
7209                    ::std::vec::Vec::new();
7210                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7211                    ::std::vec::Vec::new();
7212                #( #pushes )*
7213                let query = #root::core::InsertQuery::new(
7214                    <Self as #root::core::Model>::SCHEMA,
7215                    _columns,
7216                    _values,
7217                )
7218                .returning(::std::vec![ #( #returning_cols ),* ]);
7219                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7220                    #executor_passes_to_data_write,
7221                    &query,
7222                ).await?;
7223                let _returning_row = &_returning_row_v;
7224                #( #auto_assigns )*
7225                #audit_insert_emit
7226                ::core::result::Result::Ok(())
7227            }
7228
7229            /// Per-call audit-source override for [`Self::insert_on`].
7230            /// See [`Self::save_on_with`] for shape rationale.
7231            ///
7232            /// # Errors
7233            /// As [`Self::insert_on`].
7234            #[cfg(feature = "postgres")]
7235            pub async fn insert_on_with #executor_generics (
7236                &mut self,
7237                #executor_param,
7238                source: #root::audit::AuditSource,
7239            ) -> ::core::result::Result<(), #root::sql::ExecError>
7240            #executor_where
7241            {
7242                #root::audit::with_source(source, self.insert_on(_executor)).await
7243            }
7244        }
7245    } else {
7246        let insert_columns = &fields.insert_columns;
7247        let insert_values = &fields.insert_values;
7248        quote! {
7249            /// Insert this row into its table.
7250            ///
7251            /// # Errors
7252            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7253            /// driver failures.
7254            #[cfg(feature = "postgres")]
7255            pub async fn insert(
7256                &self,
7257                pool: &#root::sql::sqlx::PgPool,
7258            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7259                self.insert_on(pool).await
7260            }
7261
7262            /// Like [`Self::insert`] but accepts any sqlx executor.
7263            /// See [`Self::save_on`] for tenancy-scoped rationale.
7264            ///
7265            /// # Errors
7266            /// As [`Self::insert`].
7267            #[cfg(feature = "postgres")]
7268            pub async fn insert_on<'_c, _E>(
7269                &self,
7270                _executor: _E,
7271            ) -> ::core::result::Result<(), #root::sql::ExecError>
7272            where
7273                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7274            {
7275                let query = #root::core::InsertQuery::new(
7276                    <Self as #root::core::Model>::SCHEMA,
7277                    ::std::vec![ #( #insert_columns ),* ],
7278                    ::std::vec![ #( #insert_values ),* ],
7279                );
7280                #root::sql::__macro_internals::insert_on(_executor, &query).await
7281            }
7282        }
7283    };
7284
7285    let bulk_insert_method = if fields.has_auto {
7286        let cols_no_auto = &fields.bulk_columns_no_auto;
7287        let cols_all = &fields.bulk_columns_all;
7288        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7289        let pushes_all = &fields.bulk_pushes_all;
7290        let returning_cols = &fields.returning_cols;
7291        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7292        let uniformity = &fields.bulk_auto_uniformity;
7293        let fill_in_place = &fields.bulk_fill_in_place;
7294        // Nothing to read back when every Auto field is filled Rust-side (#1934).
7295        let read_back = if fields.returning_cols.is_empty() {
7296            quote!()
7297        } else {
7298            quote! {
7299                if _returned.len() != rows.len() {
7300                    return ::core::result::Result::Err(
7301                        #root::sql::ExecError::Sql(
7302                            #root::sql::SqlError::BulkInsertReturningMismatch {
7303                                expected: rows.len(),
7304                                actual: _returned.len(),
7305                            }
7306                        )
7307                    );
7308                }
7309                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7310                    #auto_assigns_for_row
7311                }
7312            }
7313        };
7314        // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7315        let first_auto_ident = fields
7316            .first_db_auto
7317            .as_ref()
7318            .map(|(ident, _)| ident)
7319            .or(fields.first_auto_ident.as_ref())
7320            .expect("has_auto implies first_auto_ident is Some");
7321        quote! {
7322            /// Bulk-insert `rows` in a single round-trip. Every row's
7323            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7324            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7325            /// (caller-supplied values). Mixed Set/Unset is rejected
7326            /// — call `insert` per row for that case.
7327            ///
7328            /// Empty slice is a no-op. Each row's `Auto` fields are
7329            /// populated from the `RETURNING` clause in input order
7330            /// before this returns.
7331            ///
7332            /// # Errors
7333            /// Returns [`#root::sql::ExecError`] for validation,
7334            /// SQL-writing, mixed-Auto rejection, or driver failures.
7335            #[cfg(feature = "postgres")]
7336            pub async fn bulk_insert(
7337                rows: &mut [Self],
7338                pool: &#root::sql::sqlx::PgPool,
7339            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7340                #pool_to_bulk_insert_on
7341            }
7342
7343            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7344            /// See [`Self::save_on`] for tenancy-scoped rationale.
7345            ///
7346            /// # Errors
7347            /// As [`Self::bulk_insert`].
7348            #[cfg(feature = "postgres")]
7349            pub async fn bulk_insert_on #executor_generics (
7350                rows: &mut [Self],
7351                #executor_param,
7352            ) -> ::core::result::Result<(), #root::sql::ExecError>
7353            #executor_where
7354            {
7355                if rows.is_empty() {
7356                    return ::core::result::Result::Ok(());
7357                }
7358                let _first_unset = matches!(
7359                    rows[0].#first_auto_ident,
7360                    #root::sql::Auto::Unset
7361                );
7362                #( #uniformity )*
7363                for _row in rows.iter_mut() {
7364                    #( #fill_in_place )*
7365                }
7366
7367                let mut _all_rows: ::std::vec::Vec<
7368                    ::std::vec::Vec<#root::core::SqlValue>,
7369                > = ::std::vec::Vec::with_capacity(rows.len());
7370                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7371                    for _row in rows.iter() {
7372                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7373                            ::std::vec::Vec::new();
7374                        #( #pushes_no_auto )*
7375                        _all_rows.push(_row_vals);
7376                    }
7377                    ::std::vec![ #( #cols_no_auto ),* ]
7378                } else {
7379                    for _row in rows.iter() {
7380                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7381                            ::std::vec::Vec::new();
7382                        #( #pushes_all )*
7383                        _all_rows.push(_row_vals);
7384                    }
7385                    ::std::vec![ #( #cols_all ),* ]
7386                };
7387
7388                let _query = #root::core::BulkInsertQuery::new(
7389                    <Self as #root::core::Model>::SCHEMA,
7390                    _columns,
7391                    _all_rows,
7392                )
7393                .returning(::std::vec![ #( #returning_cols ),* ]);
7394                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7395                    #executor_passes_to_data_write,
7396                    &_query,
7397                ).await?;
7398                #read_back
7399                #audit_bulk_insert_emit
7400                ::core::result::Result::Ok(())
7401            }
7402        }
7403    } else {
7404        let cols_all = &fields.bulk_columns_all;
7405        let pushes_all = &fields.bulk_pushes_all;
7406        quote! {
7407            /// Bulk-insert `rows` in a single round-trip. Every row's
7408            /// fields are written verbatim — there are no `Auto<T>`
7409            /// fields on this model.
7410            ///
7411            /// Empty slice is a no-op.
7412            ///
7413            /// # Errors
7414            /// Returns [`#root::sql::ExecError`] for validation,
7415            /// SQL-writing, or driver failures.
7416            #[cfg(feature = "postgres")]
7417            pub async fn bulk_insert(
7418                rows: &[Self],
7419                pool: &#root::sql::sqlx::PgPool,
7420            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7421                #pool_to_bulk_insert_on
7422            }
7423
7424            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7425            /// See [`Self::save_on`] for tenancy-scoped rationale.
7426            ///
7427            /// # Errors
7428            /// As [`Self::bulk_insert`].
7429            #[cfg(feature = "postgres")]
7430            pub async fn bulk_insert_on #executor_generics (
7431                rows: &[Self],
7432                #executor_param,
7433            ) -> ::core::result::Result<(), #root::sql::ExecError>
7434            #executor_where
7435            {
7436                if rows.is_empty() {
7437                    return ::core::result::Result::Ok(());
7438                }
7439                let mut _all_rows: ::std::vec::Vec<
7440                    ::std::vec::Vec<#root::core::SqlValue>,
7441                > = ::std::vec::Vec::with_capacity(rows.len());
7442                for _row in rows.iter() {
7443                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7444                        ::std::vec::Vec::new();
7445                    #( #pushes_all )*
7446                    _all_rows.push(_row_vals);
7447                }
7448                let _query = #root::core::BulkInsertQuery::new(
7449                    <Self as #root::core::Model>::SCHEMA,
7450                    ::std::vec![ #( #cols_all ),* ],
7451                    _all_rows,
7452                );
7453                let _ = #root::sql::__macro_internals::bulk_insert_on(
7454                    #executor_passes_to_data_write,
7455                    &_query,
7456                ).await?;
7457                #audit_bulk_insert_emit
7458                ::core::result::Result::Ok(())
7459            }
7460        }
7461    };
7462
7463    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7464    // (no postgres-feature gate); routes through the existing
7465    // `bulk_insert_pool` + per-dialect conflict writer.
7466    //
7467    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7468    // sequence picks the PK for fresh inserts; the UPDATE branch never
7469    // touches the Auto column.
7470    // Audited models audit each written row; MySQL, with no RETURNING,
7471    // is refused there (#1747, #1795).
7472    let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7473        (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7474        (true, true) => quote! {
7475            #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7476                _r.__rustango_audit_entry(_op)
7477            })
7478            .await
7479        },
7480        (true, false) => quote! {
7481            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7482                table: <Self as #root::core::Model>::SCHEMA.table,
7483            })
7484        },
7485    };
7486    let bulk_upsert_pool_method = {
7487        // Pick the "no Auto" columns when the model has Auto fields,
7488        // else every column.
7489        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7490            (
7491                fields.bulk_columns_no_auto.clone(),
7492                fields.bulk_pushes_no_auto.clone(),
7493            )
7494        } else {
7495            (
7496                fields.bulk_columns_all.clone(),
7497                fields.bulk_pushes_all.clone(),
7498            )
7499        };
7500        quote! {
7501            /// Tri-dialect bulk insert that updates on conflict — the
7502            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7503            ///
7504            /// Per-row values are extracted and lowered into a
7505            /// [`#root::core::BulkInsertQuery`] with
7506            /// `on_conflict = DoUpdate { target, update_columns }`. The
7507            /// writer dispatches per-dialect:
7508            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7509            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7510            ///
7511            /// `target` names the column(s) whose unique constraint
7512            /// defines the conflict (typically a `unique` or
7513            /// `unique_together` natural-key column, NOT the `Auto<T>`
7514            /// PK). `update_cols` names the columns to overwrite on
7515            /// conflict — every other column is left untouched on the
7516            /// existing row.
7517            ///
7518            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7519            /// picks the PK on insert; the update path never touches
7520            /// the Auto column). Auto-set rows trigger a hard error.
7521            /// Empty slice is a no-op. On an audited model each written
7522            /// row gets a `create` or `update` audit row; MySQL refuses it.
7523            ///
7524            /// # Errors
7525            /// Returns [`#root::sql::ExecError`] for validation,
7526            /// SQL-writing, or driver failures.
7527            pub async fn bulk_upsert_pool(
7528                rows: &[Self],
7529                target: &[&'static str],
7530                update_cols: &[&'static str],
7531                pool: &#root::sql::Pool,
7532            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7533                if rows.is_empty() {
7534                    return ::core::result::Result::Ok(());
7535                }
7536                let mut _all_rows: ::std::vec::Vec<
7537                    ::std::vec::Vec<#root::core::SqlValue>,
7538                > = ::std::vec::Vec::with_capacity(rows.len());
7539                for _row in rows.iter() {
7540                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7541                        ::std::vec::Vec::new();
7542                    #( #upsert_pushes )*
7543                    _all_rows.push(_row_vals);
7544                }
7545                let _query = #root::core::BulkInsertQuery::new(
7546                    <Self as #root::core::Model>::SCHEMA,
7547                    ::std::vec![ #( #upsert_cols ),* ],
7548                    _all_rows,
7549                )
7550                .on_conflict_do_update(target, update_cols);
7551                #run_conflict_bulk_insert
7552            }
7553
7554            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7555            /// skip rows that would violate a unique constraint. Issue
7556            /// #267 / T1.5. Same per-dialect dispatch as
7557            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7558            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7559            /// <pivot> = <pivot>` (MySQL no-op write).
7560            ///
7561            /// # Errors
7562            /// As [`Self::bulk_upsert_pool`].
7563            pub async fn bulk_insert_or_ignore_pool(
7564                rows: &[Self],
7565                pool: &#root::sql::Pool,
7566            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7567                if rows.is_empty() {
7568                    return ::core::result::Result::Ok(());
7569                }
7570                let mut _all_rows: ::std::vec::Vec<
7571                    ::std::vec::Vec<#root::core::SqlValue>,
7572                > = ::std::vec::Vec::with_capacity(rows.len());
7573                for _row in rows.iter() {
7574                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7575                        ::std::vec::Vec::new();
7576                    #( #upsert_pushes )*
7577                    _all_rows.push(_row_vals);
7578                }
7579                let _query = #root::core::BulkInsertQuery::new(
7580                    <Self as #root::core::Model>::SCHEMA,
7581                    ::std::vec![ #( #upsert_cols ),* ],
7582                    _all_rows,
7583                )
7584                .on_conflict_do_nothing();
7585                #run_conflict_bulk_insert
7586            }
7587        }
7588    };
7589
7590    // Ergonomic `Model::bulk_update(objs, fields)`.
7591    // The SQL/IR/executor stack
7592    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7593    // `write_bulk_update_*` writers) already existed; what was missing
7594    // was the per-model constructor that maps `&[Self]` + a runtime
7595    // column list into rows of `[pk, col_vals…]` so callers don't
7596    // hand-build the IR. Emitted only when the model has a primary key
7597    // (the PK is the join key and can't itself be updated).
7598    let bulk_update_rows_run = if audited_fields.is_some() {
7599        quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7600            _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7601        })
7602        .await)
7603    } else {
7604        quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7605    };
7606    let bulk_update_method = match &fields.primary_key {
7607        None => quote! {},
7608        Some((pk_ident, pk_col)) => {
7609            // One pair of match arms per non-PK column: a validation arm
7610            // resolving the runtime name to its `&'static str` column,
7611            // and a value arm pushing that field off the row.
7612            let mut col_arms: Vec<TokenStream2> = Vec::new();
7613            let mut val_arms: Vec<TokenStream2> = Vec::new();
7614            for entry in &fields.column_entries {
7615                if &entry.column == pk_col {
7616                    continue;
7617                }
7618                let col = &entry.column;
7619                let ident = &entry.ident;
7620                col_arms.push(quote! { #col => #col, });
7621                val_arms.push(quote! {
7622                    #col => _row_vals.push(
7623                        ::core::convert::Into::<#root::core::SqlValue>::into(
7624                            ::core::clone::Clone::clone(&_o.#ident)
7625                        )
7626                    ),
7627                });
7628            }
7629            quote! {
7630                /// Write per-row-different values for the named `fields`
7631                /// across every object in `objs`, matched by primary key.
7632                /// Past the backend's bind cap the rows go in batches inside
7633                /// one transaction, so the update is all or none.
7634                ///
7635                /// `fields` names the **columns** to update. The primary
7636                /// key identifies each row and cannot itself be updated
7637                /// (pass it and you get
7638                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7639                /// Empty `objs` or `fields` is a no-op returning `0`.
7640                /// Objects whose PK matches no row are simply not updated.
7641                /// Returns the number of rows affected.
7642                ///
7643                /// Tri-dialect: lowers to a
7644                /// [`#root::core::BulkUpdateQuery`] and dispatches
7645                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7646                /// a CTE + correlated subquery on SQLite, an inner
7647                /// `JOIN (VALUES …)` on MySQL.
7648                ///
7649                /// # Errors
7650                /// [`#root::core::QueryError::UnknownField`] for a name
7651                /// that isn't a column on this model,
7652                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7653                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7654                /// SQL-writing / driver failures.
7655                pub async fn bulk_update(
7656                    objs: &[Self],
7657                    fields: &[&str],
7658                    pool: &#root::sql::Pool,
7659                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7660                    if objs.is_empty() || fields.is_empty() {
7661                        return ::core::result::Result::Ok(0);
7662                    }
7663                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7664                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7665                        ::std::vec::Vec::with_capacity(fields.len());
7666                    for &_f in fields {
7667                        let _col: &'static str = match _f {
7668                            #pk_col => {
7669                                return ::core::result::Result::Err(
7670                                    ::core::convert::Into::into(
7671                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7672                                            model: _model_name,
7673                                            field: ::std::string::ToString::to_string(_f),
7674                                        }
7675                                    )
7676                                );
7677                            }
7678                            #( #col_arms )*
7679                            _ => {
7680                                return ::core::result::Result::Err(
7681                                    ::core::convert::Into::into(
7682                                        #root::core::QueryError::UnknownField {
7683                                            model: _model_name,
7684                                            field: ::std::string::ToString::to_string(_f),
7685                                        }
7686                                    )
7687                                );
7688                            }
7689                        };
7690                        // A model whose only field is the primary key emits no
7691                        // non-PK arms, so every arm above diverges and this push
7692                        // is genuinely unreachable — correct, since such a model
7693                        // has nothing to bulk-update. Scoped to the statement
7694                        // rather than the fn so a real unreachability elsewhere
7695                        // in this body still warns. Without it every PK-only
7696                        // model warns in the *user's* crate, which they cannot
7697                        // act on (#1290).
7698                        #[allow(unreachable_code)]
7699                        _update_columns.push(_col);
7700                    }
7701                    let mut _rows: ::std::vec::Vec<
7702                        ::std::vec::Vec<#root::core::SqlValue>,
7703                    > = ::std::vec::Vec::with_capacity(objs.len());
7704                    for _o in objs.iter() {
7705                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7706                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7707                        // PK first — the writers expect `[pk, …update cols]`.
7708                        _row_vals.push(
7709                            ::core::convert::Into::<#root::core::SqlValue>::into(
7710                                ::core::clone::Clone::clone(&_o.#pk_ident)
7711                            )
7712                        );
7713                        for &_f in fields {
7714                            match _f {
7715                                #( #val_arms )*
7716                                // Unreachable: every name was validated
7717                                // against the same arm set above.
7718                                _ => {}
7719                            }
7720                        }
7721                        _rows.push(_row_vals);
7722                    }
7723                    let _query = #root::core::BulkUpdateQuery::new(
7724                        <Self as #root::core::Model>::SCHEMA,
7725                        _update_columns,
7726                        _rows,
7727                    );
7728                    #bulk_update_rows_run
7729                }
7730            }
7731        }
7732    };
7733
7734    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7735        quote! {
7736            /// Hidden runtime accessor for the primary-key value as a
7737            /// [`SqlValue`]. Used by reverse-relation helpers
7738            /// (`<parent>::<child>_set`) emitted from sibling models'
7739            /// FK fields. Not part of the public API.
7740            #[doc(hidden)]
7741            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7742                ::core::convert::Into::<#root::core::SqlValue>::into(
7743                    ::core::clone::Clone::clone(&self.#pk_ident)
7744                )
7745            }
7746        }
7747    });
7748
7749    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7750        quote! {
7751            impl #root::sql::HasPkValue for #struct_name {
7752                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7753                    ::core::convert::Into::<#root::core::SqlValue>::into(
7754                        ::core::clone::Clone::clone(&self.#pk_ident)
7755                    )
7756                }
7757            }
7758        }
7759    });
7760
7761    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7762
7763    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7764    // dispatch to the right per-backend body without the macro emitting
7765    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7766    // so audited models with non-Auto PKs (which still go through
7767    // `insert_one_with_audit` → `apply_auto_pk`) link.
7768    let assign_auto_pk_pool_impl = {
7769        let auto_assigns = &fields.auto_assigns;
7770        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7771        // so the body is structurally identical to `auto_assigns` —
7772        // only the helper name swaps from `try_get_returning` to
7773        // `try_get_returning_sqlite` so the closure typechecks against
7774        // a `SqliteRow` instead of a `PgRow`.
7775        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7776            .auto_field_idents
7777            .iter()
7778            .map(|(ident, column)| {
7779                quote! {
7780                    self.#ident = #root::sql::try_get_returning_sqlite(
7781                        _returning_row, #column
7782                    )?;
7783                }
7784            })
7785            .collect();
7786        // #1028 — decode each `generated_as` column from the same
7787        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7788        // own type drives the decode (no `Auto<T>` wrapper).
7789        let generated_assigns: Vec<TokenStream2> = fields
7790            .generated_field_idents
7791            .iter()
7792            .map(|(ident, column)| {
7793                quote! {
7794                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7795                }
7796            })
7797            .collect();
7798        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7799            .generated_field_idents
7800            .iter()
7801            .map(|(ident, column)| {
7802                quote! {
7803                    self.#ident = #root::sql::try_get_returning_sqlite(
7804                        _returning_row, #column
7805                    )?;
7806                }
7807            })
7808            .collect();
7809        let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
7810            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7811            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7812            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7813            // those models can't use AUTO_INCREMENT). The trait is only
7814            // touched on the MySQL arm at runtime, so PG-only consumers
7815            // never see the bound failure.
7816            //
7817            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7818            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7819            // runtime with "multi-column RETURNING". MySQL has no
7820            // multi-column RETURNING semantic and a follow-up SELECT
7821            // would need cross-trait plumbing. Pragmatic shape: succeed
7822            // with the FIRST Auto field populated from LAST_INSERT_ID();
7823            // any other Auto fields stay `Auto::Unset`. Callers that
7824            // need the DB-defaulted timestamp / UUID can re-fetch the
7825            // row by PK after `save_pool`. Fixes the cookbook chapter
7826            // 12 dialect divergence.
7827            quote! {
7828                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7829                    ::rustango_from_mysql_auto_id(_id)?;
7830                self.#first = #root::sql::Auto::Set(_converted);
7831                ::core::result::Result::Ok(())
7832            }
7833        } else {
7834            quote! {
7835                let _ = _id;
7836                ::core::result::Result::Ok(())
7837            }
7838        };
7839        quote! {
7840            impl #root::sql::AssignAutoPkPool for #struct_name {
7841                fn __rustango_assign_from_pg_row(
7842                    &mut self,
7843                    _returning_row: &#root::sql::PgReturningRow,
7844                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7845                    #( #auto_assigns )*
7846                    #( #generated_assigns )*
7847                    ::core::result::Result::Ok(())
7848                }
7849                fn __rustango_assign_from_mysql_id(
7850                    &mut self,
7851                    _id: i64,
7852                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7853                    #mysql_body
7854                }
7855                fn __rustango_assign_from_sqlite_row(
7856                    &mut self,
7857                    _returning_row: &#root::sql::SqliteReturningRow,
7858                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7859                    #( #auto_assigns_sqlite )*
7860                    #( #generated_assigns_sqlite )*
7861                    ::core::result::Result::Ok(())
7862                }
7863            }
7864        }
7865    };
7866
7867    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7868    let aliased_row_helper = quote! {
7869        /// Decode a row's aliased target columns (produced by
7870        /// `select_related`'s LEFT JOIN) into a fresh instance of
7871        /// this model. Reads each column via
7872        /// `format!("{prefix}__{col}")`, matching the alias the
7873        /// SELECT writer emitted. Slice 9.0d.
7874        #[doc(hidden)]
7875        #[cfg(feature = "postgres")]
7876        pub fn __rustango_from_aliased_row(
7877            row: &#root::sql::sqlx::postgres::PgRow,
7878            prefix: &str,
7879        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7880            ::core::result::Result::Ok(Self {
7881                #( #from_aliased_row_inits ),*
7882            })
7883        }
7884    };
7885    // v0.23.0-batch8 — MySQL counterpart, gated through the
7886    // cfg-aware macro_rules so PG-only builds expand to nothing.
7887    let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
7888    let aliased_row_helper_my = quote! {
7889        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7890            #( #from_aliased_row_inits_my ),*
7891        });
7892    };
7893
7894    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7895    // pattern + cfg gate on the `sqlite` feature.
7896    let aliased_row_helper_sqlite = quote! {
7897        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7898            #( #from_aliased_row_inits ),*
7899        });
7900    };
7901
7902    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7903    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7904    let load_related_impl_sqlite =
7905        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7906
7907    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7908    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7909    // default `Self::objects()`. Each accessor returns a fresh
7910    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7911    // methods the user defined.
7912    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7913        .iter()
7914        .map(|fn_ident| {
7915            let model_name_str = struct_name.to_string();
7916            let fn_name_str = fn_ident.to_string();
7917            let doc = format!(
7918                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7919                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7920                 equivalent to `Self::objects()`. Chains with any \
7921                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7922                 extension methods."
7923            );
7924            quote! {
7925                #[doc = #doc]
7926                #[must_use]
7927                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7928                    #root::query::QuerySet::new()
7929                }
7930            }
7931        })
7932        .collect();
7933
7934    quote! {
7935        impl #struct_name {
7936            /// Start a new `QuerySet` over this model.
7937            #[must_use]
7938            pub fn objects() -> #root::query::QuerySet<#struct_name> {
7939                #root::query::QuerySet::new()
7940            }
7941
7942            /// Eloquent-shape alias of [`Self::objects`]. Returns
7943            /// a fresh `QuerySet<Self>` ready for `.filter()` /
7944            /// `.where_()` / etc. Matches Laravel muscle-memory:
7945            ///
7946            /// ```ignore
7947            /// // Eloquent:    Post::query()->where('published', true)
7948            /// // rustango:    Post::query().filter("published", true)
7949            /// //         or:  Post::objects().filter("published", true)
7950            /// ```
7951            ///
7952            /// Both names point at the same underlying constructor;
7953            /// neither is preferred.
7954            #[must_use]
7955            pub fn query() -> #root::query::QuerySet<#struct_name> {
7956                #root::query::QuerySet::new()
7957            }
7958
7959            #( #extra_manager_fns )*
7960
7961            #insert_method
7962
7963            #bulk_insert_method
7964
7965            #bulk_upsert_pool_method
7966
7967            #bulk_update_method
7968
7969            #save_method
7970
7971            #pk_methods
7972
7973            #pk_value_helper
7974
7975            #aliased_row_helper
7976
7977            #column_consts
7978        }
7979
7980        #aliased_row_helper_my
7981
7982        #aliased_row_helper_sqlite
7983
7984        #load_related_impl
7985
7986        #load_related_impl_my
7987
7988        #load_related_impl_sqlite
7989
7990        #has_pk_value_impl
7991
7992        #fk_pk_access_impl
7993
7994        #assign_auto_pk_pool_impl
7995    }
7996}
7997
7998/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7999/// `auto_assigns` but reading from `_returning_row` and writing to
8000/// `_row_mut` instead of `self`.
8001fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8002    let root = rustango_root();
8003    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
8004        let col_lit = column.as_str();
8005        quote! {
8006            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8007                _returning_row,
8008                #col_lit,
8009            )?;
8010        }
8011    });
8012    quote! { #( #lines )* }
8013}
8014
8015/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8016fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8017    let lines = entries.iter().map(|e| {
8018        let ident = &e.ident;
8019        let col_ty = column_type_ident(ident);
8020        quote! {
8021            #[allow(non_upper_case_globals)]
8022            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8023        }
8024    });
8025    quote! { #(#lines)* }
8026}
8027
8028/// Emit a hidden per-model module carrying one zero-sized type per field,
8029/// each with a `Column` impl pointing back at the model.
8030fn column_module_tokens(
8031    module_ident: &syn::Ident,
8032    struct_name: &syn::Ident,
8033    entries: &[ColumnEntry],
8034) -> TokenStream2 {
8035    let root = rustango_root();
8036    let items = entries.iter().map(|e| {
8037        let col_ty = column_type_ident(&e.ident);
8038        let value_ty = &e.value_ty;
8039        let name = &e.name;
8040        let column = &e.column;
8041        let field_type_tokens = &e.field_type_tokens;
8042        quote! {
8043            #[derive(::core::clone::Clone, ::core::marker::Copy)]
8044            pub struct #col_ty;
8045
8046            impl #root::core::Column for #col_ty {
8047                type Model = super::#struct_name;
8048                type Value = #value_ty;
8049                const NAME: &'static str = #name;
8050                const COLUMN: &'static str = #column;
8051                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8052            }
8053        }
8054    });
8055    quote! {
8056        #[doc(hidden)]
8057        #[allow(non_camel_case_types, non_snake_case)]
8058        pub mod #module_ident {
8059            // Re-import the parent scope so field types referencing
8060            // sibling models (e.g. `ForeignKey<Author>`) resolve
8061            // inside this submodule. Without this we'd hit
8062            // `proc_macro_derive_resolution_fallback` warnings.
8063            #[allow(unused_imports)]
8064            use super::*;
8065            #(#items)*
8066        }
8067    }
8068}
8069
8070fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8071    let name = ident_name(field_ident);
8072    syn::Ident::new(&format!("{name}_col"), field_ident.span())
8073}
8074
8075/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8076fn ident_name(ident: &syn::Ident) -> String {
8077    syn::ext::IdentExt::unraw(ident).to_string()
8078}
8079
8080/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8081fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8082    match syn::parse_str::<syn::Ident>(name) {
8083        Ok(mut ident) => {
8084            ident.set_span(span);
8085            ident
8086        }
8087        Err(_) => syn::Ident::new_raw(name, span),
8088    }
8089}
8090
8091fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8092    syn::Ident::new(
8093        &format!("__rustango_cols_{struct_name}"),
8094        struct_name.span(),
8095    )
8096}
8097
8098fn from_row_impl_tokens(
8099    struct_name: &syn::Ident,
8100    from_row_inits: &[TokenStream2],
8101    from_row_inits_my: &[TokenStream2],
8102) -> TokenStream2 {
8103    let root = rustango_root();
8104    // The Postgres impl is always emitted — every rustango build pulls in
8105    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8106    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8107    // macro_rules whose body collapses to nothing when rustango is built
8108    // without the `mysql` feature. No user-facing feature shim required.
8109    //
8110    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8111    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8112    // back into a comma-separated list inside square brackets. Since each
8113    // entry is already in `field: expr` shape, the existing tokens slot in.
8114    quote! {
8115        #[cfg(feature = "postgres")]
8116        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8117            for #struct_name
8118        {
8119            fn from_row(
8120                row: &'r #root::sql::sqlx::postgres::PgRow,
8121            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8122                ::core::result::Result::Ok(Self {
8123                    #( #from_row_inits ),*
8124                })
8125            }
8126        }
8127
8128        #root::__impl_my_from_row!(#struct_name, |row| {
8129            #( #from_row_inits_my ),*
8130        });
8131
8132        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8133            #( #from_row_inits ),*
8134        });
8135    }
8136}
8137
8138struct ContainerAttrs {
8139    table: Option<String>,
8140    display: Option<(String, proc_macro2::Span)>,
8141    /// Explicit app label from `#[rustango(app = "blog")]`.
8142    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8143    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8144    /// at runtime — this attribute is the override for cases where
8145    /// the inference is wrong (e.g. a model that conceptually belongs
8146    /// to one app but is physically in another module).
8147    app: Option<String>,
8148    /// Per-model admin knobs from
8149    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8150    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8151    /// admin code falls back to `AdminConfig::DEFAULT`.
8152    admin: Option<AdminAttrs>,
8153    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8154    /// `None` when the model isn't audited — write paths emit no
8155    /// audit entries. When present, single-row writes capture
8156    /// before/after for the listed fields and bulk writes batch
8157    /// snapshots into one INSERT into `rustango_audit_log`.
8158    audit: Option<AuditAttrs>,
8159    /// `true` when `#[rustango(permissions)]` is present. Signals that
8160    /// `auto_create_permissions` should seed the four CRUD codenames for
8161    /// this model.
8162    permissions: bool,
8163    /// Many-to-many relations declared via
8164    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8165    ///                 src = "post_id", dst = "tag_id"))]`.
8166    m2m: Vec<M2MAttr>,
8167    /// Polymorphic M2M relations declared via
8168    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8169    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8170    ///   related_column = "tag_id"))]` (issue #818).
8171    generic_m2m: Vec<GenericM2MAttr>,
8172    /// Composite indexes declared via
8173    /// `#[rustango(index("col1, col2"))]` or
8174    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8175    /// Single-column indexes from `#[rustango(index)]` on fields are
8176    /// accumulated here during field collection.
8177    indexes: Vec<IndexAttr>,
8178    /// Table-level CHECK constraints declared via
8179    /// `#[rustango(check(name = "…", expr = "…"))]`.
8180    checks: Vec<CheckAttr>,
8181    /// Table-level PG `EXCLUDE` constraints declared via
8182    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8183    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8184    /// migration writer renders them on Postgres and skips with a
8185    /// warning on MySQL/SQLite. Issue #319.
8186    excludes: Vec<ExcludeAttr>,
8187    /// Composite (multi-column) FKs declared via
8188    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8189    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8190    composite_fks: Vec<CompositeFkAttr>,
8191    /// Generic ("any model") FKs declared via
8192    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8193    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8194    generic_fks: Vec<GenericFkAttr>,
8195    /// Where this model lives in a tenancy deployment, declared via
8196    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8197    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8198    /// to partition diff output between registry-scoped and
8199    /// tenant-scoped migration files.
8200    scope: Option<String>,
8201    /// Extra QuerySet accessor names from
8202    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8203    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8204    /// the default `Self::objects()`. Multiple attributes allowed.
8205    manager_fns: Vec<syn::Ident>,
8206    /// Default ordering declared via `#[rustango(default_order =
8207    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8208    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8209    /// `-` prefix means descending; the `+` prefix or no prefix means
8210    /// ascending.
8211    default_order: Vec<(String, bool, proc_macro2::Span)>,
8212    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8213    /// Routes the emitted schema's `is_view = true` so the migration
8214    /// snapshot skips this model (its underlying SQL view is operator-
8215    /// managed, not rustango-managed).
8216    is_view: bool,
8217    /// `#[rustango(managed = false)]` — who owns the table.
8218    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8219    /// the migration snapshot skips this model so `makemigrations` /
8220    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8221    /// TABLE` against it (operator-managed schema).
8222    managed: bool,
8223    /// Name of the model's base manager, from
8224    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8225    /// `ModelSchema::base_manager_name`. Declarative-only today.
8226    base_manager_name: Option<String>,
8227    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8228    /// field this model's instances are ordered relative to.
8229    /// Declarative-only today; threaded onto
8230    /// `ModelSchema::order_with_respect_to`.
8231    order_with_respect_to: Option<String>,
8232    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8233    /// Marks the model as a proxy that
8234    /// shares its DB table with another struct. Threaded into
8235    /// `ModelSchema::proxy` so future codegen can skip table-owning
8236    /// behavior for proxies.
8237    proxy: bool,
8238    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8239    /// Each comma-separated capability token surfaces on
8240    /// `ModelSchema::required_db_features` so `manage check --deploy`
8241    /// can warn when the active dialect lacks one.
8242    required_db_features: Vec<String>,
8243    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8244    /// Normalized to the dialect name `manage check --deploy`
8245    /// compares against `Settings.database.backend`. Aliases
8246    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8247    /// stored under the canonical name.
8248    required_db_vendor: Option<String>,
8249    /// `#[rustango(default_related_name = "...")]`. Threaded into
8250    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8251    /// name to use when an FK / M2M field doesn't override it.
8252    /// Today rustango doesn't auto-emit reverse managers; the
8253    /// metadata is the foundation for that work.
8254    default_related_name: Option<String>,
8255    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8256    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8257    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8258    /// inline `COMMENT='...'`, SQLite: no-op).
8259    db_table_comment: Option<String>,
8260    /// `#[rustango(get_latest_by = "created_at")]` /
8261    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8262    /// `(column, descending)` where `descending = true` when the
8263    /// attribute value starts with `-`. Threaded into
8264    /// `ModelSchema::get_latest_by`.
8265    get_latest_by: Option<(String, bool)>,
8266    /// Extra `(codename, label)` permissions
8267    /// from `#[rustango(extra_permissions = "approve:Can approve,
8268    /// archive:Can archive")]`. Comma-separated `codename:label`
8269    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8270    extra_permissions: Vec<(String, String)>,
8271    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8272    /// `"view"`) the framework
8273    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8274    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8275    /// Validated at parse time; unknown actions fail with a span-pointing
8276    /// error.
8277    default_permissions: Vec<String>,
8278    /// `#[rustango(verbose_name = "blog post")]` — the
8279    /// human-readable singular label for the model. Threaded into
8280    /// `ModelSchema::verbose_name` so admin section headers /
8281    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8282    /// over the Rust struct identifier.
8283    verbose_name: Option<String>,
8284    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8285    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8286    /// When unset, `display_label_plural()` auto-suffixes `s`.
8287    verbose_name_plural: Option<String>,
8288    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8289    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8290    /// a name (used by `QuerySet::without_global_scope`) with a
8291    /// `fn() -> WhereExpr` path that the macro emits into
8292    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8293    global_scopes: Vec<GlobalScopeAttr>,
8294    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8295    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8296    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8297    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8298    /// entry emits an inherent `<name>_through(&self)` accessor that
8299    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8300    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8301    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8302    through_relations: Vec<ThroughAttr>,
8303    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8304    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8305    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8306    /// Each entry emits two associated functions on the parent —
8307    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8308    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8309    /// over a correlated subquery against the child table. Users
8310    /// drop the result into `QuerySet::where_raw(...)`.
8311    reverse_has_relations: Vec<ReverseHasAttr>,
8312    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8313    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8314    /// entry so the relation-existence family resolves polymorphic
8315    /// children by name.
8316    generic_has_relations: Vec<GenericHasAttr>,
8317}
8318
8319/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8320/// declaration. Each entry becomes one `core::GlobalScope` in the
8321/// emitted schema literal; `apply` resolves at macro-expand time
8322/// against the consumer's scope so the function must be in scope at
8323/// the use site. Issue #820.
8324struct GlobalScopeAttr {
8325    name: String,
8326    apply: syn::Path,
8327}
8328
8329/// Parsed `#[rustango(through(...))]` declaration. Issue
8330/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8331/// `hasManyThrough` / `hasOneThrough` parity.
8332///
8333/// `Country hasManyThrough Post via User` declares as:
8334///
8335/// ```ignore
8336/// #[rustango(through(
8337///     name                   = "posts",
8338///     far                    = "Post",
8339///     far_fk_column          = "author_id",
8340///     intermediate           = "User",
8341///     intermediate_fk_column = "country_id",
8342/// ))]
8343/// struct Country { ... }
8344/// ```
8345///
8346/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8347/// which returns a queryset filtered via a correlated subquery —
8348/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8349/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8350/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8351/// because the subquery lives inside a `WhereExpr::InSubquery` node
8352/// and the outer queryset's pending list stays empty.
8353///
8354/// All four required arguments use **SQL column / table names**
8355/// (not Rust field names) to sidestep the multi-hop-filter substrate
8356/// gap. Once that substrate lands, a higher-level Rust-field-name
8357/// shorthand can be added without breaking this surface.
8358struct ThroughAttr {
8359    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8360    name: String,
8361    /// Far model type identifier. `far = "Post"` → returns
8362    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8363    /// the derive expands.
8364    far: syn::Ident,
8365    /// SQL column on the far model's table that references the
8366    /// intermediate model's primary key. For `Post`'s
8367    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8368    /// default `<field>_id` convention) or whatever the user
8369    /// declared via `#[rustango(db_column = "...")]`.
8370    far_fk_column: String,
8371    /// Intermediate model type identifier. Needed to look up its
8372    /// `SCHEMA` so the subquery's `FROM` clause points at the
8373    /// intermediate table. `intermediate = "User"`.
8374    intermediate: syn::Ident,
8375    /// SQL column on the intermediate's table that references the
8376    /// source (this) model's primary key. For `User`'s
8377    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8378    intermediate_fk_column: String,
8379    /// SQL primary-key column on the intermediate's table — the
8380    /// column the subquery projects. Optional; defaults to `"id"`
8381    /// (rustango's default PK column name). Override when the
8382    /// intermediate declares a custom PK column.
8383    intermediate_pk_column: String,
8384}
8385
8386/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8387/// child_fk_column = "..."))]` declaration. Issue
8388/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8389/// `whereHas` / `whereDoesntHave` parity.
8390///
8391/// `Post hasMany Comment` declares as:
8392///
8393/// ```ignore
8394/// #[rustango(reverse_has(
8395///     name             = "comments",
8396///     child            = "Comment",
8397///     child_fk_column  = "post_id",
8398/// ))]
8399/// struct Post { ... }
8400/// ```
8401///
8402/// The macro emits two associated functions on `Post`:
8403///
8404/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8405///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8406///   <outer>.<self_pk_column>)`.
8407/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8408///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8409///
8410/// User code:
8411///
8412/// ```ignore
8413/// // Posts with at least one comment:
8414/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8415/// // Posts with no comments:
8416/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8417/// ```
8418///
8419/// As with #817, all column / table identifiers are **SQL names**
8420/// (not Rust field names) so the substrate is independent of the
8421/// outstanding multi-hop filter resolver gap. The emitted
8422/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8423/// SQL-emit time via the writer's scope stack.
8424struct ReverseHasAttr {
8425    /// Accessor name. `name = "comments"` → emits
8426    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8427    name: String,
8428    /// Child model type identifier. `child = "Comment"` — needed to
8429    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8430    /// points at the child table.
8431    child: syn::Ident,
8432    /// SQL column on the child's table that references this model's
8433    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8434    /// column is `"post_id"`.
8435    child_fk_column: String,
8436    /// SQL primary-key column on **this** model's table — the column
8437    /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8438    /// (rustango's default PK column name).
8439    self_pk_column: String,
8440}
8441
8442/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8443/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8444/// relation-existence family (issue #830). The child is a polymorphic,
8445/// content-type-discriminated model; emits a `Model::
8446/// generic_reverse_relations()` entry the queryset resolves by name.
8447struct GenericHasAttr {
8448    /// Accessor name, e.g. `name = "tags"`.
8449    name: String,
8450    /// Child model type identifier (`child = "Tag"`) — looked up for its
8451    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8452    child: syn::Ident,
8453    /// Column on the child table holding the parent's content-type id.
8454    /// Optional; defaults to `"content_type_id"`.
8455    ct_column: String,
8456    /// Column on the child table holding the parent's PK value.
8457    /// Optional; defaults to `"object_pk"`.
8458    pk_column: String,
8459    /// SQL primary-key column on **this** (parent) model's table.
8460    /// Optional; defaults to `"id"`.
8461    self_pk_column: String,
8462}
8463
8464/// Parsed form of one index declaration (field-level or container-level).
8465struct IndexAttr {
8466    /// Index name; auto-derived when `None` at parse time.
8467    name: Option<String>,
8468    /// Column names in the index.
8469    columns: Vec<String>,
8470    /// `true` for `CREATE UNIQUE INDEX`.
8471    unique: bool,
8472    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8473    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8474    /// `"btree"` when the attribute is absent — the DDL writer omits
8475    /// the `USING` clause and the backend uses its own default
8476    /// (btree on every supported dialect).
8477    method: String,
8478    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8479    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8480    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8481    where_clause: Option<String>,
8482    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8483    /// Empty `Vec` (the default) means "no covering columns".
8484    include: Vec<String>,
8485}
8486
8487/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8488struct CheckAttr {
8489    name: String,
8490    expr: String,
8491}
8492
8493/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8494/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8495/// PG-only — surfaced on every backend in the macro emit; the migration
8496/// writer skips the op on MySQL/SQLite. Issue #319.
8497struct ExcludeAttr {
8498    /// Constraint name (free-form Rust identifier).
8499    name: String,
8500    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8501    using: String,
8502    /// Comma-separated `(column, operator)` pairs, in declaration
8503    /// order. Parsed from `"col WITH op, col WITH op"`.
8504    elements: Vec<(String, String)>,
8505    /// Optional `WHERE` predicate (raw SQL).
8506    where_clause: Option<String>,
8507}
8508
8509/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8510/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8511/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8512/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8513/// the model, not the field.
8514struct CompositeFkAttr {
8515    /// Logical relation name (free-form Rust identifier).
8516    name: String,
8517    /// SQL table name of the target.
8518    to: String,
8519    /// Source-side column names, in declaration order.
8520    from: Vec<String>,
8521    /// Target-side column names, same length / order as `from`.
8522    on: Vec<String>,
8523}
8524
8525/// Parsed form of one `#[rustango(generic_fk(name = "target",
8526/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8527/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8528/// generic ("any model") FKs live on the model, not the field.
8529struct GenericFkAttr {
8530    /// Logical relation name (free-form Rust identifier).
8531    name: String,
8532    /// Source-side column carrying the `content_type_id` value.
8533    ct_column: String,
8534    /// Source-side column carrying the target row's primary key.
8535    pk_column: String,
8536}
8537
8538/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8539struct M2MAttr {
8540    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8541    name: String,
8542    /// Target table (e.g. `"app_tags"`).
8543    to: String,
8544    /// Junction table (e.g. `"post_tags"`).
8545    through: String,
8546    /// Source FK column in the junction table (e.g. `"post_id"`).
8547    src: String,
8548    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8549    dst: String,
8550    /// Whether the migration writer should auto-create the junction
8551    /// table. Default `true`. Set `auto_create = false` (#324) when
8552    /// the operator declares the through table as its own
8553    /// `#[derive(Model)]` struct with extra columns.
8554    auto_create: bool,
8555}
8556
8557/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8558/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8559/// junction carries a ContentType discriminator so unrelated models
8560/// share one pivot + related set.
8561struct GenericM2MAttr {
8562    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8563    name: String,
8564    /// Polymorphic junction table (e.g. `"taggables"`).
8565    through: String,
8566    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8567    pk_column: String,
8568    /// Junction column holding the owning model's `content_type_id`
8569    /// discriminator (e.g. `"taggable_type"`).
8570    ct_column: String,
8571    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8572    related_column: String,
8573}
8574
8575/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8576/// "user"))]`. `track` is a comma-separated list of field names whose
8577/// before/after values land in the JSONB `changes` column. `source`
8578/// is informational only — it pins a default source when the model
8579/// is written outside any `audit::with_source(...)` scope (rare).
8580#[derive(Default)]
8581struct AuditAttrs {
8582    /// Field names to capture in the `changes` JSONB. Validated
8583    /// against declared scalar fields at compile time. Empty means
8584    /// "track every scalar field".
8585    track: Option<(Vec<String>, proc_macro2::Span)>,
8586}
8587
8588/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8589/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8590/// comma-separated strings; we validate each ident against the model's
8591/// declared fields at compile time.
8592#[derive(Default)]
8593struct AdminAttrs {
8594    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8595    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8596    list_per_page: Option<usize>,
8597    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8598    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8599    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8600    /// Bulk action names. No field-validation against model fields —
8601    /// these are action handlers, not column references.
8602    actions: Option<(Vec<String>, proc_macro2::Span)>,
8603    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8604    /// sections, comma-separated fields per section, optional
8605    /// `Title:` prefix. Empty title omits the `<legend>`.
8606    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8607    /// `admin(list_display_links = "title")`. Names
8608    /// from `list_display` whose cells should link to detail/edit.
8609    /// Issue #350.
8610    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8611    /// `admin(search_help_text = "...")`. Short
8612    /// caption rendered beside the admin list view's search box.
8613    /// Issue #353.
8614    search_help_text: Option<String>,
8615    /// `admin(actions_on_top = false)`. Hides the
8616    /// action-bar above the table. Default `true`. Issue #354.
8617    actions_on_top: Option<bool>,
8618    /// `admin(actions_on_bottom = true)`. Renders an
8619    /// additional action-bar below the table. Default `false`.
8620    /// Issue #354.
8621    actions_on_bottom: Option<bool>,
8622    /// `admin(date_hierarchy = "created_at")`. Name of
8623    /// a date / datetime field whose values render as a clickable
8624    /// year / month / day drill-down strip above the list table.
8625    /// Empty / unset disables the strip. Issue #355.
8626    date_hierarchy: Option<String>,
8627    /// `admin(prepopulated_fields = "slug:title")`.
8628    /// Each entry is `target:source[+source2]`; multiple entries are
8629    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8630    /// The admin change-form emits JS that slugifies the source values
8631    /// into the target field on every keystroke. Issue #356.
8632    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8633    /// `admin(raw_id_fields = "parent, owner")`. Names
8634    /// of FK fields whose change-form widget renders a lookup link
8635    /// next to the input. Issue #357.
8636    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8637    /// `admin(autocomplete_fields = "author_id")`.
8638    /// Names of FK fields whose change-form widget renders an
8639    /// Ajax-driven typeahead populated from a `__autocomplete`
8640    /// endpoint on the target model. Issue #358.
8641    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8642    /// `admin(list_select_related = "all" | "none" | "author, …")`
8643    /// — tunes the admin list view's FK auto-JOIN
8644    /// policy. Default `"all"` matches rustango's join-everything
8645    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8646    list_select_related: Option<String>,
8647    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8648    /// Each entry is `field_name:widget_name`; multiple
8649    /// entries comma-separated. Empty / unset → no overrides. The
8650    /// list of widget names supported is documented on
8651    /// `AdminConfig::formfield_overrides`. Issue #359.
8652    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8653}
8654
8655fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8656    let mut out = ContainerAttrs {
8657        table: None,
8658        display: None,
8659        app: None,
8660        admin: None,
8661        audit: None,
8662        // Default `permissions = true` so every `#[derive(Model)]`
8663        // gets the four CRUD codenames seeded by `auto_create_permissions`
8664        // and is visible to non-superusers in the tenant admin without
8665        // manual per-model annotation. Models that intentionally don't
8666        // want permission rows (registry-internal types, framework
8667        // tables operators shouldn't manage directly) opt out via
8668        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8669        // out-of-the-box admin invisibility regression (#62).
8670        permissions: true,
8671        m2m: Vec::new(),
8672        generic_m2m: Vec::new(),
8673        indexes: Vec::new(),
8674        checks: Vec::new(),
8675        excludes: Vec::new(),
8676        composite_fks: Vec::new(),
8677        generic_fks: Vec::new(),
8678        scope: None,
8679        manager_fns: Vec::new(),
8680        default_order: Vec::new(),
8681        is_view: false,
8682        managed: true,
8683        verbose_name: None,
8684        verbose_name_plural: None,
8685        base_manager_name: None,
8686        order_with_respect_to: None,
8687        proxy: false,
8688        required_db_features: Vec::new(),
8689        required_db_vendor: None,
8690        default_related_name: None,
8691        db_table_comment: None,
8692        get_latest_by: None,
8693        extra_permissions: Vec::new(),
8694        default_permissions: Vec::new(),
8695        global_scopes: Vec::new(),
8696        through_relations: Vec::new(),
8697        reverse_has_relations: Vec::new(),
8698        generic_has_relations: Vec::new(),
8699    };
8700    for attr in &input.attrs {
8701        if !attr.path().is_ident("rustango") {
8702            continue;
8703        }
8704        attr.parse_nested_meta(|meta| {
8705            if meta.path.is_ident("table") {
8706                let s: LitStr = meta.value()?.parse()?;
8707                let name = s.value();
8708                // v0.27.3 (#65) — macro-time guard against table names
8709                // that compile but break SQL downstream. Hyphens are
8710                // the common footgun: PostgreSQL accepts a quoted
8711                // `"intermediate-region"` in CREATE TABLE, but the
8712                // FK / index name derivation in `migrate::ddl`
8713                // emits `intermediate-region_field_fkey` unquoted,
8714                // which then fails the SQL parser. Same shape rule
8715                // as Postgres regular identifiers so the safe path
8716                // is the only path.
8717                validate_table_name(&name, s.span())?;
8718                out.table = Some(name);
8719                return Ok(());
8720            }
8721            if meta.path.is_ident("display") {
8722                let s: LitStr = meta.value()?.parse()?;
8723                out.display = Some((s.value(), s.span()));
8724                return Ok(());
8725            }
8726            if meta.path.is_ident("app") {
8727                let s: LitStr = meta.value()?.parse()?;
8728                out.app = Some(s.value());
8729                return Ok(());
8730            }
8731            if meta.path.is_ident("scope") {
8732                let s: LitStr = meta.value()?.parse()?;
8733                let val = s.value();
8734                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8735                    return Err(meta.error(format!(
8736                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8737                    )));
8738                }
8739                out.scope = Some(val);
8740                return Ok(());
8741            }
8742            if meta.path.is_ident("admin") {
8743                let mut admin = AdminAttrs::default();
8744                meta.parse_nested_meta(|inner| {
8745                    if inner.path.is_ident("list_display") {
8746                        let s: LitStr = inner.value()?.parse()?;
8747                        admin.list_display =
8748                            Some((split_field_list(&s.value()), s.span()));
8749                        return Ok(());
8750                    }
8751                    if inner.path.is_ident("search_fields") {
8752                        let s: LitStr = inner.value()?.parse()?;
8753                        admin.search_fields =
8754                            Some((split_field_list(&s.value()), s.span()));
8755                        return Ok(());
8756                    }
8757                    if inner.path.is_ident("readonly_fields") {
8758                        let s: LitStr = inner.value()?.parse()?;
8759                        admin.readonly_fields =
8760                            Some((split_field_list(&s.value()), s.span()));
8761                        return Ok(());
8762                    }
8763                    if inner.path.is_ident("list_per_page") {
8764                        let lit: syn::LitInt = inner.value()?.parse()?;
8765                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8766                        return Ok(());
8767                    }
8768                    if inner.path.is_ident("ordering") {
8769                        let s: LitStr = inner.value()?.parse()?;
8770                        admin.ordering = Some((
8771                            parse_ordering_list(&s.value()),
8772                            s.span(),
8773                        ));
8774                        return Ok(());
8775                    }
8776                    if inner.path.is_ident("list_filter") {
8777                        let s: LitStr = inner.value()?.parse()?;
8778                        admin.list_filter =
8779                            Some((split_field_list(&s.value()), s.span()));
8780                        return Ok(());
8781                    }
8782                    if inner.path.is_ident("actions") {
8783                        let s: LitStr = inner.value()?.parse()?;
8784                        admin.actions =
8785                            Some((split_field_list(&s.value()), s.span()));
8786                        return Ok(());
8787                    }
8788                    if inner.path.is_ident("fieldsets") {
8789                        let s: LitStr = inner.value()?.parse()?;
8790                        admin.fieldsets =
8791                            Some((parse_fieldset_list(&s.value()), s.span()));
8792                        return Ok(());
8793                    }
8794                    if inner.path.is_ident("list_display_links") {
8795                        let s: LitStr = inner.value()?.parse()?;
8796                        admin.list_display_links =
8797                            Some((split_field_list(&s.value()), s.span()));
8798                        return Ok(());
8799                    }
8800                    if inner.path.is_ident("search_help_text") {
8801                        let s: LitStr = inner.value()?.parse()?;
8802                        admin.search_help_text = Some(s.value());
8803                        return Ok(());
8804                    }
8805                    if inner.path.is_ident("actions_on_top") {
8806                        let lit: syn::LitBool = inner.value()?.parse()?;
8807                        admin.actions_on_top = Some(lit.value);
8808                        return Ok(());
8809                    }
8810                    if inner.path.is_ident("actions_on_bottom") {
8811                        let lit: syn::LitBool = inner.value()?.parse()?;
8812                        admin.actions_on_bottom = Some(lit.value);
8813                        return Ok(());
8814                    }
8815                    if inner.path.is_ident("date_hierarchy") {
8816                        let s: LitStr = inner.value()?.parse()?;
8817                        admin.date_hierarchy = Some(s.value());
8818                        return Ok(());
8819                    }
8820                    if inner.path.is_ident("prepopulated_fields") {
8821                        let s: LitStr = inner.value()?.parse()?;
8822                        admin.prepopulated_fields =
8823                            Some((parse_prepopulated_list(&s.value()), s.span()));
8824                        return Ok(());
8825                    }
8826                    if inner.path.is_ident("raw_id_fields") {
8827                        let s: LitStr = inner.value()?.parse()?;
8828                        admin.raw_id_fields =
8829                            Some((split_field_list(&s.value()), s.span()));
8830                        return Ok(());
8831                    }
8832                    if inner.path.is_ident("autocomplete_fields") {
8833                        let s: LitStr = inner.value()?.parse()?;
8834                        admin.autocomplete_fields =
8835                            Some((split_field_list(&s.value()), s.span()));
8836                        return Ok(());
8837                    }
8838                    if inner.path.is_ident("list_select_related") {
8839                        let s: LitStr = inner.value()?.parse()?;
8840                        admin.list_select_related = Some(s.value());
8841                        return Ok(());
8842                    }
8843                    if inner.path.is_ident("formfield_overrides") {
8844                        let s: LitStr = inner.value()?.parse()?;
8845                        admin.formfield_overrides =
8846                            Some((parse_formfield_overrides(&s.value()), s.span()));
8847                        return Ok(());
8848                    }
8849                    Err(inner.error(
8850                        "unknown admin attribute (supported: \
8851                         `list_display`, `list_display_links`, \
8852                         `search_fields`, `search_help_text`, \
8853                         `readonly_fields`, \
8854                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8855                         `actions_on_top`, `actions_on_bottom`, \
8856                         `date_hierarchy`, \
8857                         `prepopulated_fields`, \
8858                         `raw_id_fields`, \
8859                         `autocomplete_fields`, \
8860                         `list_select_related`, \
8861                         `formfield_overrides`, \
8862                         `fieldsets`)",
8863                    ))
8864                })?;
8865                out.admin = Some(admin);
8866                return Ok(());
8867            }
8868            if meta.path.is_ident("manager") {
8869                // Its empty trait could not take methods (#2132).
8870                return Err(meta.error(
8871                    "`manager(ext = ...)` was removed (#2132): declare your own \
8872                     `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
8873                ));
8874            }
8875            if meta.path.is_ident("manager_fn") {
8876                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8877                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8878                // next to the default `Self::objects()`. Multiple
8879                // attributes accumulate.
8880                let s: LitStr = meta.value()?.parse()?;
8881                let name = s.value();
8882                if name.is_empty() {
8883                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8884                }
8885                if name == "objects" {
8886                    return Err(meta.error(
8887                        "`manager_fn = \"objects\"` collides with the default \
8888                         accessor — pick a different name",
8889                    ));
8890                }
8891                let ident = syn::Ident::new(&name, s.span());
8892                if out.manager_fns.iter().any(|prev| *prev == ident) {
8893                    return Err(meta.error(format!(
8894                        "duplicate `manager_fn = \"{name}\"`"
8895                    )));
8896                }
8897                out.manager_fns.push(ident);
8898                return Ok(());
8899            }
8900            if meta.path.is_ident("default_order") {
8901                // `#[rustango(default_order = "-created_at, status")]`
8902                // — issue #291 / T2.5. Comma-separated list; `-prefix`
8903                // means descending, `+prefix` or bare name means ascending.
8904                // Per-query opt-in via `QuerySet::with_default_order()`.
8905                let s: LitStr = meta.value()?.parse()?;
8906                let raw = s.value();
8907                let span = s.span();
8908                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8909                    Vec::new();
8910                for entry in raw.split(',') {
8911                    let trimmed = entry.trim();
8912                    if trimmed.is_empty() {
8913                        return Err(syn::Error::new(
8914                            span,
8915                            "`default_order = \"...\"` has an empty entry — \
8916                             check for a stray comma",
8917                        ));
8918                    }
8919                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8920                        (true, rest.trim().to_owned())
8921                    } else if let Some(rest) = trimmed.strip_prefix('+') {
8922                        (false, rest.trim().to_owned())
8923                    } else {
8924                        (false, trimmed.to_owned())
8925                    };
8926                    if name.is_empty() {
8927                        return Err(syn::Error::new(
8928                            span,
8929                            "`default_order` entry has no column name after the prefix",
8930                        ));
8931                    }
8932                    if parsed.iter().any(|(n, _, _)| *n == name) {
8933                        return Err(syn::Error::new(
8934                            span,
8935                            format!("duplicate column `{name}` in `default_order`"),
8936                        ));
8937                    }
8938                    parsed.push((name, desc, span));
8939                }
8940                if parsed.is_empty() {
8941                    return Err(syn::Error::new(
8942                        span,
8943                        "`default_order = \"...\"` cannot be empty",
8944                    ));
8945                }
8946                out.default_order = parsed;
8947                return Ok(());
8948            }
8949            if meta.path.is_ident("global_scope") {
8950                // `#[rustango(global_scope(name = "active", apply =
8951                //  path::to::fn))]` — issue #820. The apply function
8952                // path resolves at macro-expand time in the consumer's
8953                // scope; the macro re-emits it verbatim into the
8954                // `ModelSchema::global_scopes` slice literal.
8955                let span = meta.path.span();
8956                let mut scope_name: Option<String> = None;
8957                let mut apply_path: Option<syn::Path> = None;
8958                meta.parse_nested_meta(|inner| {
8959                    if inner.path.is_ident("name") {
8960                        let s: LitStr = inner.value()?.parse()?;
8961                        let raw = s.value();
8962                        if raw.trim().is_empty() {
8963                            return Err(syn::Error::new(
8964                                s.span(),
8965                                "`global_scope(name = \"...\")` must not be empty",
8966                            ));
8967                        }
8968                        scope_name = Some(raw);
8969                        return Ok(());
8970                    }
8971                    if inner.path.is_ident("apply") {
8972                        let p: syn::Path = inner.value()?.parse()?;
8973                        apply_path = Some(p);
8974                        return Ok(());
8975                    }
8976                    Err(inner.error(
8977                        "unknown `global_scope` attribute (supported: \
8978                         `name`, `apply`)",
8979                    ))
8980                })?;
8981                let Some(name) = scope_name else {
8982                    return Err(syn::Error::new(
8983                        span,
8984                        "`global_scope` requires `name = \"...\"`",
8985                    ));
8986                };
8987                let Some(apply) = apply_path else {
8988                    return Err(syn::Error::new(
8989                        span,
8990                        "`global_scope` requires `apply = fn_path`",
8991                    ));
8992                };
8993                if out.global_scopes.iter().any(|s| s.name == name) {
8994                    return Err(syn::Error::new(
8995                        span,
8996                        format!(
8997                            "duplicate global scope name `{name}` — \
8998                             pick a unique identifier so \
8999                             `QuerySet::without_global_scope(\"{name}\")` \
9000                             is unambiguous"
9001                        ),
9002                    ));
9003                }
9004                out.global_scopes.push(GlobalScopeAttr { name, apply });
9005                return Ok(());
9006            }
9007            if meta.path.is_ident("through") {
9008                // `#[rustango(through(name = "posts", far = "Post",
9009                //  far_fk_column = "author_id", intermediate = "User",
9010                //  intermediate_fk_column = "country_id"
9011                //  [, intermediate_pk_column = "id"]))]` — issue #817.
9012                let span = meta.path.span();
9013                let mut accessor_name: Option<String> = None;
9014                let mut far_ident: Option<syn::Ident> = None;
9015                let mut far_fk_column: Option<String> = None;
9016                let mut intermediate_ident: Option<syn::Ident> = None;
9017                let mut intermediate_fk_column: Option<String> = None;
9018                let mut intermediate_pk_column: Option<String> = None;
9019                fn parse_nonempty_string(
9020                    inner: &syn::meta::ParseNestedMeta<'_>,
9021                    field: &str,
9022                ) -> syn::Result<String> {
9023                    let s: LitStr = inner.value()?.parse()?;
9024                    let raw = s.value();
9025                    let trimmed = raw.trim();
9026                    if trimmed.is_empty() {
9027                        return Err(syn::Error::new(
9028                            s.span(),
9029                            format!("`through({field} = \"...\")` must not be empty"),
9030                        ));
9031                    }
9032                    Ok(trimmed.to_owned())
9033                }
9034                meta.parse_nested_meta(|inner| {
9035                    if inner.path.is_ident("name") {
9036                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9037                        return Ok(());
9038                    }
9039                    if inner.path.is_ident("far") {
9040                        // Far model name accepted as a string literal
9041                        // so the attribute fits inside the existing
9042                        // `parse_nested_meta` shape; parsed back into a
9043                        // `syn::Ident` so the emitted accessor can
9044                        // reference the type directly.
9045                        let s: LitStr = inner.value()?.parse()?;
9046                        let raw = s.value();
9047                        let trimmed = raw.trim();
9048                        if trimmed.is_empty() {
9049                            return Err(syn::Error::new(
9050                                s.span(),
9051                                "`through(far = \"...\")` must not be empty",
9052                            ));
9053                        }
9054                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9055                        return Ok(());
9056                    }
9057                    if inner.path.is_ident("far_fk_column") {
9058                        far_fk_column =
9059                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9060                        return Ok(());
9061                    }
9062                    if inner.path.is_ident("intermediate") {
9063                        let s: LitStr = inner.value()?.parse()?;
9064                        let raw = s.value();
9065                        let trimmed = raw.trim();
9066                        if trimmed.is_empty() {
9067                            return Err(syn::Error::new(
9068                                s.span(),
9069                                "`through(intermediate = \"...\")` must not be empty",
9070                            ));
9071                        }
9072                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9073                        return Ok(());
9074                    }
9075                    if inner.path.is_ident("intermediate_fk_column") {
9076                        intermediate_fk_column =
9077                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9078                        return Ok(());
9079                    }
9080                    if inner.path.is_ident("intermediate_pk_column") {
9081                        intermediate_pk_column =
9082                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9083                        return Ok(());
9084                    }
9085                    Err(inner.error(
9086                        "unknown `through` attribute (supported: \
9087                         `name`, `far`, `far_fk_column`, \
9088                         `intermediate`, `intermediate_fk_column`, \
9089                         `intermediate_pk_column`)",
9090                    ))
9091                })?;
9092                let Some(name) = accessor_name else {
9093                    return Err(syn::Error::new(
9094                        span,
9095                        "`through` requires `name = \"...\"`",
9096                    ));
9097                };
9098                let Some(far) = far_ident else {
9099                    return Err(syn::Error::new(
9100                        span,
9101                        "`through` requires `far = \"FarModelType\"`",
9102                    ));
9103                };
9104                let Some(far_fk_column) = far_fk_column else {
9105                    return Err(syn::Error::new(
9106                        span,
9107                        "`through` requires `far_fk_column = \"<column>\"`",
9108                    ));
9109                };
9110                let Some(intermediate) = intermediate_ident else {
9111                    return Err(syn::Error::new(
9112                        span,
9113                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9114                    ));
9115                };
9116                let Some(intermediate_fk_column) = intermediate_fk_column else {
9117                    return Err(syn::Error::new(
9118                        span,
9119                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9120                    ));
9121                };
9122                let intermediate_pk_column =
9123                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9124                if out.through_relations.iter().any(|t| t.name == name) {
9125                    return Err(syn::Error::new(
9126                        span,
9127                        format!(
9128                            "duplicate `through(name = \"{name}\")` — \
9129                             pick a unique accessor name"
9130                        ),
9131                    ));
9132                }
9133                out.through_relations.push(ThroughAttr {
9134                    name,
9135                    far,
9136                    far_fk_column,
9137                    intermediate,
9138                    intermediate_fk_column,
9139                    intermediate_pk_column,
9140                });
9141                return Ok(());
9142            }
9143            if meta.path.is_ident("reverse_has") {
9144                // `#[rustango(reverse_has(name = "comments",
9145                //  child = "Comment", child_fk_column = "post_id"
9146                //  [, self_pk_column = "id"]))]` — issue #830.
9147                let span = meta.path.span();
9148                let mut accessor_name: Option<String> = None;
9149                let mut child_ident: Option<syn::Ident> = None;
9150                let mut child_fk_column: Option<String> = None;
9151                let mut self_pk_column: Option<String> = None;
9152                meta.parse_nested_meta(|inner| {
9153                    if inner.path.is_ident("name") {
9154                        let s: LitStr = inner.value()?.parse()?;
9155                        let raw = s.value();
9156                        if raw.trim().is_empty() {
9157                            return Err(syn::Error::new(
9158                                s.span(),
9159                                "`reverse_has(name = \"...\")` must not be empty",
9160                            ));
9161                        }
9162                        accessor_name = Some(raw);
9163                        return Ok(());
9164                    }
9165                    if inner.path.is_ident("child") {
9166                        let s: LitStr = inner.value()?.parse()?;
9167                        let raw = s.value();
9168                        let trimmed = raw.trim();
9169                        if trimmed.is_empty() {
9170                            return Err(syn::Error::new(
9171                                s.span(),
9172                                "`reverse_has(child = \"...\")` must not be empty",
9173                            ));
9174                        }
9175                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9176                        return Ok(());
9177                    }
9178                    if inner.path.is_ident("child_fk_column") {
9179                        let s: LitStr = inner.value()?.parse()?;
9180                        let raw = s.value();
9181                        let trimmed = raw.trim();
9182                        if trimmed.is_empty() {
9183                            return Err(syn::Error::new(
9184                                s.span(),
9185                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9186                            ));
9187                        }
9188                        child_fk_column = Some(trimmed.to_owned());
9189                        return Ok(());
9190                    }
9191                    if inner.path.is_ident("self_pk_column") {
9192                        let s: LitStr = inner.value()?.parse()?;
9193                        let raw = s.value();
9194                        let trimmed = raw.trim();
9195                        if trimmed.is_empty() {
9196                            return Err(syn::Error::new(
9197                                s.span(),
9198                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9199                            ));
9200                        }
9201                        self_pk_column = Some(trimmed.to_owned());
9202                        return Ok(());
9203                    }
9204                    Err(inner.error(
9205                        "unknown `reverse_has` attribute (supported: \
9206                         `name`, `child`, `child_fk_column`, \
9207                         `self_pk_column`)",
9208                    ))
9209                })?;
9210                let Some(name) = accessor_name else {
9211                    return Err(syn::Error::new(
9212                        span,
9213                        "`reverse_has` requires `name = \"...\"`",
9214                    ));
9215                };
9216                let Some(child) = child_ident else {
9217                    return Err(syn::Error::new(
9218                        span,
9219                        "`reverse_has` requires `child = \"ChildModelType\"`",
9220                    ));
9221                };
9222                let Some(child_fk_column) = child_fk_column else {
9223                    return Err(syn::Error::new(
9224                        span,
9225                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9226                    ));
9227                };
9228                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9229                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9230                    return Err(syn::Error::new(
9231                        span,
9232                        format!(
9233                            "duplicate `reverse_has(name = \"{name}\")` — \
9234                             pick a unique accessor name"
9235                        ),
9236                    ));
9237                }
9238                out.reverse_has_relations.push(ReverseHasAttr {
9239                    name,
9240                    child,
9241                    child_fk_column,
9242                    self_pk_column,
9243                });
9244                return Ok(());
9245            }
9246            if meta.path.is_ident("generic_has") {
9247                // `#[rustango(generic_has(name = "tags",
9248                //  child = "Tag", ct_column = "content_type_id",
9249                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9250                //  — issue #830, the reverse generic-FK (GFK) arm of the
9251                //  relation-existence family.
9252                let span = meta.path.span();
9253                let mut accessor_name: Option<String> = None;
9254                let mut child_ident: Option<syn::Ident> = None;
9255                let mut ct_column: Option<String> = None;
9256                let mut pk_column: Option<String> = None;
9257                let mut self_pk_column: Option<String> = None;
9258                meta.parse_nested_meta(|inner| {
9259                    if inner.path.is_ident("name") {
9260                        let s: LitStr = inner.value()?.parse()?;
9261                        let raw = s.value();
9262                        if raw.trim().is_empty() {
9263                            return Err(syn::Error::new(
9264                                s.span(),
9265                                "`generic_has(name = \"...\")` must not be empty",
9266                            ));
9267                        }
9268                        accessor_name = Some(raw);
9269                        return Ok(());
9270                    }
9271                    if inner.path.is_ident("child") {
9272                        let s: LitStr = inner.value()?.parse()?;
9273                        let trimmed = s.value().trim().to_owned();
9274                        if trimmed.is_empty() {
9275                            return Err(syn::Error::new(
9276                                s.span(),
9277                                "`generic_has(child = \"...\")` must not be empty",
9278                            ));
9279                        }
9280                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9281                        return Ok(());
9282                    }
9283                    if inner.path.is_ident("ct_column") {
9284                        let s: LitStr = inner.value()?.parse()?;
9285                        let trimmed = s.value().trim().to_owned();
9286                        if trimmed.is_empty() {
9287                            return Err(syn::Error::new(
9288                                s.span(),
9289                                "`generic_has(ct_column = \"...\")` must not be empty",
9290                            ));
9291                        }
9292                        ct_column = Some(trimmed);
9293                        return Ok(());
9294                    }
9295                    if inner.path.is_ident("pk_column") {
9296                        let s: LitStr = inner.value()?.parse()?;
9297                        let trimmed = s.value().trim().to_owned();
9298                        if trimmed.is_empty() {
9299                            return Err(syn::Error::new(
9300                                s.span(),
9301                                "`generic_has(pk_column = \"...\")` must not be empty",
9302                            ));
9303                        }
9304                        pk_column = Some(trimmed);
9305                        return Ok(());
9306                    }
9307                    if inner.path.is_ident("self_pk_column") {
9308                        let s: LitStr = inner.value()?.parse()?;
9309                        let trimmed = s.value().trim().to_owned();
9310                        if trimmed.is_empty() {
9311                            return Err(syn::Error::new(
9312                                s.span(),
9313                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9314                            ));
9315                        }
9316                        self_pk_column = Some(trimmed);
9317                        return Ok(());
9318                    }
9319                    Err(inner.error(
9320                        "unknown `generic_has` attribute (supported: \
9321                         `name`, `child`, `ct_column`, `pk_column`, \
9322                         `self_pk_column`)",
9323                    ))
9324                })?;
9325                let Some(name) = accessor_name else {
9326                    return Err(syn::Error::new(
9327                        span,
9328                        "`generic_has` requires `name = \"...\"`",
9329                    ));
9330                };
9331                let Some(child) = child_ident else {
9332                    return Err(syn::Error::new(
9333                        span,
9334                        "`generic_has` requires `child = \"ChildModelType\"`",
9335                    ));
9336                };
9337                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9338                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9339                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9340                if out.generic_has_relations.iter().any(|r| r.name == name) {
9341                    return Err(syn::Error::new(
9342                        span,
9343                        format!(
9344                            "duplicate `generic_has(name = \"{name}\")` — \
9345                             pick a unique accessor name"
9346                        ),
9347                    ));
9348                }
9349                out.generic_has_relations.push(GenericHasAttr {
9350                    name,
9351                    child,
9352                    ct_column,
9353                    pk_column,
9354                    self_pk_column,
9355                });
9356                return Ok(());
9357            }
9358            if meta.path.is_ident("audit") {
9359                let mut audit = AuditAttrs::default();
9360                meta.parse_nested_meta(|inner| {
9361                    if inner.path.is_ident("track") {
9362                        let s: LitStr = inner.value()?.parse()?;
9363                        audit.track =
9364                            Some((split_field_list(&s.value()), s.span()));
9365                        return Ok(());
9366                    }
9367                    Err(inner.error(
9368                        "unknown audit attribute (supported: `track`)",
9369                    ))
9370                })?;
9371                out.audit = Some(audit);
9372                return Ok(());
9373            }
9374            if meta.path.is_ident("permissions") {
9375                // Two forms accepted:
9376                //   #[rustango(permissions)]          — flag form, true
9377                //   #[rustango(permissions = false)]  — explicit opt-out
9378                //   #[rustango(permissions = true)]   — explicit opt-in
9379                if let Ok(v) = meta.value() {
9380                    let lit: syn::LitBool = v.parse()?;
9381                    out.permissions = lit.value;
9382                } else {
9383                    out.permissions = true;
9384                }
9385                return Ok(());
9386            }
9387            if meta.path.is_ident("view") {
9388                // Issue #293 / T2.10. Two forms accepted, matching
9389                // the `permissions` flag pattern:
9390                //   #[rustango(view)]          — flag form, true
9391                //   #[rustango(view = false)]  — explicit opt-out
9392                //   #[rustango(view = true)]   — explicit opt-in
9393                if let Ok(v) = meta.value() {
9394                    let lit: syn::LitBool = v.parse()?;
9395                    out.is_view = lit.value;
9396                } else {
9397                    out.is_view = true;
9398                }
9399                return Ok(());
9400            }
9401            if meta.path.is_ident("managed") {
9402                // Who owns the table. Issue #321.
9403                //   #[rustango(managed = false)]  — operator-managed table
9404                //   #[rustango(managed = true)]   — rustango-managed (the default)
9405                // Bare-flag form is intentionally not accepted: writing
9406                // `#[rustango(managed)]` reads as "yes please manage it"
9407                // which is already the default. The opt-out is the only
9408                // useful state, so it must be explicit.
9409                let v = meta.value()?;
9410                let lit: syn::LitBool = v.parse()?;
9411                out.managed = lit.value;
9412                return Ok(());
9413            }
9414            if meta.path.is_ident("verbose_name") {
9415                let s: LitStr = meta.value()?.parse()?;
9416                out.verbose_name = Some(s.value());
9417                return Ok(());
9418            }
9419            if meta.path.is_ident("verbose_name_plural") {
9420                let s: LitStr = meta.value()?.parse()?;
9421                out.verbose_name_plural = Some(s.value());
9422                return Ok(());
9423            }
9424            if meta.path.is_ident("db_table_comment") {
9425                // Free-form table-level comment attached to the DB
9426                // catalog.
9427                let s: LitStr = meta.value()?.parse()?;
9428                out.db_table_comment = Some(s.value());
9429                return Ok(());
9430            }
9431            if meta.path.is_ident("proxy") {
9432                // Declarative flag
9433                // marking the struct as a proxy of another model that
9434                // shares its DB table. Stored on `ModelSchema::proxy`
9435                // so future codegen can skip `CreateTable` emission
9436                // for proxies (parent owns the table) and route
9437                // per-instance method resolution to the proxy class.
9438                //
9439                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9440                let value = if meta.input.peek(syn::Token![=]) {
9441                    meta.value()?.parse::<syn::LitBool>()?.value
9442                } else {
9443                    true
9444                };
9445                out.proxy = value;
9446                return Ok(());
9447            }
9448            if meta.path.is_ident("order_with_respect_to") {
9449                // `order_with_respect_to = "parent_fk"` — the model's
9450                // instances are intrinsically ordered relative to
9451                // their parent FK.
9452                //
9453                // rustango stores the FK field name on
9454                // `ModelSchema::order_with_respect_to`. Declarative-only
9455                // today: the migration writer + admin surfaces still
9456                // treat every model identically. Future codegen will
9457                // key off the metadata to auto-emit the `_order`
9458                // column and reorder helpers.
9459                //
9460                // Validated as a Rust-shape identifier so the macro
9461                // can reject typos at derive time.
9462                let s: LitStr = meta.value()?.parse()?;
9463                let raw = s.value();
9464                if raw.is_empty() {
9465                    return Err(syn::Error::new(
9466                        s.span(),
9467                        "`order_with_respect_to` must be a non-empty FK field name",
9468                    ));
9469                }
9470                let valid = raw
9471                    .chars()
9472                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9473                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9474                if !valid {
9475                    return Err(syn::Error::new(
9476                        s.span(),
9477                        format!(
9478                            "`order_with_respect_to` must be a valid Rust \
9479                             identifier (letters / digits / underscores, \
9480                             not starting with a digit); got `{raw}`"
9481                        ),
9482                    ));
9483                }
9484                out.order_with_respect_to = Some(raw);
9485                return Ok(());
9486            }
9487            if meta.path.is_ident("required_db_features") {
9488                // Capability tokens the model needs (e.g.
9489                // `"json_extract"`, `"window_functions"`,
9490                // `"row_security"`). Comma-separated.
9491                // `manage check --deploy` walks every model and warns
9492                // when the active backend doesn't advertise the
9493                // capability.
9494                //
9495                // rustango ships a small registry of capability tokens
9496                // each dialect supports — see `Dialect::supports`.
9497                // Unknown tokens still parse (they end up on the
9498                // schema and show up in the warning) so projects can
9499                // declare aspirational capabilities and the check
9500                // verb will keep nagging until the dialect implements
9501                // them.
9502                let s: LitStr = meta.value()?.parse()?;
9503                let raw = s.value();
9504                let features: Vec<String> = raw
9505                    .split(',')
9506                    .map(str::trim)
9507                    .filter(|s| !s.is_empty())
9508                    .map(str::to_owned)
9509                    .collect();
9510                if features.is_empty() {
9511                    return Err(syn::Error::new(
9512                        s.span(),
9513                        "`required_db_features` must list at least one \
9514                         comma-separated capability token",
9515                    ));
9516                }
9517                out.required_db_features = features;
9518                return Ok(());
9519            }
9520            if meta.path.is_ident("required_db_vendor") {
9521                // The model is only meant to run against the named DB
9522                // backend. `manage check --deploy` flags a mismatch so
9523                // ops catches "I forgot to switch DATABASE_URL" at
9524                // deploy time rather than runtime.
9525                //
9526                // Restricted to the three backends rustango ships
9527                // dialects for so the check verb can compare reliably.
9528                let s: LitStr = meta.value()?.parse()?;
9529                let raw = s.value().to_ascii_lowercase();
9530                match raw.as_str() {
9531                    "postgresql" | "postgres" | "pg" => {
9532                        out.required_db_vendor = Some("postgres".to_owned());
9533                    }
9534                    "mysql" | "mariadb" => {
9535                        out.required_db_vendor = Some("mysql".to_owned());
9536                    }
9537                    "sqlite" | "sqlite3" => {
9538                        out.required_db_vendor = Some("sqlite".to_owned());
9539                    }
9540                    _ => {
9541                        return Err(syn::Error::new(
9542                            s.span(),
9543                            format!(
9544                                "unknown required_db_vendor `{raw}` — \
9545                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9546                                 `mysql` (alias: `mariadb`), or `sqlite` \
9547                                 (alias: `sqlite3`)"
9548                            ),
9549                        ));
9550                    }
9551                }
9552                return Ok(());
9553            }
9554            if meta.path.is_ident("base_manager_name") {
9555                // Name of the manager that `<instance>.<relation>_set`
9556                // uses when resolving reverse-relation managers.
9557                // Distinct from `default_manager_name` (what
9558                // `Self::objects()` returns). Stored on
9559                // `ModelSchema::base_manager_name`.
9560                //
9561                // Validated as a Rust identifier so it stays safe to
9562                // re-emit as code in future reverse-manager codegen.
9563                let s: LitStr = meta.value()?.parse()?;
9564                let raw = s.value();
9565                if raw.is_empty() {
9566                    return Err(syn::Error::new(
9567                        s.span(),
9568                        "`base_manager_name` must be a non-empty string",
9569                    ));
9570                }
9571                let valid = raw
9572                    .chars()
9573                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9574                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9575                if !valid {
9576                    return Err(syn::Error::new(
9577                        s.span(),
9578                        format!(
9579                            "`base_manager_name` must be a valid Rust \
9580                             identifier (letters / digits / underscores, \
9581                             not starting with a digit); got `{raw}`"
9582                        ),
9583                    ));
9584                }
9585                out.base_manager_name = Some(raw);
9586                return Ok(());
9587            }
9588            if meta.path.is_ident("default_related_name") {
9589                // The name reverse-relation accessors use when callers
9590                // don't override `related_name = "..."` on the FK / M2M
9591                // field. Stored on `ModelSchema::default_related_name`
9592                // so future reverse-manager codegen / schema emit /
9593                // admin templates can pick the right accessor name
9594                // (today rustango doesn't auto-emit reverse managers;
9595                // the metadata is the foundation for that work).
9596                //
9597                // Enforced non-empty + ASCII identifier-shape so the
9598                // string is safe to use as a Rust ident later.
9599                let s: LitStr = meta.value()?.parse()?;
9600                let raw = s.value();
9601                if raw.is_empty() {
9602                    return Err(syn::Error::new(
9603                        s.span(),
9604                        "`default_related_name` must be a non-empty string",
9605                    ));
9606                }
9607                let valid = raw
9608                    .chars()
9609                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9610                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9611                if !valid {
9612                    return Err(syn::Error::new(
9613                        s.span(),
9614                        format!(
9615                            "`default_related_name` must be snake_case ASCII \
9616                             (lowercase letters / digits / underscores, not \
9617                             starting with a digit); got `{raw}`"
9618                        ),
9619                    ));
9620                }
9621                out.default_related_name = Some(raw);
9622                return Ok(());
9623            }
9624            if meta.path.is_ident("extra_permissions") {
9625                // Comma-separated `codename:label` pairs.
9626                let s: LitStr = meta.value()?.parse()?;
9627                let raw = s.value();
9628                let mut pairs = Vec::new();
9629                for entry in raw.split(',') {
9630                    let entry = entry.trim();
9631                    if entry.is_empty() {
9632                        continue;
9633                    }
9634                    let (codename, label) = match entry.split_once(':') {
9635                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9636                        None => (entry.to_owned(), entry.to_owned()),
9637                    };
9638                    if codename.is_empty() {
9639                        return Err(meta.error(
9640                            "`extra_permissions` entries must be `codename:label` pairs",
9641                        ));
9642                    }
9643                    pairs.push((codename, label));
9644                }
9645                if pairs.is_empty() {
9646                    return Err(meta
9647                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9648                }
9649                out.extra_permissions = pairs;
9650                return Ok(());
9651            }
9652            if meta.path.is_ident("default_permissions") {
9653                // Comma-separated subset of the CRUD action set.
9654                // Empty means all four (the framework default).
9655                let s: LitStr = meta.value()?.parse()?;
9656                let raw = s.value();
9657                let mut actions: Vec<String> = Vec::new();
9658                for entry in raw.split(',') {
9659                    let action = entry.trim().to_ascii_lowercase();
9660                    if action.is_empty() {
9661                        continue;
9662                    }
9663                    match action.as_str() {
9664                        "add" | "change" | "delete" | "view" => {}
9665                        other => {
9666                            return Err(syn::Error::new(
9667                                s.span(),
9668                                format!(
9669                                    "unknown default_permissions action `{other}` — \
9670                                     expected one of `add`, `change`, `delete`, `view`"
9671                                ),
9672                            ));
9673                        }
9674                    }
9675                    if !actions.contains(&action) {
9676                        actions.push(action);
9677                    }
9678                }
9679                if actions.is_empty() {
9680                    return Err(syn::Error::new(
9681                        s.span(),
9682                        "`default_permissions = \"…\"` must list at least one action; \
9683                         use `permissions = false` on the container if you want NO \
9684                         permissions seeded for this model.",
9685                    ));
9686                }
9687                out.default_permissions = actions;
9688                return Ok(());
9689            }
9690            if meta.path.is_ident("get_latest_by") {
9691                // The `-` prefix selects descending order.
9692                let s: LitStr = meta.value()?.parse()?;
9693                let raw = s.value();
9694                let trimmed = raw.trim();
9695                if trimmed.is_empty() {
9696                    return Err(meta.error("`get_latest_by` must name a column"));
9697                }
9698                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9699                    (stripped.to_owned(), true)
9700                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9701                    (stripped.to_owned(), false)
9702                } else {
9703                    (trimmed.to_owned(), false)
9704                };
9705                if col.is_empty() {
9706                    return Err(meta.error("`get_latest_by` must name a column"));
9707                }
9708                out.get_latest_by = Some((col, desc));
9709                return Ok(());
9710            }
9711            if meta.path.is_ident("unique_together") {
9712                // Composite UNIQUE index. Two syntaxes:
9713                //
9714                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9715                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9716                //
9717                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9718                // (col1, col2)`, where <name> defaults to
9719                // `<table>_<col1>_<col2>_uq` when not supplied.
9720                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9721                out.indexes.push(IndexAttr {
9722                    name,
9723                    columns,
9724                    unique: true,
9725                    method: "btree".to_owned(),
9726                    where_clause: None,
9727                include: Vec::new(),
9728                });
9729                return Ok(());
9730            }
9731            if meta.path.is_ident("index_together") {
9732                // Composite (non-unique) index. Two syntaxes
9733                // mirroring `unique_together`.
9734                //
9735                //   #[rustango(index_together = "created_at, status")]
9736                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9737                let (columns, name) = parse_together_attr(&meta, "index_together")?;
9738                out.indexes.push(IndexAttr {
9739                    name,
9740                    columns,
9741                    unique: false,
9742                    method: "btree".to_owned(),
9743                    where_clause: None,
9744                include: Vec::new(),
9745                });
9746                return Ok(());
9747            }
9748            if meta.path.is_ident("unique_when") {
9749                // Partial unique index. Issue #265 / T1.3.
9750                //
9751                //   #[rustango(unique_when(
9752                //       columns   = "email",
9753                //       condition = "deleted_at IS NULL",
9754                //       name      = "unique_active_email"
9755                //   ))]
9756                //
9757                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9758                // on PG / SQLite (both ship partial indexes natively).
9759                // MySQL falls back to a plain UNIQUE index — the
9760                // condition is lost; document the limitation in the
9761                // generated migration.
9762                let mut columns: Option<Vec<String>> = None;
9763                let mut condition: Option<String> = None;
9764                let mut name: Option<String> = None;
9765                let mut include: Vec<String> = Vec::new();
9766                meta.parse_nested_meta(|inner| {
9767                    if inner.path.is_ident("columns") {
9768                        let s: LitStr = inner.value()?.parse()?;
9769                        columns = Some(split_field_list(&s.value()));
9770                        return Ok(());
9771                    }
9772                    if inner.path.is_ident("condition") {
9773                        let s: LitStr = inner.value()?.parse()?;
9774                        condition = Some(s.value());
9775                        return Ok(());
9776                    }
9777                    if inner.path.is_ident("name") {
9778                        let s: LitStr = inner.value()?.parse()?;
9779                        name = Some(s.value());
9780                        return Ok(());
9781                    }
9782                    if inner.path.is_ident("include") {
9783                        // PG 11+ covering-index columns. Non-key columns
9784                        // travel with the index leaf for index-only
9785                        // scans. Dropped on MySQL/SQLite by the writer.
9786                        let s: LitStr = inner.value()?.parse()?;
9787                        include = split_field_list(&s.value());
9788                        return Ok(());
9789                    }
9790                    Err(inner.error(
9791                        "unknown unique_when attribute (supported: \
9792                         `columns = \"...\"`, `condition = \"...\"`, \
9793                         `name = \"...\"`, `include = \"...\"`)",
9794                    ))
9795                })?;
9796                let columns = columns.ok_or_else(|| {
9797                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9798                })?;
9799                let condition = condition.ok_or_else(|| {
9800                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9801                })?;
9802                if columns.is_empty() {
9803                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9804                }
9805                out.indexes.push(IndexAttr {
9806                    name,
9807                    columns,
9808                    unique: true,
9809                    method: "btree".to_owned(),
9810                    where_clause: Some(condition),
9811                    include,
9812                });
9813                return Ok(());
9814            }
9815            if meta.path.is_ident("index_when") {
9816                // Non-unique partial index. Sibling of `unique_when`
9817                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9818                //
9819                //   #[rustango(index_when(
9820                //       columns   = "status, created_at",
9821                //       condition = "deleted_at IS NULL",
9822                //       name      = "active_status_created_idx"
9823                //   ))]
9824                //
9825                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9826                // on PG / SQLite (both ship partial indexes natively).
9827                // MySQL has no native partial-index support — the writer
9828                // emits a plain CREATE INDEX and the condition is lost;
9829                // operators wanting that selectivity on MySQL should
9830                // declare a covering index plus an application-level
9831                // filter.
9832                let mut columns: Option<Vec<String>> = None;
9833                let mut condition: Option<String> = None;
9834                let mut name: Option<String> = None;
9835                let mut method: String = "btree".to_owned();
9836                let mut include: Vec<String> = Vec::new();
9837                meta.parse_nested_meta(|inner| {
9838                    if inner.path.is_ident("columns") {
9839                        let s: LitStr = inner.value()?.parse()?;
9840                        columns = Some(split_field_list(&s.value()));
9841                        return Ok(());
9842                    }
9843                    if inner.path.is_ident("condition") {
9844                        let s: LitStr = inner.value()?.parse()?;
9845                        condition = Some(s.value());
9846                        return Ok(());
9847                    }
9848                    if inner.path.is_ident("name") {
9849                        let s: LitStr = inner.value()?.parse()?;
9850                        name = Some(s.value());
9851                        return Ok(());
9852                    }
9853                    if inner.path.is_ident("method") {
9854                        let s: LitStr = inner.value()?.parse()?;
9855                        method = s.value();
9856                        return Ok(());
9857                    }
9858                    if inner.path.is_ident("include") {
9859                        // PG 11+ covering-index columns; non-key
9860                        // columns travel with the index leaf. Dropped
9861                        // on MySQL/SQLite.
9862                        let s: LitStr = inner.value()?.parse()?;
9863                        include = split_field_list(&s.value());
9864                        return Ok(());
9865                    }
9866                    Err(inner.error(
9867                        "unknown index_when attribute (supported: \
9868                         `columns = \"...\"`, `condition = \"...\"`, \
9869                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9870                         `include = \"...\"`)",
9871                    ))
9872                })?;
9873                let columns = columns
9874                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9875                let condition = condition.ok_or_else(|| {
9876                    meta.error("`index_when(...)` requires `condition = \"...\"`")
9877                })?;
9878                if columns.is_empty() {
9879                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
9880                }
9881                out.indexes.push(IndexAttr {
9882                    name,
9883                    columns,
9884                    unique: false,
9885                    method,
9886                    where_clause: Some(condition),
9887                    include,
9888                });
9889                return Ok(());
9890            }
9891            if meta.path.is_ident("index") {
9892                // Container-level composite index. Two syntaxes:
9893                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
9894                //   #[rustango(index("col1, col2"))]    — call form (same result)
9895                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9896                // The call form takes the column list as a leading string
9897                // literal, then optional `unique` / `name = "..."` /
9898                // `method = "..."` flags (a leading literal can't compose
9899                // under `parse_nested_meta`, so the paren body is parsed by
9900                // hand). `unique_together` / `index_together` remain
9901                // aliases for the same feature.
9902                let cols_lit: LitStr;
9903                let mut unique = false;
9904                let mut name: Option<String> = None;
9905                let mut method = "btree".to_owned();
9906                if meta.input.peek(syn::token::Paren) {
9907                    let content;
9908                    syn::parenthesized!(content in meta.input);
9909                    cols_lit = content.parse()?;
9910                    while content.peek(syn::Token![,]) {
9911                        content.parse::<syn::Token![,]>()?;
9912                        if content.is_empty() {
9913                            break;
9914                        }
9915                        let flag: syn::Ident = content.parse()?;
9916                        if flag == "unique" {
9917                            unique = true;
9918                        } else if flag == "name" {
9919                            content.parse::<syn::Token![=]>()?;
9920                            let s: LitStr = content.parse()?;
9921                            name = Some(s.value());
9922                        } else if flag == "method" {
9923                            content.parse::<syn::Token![=]>()?;
9924                            let s: LitStr = content.parse()?;
9925                            let v = s.value();
9926                            match v.as_str() {
9927                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9928                                | "bloom" => method = v,
9929                                other => {
9930                                    return Err(syn::Error::new(
9931                                        s.span(),
9932                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9933                                    ));
9934                                }
9935                            }
9936                        } else {
9937                            return Err(syn::Error::new(
9938                                flag.span(),
9939                                "unknown index flag (supported: `unique`, `name`, `method`)",
9940                            ));
9941                        }
9942                    }
9943                } else {
9944                    cols_lit = meta.value()?.parse()?;
9945                }
9946                let columns = split_field_list(&cols_lit.value());
9947                out.indexes.push(IndexAttr {
9948                    name,
9949                    columns,
9950                    unique,
9951                    method,
9952                    where_clause: None,
9953                    include: Vec::new(),
9954                });
9955                return Ok(());
9956            }
9957            if meta.path.is_ident("check") {
9958                // #[rustango(check(name = "…", expr = "…"))]
9959                let mut name: Option<String> = None;
9960                let mut expr: Option<String> = None;
9961                meta.parse_nested_meta(|inner| {
9962                    if inner.path.is_ident("name") {
9963                        let s: LitStr = inner.value()?.parse()?;
9964                        name = Some(s.value());
9965                        return Ok(());
9966                    }
9967                    if inner.path.is_ident("expr") {
9968                        let s: LitStr = inner.value()?.parse()?;
9969                        expr = Some(s.value());
9970                        return Ok(());
9971                    }
9972                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9973                })?;
9974                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9975                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9976                out.checks.push(CheckAttr { name, expr });
9977                return Ok(());
9978            }
9979            if meta.path.is_ident("exclude") {
9980                // #[rustango(exclude(name = "…", using = "gist",
9981                //                    elements = "col WITH op, col WITH op",
9982                //                    where = "…"))]
9983                let mut name: Option<String> = None;
9984                let mut using: Option<String> = None;
9985                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9986                let mut where_clause: Option<String> = None;
9987                meta.parse_nested_meta(|inner| {
9988                    if inner.path.is_ident("name") {
9989                        let s: LitStr = inner.value()?.parse()?;
9990                        name = Some(s.value());
9991                        return Ok(());
9992                    }
9993                    if inner.path.is_ident("using") {
9994                        let s: LitStr = inner.value()?.parse()?;
9995                        using = Some(s.value());
9996                        return Ok(());
9997                    }
9998                    if inner.path.is_ident("elements") {
9999                        let s: LitStr = inner.value()?.parse()?;
10000                        elements_raw = Some((s.value(), s.span()));
10001                        return Ok(());
10002                    }
10003                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10004                        let s: LitStr = inner.value()?.parse()?;
10005                        where_clause = Some(s.value());
10006                        return Ok(());
10007                    }
10008                    Err(inner.error(
10009                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10010                    ))
10011                })?;
10012                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10013                let using = using.unwrap_or_else(|| "gist".to_owned());
10014                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10015                    meta.error(
10016                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
10017                    )
10018                })?;
10019                // Parse `col WITH op` pairs separated by commas.
10020                let mut elements: Vec<(String, String)> = Vec::new();
10021                for pair in elements_str.split(',') {
10022                    let pair = pair.trim();
10023                    if pair.is_empty() {
10024                        continue;
10025                    }
10026                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10027                    let col = split.next().unwrap_or("").trim();
10028                    let rest = split.next().unwrap_or("").trim();
10029                    // `WITH op` — case-insensitive on `WITH`, then op.
10030                    let rest_lc = rest.to_ascii_lowercase();
10031                    let op = rest_lc
10032                        .strip_prefix("with")
10033                        .map(|r| r.trim_start())
10034                        .filter(|r| !r.is_empty())
10035                        .map(|_| {
10036                            // Pull the original-case op from `rest` after the
10037                            // `WITH ` token (5 chars).
10038                            rest[4..].trim_start().to_owned()
10039                        });
10040                    let Some(op) = op else {
10041                        return Err(syn::Error::new(
10042                            elements_span,
10043                            format!(
10044                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10045                                 (e.g. `room_id WITH =` or `during WITH &&`)"
10046                            ),
10047                        ));
10048                    };
10049                    if col.is_empty() || op.is_empty() {
10050                        return Err(syn::Error::new(
10051                            elements_span,
10052                            format!(
10053                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10054                                 (both sides non-empty)"
10055                            ),
10056                        ));
10057                    }
10058                    elements.push((col.to_owned(), op));
10059                }
10060                if elements.is_empty() {
10061                    return Err(syn::Error::new(
10062                        elements_span,
10063                        "exclude requires at least one `col WITH op` element",
10064                    ));
10065                }
10066                out.excludes.push(ExcludeAttr {
10067                    name,
10068                    using,
10069                    elements,
10070                    where_clause,
10071                });
10072                return Ok(());
10073            }
10074            if meta.path.is_ident("generic_fk") {
10075                let mut gfk = GenericFkAttr {
10076                    name: String::new(),
10077                    ct_column: String::new(),
10078                    pk_column: String::new(),
10079                };
10080                meta.parse_nested_meta(|inner| {
10081                    if inner.path.is_ident("name") {
10082                        let s: LitStr = inner.value()?.parse()?;
10083                        gfk.name = s.value();
10084                        return Ok(());
10085                    }
10086                    if inner.path.is_ident("ct_column") {
10087                        let s: LitStr = inner.value()?.parse()?;
10088                        gfk.ct_column = s.value();
10089                        return Ok(());
10090                    }
10091                    if inner.path.is_ident("pk_column") {
10092                        let s: LitStr = inner.value()?.parse()?;
10093                        gfk.pk_column = s.value();
10094                        return Ok(());
10095                    }
10096                    Err(inner.error(
10097                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10098                    ))
10099                })?;
10100                if gfk.name.is_empty() {
10101                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10102                }
10103                if gfk.ct_column.is_empty() {
10104                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10105                }
10106                if gfk.pk_column.is_empty() {
10107                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10108                }
10109                out.generic_fks.push(gfk);
10110                return Ok(());
10111            }
10112            if meta.path.is_ident("fk_composite") {
10113                let mut fk = CompositeFkAttr {
10114                    name: String::new(),
10115                    to: String::new(),
10116                    from: Vec::new(),
10117                    on: Vec::new(),
10118                };
10119                meta.parse_nested_meta(|inner| {
10120                    if inner.path.is_ident("name") {
10121                        let s: LitStr = inner.value()?.parse()?;
10122                        fk.name = s.value();
10123                        return Ok(());
10124                    }
10125                    if inner.path.is_ident("to") {
10126                        let s: LitStr = inner.value()?.parse()?;
10127                        fk.to = s.value();
10128                        return Ok(());
10129                    }
10130                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10131                    // comma-list of string literals.
10132                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10133                        let value = inner.value()?;
10134                        let content;
10135                        syn::parenthesized!(content in value);
10136                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10137                            content.parse_terminated(
10138                                |p| p.parse::<syn::LitStr>(),
10139                                syn::Token![,],
10140                            )?;
10141                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10142                        if inner.path.is_ident("on") {
10143                            fk.on = cols;
10144                        } else {
10145                            fk.from = cols;
10146                        }
10147                        return Ok(());
10148                    }
10149                    Err(inner.error(
10150                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10151                    ))
10152                })?;
10153                if fk.name.is_empty() {
10154                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10155                }
10156                if fk.to.is_empty() {
10157                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10158                }
10159                if fk.from.is_empty() || fk.on.is_empty() {
10160                    return Err(meta.error(
10161                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10162                    ));
10163                }
10164                if fk.from.len() != fk.on.len() {
10165                    return Err(meta.error(format!(
10166                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10167                        fk.from.len(),
10168                        fk.on.len(),
10169                    )));
10170                }
10171                out.composite_fks.push(fk);
10172                return Ok(());
10173            }
10174            if meta.path.is_ident("m2m") {
10175                let mut m2m = M2MAttr {
10176                    name: String::new(),
10177                    to: String::new(),
10178                    through: String::new(),
10179                    src: String::new(),
10180                    dst: String::new(),
10181                    auto_create: true,
10182                };
10183                meta.parse_nested_meta(|inner| {
10184                    if inner.path.is_ident("name") {
10185                        let s: LitStr = inner.value()?.parse()?;
10186                        m2m.name = s.value();
10187                        return Ok(());
10188                    }
10189                    if inner.path.is_ident("to") {
10190                        let s: LitStr = inner.value()?.parse()?;
10191                        m2m.to = s.value();
10192                        return Ok(());
10193                    }
10194                    if inner.path.is_ident("through") {
10195                        let s: LitStr = inner.value()?.parse()?;
10196                        m2m.through = s.value();
10197                        return Ok(());
10198                    }
10199                    if inner.path.is_ident("src") {
10200                        let s: LitStr = inner.value()?.parse()?;
10201                        m2m.src = s.value();
10202                        return Ok(());
10203                    }
10204                    if inner.path.is_ident("dst") {
10205                        let s: LitStr = inner.value()?.parse()?;
10206                        m2m.dst = s.value();
10207                        return Ok(());
10208                    }
10209                    if inner.path.is_ident("auto_create") {
10210                        let lit: syn::LitBool = inner.value()?.parse()?;
10211                        m2m.auto_create = lit.value;
10212                        return Ok(());
10213                    }
10214                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10215                })?;
10216                if m2m.name.is_empty() {
10217                    return Err(meta.error("m2m requires `name = \"...\"`"));
10218                }
10219                if m2m.to.is_empty() {
10220                    return Err(meta.error("m2m requires `to = \"...\"`"));
10221                }
10222                if m2m.through.is_empty() {
10223                    return Err(meta.error("m2m requires `through = \"...\"`"));
10224                }
10225                if m2m.src.is_empty() {
10226                    return Err(meta.error("m2m requires `src = \"...\"`"));
10227                }
10228                if m2m.dst.is_empty() {
10229                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10230                }
10231                out.m2m.push(m2m);
10232                return Ok(());
10233            }
10234            if meta.path.is_ident("generic_m2m") {
10235                let mut gm = GenericM2MAttr {
10236                    name: String::new(),
10237                    through: String::new(),
10238                    pk_column: String::new(),
10239                    ct_column: String::new(),
10240                    related_column: String::new(),
10241                };
10242                meta.parse_nested_meta(|inner| {
10243                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10244                        let s: LitStr = inner.value()?.parse()?;
10245                        Ok(s.value())
10246                    };
10247                    if inner.path.is_ident("name") {
10248                        gm.name = field(&inner)?;
10249                        return Ok(());
10250                    }
10251                    if inner.path.is_ident("through") {
10252                        gm.through = field(&inner)?;
10253                        return Ok(());
10254                    }
10255                    if inner.path.is_ident("pk_column") {
10256                        gm.pk_column = field(&inner)?;
10257                        return Ok(());
10258                    }
10259                    if inner.path.is_ident("ct_column") {
10260                        gm.ct_column = field(&inner)?;
10261                        return Ok(());
10262                    }
10263                    if inner.path.is_ident("related_column") {
10264                        gm.related_column = field(&inner)?;
10265                        return Ok(());
10266                    }
10267                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10268                })?;
10269                for (val, label) in [
10270                    (&gm.name, "name"),
10271                    (&gm.through, "through"),
10272                    (&gm.pk_column, "pk_column"),
10273                    (&gm.ct_column, "ct_column"),
10274                    (&gm.related_column, "related_column"),
10275                ] {
10276                    if val.is_empty() {
10277                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10278                    }
10279                }
10280                out.generic_m2m.push(gm);
10281                return Ok(());
10282            }
10283            Err(meta.error("unknown rustango container attribute"))
10284        })?;
10285    }
10286    Ok(out)
10287}
10288
10289/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10290/// owned field names, trimming whitespace and skipping empty entries.
10291/// Field-name validation against the model is done by the caller.
10292fn split_field_list(raw: &str) -> Vec<String> {
10293    raw.split(',')
10294        .map(str::trim)
10295        .filter(|s| !s.is_empty())
10296        .map(str::to_owned)
10297        .collect()
10298}
10299
10300/// Shared parser for `unique_together` and `index_together` container
10301/// attrs. Accepts both shapes:
10302///
10303///   * `attr = "col1, col2"`              — auto-derived index name.
10304///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10305///
10306/// Returns `(columns, name)`.
10307fn parse_together_attr(
10308    meta: &syn::meta::ParseNestedMeta<'_>,
10309    attr: &str,
10310) -> syn::Result<(Vec<String>, Option<String>)> {
10311    // Disambiguate by whether the next token is `=` (key-value) or
10312    // `(` (parenthesized).
10313    if meta.input.peek(syn::Token![=]) {
10314        let cols_lit: LitStr = meta.value()?.parse()?;
10315        let columns = split_field_list(&cols_lit.value());
10316        check_together_columns(meta, attr, &columns)?;
10317        return Ok((columns, None));
10318    }
10319    let mut columns: Option<Vec<String>> = None;
10320    let mut name: Option<String> = None;
10321    meta.parse_nested_meta(|inner| {
10322        if inner.path.is_ident("columns") {
10323            let s: LitStr = inner.value()?.parse()?;
10324            columns = Some(split_field_list(&s.value()));
10325            return Ok(());
10326        }
10327        if inner.path.is_ident("name") {
10328            let s: LitStr = inner.value()?.parse()?;
10329            name = Some(s.value());
10330            return Ok(());
10331        }
10332        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10333    })?;
10334    let columns = columns.ok_or_else(|| {
10335        meta.error(format!(
10336            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10337        ))
10338    })?;
10339    check_together_columns(meta, attr, &columns)?;
10340    Ok((columns, name))
10341}
10342
10343fn check_together_columns(
10344    meta: &syn::meta::ParseNestedMeta<'_>,
10345    attr: &str,
10346    columns: &[String],
10347) -> syn::Result<()> {
10348    if columns.len() < 2 {
10349        let single = if attr == "unique_together" {
10350            "#[rustango(unique)] on the field"
10351        } else {
10352            "#[rustango(index)] on the field"
10353        };
10354        return Err(meta.error(format!(
10355            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10356        )));
10357    }
10358    Ok(())
10359}
10360
10361/// Parse the fieldsets DSL: pipe-separated sections, optional
10362/// `"Title:"` prefix on each, comma-separated field names after.
10363/// Examples:
10364/// * `"name, office"` → one untitled section with two fields
10365/// * `"Identity: name, office | Metadata: created_at"` → two titled
10366///   sections
10367///
10368/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10369fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10370    raw.split('|')
10371        .map(str::trim)
10372        .filter(|s| !s.is_empty())
10373        .map(|section| {
10374            // Split off an optional `Title:` prefix (first colon).
10375            let (title, rest) = match section.split_once(':') {
10376                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10377                _ => (String::new(), section),
10378            };
10379            let fields = split_field_list(rest);
10380            (title, fields)
10381        })
10382        .collect()
10383}
10384
10385/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10386/// comma-separated entry maps a target field to one or more source
10387/// fields joined with `+`. Whitespace around tokens is trimmed.
10388/// Entries missing `:` or with empty target/source lists are dropped.
10389fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10390    raw.split(',')
10391        .map(str::trim)
10392        .filter(|s| !s.is_empty())
10393        .filter_map(|entry| {
10394            let (target, sources_raw) = entry.split_once(':')?;
10395            let target = target.trim().to_owned();
10396            if target.is_empty() {
10397                return None;
10398            }
10399            let sources: Vec<String> = sources_raw
10400                .split('+')
10401                .map(|s| s.trim().to_owned())
10402                .filter(|s| !s.is_empty())
10403                .collect();
10404            if sources.is_empty() {
10405                return None;
10406            }
10407            Some((target, sources))
10408        })
10409        .collect()
10410}
10411
10412/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10413/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10414/// and empty halves drop silently — the macro layer only enforces shape,
10415/// not field-name vs. widget-name validity (those checks happen at
10416/// `AdminConfig` consumption time). Issue #359.
10417fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10418    raw.split(',')
10419        .map(str::trim)
10420        .filter(|s| !s.is_empty())
10421        .filter_map(|entry| {
10422            let (field, widget) = entry.split_once(':')?;
10423            let field = field.trim().to_owned();
10424            let widget = widget.trim().to_owned();
10425            if field.is_empty() || widget.is_empty() {
10426                return None;
10427            }
10428            Some((field, widget))
10429        })
10430        .collect()
10431}
10432
10433/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10434/// Returns `(field_name, desc)` pairs in the same order as the input.
10435fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10436    raw.split(',')
10437        .map(str::trim)
10438        .filter(|s| !s.is_empty())
10439        .map(|spec| {
10440            spec.strip_prefix('-')
10441                .map_or((spec.to_owned(), false), |rest| {
10442                    (rest.trim().to_owned(), true)
10443                })
10444        })
10445        .collect()
10446}
10447
10448struct FieldAttrs {
10449    column: Option<String>,
10450    primary_key: bool,
10451    fk: Option<String>,
10452    o2o: Option<String>,
10453    on: Option<String>,
10454    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10455    /// "set_default" | "no_action")]` — what happens to this row when
10456    /// the row it points at is deleted.
10457    /// Only meaningful when `fk` / `o2o` is
10458    /// also set; the macro errors at compile time if applied to a
10459    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10460    /// DDL writer renders `ON DELETE <action>` after the constraint
10461    /// clause when this is `Some`; `None` falls back to the database
10462    /// default (NO ACTION on every backend rustango supports).
10463    on_delete: Option<String>,
10464    /// `#[rustango(related_name = "...")]` — per-FK
10465    /// reverse-accessor override. When set, the derive emits
10466    /// `Parent::<related_name>[_pool]` instead of the container-level
10467    /// `default_related_name` or the `<child_snake>_set[_pool]`
10468    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10469    /// silently ignored on non-FK fields. Follow-up to #816.
10470    related_name: Option<String>,
10471    max_length: Option<u32>,
10472    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10473    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10474    /// unconstrained `vector` column.
10475    vector_dims: Option<u32>,
10476    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10477    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10478    /// unconstrained `geometry(Point)` column (SRID 0).
10479    geometry_srid: Option<u32>,
10480    min: Option<i64>,
10481    max: Option<i64>,
10482    default: Option<String>,
10483    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10484    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10485    /// "gen_random_uuid()"`. The Rust field type must be
10486    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10487    /// INSERTs so the DB DEFAULT fires.
10488    auto_uuid: bool,
10489    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10490    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10491    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10492    /// the field is `Auto::Unset`, then bound as a normal parameter
10493    /// rather than relying on a per-dialect DB function. Issue #823
10494    /// (Eloquent `HasUuids`).
10495    ///
10496    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10497    /// Composes with every backend (PG / MySQL / SQLite) — no
10498    /// `gen_random_uuid()` requirement on the database.
10499    default_uuid_v7: bool,
10500    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10501    /// Server-set on insert, immutable from app code afterwards.
10502    /// Implies `auto + default = "now()"`. Field type must be
10503    /// `DateTime<Utc>`.
10504    auto_now_add: bool,
10505    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10506    /// every insert AND every update. Implies `auto + default =
10507    /// "now()"`; the macro additionally rewrites `update_on` /
10508    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10509    /// field value.
10510    auto_now: bool,
10511    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10512    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10513    /// `soft_delete_on(executor)` and `restore_on(executor)`
10514    /// methods on the model.
10515    soft_delete: bool,
10516    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10517    /// the column in the generated DDL.
10518    unique: bool,
10519    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10520    /// generates a `CREATE INDEX` for this column. `unique` here means
10521    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10522    index: bool,
10523    index_unique: bool,
10524    index_name: Option<String>,
10525    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10526    /// `"btree"`. Issue #34.
10527    index_method: String,
10528    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10529    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10530    /// the macro skips this column from every INSERT and UPDATE
10531    /// path, so the database always recomputes the value from
10532    /// `EXPR`. Backlog item #35.
10533    generated_as: Option<String>,
10534    /// `#[rustango(help_text = "…")]` — help text
10535    /// rendered below the admin form's input. Threaded into
10536    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10537    /// layers can read it.
10538    help_text: Option<String>,
10539    /// `#[rustango(choices = "value:Label, value:Label")]` — the
10540    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10541    /// as a `&'static [(&'static str, &'static str)]` slice. When
10542    /// present, the admin form renders a `<select>` instead of `<input>`
10543    /// and the validator rejects values not in the list. Only meaningful
10544    /// for `FieldType::String`; the macro errors at compile time if
10545    /// applied to a non-string field.
10546    choices: Option<Vec<(String, String)>>,
10547    /// `#[rustango(db_comment = "…")]` — DB-side column
10548    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10549    /// the comment in CREATE TABLE; Postgres emits a separate
10550    /// `COMMENT ON COLUMN` statement after the table is created;
10551    /// SQLite silently drops the value (no native column comments).
10552    db_comment: Option<String>,
10553    /// `#[rustango(verbose_name = "…")]` — human-readable
10554    /// label for the field. Threaded into `FieldSchema::verbose_name`
10555    /// so admin column headers, form labels, and other display
10556    /// surfaces can prefer the friendly caption over the Rust
10557    /// identifier. `None` means renderers fall back to the field name.
10558    verbose_name: Option<String>,
10559    /// `#[rustango(editable = false)]` — opt out of
10560    /// auto-generated form rendering. Defaults to `true` so existing
10561    /// fields keep their current admin / form behavior; setting
10562    /// `false` removes the field from the admin change-form entirely
10563    /// (the value is still visible on detail / list views, just not
10564    /// editable).
10565    editable: bool,
10566    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10567    /// "form may submit empty even when DB is NOT NULL". Threaded into
10568    /// `FieldSchema::blank`. Defaults to `false`.
10569    blank: bool,
10570    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10571    /// case-insensitive text column. Threaded into
10572    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10573    /// fields; the macro errors at derive time if applied elsewhere.
10574    case_insensitive: bool,
10575    /// `#[rustango(validators = "email,url")]` — the
10576    /// model-level validator chain. Comma-separated names that
10577    /// dispatch to the `validators::*` family in `validate_value`.
10578    /// Empty by default; fires on every typed INSERT/UPDATE.
10579    validators: Vec<String>,
10580}
10581
10582fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10583    let mut out = FieldAttrs {
10584        column: None,
10585        primary_key: false,
10586        fk: None,
10587        o2o: None,
10588        on: None,
10589        on_delete: None,
10590        related_name: None,
10591        max_length: None,
10592        vector_dims: None,
10593        geometry_srid: None,
10594        min: None,
10595        max: None,
10596        default: None,
10597        auto_uuid: false,
10598        default_uuid_v7: false,
10599        auto_now_add: false,
10600        auto_now: false,
10601        soft_delete: false,
10602        unique: false,
10603        index: false,
10604        index_unique: false,
10605        index_name: None,
10606        index_method: "btree".to_owned(),
10607        generated_as: None,
10608        help_text: None,
10609        choices: None,
10610        db_comment: None,
10611        verbose_name: None,
10612        editable: true,
10613        blank: false,
10614        case_insensitive: false,
10615        validators: Vec::new(),
10616    };
10617    for attr in &field.attrs {
10618        if !attr.path().is_ident("rustango") {
10619            continue;
10620        }
10621        attr.parse_nested_meta(|meta| {
10622            if meta.path.is_ident("column") {
10623                let s: LitStr = meta.value()?.parse()?;
10624                let name = s.value();
10625                validate_sql_identifier(&name, "column", s.span())?;
10626                out.column = Some(name);
10627                return Ok(());
10628            }
10629            if meta.path.is_ident("primary_key") {
10630                out.primary_key = true;
10631                return Ok(());
10632            }
10633            if meta.path.is_ident("fk") {
10634                let s: LitStr = meta.value()?.parse()?;
10635                out.fk = Some(s.value());
10636                return Ok(());
10637            }
10638            if meta.path.is_ident("o2o") {
10639                let s: LitStr = meta.value()?.parse()?;
10640                out.o2o = Some(s.value());
10641                return Ok(());
10642            }
10643            if meta.path.is_ident("on") {
10644                let s: LitStr = meta.value()?.parse()?;
10645                out.on = Some(s.value());
10646                return Ok(());
10647            }
10648            if meta.path.is_ident("on_delete") {
10649                let s: LitStr = meta.value()?.parse()?;
10650                let raw = s.value();
10651                let normalized = raw.trim().to_ascii_lowercase();
10652                // Validate at parse time so the user gets a clear span
10653                // rather than a downstream compile error in the emit.
10654                match normalized.as_str() {
10655                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10656                    _ => {
10657                        return Err(syn::Error::new(
10658                            s.span(),
10659                            format!(
10660                                "unknown on_delete action `{raw}`; expected one of \
10661                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10662                            ),
10663                        ));
10664                    }
10665                }
10666                out.on_delete = Some(normalized);
10667                return Ok(());
10668            }
10669            if meta.path.is_ident("related_name") {
10670                let s: LitStr = meta.value()?.parse()?;
10671                let raw = s.value();
10672                if raw.trim().is_empty() {
10673                    return Err(syn::Error::new(
10674                        s.span(),
10675                        "`related_name` must be a non-empty identifier",
10676                    ));
10677                }
10678                // Validate as a Rust-ident shape — the value becomes
10679                // a method name on the parent type. Same rule as
10680                // `default_related_name` so the two surfaces match.
10681                if !raw
10682                    .chars()
10683                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10684                    || raw.starts_with(char::is_numeric)
10685                {
10686                    return Err(syn::Error::new(
10687                        s.span(),
10688                        "`related_name` must be snake_case ASCII (lowercase letters, \
10689                         digits, underscores; no leading digit)",
10690                    ));
10691                }
10692                out.related_name = Some(raw);
10693                return Ok(());
10694            }
10695            if meta.path.is_ident("max_length") {
10696                let lit: syn::LitInt = meta.value()?.parse()?;
10697                out.max_length = Some(lit.base10_parse::<u32>()?);
10698                return Ok(());
10699            }
10700            // `#[rustango(vector(dims = N))]` — pgvector column
10701            // dimension (#824). Nested-meta form so it reads like the
10702            // other typed-column attrs.
10703            if meta.path.is_ident("vector") {
10704                meta.parse_nested_meta(|inner| {
10705                    if inner.path.is_ident("dims") {
10706                        let lit: syn::LitInt = inner.value()?.parse()?;
10707                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10708                        return Ok(());
10709                    }
10710                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10711                })?;
10712                return Ok(());
10713            }
10714            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10715            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10716            if meta.path.is_ident("geometry") {
10717                meta.parse_nested_meta(|inner| {
10718                    if inner.path.is_ident("srid") {
10719                        let lit: syn::LitInt = inner.value()?.parse()?;
10720                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10721                        return Ok(());
10722                    }
10723                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10724                })?;
10725                return Ok(());
10726            }
10727            if meta.path.is_ident("min") {
10728                out.min = Some(parse_signed_i64(&meta)?);
10729                return Ok(());
10730            }
10731            if meta.path.is_ident("max") {
10732                out.max = Some(parse_signed_i64(&meta)?);
10733                return Ok(());
10734            }
10735            if meta.path.is_ident("default") {
10736                let s: LitStr = meta.value()?.parse()?;
10737                out.default = Some(s.value());
10738                return Ok(());
10739            }
10740            if meta.path.is_ident("generated_as") {
10741                let s: LitStr = meta.value()?.parse()?;
10742                out.generated_as = Some(s.value());
10743                return Ok(());
10744            }
10745            if meta.path.is_ident("help_text") {
10746                let s: LitStr = meta.value()?.parse()?;
10747                out.help_text = Some(s.value());
10748                return Ok(());
10749            }
10750            if meta.path.is_ident("choices") {
10751                let s: LitStr = meta.value()?.parse()?;
10752                let raw = s.value();
10753                let mut pairs: Vec<(String, String)> = Vec::new();
10754                for chunk in raw.split(',') {
10755                    let chunk = chunk.trim();
10756                    if chunk.is_empty() {
10757                        continue;
10758                    }
10759                    let (value, label) = match chunk.split_once(':') {
10760                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10761                        None => (chunk.to_owned(), chunk.to_owned()),
10762                    };
10763                    if value.is_empty() {
10764                        return Err(syn::Error::new(
10765                            s.span(),
10766                            "`choices` entry has empty value before `:`",
10767                        ));
10768                    }
10769                    pairs.push((value, label));
10770                }
10771                if pairs.is_empty() {
10772                    return Err(syn::Error::new(
10773                        s.span(),
10774                        "`choices = \"…\"` must contain at least one value",
10775                    ));
10776                }
10777                out.choices = Some(pairs);
10778                return Ok(());
10779            }
10780            if meta.path.is_ident("db_comment") {
10781                let s: LitStr = meta.value()?.parse()?;
10782                out.db_comment = Some(s.value());
10783                return Ok(());
10784            }
10785            if meta.path.is_ident("verbose_name") {
10786                let s: LitStr = meta.value()?.parse()?;
10787                out.verbose_name = Some(s.value());
10788                return Ok(());
10789            }
10790            if meta.path.is_ident("editable") {
10791                // Two forms accepted:
10792                //   #[rustango(editable = false)] / true — explicit
10793                //   #[rustango(editable)] — flag form (= true, the
10794                //   default, so harmless; included for symmetry)
10795                if let Ok(v) = meta.value() {
10796                    let lit: syn::LitBool = v.parse()?;
10797                    out.editable = lit.value;
10798                } else {
10799                    out.editable = true;
10800                }
10801                return Ok(());
10802            }
10803            if meta.path.is_ident("blank") {
10804                // Two forms accepted:
10805                //   #[rustango(blank)] — flag form, true
10806                //   #[rustango(blank = false)] / true — explicit
10807                if let Ok(v) = meta.value() {
10808                    let lit: syn::LitBool = v.parse()?;
10809                    out.blank = lit.value;
10810                } else {
10811                    out.blank = true;
10812                }
10813                return Ok(());
10814            }
10815            if meta.path.is_ident("citext") {
10816                // Case-insensitive text column (#344). Two forms:
10817                //   #[rustango(citext)]          — flag form, true
10818                //   #[rustango(citext = true)]   — explicit
10819                //   #[rustango(citext = false)]  — explicit opt-out
10820                // String-only validation lives in the field-type
10821                // emitter (the FieldType discriminant is computed in
10822                // `detect_type`); the macro records the flag and the
10823                // DDL writer emits dialect-specific COLLATE / CITEXT.
10824                if let Ok(v) = meta.value() {
10825                    let lit: syn::LitBool = v.parse()?;
10826                    out.case_insensitive = lit.value;
10827                } else {
10828                    out.case_insensitive = true;
10829                }
10830                return Ok(());
10831            }
10832            if meta.path.is_ident("validators") {
10833                let s: LitStr = meta.value()?.parse()?;
10834                let raw = s.value();
10835                out.validators = raw
10836                    .split(',')
10837                    .map(str::trim)
10838                    .filter(|s| !s.is_empty())
10839                    .map(str::to_owned)
10840                    .collect();
10841                if out.validators.is_empty() {
10842                    return Err(syn::Error::new(
10843                        s.span(),
10844                        "`validators = \"…\"` must list at least one name",
10845                    ));
10846                }
10847                return Ok(());
10848            }
10849            if meta.path.is_ident("auto_uuid") {
10850                out.auto_uuid = true;
10851                // Implied: PK + auto + DEFAULT gen_random_uuid().
10852                // Each is also explicitly settable; the explicit
10853                // value wins if conflicting.
10854                out.primary_key = true;
10855                if out.default.is_none() {
10856                    out.default = Some("gen_random_uuid()".into());
10857                }
10858                return Ok(());
10859            }
10860            if meta.path.is_ident("default_uuid_v7") {
10861                // Backend-neutral counterpart of `auto_uuid` — issue #823.
10862                // No SQL DEFAULT (the macro fills the value Rust-side
10863                // before binding); just mark the field as PK + Auto
10864                // so the insert path is the `Auto::Unset → generate`
10865                // branch.
10866                out.default_uuid_v7 = true;
10867                out.primary_key = true;
10868                return Ok(());
10869            }
10870            if meta.path.is_ident("auto_now_add") {
10871                out.auto_now_add = true;
10872                if out.default.is_none() {
10873                    out.default = Some("now()".into());
10874                }
10875                return Ok(());
10876            }
10877            if meta.path.is_ident("auto_now") {
10878                out.auto_now = true;
10879                if out.default.is_none() {
10880                    out.default = Some("now()".into());
10881                }
10882                return Ok(());
10883            }
10884            if meta.path.is_ident("soft_delete") {
10885                out.soft_delete = true;
10886                return Ok(());
10887            }
10888            if meta.path.is_ident("unique") {
10889                out.unique = true;
10890                return Ok(());
10891            }
10892            if meta.path.is_ident("index") {
10893                out.index = true;
10894                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10895                if meta.input.peek(syn::token::Paren) {
10896                    meta.parse_nested_meta(|inner| {
10897                        if inner.path.is_ident("unique") {
10898                            out.index_unique = true;
10899                            return Ok(());
10900                        }
10901                        if inner.path.is_ident("name") {
10902                            let s: LitStr = inner.value()?.parse()?;
10903                            out.index_name = Some(s.value());
10904                            return Ok(());
10905                        }
10906                        if inner.path.is_ident("method") {
10907                            let s: LitStr = inner.value()?.parse()?;
10908                            let v = s.value();
10909                            match v.as_str() {
10910                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10911                                    out.index_method = v;
10912                                }
10913                                other => {
10914                                    return Err(inner.error(format!(
10915                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10916                                    )));
10917                                }
10918                            }
10919                            return Ok(());
10920                        }
10921                        Err(inner.error(
10922                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10923                        ))
10924                    })?;
10925                }
10926                return Ok(());
10927            }
10928            Err(meta.error("unknown rustango field attribute"))
10929        })?;
10930    }
10931    Ok(out)
10932}
10933
10934/// Parse a signed integer literal, accepting optional leading `-`.
10935fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10936    let expr: syn::Expr = meta.value()?.parse()?;
10937    match expr {
10938        syn::Expr::Lit(syn::ExprLit {
10939            lit: syn::Lit::Int(lit),
10940            ..
10941        }) => lit.base10_parse::<i64>(),
10942        syn::Expr::Unary(syn::ExprUnary {
10943            op: syn::UnOp::Neg(_),
10944            expr,
10945            ..
10946        }) => {
10947            if let syn::Expr::Lit(syn::ExprLit {
10948                lit: syn::Lit::Int(lit),
10949                ..
10950            }) = *expr
10951            {
10952                let v: i64 = lit.base10_parse()?;
10953                Ok(-v)
10954            } else {
10955                Err(syn::Error::new_spanned(expr, "expected integer literal"))
10956            }
10957        }
10958        other => Err(syn::Error::new_spanned(
10959            other,
10960            "expected integer literal (signed)",
10961        )),
10962    }
10963}
10964
10965struct FieldInfo<'a> {
10966    ident: &'a syn::Ident,
10967    column: String,
10968    primary_key: bool,
10969    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10970    /// skip this column when `Auto::Unset` and emit it under
10971    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10972    auto: bool,
10973    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10974    /// the `Column::Value` associated type for typed-column tokens.
10975    value_ty: &'a Type,
10976    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10977    field_type_tokens: TokenStream2,
10978    schema: TokenStream2,
10979    from_row_init: TokenStream2,
10980    /// Variant of [`Self::from_row_init`] that reads the column via
10981    /// `format!("{prefix}__{col}")` so a model can be decoded out of
10982    /// the aliased columns of a JOINed row. Drives slice 9.0d's
10983    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10984    /// helper that `select_related` calls when stitching loaded FKs.
10985    from_aliased_row_init: TokenStream2,
10986    /// MySQL twins of the two inits above. They differ only for a UUID
10987    /// column, which MySQL keeps as `CHAR(36)` text (#1733).
10988    from_row_init_my: TokenStream2,
10989    from_aliased_row_init_my: TokenStream2,
10990    /// A bare or optional `Uuid` field.
10991    uuid_column: bool,
10992    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10993    /// relation helper emit (`Author::<child>_set`) needs to know `T`
10994    /// to point the generated method at the right child model.
10995    fk_inner: Option<Type>,
10996    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10997    /// `kind` (since ForeignKey detection sets `kind` to K's
10998    /// underlying type) but stored separately for clarity at the
10999    /// `FkRelation` construction site, which only sees the FK's
11000    /// surface fields.
11001    fk_pk_kind: DetectedKind,
11002    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11003    /// the bare `ForeignKey<T, K>`. Routes the load_related and
11004    /// fk_pk_access emitters to wrap assignments / accessors in
11005    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11006    /// FK column compiles end-to-end. The DDL writer reads this off
11007    /// the field schema (`nullable` flag); the macro just needs to
11008    /// keep the Rust-side codegen consistent.
11009    nullable: bool,
11010    /// `true` when this column was marked `#[rustango(auto_now)]` —
11011    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11012    /// column instead of the user-supplied value, so `updated_at`
11013    /// always reflects the latest write without the caller having
11014    /// to remember to set it.
11015    auto_now: bool,
11016    /// `true` when this column was marked `#[rustango(auto_now_add)]`
11017    /// — the column is server-set on INSERT (DB DEFAULT) and
11018    /// **immutable** afterwards. `update_on` / `save_on` skip the
11019    /// column entirely so a stale `created_at` value in memory never
11020    /// rewrites the persisted timestamp.
11021    auto_now_add: bool,
11022    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11023    /// Triggers emission of `soft_delete_on(executor)` and
11024    /// `restore_on(executor)` on the model's inherent impl. There is
11025    /// at most one such column per model — emission asserts this.
11026    soft_delete: bool,
11027    /// `Some` when this column was marked
11028    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11029    /// every INSERT and UPDATE path; the database recomputes the
11030    /// value from `EXPR`. Backlog item #35.
11031    generated_as: Option<String>,
11032    /// `true` when this column was marked
11033    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11034    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11035    /// with `Uuid::now_v7()` before binding, so the PK is generated
11036    /// Rust-side and the column is always present in the INSERT
11037    /// statement (no DB DEFAULT requirement). Issue #823.
11038    default_uuid_v7: bool,
11039    /// `Some` when this FK field carried `#[rustango(related_name =
11040    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11041    /// consumed by [`reverse_helper_tokens`] to override the default
11042    /// `<child_snake>_set` accessor name. Follow-up to #816.
11043    related_name: Option<String>,
11044}
11045
11046/// Reject table names that won't survive SQL identifier
11047/// derivation downstream. Postgres' regular-identifier rule
11048/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11049/// through the framework's unquoted FK / index / constraint name
11050/// emission without surprises. We also disallow leading-digit and
11051/// the empty string for clarity.
11052///
11053/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11054/// here — those produce a runtime error from the SQL parser,
11055/// which is loud enough; statically enumerating reserved words
11056/// across the three supported dialects is more friction than help.
11057///
11058/// Backlog item #65.
11059fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11060    validate_sql_identifier(name, "table", span)
11061}
11062
11063/// Reject SQL identifiers that compile but break downstream SQL
11064/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11065/// `kind` is "table" / "column" — used for the error message so users
11066/// see which attribute caused the failure.
11067fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11068    if name.is_empty() {
11069        return Err(syn::Error::new(
11070            span,
11071            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11072        ));
11073    }
11074    let mut chars = name.chars();
11075    let first = chars.next().unwrap();
11076    if !(first.is_ascii_alphabetic() || first == '_') {
11077        return Err(syn::Error::new(
11078            span,
11079            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11080        ));
11081    }
11082    for c in chars {
11083        if !(c.is_ascii_alphanumeric() || c == '_') {
11084            return Err(syn::Error::new(
11085                span,
11086                format!(
11087                    "{kind} name `{name}` contains invalid character {c:?} — \
11088                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11089                     Hyphens in particular break FK / index name derivation \
11090                     downstream; use underscores instead (e.g. `{}`)",
11091                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11092                ),
11093            ));
11094        }
11095    }
11096    Ok(())
11097}
11098
11099fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11100    let root = rustango_root();
11101    let attrs = parse_field_attrs(field)?;
11102    let ident = field
11103        .ident
11104        .as_ref()
11105        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11106    let name = ident_name(ident);
11107    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11108    let primary_key = attrs.primary_key;
11109    let DetectedType {
11110        kind,
11111        nullable,
11112        auto: detected_auto,
11113        fk_inner,
11114    } = detect_type(&field.ty)?;
11115    check_bound_compatibility(field, &attrs, kind)?;
11116    let auto = detected_auto;
11117    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11118    // INSERT path: the user must wrap the field in `Auto<T>`, which
11119    // marks the column as DB-default-supplied. The mixin attrs then
11120    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11121    // for `auto_now`, force the value on UPDATE too.
11122    if attrs.auto_uuid {
11123        if kind != DetectedKind::Uuid {
11124            return Err(syn::Error::new_spanned(
11125                field,
11126                "`#[rustango(auto_uuid)]` requires the field type to be \
11127                 `Auto<uuid::Uuid>`",
11128            ));
11129        }
11130        if !detected_auto {
11131            return Err(syn::Error::new_spanned(
11132                field,
11133                "`#[rustango(auto_uuid)]` requires the field type to be \
11134                 wrapped in `Auto<...>` so the macro skips the column on \
11135                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11136            ));
11137        }
11138    }
11139    if attrs.default_uuid_v7 {
11140        if kind != DetectedKind::Uuid {
11141            return Err(syn::Error::new_spanned(
11142                field,
11143                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11144                 `Auto<uuid::Uuid>`",
11145            ));
11146        }
11147        if !detected_auto {
11148            return Err(syn::Error::new_spanned(
11149                field,
11150                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11151                 wrapped in `Auto<...>` so the macro can detect the \
11152                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11153            ));
11154        }
11155        if attrs.auto_uuid {
11156            return Err(syn::Error::new_spanned(
11157                field,
11158                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11159                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11160                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11161                 Pick one.",
11162            ));
11163        }
11164    }
11165    if attrs.auto_now_add || attrs.auto_now {
11166        if kind != DetectedKind::DateTime {
11167            return Err(syn::Error::new_spanned(
11168                field,
11169                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11170                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11171            ));
11172        }
11173        if !detected_auto {
11174            return Err(syn::Error::new_spanned(
11175                field,
11176                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11177                 the field type to be wrapped in `Auto<...>` so the macro skips \
11178                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11179            ));
11180        }
11181    }
11182    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11183        return Err(syn::Error::new_spanned(
11184            field,
11185            "`#[rustango(soft_delete)]` requires the field type to be \
11186             `Option<chrono::DateTime<chrono::Utc>>`",
11187        ));
11188    }
11189    let is_mixin_auto =
11190        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11191    if detected_auto && !primary_key && !is_mixin_auto {
11192        return Err(syn::Error::new_spanned(
11193            field,
11194            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11195             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11196             `auto_now`",
11197        ));
11198    }
11199    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11200        return Err(syn::Error::new_spanned(
11201            field,
11202            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11203             SERIAL / BIGSERIAL already supplies a default sequence.",
11204        ));
11205    }
11206    if fk_inner.is_some() && primary_key {
11207        return Err(syn::Error::new_spanned(
11208            field,
11209            "`ForeignKey<T>` is not allowed on a primary-key field — \
11210             a row's PK is its own identity, not a reference to a parent.",
11211        ));
11212    }
11213    if attrs.generated_as.is_some() {
11214        if primary_key {
11215            return Err(syn::Error::new_spanned(
11216                field,
11217                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11218                 primary-key field — a PK must be writable so the row \
11219                 has an identity at INSERT time.",
11220            ));
11221        }
11222        if attrs.default.is_some() {
11223            return Err(syn::Error::new_spanned(
11224                field,
11225                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11226                 `default = \"…\"` — Postgres rejects DEFAULT on \
11227                 generated columns. The expression IS the default.",
11228            ));
11229        }
11230        if detected_auto {
11231            return Err(syn::Error::new_spanned(
11232                field,
11233                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11234                 an `Auto<T>` field — generated columns are computed \
11235                 by the DB, not server-assigned via a sequence. Use a \
11236                 plain Rust type (e.g. `f64`).",
11237            ));
11238        }
11239        if fk_inner.is_some() {
11240            return Err(syn::Error::new_spanned(
11241                field,
11242                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11243                 ForeignKey field.",
11244            ));
11245        }
11246    }
11247    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11248    let column_lit = column.as_str();
11249    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11250    // dimension that the bare `Vector` Rust type can't carry, so emit
11251    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11252    // fallback of `Vector(0)`.
11253    let field_type_tokens = if kind == DetectedKind::Vector {
11254        let root = rustango_root();
11255        let dims = attrs.vector_dims.unwrap_or(0);
11256        quote!(#root::core::FieldType::Vector(#dims))
11257    } else if kind == DetectedKind::Geometry {
11258        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11259        // SRID that the bare `Point` Rust type can't carry, so emit
11260        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11261        let root = rustango_root();
11262        let srid = attrs.geometry_srid.unwrap_or(0);
11263        quote!(#root::core::FieldType::Geometry(#srid))
11264    } else {
11265        kind.variant_tokens()
11266    };
11267    let max_length = optional_u32(attrs.max_length);
11268    let min = optional_i64(attrs.min);
11269    let max = optional_i64(attrs.max);
11270    let default = optional_str(attrs.default.as_deref());
11271
11272    let unique = attrs.unique;
11273    let generated_as = optional_str(attrs.generated_as.as_deref());
11274    let help_text = optional_str(attrs.help_text.as_deref());
11275    let choices = optional_choices(attrs.choices.as_deref());
11276    let db_comment = optional_str(attrs.db_comment.as_deref());
11277    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11278    let editable = attrs.editable;
11279    let blank = attrs.blank;
11280    let case_insensitive = attrs.case_insensitive;
11281    let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11282    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11283        return Err(syn::Error::new_spanned(
11284            field,
11285            "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11286             or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11287             non-FK column.",
11288        ));
11289    }
11290    let fk_on_delete = match attrs.on_delete.as_deref() {
11291        None => quote!(::core::option::Option::None),
11292        Some(action) => {
11293            let variant = match action {
11294                "cascade" => quote!(Cascade),
11295                "restrict" => quote!(Restrict),
11296                "set_null" => quote!(SetNull),
11297                "set_default" => quote!(SetDefault),
11298                "no_action" => quote!(NoAction),
11299                // parse_field_attrs already validated this — guard against future drift.
11300                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11301            };
11302            quote!(::core::option::Option::Some(
11303                #root::core::OnDeleteAction::#variant
11304            ))
11305        }
11306    };
11307    let auto_now_flag = attrs.auto_now;
11308    let schema = quote! {{
11309        #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11310            name: #name,
11311            column: #column_lit,
11312            ty: #field_type_tokens,
11313            nullable: #nullable,
11314            primary_key: #primary_key,
11315            relation: #relation,
11316            max_length: #max_length,
11317            min: #min,
11318            max: #max,
11319            default: #default,
11320            auto: #auto,
11321            auto_now: #auto_now_flag,
11322            unique: #unique,
11323            generated_as: #generated_as,
11324            help_text: #help_text,
11325            choices: #choices,
11326            db_comment: #db_comment,
11327            verbose_name: #verbose_name,
11328            editable: #editable,
11329            blank: #blank,
11330            case_insensitive: #case_insensitive,
11331            fk_on_delete: #fk_on_delete,
11332            validators: &[ #(#validators_lits),* ],
11333        })
11334    }};
11335
11336    let from_row_init = quote! {
11337        #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11338    };
11339    let from_aliased_row_init = quote! {
11340        #ident: #root::sql::sqlx::Row::try_get(
11341            row,
11342            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11343        )?
11344    };
11345    // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11346    // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11347    let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11348    let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11349        (
11350            quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11351            quote! {
11352                #ident: #root::sql::try_get_flat_my(
11353                    row,
11354                    ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11355                )?
11356            },
11357        )
11358    } else {
11359        (from_row_init.clone(), from_aliased_row_init.clone())
11360    };
11361
11362    Ok(FieldInfo {
11363        ident,
11364        column,
11365        primary_key,
11366        auto,
11367        value_ty: &field.ty,
11368        field_type_tokens,
11369        schema,
11370        from_row_init,
11371        from_aliased_row_init,
11372        from_row_init_my,
11373        from_aliased_row_init_my,
11374        uuid_column,
11375        fk_inner: fk_inner.cloned(),
11376        fk_pk_kind: kind,
11377        nullable,
11378        auto_now: attrs.auto_now,
11379        auto_now_add: attrs.auto_now_add,
11380        soft_delete: attrs.soft_delete,
11381        generated_as: attrs.generated_as.clone(),
11382        default_uuid_v7: attrs.default_uuid_v7,
11383        related_name: attrs.related_name.clone(),
11384    })
11385}
11386
11387fn check_bound_compatibility(
11388    field: &syn::Field,
11389    attrs: &FieldAttrs,
11390    kind: DetectedKind,
11391) -> syn::Result<()> {
11392    if attrs.max_length.is_some() && kind != DetectedKind::String {
11393        return Err(syn::Error::new_spanned(
11394            field,
11395            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11396        ));
11397    }
11398    // These were silently dropped on the wrong type (#1937).
11399    if attrs.case_insensitive && kind != DetectedKind::String {
11400        return Err(syn::Error::new_spanned(
11401            field,
11402            "`citext` is only valid on `String` fields (or `Option<String>`)",
11403        ));
11404    }
11405    if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11406        return Err(syn::Error::new_spanned(
11407            field,
11408            "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11409        ));
11410    }
11411    if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11412        return Err(syn::Error::new_spanned(
11413            field,
11414            "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11415        ));
11416    }
11417    if attrs.choices.is_some() && kind != DetectedKind::String {
11418        return Err(syn::Error::new_spanned(
11419            field,
11420            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11421             integer-valued enumerations should be modeled with a Rust enum and \
11422             custom (de)serializer for now",
11423        ));
11424    }
11425    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11426        return Err(syn::Error::new_spanned(
11427            field,
11428            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11429        ));
11430    }
11431    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11432        if min > max {
11433            return Err(syn::Error::new_spanned(
11434                field,
11435                format!("`min` ({min}) is greater than `max` ({max})"),
11436            ));
11437        }
11438    }
11439    Ok(())
11440}
11441
11442/// A `&'static [ty]` of `const fn` results. The slice needs its own
11443/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11444fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11445    quote!({
11446        const ITEMS: &[#ty] = &[ #(#items),* ];
11447        ITEMS
11448    })
11449}
11450
11451fn optional_u32(value: Option<u32>) -> TokenStream2 {
11452    if let Some(v) = value {
11453        quote!(::core::option::Option::Some(#v))
11454    } else {
11455        quote!(::core::option::Option::None)
11456    }
11457}
11458
11459fn optional_i64(value: Option<i64>) -> TokenStream2 {
11460    if let Some(v) = value {
11461        quote!(::core::option::Option::Some(#v))
11462    } else {
11463        quote!(::core::option::Option::None)
11464    }
11465}
11466
11467fn optional_str(value: Option<&str>) -> TokenStream2 {
11468    if let Some(v) = value {
11469        quote!(::core::option::Option::Some(#v))
11470    } else {
11471        quote!(::core::option::Option::None)
11472    }
11473}
11474
11475fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11476    let Some(pairs) = pairs else {
11477        return quote!(::core::option::Option::None);
11478    };
11479    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11480    quote!(::core::option::Option::Some(&[#(#entries),*]))
11481}
11482
11483fn relation_tokens(
11484    field: &syn::Field,
11485    attrs: &FieldAttrs,
11486    fk_inner: Option<&syn::Type>,
11487    table: &str,
11488) -> syn::Result<TokenStream2> {
11489    let root = rustango_root();
11490    if let Some(inner) = fk_inner {
11491        if attrs.fk.is_some() || attrs.o2o.is_some() {
11492            return Err(syn::Error::new_spanned(
11493                field,
11494                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11495                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11496            ));
11497        }
11498        let on = attrs.on.as_deref().unwrap_or("id");
11499        return Ok(quote! {
11500            ::core::option::Option::Some(#root::core::Relation::fk(
11501                <#inner as #root::core::Model>::SCHEMA.table,
11502                #on,
11503            ))
11504        });
11505    }
11506    match (&attrs.fk, &attrs.o2o) {
11507        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11508            field,
11509            "`fk` and `o2o` are mutually exclusive",
11510        )),
11511        (Some(to), None) => {
11512            let on = attrs.on.as_deref().unwrap_or("id");
11513            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11514            // the model's own table. Threaded as a literal string at
11515            // macro-expansion time to sidestep the const-eval cycle
11516            // that `Self::SCHEMA.table` would create when referenced
11517            // inside Self::SCHEMA's own initializer.
11518            let resolved = if to == "self" { table } else { to };
11519            Ok(quote! {
11520                ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11521            })
11522        }
11523        (None, Some(to)) => {
11524            let on = attrs.on.as_deref().unwrap_or("id");
11525            let resolved = if to == "self" { table } else { to };
11526            Ok(quote! {
11527                ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11528            })
11529        }
11530        (None, None) => {
11531            if attrs.on.is_some() {
11532                return Err(syn::Error::new_spanned(
11533                    field,
11534                    "`on` requires `fk` or `o2o`",
11535                ));
11536            }
11537            Ok(quote!(::core::option::Option::None))
11538        }
11539    }
11540}
11541
11542/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11543/// about kinds without depending on `rustango-core` (which would require a
11544/// proc-macro/normal split it doesn't have today).
11545#[derive(Clone, Copy, PartialEq, Eq)]
11546enum DetectedKind {
11547    I16,
11548    I32,
11549    I64,
11550    F32,
11551    F64,
11552    Bool,
11553    String,
11554    DateTime,
11555    Date,
11556    Time,
11557    Uuid,
11558    Json,
11559    Decimal,
11560    Binary,
11561    /// `Array<String>` → PG `text[]` (#341).
11562    ArrayText,
11563    /// `Array<i32>` → PG `integer[]` (#341).
11564    ArrayInt,
11565    /// `Array<i64>` → PG `bigint[]` (#341).
11566    ArrayBigInt,
11567    /// `Range<i32>` → PG `int4range` (#343).
11568    RangeInt,
11569    /// `Range<i64>` → PG `int8range` (#343).
11570    RangeBigInt,
11571    /// `Range<Decimal>` → PG `numrange` (#343).
11572    RangeNumeric,
11573    /// `Range<NaiveDate>` → PG `daterange` (#343).
11574    RangeDate,
11575    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11576    RangeDateTime,
11577    /// `HStore` → PG `hstore` (#342).
11578    HStore,
11579    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11580    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11581    /// in at the `FieldType` emission site (not carried on this enum).
11582    Vector,
11583    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11584    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11585    /// threaded in at the `FieldType` emission site.
11586    Geometry,
11587}
11588
11589impl DetectedKind {
11590    fn variant_tokens(self) -> TokenStream2 {
11591        let root = rustango_root();
11592        match self {
11593            Self::I16 => quote!(#root::core::FieldType::I16),
11594            Self::I32 => quote!(#root::core::FieldType::I32),
11595            Self::I64 => quote!(#root::core::FieldType::I64),
11596            Self::F32 => quote!(#root::core::FieldType::F32),
11597            Self::F64 => quote!(#root::core::FieldType::F64),
11598            Self::Bool => quote!(#root::core::FieldType::Bool),
11599            Self::String => quote!(#root::core::FieldType::String),
11600            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11601            Self::Date => quote!(#root::core::FieldType::Date),
11602            Self::Time => quote!(#root::core::FieldType::Time),
11603            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11604            Self::Json => quote!(#root::core::FieldType::Json),
11605            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11606            Self::Binary => quote!(#root::core::FieldType::Binary),
11607            Self::ArrayText => {
11608                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11609            }
11610            Self::ArrayInt => {
11611                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11612            }
11613            Self::ArrayBigInt => {
11614                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11615            }
11616            Self::RangeInt => {
11617                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11618            }
11619            Self::RangeBigInt => {
11620                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11621            }
11622            Self::RangeNumeric => {
11623                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11624            }
11625            Self::RangeDate => {
11626                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11627            }
11628            Self::RangeDateTime => {
11629                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11630            }
11631            Self::HStore => quote!(#root::core::FieldType::HStore),
11632            // Dimension comes from the `vector(dims = N)` attribute,
11633            // applied at the emission site; `0` here is just a fallback.
11634            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11635            // SRID comes from the `geometry(srid = N)` attribute, applied
11636            // at the emission site; `0` here is just a fallback.
11637            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11638        }
11639    }
11640
11641    fn is_integer(self) -> bool {
11642        matches!(self, Self::I16 | Self::I32 | Self::I64)
11643    }
11644
11645    /// `(SqlValue::<Variant>, default expr)` for emitting the
11646    /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11647    /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11648    /// fires only when the parent's `__rustango_pk_value` returns a
11649    /// different variant than expected, which is a compile-time bug —
11650    /// but we still need a value-typed fallback to keep the match
11651    /// total.
11652    fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11653        let root = rustango_root();
11654        match self {
11655            Self::I16 => (quote!(I16), quote!(0i16)),
11656            Self::I32 => (quote!(I32), quote!(0i32)),
11657            Self::I64 => (quote!(I64), quote!(0i64)),
11658            Self::F32 => (quote!(F32), quote!(0f32)),
11659            Self::F64 => (quote!(F64), quote!(0f64)),
11660            Self::Bool => (quote!(Bool), quote!(false)),
11661            Self::String => (quote!(String), quote!(::std::string::String::new())),
11662            Self::DateTime => (
11663                quote!(DateTime),
11664                quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11665            ),
11666            Self::Date => (
11667                quote!(Date),
11668                quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11669            ),
11670            Self::Time => (
11671                quote!(Time),
11672                quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11673            ),
11674            Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11675            Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11676            Self::Decimal => (
11677                quote!(Decimal),
11678                quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11679            ),
11680            Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11681            // Arrays (#341) can never be a foreign-key primary key, so
11682            // this arm is never reached at runtime — but the match must
11683            // stay total. A bare empty `SqlValue::Array` is the dummy.
11684            Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11685                (quote!(Array), quote!(::std::vec::Vec::new()))
11686            }
11687            // Ranges (#343) likewise can't be a FK PK — never reached.
11688            Self::RangeInt
11689            | Self::RangeBigInt
11690            | Self::RangeNumeric
11691            | Self::RangeDate
11692            | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11693            // HStore (#342) can't be a FK PK — never reached.
11694            Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11695            // Vector (#824) can't be a FK PK — never reached.
11696            Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11697            // Geometry (#443) can't be a FK PK — never reached (arm is
11698            // exhaustiveness-only; never interpolated into emitted code).
11699            Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11700        }
11701    }
11702}
11703
11704/// Result of walking a field's Rust type. `kind` is the underlying
11705/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11706/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11707/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11708/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11709#[derive(Clone, Copy)]
11710struct DetectedType<'a> {
11711    kind: DetectedKind,
11712    nullable: bool,
11713    auto: bool,
11714    fk_inner: Option<&'a syn::Type>,
11715}
11716
11717/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11718/// non-`Auto` types — the caller should already have routed Auto-only
11719/// codegen through this helper, so a `None` indicates a macro-internal
11720/// invariant break.
11721fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11722    let Type::Path(TypePath { path, qself: None }) = ty else {
11723        return None;
11724    };
11725    let last = path.segments.last()?;
11726    if last.ident != "Auto" {
11727        return None;
11728    }
11729    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11730        return None;
11731    };
11732    args.args.iter().find_map(|a| match a {
11733        syn::GenericArgument::Type(t) => Some(t),
11734        _ => None,
11735    })
11736}
11737
11738fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11739    let Type::Path(TypePath { path, qself: None }) = ty else {
11740        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11741    };
11742    let last = path
11743        .segments
11744        .last()
11745        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11746
11747    if last.ident == "Option" {
11748        let inner = generic_inner(ty, &last.arguments, "Option")?;
11749        let inner_det = detect_type(inner)?;
11750        if inner_det.nullable {
11751            return Err(syn::Error::new_spanned(
11752                ty,
11753                "nested Option is not supported",
11754            ));
11755        }
11756        if inner_det.auto {
11757            return Err(syn::Error::new_spanned(
11758                ty,
11759                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11760            ));
11761        }
11762        return Ok(DetectedType {
11763            nullable: true,
11764            ..inner_det
11765        });
11766    }
11767
11768    if last.ident == "Auto" {
11769        let inner = generic_inner(ty, &last.arguments, "Auto")?;
11770        let inner_det = detect_type(inner)?;
11771        if inner_det.auto {
11772            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11773        }
11774        if inner_det.nullable {
11775            return Err(syn::Error::new_spanned(
11776                ty,
11777                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11778            ));
11779        }
11780        if inner_det.fk_inner.is_some() {
11781            return Err(syn::Error::new_spanned(
11782                ty,
11783                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11784            ));
11785        }
11786        if !matches!(
11787            inner_det.kind,
11788            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11789        ) {
11790            return Err(syn::Error::new_spanned(
11791                ty,
11792                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11793                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11794                 (DEFAULT now())",
11795            ));
11796        }
11797        return Ok(DetectedType {
11798            auto: true,
11799            ..inner_det
11800        });
11801    }
11802
11803    if last.ident == "ForeignKey" {
11804        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11805        // Resolve the FK column's underlying SQL type from `K`. When the
11806        // user wrote `ForeignKey<T>` without a key parameter, the type
11807        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11808        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11809        // recurse into K so the column DDL emits the right SQL type
11810        // (VARCHAR for String, UUID for Uuid, …) and the load_related
11811        // emitter knows which `SqlValue` variant to match.
11812        let kind = match key_ty {
11813            Some(k) => detect_type(k)?.kind,
11814            None => DetectedKind::I64,
11815        };
11816        return Ok(DetectedType {
11817            kind,
11818            nullable: false,
11819            auto: false,
11820            fk_inner: Some(inner),
11821        });
11822    }
11823
11824    let kind = match last.ident.to_string().as_str() {
11825        "i16" => DetectedKind::I16,
11826        "i32" => DetectedKind::I32,
11827        "i64" => DetectedKind::I64,
11828        "f32" => DetectedKind::F32,
11829        "f64" => DetectedKind::F64,
11830        "bool" => DetectedKind::Bool,
11831        "String" => DetectedKind::String,
11832        "DateTime" => DetectedKind::DateTime,
11833        "NaiveDate" => DetectedKind::Date,
11834        "NaiveTime" => DetectedKind::Time,
11835        "Uuid" => DetectedKind::Uuid,
11836        "Value" => DetectedKind::Json,
11837        "Decimal" => DetectedKind::Decimal,
11838        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11839        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11840        // — that would emit Binary DDL and decode-fail at runtime.
11841        "Vec" => {
11842            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11843            if let Type::Path(TypePath { path, qself: None }) = inner {
11844                if let Some(seg) = path.segments.last() {
11845                    if seg.ident == "u8" && seg.arguments.is_empty() {
11846                        return Ok(DetectedType {
11847                            kind: DetectedKind::Binary,
11848                            nullable: false,
11849                            auto: false,
11850                            fk_inner: None,
11851                        });
11852                    }
11853                }
11854            }
11855            return Err(syn::Error::new_spanned(
11856                ty,
11857                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11858                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11859            ));
11860        }
11861        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11862        // `integer[]` / `bigint[]` (#341).
11863        "Array" => {
11864            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11865            let elem = match inner {
11866                Type::Path(TypePath { path, qself: None }) => {
11867                    path.segments.last().map(|s| s.ident.to_string())
11868                }
11869                _ => None,
11870            };
11871            let kind = match elem.as_deref() {
11872                Some("String") => DetectedKind::ArrayText,
11873                Some("i32") => DetectedKind::ArrayInt,
11874                Some("i64") => DetectedKind::ArrayBigInt,
11875                _ => {
11876                    return Err(syn::Error::new_spanned(
11877                        ty,
11878                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11879                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11880                    ));
11881                }
11882            };
11883            return Ok(DetectedType {
11884                kind,
11885                nullable: false,
11886                auto: false,
11887                fk_inner: None,
11888            });
11889        }
11890        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11891        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11892        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
11893        "Range" => {
11894            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11895            let elem = match inner {
11896                Type::Path(TypePath { path, qself: None }) => {
11897                    path.segments.last().map(|s| s.ident.to_string())
11898                }
11899                _ => None,
11900            };
11901            let kind = match elem.as_deref() {
11902                Some("i32") => DetectedKind::RangeInt,
11903                Some("i64") => DetectedKind::RangeBigInt,
11904                Some("Decimal") => DetectedKind::RangeNumeric,
11905                Some("NaiveDate") => DetectedKind::RangeDate,
11906                Some("DateTime") => DetectedKind::RangeDateTime,
11907                _ => {
11908                    return Err(syn::Error::new_spanned(
11909                        ty,
11910                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11911                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11912                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11913                         (→ tstzrange) are supported (#343)",
11914                    ));
11915                }
11916            };
11917            return Ok(DetectedType {
11918                kind,
11919                nullable: false,
11920                auto: false,
11921                fk_inner: None,
11922            });
11923        }
11924        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11925        // (the `CastValue` impl bridges logical↔stored); the field's own
11926        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11927        // schema just needs `FieldType::String`.
11928        "Cast" => {
11929            return Ok(DetectedType {
11930                kind: DetectedKind::String,
11931                nullable: false,
11932                auto: false,
11933                fk_inner: None,
11934            });
11935        }
11936        // `HStore` → PG `hstore` (#342). No generic
11937        // parameter — always a string→string map.
11938        "HStore" => {
11939            return Ok(DetectedType {
11940                kind: DetectedKind::HStore,
11941                nullable: false,
11942                auto: false,
11943                fk_inner: None,
11944            });
11945        }
11946        // `Vector` → pgvector `vector(N)` (#824). The dimension is
11947        // supplied by `#[rustango(vector(dims = N))]`, not the type.
11948        "Vector" => {
11949            return Ok(DetectedType {
11950                kind: DetectedKind::Vector,
11951                nullable: false,
11952                auto: false,
11953                fk_inner: None,
11954            });
11955        }
11956        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11957        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11958        "Point" => {
11959            return Ok(DetectedType {
11960                kind: DetectedKind::Geometry,
11961                nullable: false,
11962                auto: false,
11963                fk_inner: None,
11964            });
11965        }
11966        other => {
11967            return Err(syn::Error::new_spanned(
11968                ty,
11969                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)"),
11970            ));
11971        }
11972    };
11973    Ok(DetectedType {
11974        kind,
11975        nullable: false,
11976        auto: false,
11977        fk_inner: None,
11978    })
11979}
11980
11981fn generic_inner<'a>(
11982    ty: &'a Type,
11983    arguments: &'a PathArguments,
11984    wrapper: &str,
11985) -> syn::Result<&'a Type> {
11986    let PathArguments::AngleBracketed(args) = arguments else {
11987        return Err(syn::Error::new_spanned(
11988            ty,
11989            format!("{wrapper} requires a generic argument"),
11990        ));
11991    };
11992    args.args
11993        .iter()
11994        .find_map(|a| match a {
11995            GenericArgument::Type(t) => Some(t),
11996            _ => None,
11997        })
11998        .ok_or_else(|| {
11999            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12000        })
12001}
12002
12003/// Like [`generic_inner`] but pulls *two* type args — the first is
12004/// required, the second is optional. Used by the `ForeignKey<T, K>`
12005/// detection where K defaults to `i64` when omitted.
12006fn generic_pair<'a>(
12007    ty: &'a Type,
12008    arguments: &'a PathArguments,
12009    wrapper: &str,
12010) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12011    let PathArguments::AngleBracketed(args) = arguments else {
12012        return Err(syn::Error::new_spanned(
12013            ty,
12014            format!("{wrapper} requires a generic argument"),
12015        ));
12016    };
12017    let mut types = args.args.iter().filter_map(|a| match a {
12018        GenericArgument::Type(t) => Some(t),
12019        _ => None,
12020    });
12021    let first = types.next().ok_or_else(|| {
12022        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12023    })?;
12024    let second = types.next();
12025    Ok((first, second))
12026}
12027
12028fn to_snake_case(s: &str) -> String {
12029    let mut out = String::with_capacity(s.len() + 4);
12030    for (i, ch) in s.chars().enumerate() {
12031        if ch.is_ascii_uppercase() {
12032            if i > 0 {
12033                out.push('_');
12034            }
12035            out.push(ch.to_ascii_lowercase());
12036        } else {
12037            out.push(ch);
12038        }
12039    }
12040    out
12041}
12042
12043// ============================================================
12044//  #[derive(Form)]  —  slice 8.4B
12045// ============================================================
12046
12047/// Per-field `#[form(...)]` attributes recognised by the derive.
12048#[derive(Default)]
12049struct FormFieldAttrs {
12050    min: Option<i64>,
12051    max: Option<i64>,
12052    min_length: Option<u32>,
12053    max_length: Option<u32>,
12054    /// `#[form(clean = "fn_name")]` — per-field clean hook.
12055    /// The named static method on the form struct is called after the
12056    /// field's typed parse + length/range checks; it gets the parsed
12057    /// value by reference and returns `Result<<FieldType>, String>`.
12058    /// On Ok, the returned value replaces the parsed one; on Err, the
12059    /// message is attached to the field error list. Issue #372.
12060    clean: Option<syn::Ident>,
12061}
12062
12063/// Container-level `#[form(...)]` attributes. Currently only the
12064/// cross-field `validate` hook (issue #373).
12065#[derive(Default)]
12066struct FormContainerAttrs {
12067    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12068    /// After every per-field parse succeeds, the named method on the
12069    /// form struct is called with `&self` and may return
12070    /// `Result<(), FormErrors>`. Errors merge into the field error
12071    /// list. Issue #373.
12072    validate: Option<syn::Ident>,
12073}
12074
12075/// Detected shape of a form field's Rust type.
12076#[derive(Clone, Copy)]
12077enum FormFieldKind {
12078    String,
12079    I16,
12080    I32,
12081    I64,
12082    F32,
12083    F64,
12084    Bool,
12085}
12086
12087impl FormFieldKind {
12088    fn parse_method(self) -> &'static str {
12089        match self {
12090            Self::I16 => "i16",
12091            Self::I32 => "i32",
12092            Self::I64 => "i64",
12093            Self::F32 => "f32",
12094            Self::F64 => "f64",
12095            // String + Bool don't go through `str::parse`; the codegen
12096            // handles them inline.
12097            Self::String | Self::Bool => "",
12098        }
12099    }
12100}
12101
12102fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12103    let root = rustango_root();
12104    let struct_name = &input.ident;
12105
12106    let Data::Struct(data) = &input.data else {
12107        return Err(syn::Error::new_spanned(
12108            struct_name,
12109            "Form can only be derived on structs",
12110        ));
12111    };
12112    let Fields::Named(named) = &data.fields else {
12113        return Err(syn::Error::new_spanned(
12114            struct_name,
12115            "Form requires a struct with named fields",
12116        ));
12117    };
12118
12119    // #373 — container-level `#[form(validate = "fn")]` hook.
12120    let container = parse_form_container_attrs(input)?;
12121    let post_field_clean: Vec<TokenStream2> = Vec::new();
12122    let _ = post_field_clean;
12123
12124    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12125    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12126
12127    for field in &named.named {
12128        let ident = field
12129            .ident
12130            .as_ref()
12131            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12132        let attrs = parse_form_field_attrs(field)?;
12133        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12134
12135        let name_lit = ident_name(ident);
12136        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12137        // #372 — append the per-field `clean_<field>` call right after
12138        // the parse block when the attribute is set. The clean fn
12139        // takes &T and returns Result<T, String>; on Err we attach
12140        // the message to the field error list without aborting, so
12141        // the caller sees every field error at once.
12142        let clean_block = if let Some(clean_fn) = &attrs.clean {
12143            quote! {
12144                if __errors.fields().get(#name_lit).is_none() {
12145                    match Self::#clean_fn(&#ident) {
12146                        ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12147                        ::core::result::Result::Err(__msg) => {
12148                            __errors.add(#name_lit, __msg);
12149                        }
12150                    }
12151                }
12152            }
12153        } else {
12154            quote! {}
12155        };
12156        field_blocks.push(quote! {
12157            #parse_block
12158            #clean_block
12159        });
12160        field_idents.push(ident);
12161    }
12162
12163    // #373 — after every per-field parse + clean succeeds, call the
12164    // cross-field validator if declared. Errors merge into the
12165    // outgoing FormErrors via the existing `FormErrors::merge` helper
12166    // (same primitive the serializer cross-field hook uses).
12167    let cross_field_call = if let Some(validate_fn) = &container.validate {
12168        quote! {
12169            if __errors.is_empty() {
12170                let __candidate = Self { #( #field_idents ),* };
12171                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12172                    __errors.merge(__other);
12173                }
12174                if !__errors.is_empty() {
12175                    return ::core::result::Result::Err(__errors);
12176                }
12177                return ::core::result::Result::Ok(__candidate);
12178            }
12179        }
12180    } else {
12181        quote! {}
12182    };
12183
12184    Ok(quote! {
12185        impl #root::forms::Form for #struct_name {
12186            fn parse(
12187                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12188            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12189                let mut __errors = #root::forms::FormErrors::default();
12190                #( #field_blocks )*
12191                #cross_field_call
12192                if !__errors.is_empty() {
12193                    return ::core::result::Result::Err(__errors);
12194                }
12195                ::core::result::Result::Ok(Self {
12196                    #( #field_idents ),*
12197                })
12198            }
12199        }
12200    })
12201}
12202
12203fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12204    let mut out = FormContainerAttrs::default();
12205    for attr in &input.attrs {
12206        if !attr.path().is_ident("form") {
12207            continue;
12208        }
12209        attr.parse_nested_meta(|meta| {
12210            if meta.path.is_ident("validate") {
12211                let s: LitStr = meta.value()?.parse()?;
12212                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12213                return Ok(());
12214            }
12215            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12216        })?;
12217    }
12218    Ok(out)
12219}
12220
12221fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12222    let mut out = FormFieldAttrs::default();
12223    for attr in &field.attrs {
12224        if !attr.path().is_ident("form") {
12225            continue;
12226        }
12227        attr.parse_nested_meta(|meta| {
12228            if meta.path.is_ident("min") {
12229                let lit: syn::LitInt = meta.value()?.parse()?;
12230                out.min = Some(lit.base10_parse::<i64>()?);
12231                return Ok(());
12232            }
12233            if meta.path.is_ident("max") {
12234                let lit: syn::LitInt = meta.value()?.parse()?;
12235                out.max = Some(lit.base10_parse::<i64>()?);
12236                return Ok(());
12237            }
12238            if meta.path.is_ident("min_length") {
12239                let lit: syn::LitInt = meta.value()?.parse()?;
12240                out.min_length = Some(lit.base10_parse::<u32>()?);
12241                return Ok(());
12242            }
12243            if meta.path.is_ident("max_length") {
12244                let lit: syn::LitInt = meta.value()?.parse()?;
12245                out.max_length = Some(lit.base10_parse::<u32>()?);
12246                return Ok(());
12247            }
12248            if meta.path.is_ident("clean") {
12249                let s: LitStr = meta.value()?.parse()?;
12250                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12251                return Ok(());
12252            }
12253            Err(meta.error(
12254                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12255            ))
12256        })?;
12257    }
12258    Ok(out)
12259}
12260
12261fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12262    let Type::Path(TypePath { path, qself: None }) = ty else {
12263        return Err(syn::Error::new(
12264            span,
12265            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12266        ));
12267    };
12268    let last = path
12269        .segments
12270        .last()
12271        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12272
12273    if last.ident == "Option" {
12274        let inner = generic_inner(ty, &last.arguments, "Option")?;
12275        let (kind, nested) = detect_form_field(inner, span)?;
12276        if nested {
12277            return Err(syn::Error::new(
12278                span,
12279                "nested Option in Form fields is not supported",
12280            ));
12281        }
12282        return Ok((kind, true));
12283    }
12284
12285    let kind = match last.ident.to_string().as_str() {
12286        "String" => FormFieldKind::String,
12287        "i16" => FormFieldKind::I16,
12288        "i32" => FormFieldKind::I32,
12289        "i64" => FormFieldKind::I64,
12290        "f32" => FormFieldKind::F32,
12291        "f64" => FormFieldKind::F64,
12292        "bool" => FormFieldKind::Bool,
12293        other => {
12294            return Err(syn::Error::new(
12295                span,
12296                format!(
12297                    "Form field type `{other}` is not supported in v0.8 — use String / \
12298                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12299                ),
12300            ));
12301        }
12302    };
12303    Ok((kind, false))
12304}
12305
12306#[allow(clippy::too_many_lines)]
12307fn render_form_field_parse(
12308    ident: &syn::Ident,
12309    name_lit: &str,
12310    kind: FormFieldKind,
12311    nullable: bool,
12312    attrs: &FormFieldAttrs,
12313) -> TokenStream2 {
12314    // Pull the raw &str from the payload. Uses variable name `data` to
12315    // match the new `Form::parse(data: &HashMap<…>)` signature.
12316    let lookup = quote! {
12317        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12318    };
12319
12320    let parsed_value = match kind {
12321        FormFieldKind::Bool => quote! {
12322            let __v: bool = match __raw {
12323                ::core::option::Option::None => false,
12324                ::core::option::Option::Some(__s) => !matches!(
12325                    __s.to_ascii_lowercase().as_str(),
12326                    "" | "false" | "0" | "off" | "no"
12327                ),
12328            };
12329        },
12330        FormFieldKind::String => {
12331            if nullable {
12332                quote! {
12333                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12334                        ::core::option::Option::None => ::core::option::Option::None,
12335                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12336                            ::core::option::Option::None
12337                        }
12338                        ::core::option::Option::Some(__s) => {
12339                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12340                        }
12341                    };
12342                }
12343            } else {
12344                quote! {
12345                    let __v: ::std::string::String = match __raw {
12346                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12347                            ::core::clone::Clone::clone(__s)
12348                        }
12349                        _ => {
12350                            __errors.add(#name_lit, "This field is required.");
12351                            ::std::string::String::new()
12352                        }
12353                    };
12354                }
12355            }
12356        }
12357        FormFieldKind::I16
12358        | FormFieldKind::I32
12359        | FormFieldKind::I64
12360        | FormFieldKind::F32
12361        | FormFieldKind::F64 => {
12362            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12363            let ty_lit = kind.parse_method();
12364            let default_val = match kind {
12365                FormFieldKind::I16 => quote! { 0i16 },
12366                FormFieldKind::I32 => quote! { 0i32 },
12367                FormFieldKind::I64 => quote! { 0i64 },
12368                FormFieldKind::F32 => quote! { 0f32 },
12369                FormFieldKind::F64 => quote! { 0f64 },
12370                _ => quote! { Default::default() },
12371            };
12372            if nullable {
12373                quote! {
12374                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12375                        ::core::option::Option::None => ::core::option::Option::None,
12376                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12377                            ::core::option::Option::None
12378                        }
12379                        ::core::option::Option::Some(__s) => {
12380                            match __s.parse::<#parse_ty>() {
12381                                ::core::result::Result::Ok(__n) => {
12382                                    ::core::option::Option::Some(__n)
12383                                }
12384                                ::core::result::Result::Err(__e) => {
12385                                    __errors.add(
12386                                        #name_lit,
12387                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12388                                    );
12389                                    ::core::option::Option::None
12390                                }
12391                            }
12392                        }
12393                    };
12394                }
12395            } else {
12396                quote! {
12397                    let __v: #parse_ty = match __raw {
12398                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12399                            match __s.parse::<#parse_ty>() {
12400                                ::core::result::Result::Ok(__n) => __n,
12401                                ::core::result::Result::Err(__e) => {
12402                                    __errors.add(
12403                                        #name_lit,
12404                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12405                                    );
12406                                    #default_val
12407                                }
12408                            }
12409                        }
12410                        _ => {
12411                            __errors.add(#name_lit, "This field is required.");
12412                            #default_val
12413                        }
12414                    };
12415                }
12416            }
12417        }
12418    };
12419
12420    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12421
12422    quote! {
12423        // `mut` so the per-field `clean` hook (#372) can rewrite the
12424        // parsed value in-place when it returns Ok with a normalized
12425        // form (e.g. trim / lowercase).
12426        let mut #ident = {
12427            #lookup
12428            #parsed_value
12429            #validators
12430            __v
12431        };
12432    }
12433}
12434
12435fn render_form_validators(
12436    name_lit: &str,
12437    kind: FormFieldKind,
12438    nullable: bool,
12439    attrs: &FormFieldAttrs,
12440) -> TokenStream2 {
12441    let mut checks: Vec<TokenStream2> = Vec::new();
12442
12443    let val_ref = if nullable {
12444        quote! { __v.as_ref() }
12445    } else {
12446        quote! { ::core::option::Option::Some(&__v) }
12447    };
12448
12449    let is_string = matches!(kind, FormFieldKind::String);
12450    let is_numeric = matches!(
12451        kind,
12452        FormFieldKind::I16
12453            | FormFieldKind::I32
12454            | FormFieldKind::I64
12455            | FormFieldKind::F32
12456            | FormFieldKind::F64
12457    );
12458
12459    if is_string {
12460        if let Some(min_len) = attrs.min_length {
12461            let min_len_usize = min_len as usize;
12462            checks.push(quote! {
12463                if let ::core::option::Option::Some(__s) = #val_ref {
12464                    if __s.chars().count() < #min_len_usize {
12465                        __errors.add(
12466                            #name_lit,
12467                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12468                        );
12469                    }
12470                }
12471            });
12472        }
12473        if let Some(max_len) = attrs.max_length {
12474            let max_len_usize = max_len as usize;
12475            checks.push(quote! {
12476                if let ::core::option::Option::Some(__s) = #val_ref {
12477                    if __s.chars().count() > #max_len_usize {
12478                        __errors.add(
12479                            #name_lit,
12480                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12481                        );
12482                    }
12483                }
12484            });
12485        }
12486    }
12487
12488    if is_numeric {
12489        if let Some(min) = attrs.min {
12490            checks.push(quote! {
12491                if let ::core::option::Option::Some(__n) = #val_ref {
12492                    if (*__n as f64) < (#min as f64) {
12493                        __errors.add(
12494                            #name_lit,
12495                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12496                        );
12497                    }
12498                }
12499            });
12500        }
12501        if let Some(max) = attrs.max {
12502            checks.push(quote! {
12503                if let ::core::option::Option::Some(__n) = #val_ref {
12504                    if (*__n as f64) > (#max as f64) {
12505                        __errors.add(
12506                            #name_lit,
12507                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12508                        );
12509                    }
12510                }
12511            });
12512        }
12513    }
12514
12515    quote! { #( #checks )* }
12516}
12517
12518// ============================================================
12519//  #[derive(ViewSet)]
12520// ============================================================
12521
12522struct ViewSetAttrs {
12523    model: syn::Path,
12524    fields: Option<Vec<String>>,
12525    filter_fields: Vec<String>,
12526    search_fields: Vec<String>,
12527    /// (field_name, desc)
12528    ordering: Vec<(String, bool)>,
12529    page_size: Option<usize>,
12530    read_only: bool,
12531    allow_anonymous: bool,
12532    perms: ViewSetPermsAttrs,
12533    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12534    /// retrieve / create responses through this `#[derive(Serializer)]`
12535    /// type instead of the default field-level projection (requires the
12536    /// `serializer` feature). Tri-dialect.
12537    serializer: Option<syn::Path>,
12538}
12539
12540#[derive(Default)]
12541struct ViewSetPermsAttrs {
12542    list: Vec<String>,
12543    retrieve: Vec<String>,
12544    create: Vec<String>,
12545    update: Vec<String>,
12546    destroy: Vec<String>,
12547}
12548
12549fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12550    let root = rustango_root();
12551    let struct_name = &input.ident;
12552
12553    // Must be a unit struct or an empty named struct.
12554    match &input.data {
12555        Data::Struct(s) => match &s.fields {
12556            Fields::Unit | Fields::Named(_) => {}
12557            Fields::Unnamed(_) => {
12558                return Err(syn::Error::new_spanned(
12559                    struct_name,
12560                    "ViewSet can only be derived on a unit struct or an empty named struct",
12561                ));
12562            }
12563        },
12564        _ => {
12565            return Err(syn::Error::new_spanned(
12566                struct_name,
12567                "ViewSet can only be derived on a struct",
12568            ));
12569        }
12570    }
12571
12572    let attrs = parse_viewset_attrs(input)?;
12573    let model_path = &attrs.model;
12574
12575    // `.fields(&[...])` call — None means skip (use all scalar fields).
12576    let fields_call = if let Some(ref fields) = attrs.fields {
12577        let lits = fields.iter().map(|f| f.as_str());
12578        quote!(.fields(&[ #(#lits),* ]))
12579    } else {
12580        quote!()
12581    };
12582
12583    let filter_fields_call = if attrs.filter_fields.is_empty() {
12584        quote!()
12585    } else {
12586        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12587        quote!(.filter_fields(&[ #(#lits),* ]))
12588    };
12589
12590    let search_fields_call = if attrs.search_fields.is_empty() {
12591        quote!()
12592    } else {
12593        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12594        quote!(.search_fields(&[ #(#lits),* ]))
12595    };
12596
12597    let ordering_call = if attrs.ordering.is_empty() {
12598        quote!()
12599    } else {
12600        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12601            let f = f.as_str();
12602            quote!((#f, #desc))
12603        });
12604        quote!(.ordering(&[ #(#pairs),* ]))
12605    };
12606
12607    let page_size_call = if let Some(n) = attrs.page_size {
12608        quote!(.page_size(#n))
12609    } else {
12610        quote!()
12611    };
12612
12613    let read_only_call = if attrs.read_only {
12614        quote!(.read_only())
12615    } else {
12616        quote!()
12617    };
12618    let allow_anonymous_call = if attrs.allow_anonymous {
12619        quote!(.allow_anonymous())
12620    } else {
12621        quote!()
12622    };
12623
12624    // `.serializer::<S>()` — reshape responses through a derived
12625    // serializer. Requires the downstream crate to enable the
12626    // `serializer` feature (the method is gated on it).
12627    let serializer_call = if let Some(ref ser) = attrs.serializer {
12628        quote!(.serializer::<#ser>())
12629    } else {
12630        quote!()
12631    };
12632
12633    let perms = &attrs.perms;
12634    let perms_call = if perms.list.is_empty()
12635        && perms.retrieve.is_empty()
12636        && perms.create.is_empty()
12637        && perms.update.is_empty()
12638        && perms.destroy.is_empty()
12639    {
12640        quote!()
12641    } else {
12642        let list_lits = perms.list.iter().map(|s| s.as_str());
12643        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12644        let create_lits = perms.create.iter().map(|s| s.as_str());
12645        let update_lits = perms.update.iter().map(|s| s.as_str());
12646        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12647        quote! {
12648            .permissions(#root::viewset::ViewSetPerms {
12649                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
12650                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12651                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
12652                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
12653                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
12654            })
12655        }
12656    };
12657
12658    Ok(quote! {
12659        impl #struct_name {
12660            /// Build an `axum::Router` with the six standard REST endpoints
12661            /// for this ViewSet, mounted at `prefix`.
12662            ///
12663            /// Accepts any backend's pool — a `sqlx::PgPool`,
12664            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12665            /// enum — because they all `Into<Pool>` (#1273). This is why
12666            /// the derive no longer names `PgPool`, which would not
12667            /// exist on a project built without the `postgres` feature.
12668            pub fn router(
12669                prefix: &str,
12670                pool: impl ::core::convert::Into<#root::sql::Pool>,
12671            ) -> #root::__axum::Router {
12672                #root::viewset::ViewSet::for_model(
12673                    <#model_path as #root::core::Model>::SCHEMA
12674                )
12675                    #fields_call
12676                    #filter_fields_call
12677                    #search_fields_call
12678                    #ordering_call
12679                    #page_size_call
12680                    #perms_call
12681                    #read_only_call
12682                    #allow_anonymous_call
12683                    #serializer_call
12684                    .router_pool(prefix, pool.into())
12685            }
12686        }
12687    })
12688}
12689
12690fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12691    let mut model: Option<syn::Path> = None;
12692    let mut fields: Option<Vec<String>> = None;
12693    let mut filter_fields: Vec<String> = Vec::new();
12694    let mut search_fields: Vec<String> = Vec::new();
12695    let mut ordering: Vec<(String, bool)> = Vec::new();
12696    let mut page_size: Option<usize> = None;
12697    let mut read_only = false;
12698    let mut allow_anonymous = false;
12699    let mut perms = ViewSetPermsAttrs::default();
12700    let mut serializer: Option<syn::Path> = None;
12701
12702    for attr in &input.attrs {
12703        if !attr.path().is_ident("viewset") {
12704            continue;
12705        }
12706        attr.parse_nested_meta(|meta| {
12707            if meta.path.is_ident("model") {
12708                let path: syn::Path = meta.value()?.parse()?;
12709                model = Some(path);
12710                return Ok(());
12711            }
12712            if meta.path.is_ident("serializer") {
12713                let path: syn::Path = meta.value()?.parse()?;
12714                serializer = Some(path);
12715                return Ok(());
12716            }
12717            if meta.path.is_ident("fields") {
12718                let s: LitStr = meta.value()?.parse()?;
12719                fields = Some(split_field_list(&s.value()));
12720                return Ok(());
12721            }
12722            if meta.path.is_ident("filter_fields") {
12723                let s: LitStr = meta.value()?.parse()?;
12724                filter_fields = split_field_list(&s.value());
12725                return Ok(());
12726            }
12727            if meta.path.is_ident("search_fields") {
12728                let s: LitStr = meta.value()?.parse()?;
12729                search_fields = split_field_list(&s.value());
12730                return Ok(());
12731            }
12732            if meta.path.is_ident("ordering") {
12733                let s: LitStr = meta.value()?.parse()?;
12734                ordering = parse_ordering_list(&s.value());
12735                return Ok(());
12736            }
12737            if meta.path.is_ident("page_size") {
12738                let lit: syn::LitInt = meta.value()?.parse()?;
12739                page_size = Some(lit.base10_parse::<usize>()?);
12740                return Ok(());
12741            }
12742            if meta.path.is_ident("read_only") {
12743                read_only = true;
12744                return Ok(());
12745            }
12746            if meta.path.is_ident("allow_anonymous") {
12747                allow_anonymous = true;
12748                return Ok(());
12749            }
12750            if meta.path.is_ident("permissions") {
12751                meta.parse_nested_meta(|inner| {
12752                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12753                        let s: LitStr = inner.value()?.parse()?;
12754                        Ok(split_field_list(&s.value()))
12755                    };
12756                    if inner.path.is_ident("list") {
12757                        perms.list = parse_codenames(&inner)?;
12758                    } else if inner.path.is_ident("retrieve") {
12759                        perms.retrieve = parse_codenames(&inner)?;
12760                    } else if inner.path.is_ident("create") {
12761                        perms.create = parse_codenames(&inner)?;
12762                    } else if inner.path.is_ident("update") {
12763                        perms.update = parse_codenames(&inner)?;
12764                    } else if inner.path.is_ident("destroy") {
12765                        perms.destroy = parse_codenames(&inner)?;
12766                    } else {
12767                        return Err(inner.error(
12768                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12769                        ));
12770                    }
12771                    Ok(())
12772                })?;
12773                return Ok(());
12774            }
12775            Err(meta.error(
12776                "unknown viewset attribute (supported: model, fields, filter_fields, \
12777                 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
12778                 permissions(...))",
12779            ))
12780        })?;
12781    }
12782
12783    let model = model.ok_or_else(|| {
12784        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12785    })?;
12786
12787    Ok(ViewSetAttrs {
12788        model,
12789        fields,
12790        filter_fields,
12791        search_fields,
12792        ordering,
12793        page_size,
12794        read_only,
12795        allow_anonymous,
12796        perms,
12797        serializer,
12798    })
12799}
12800
12801// ============================================================ #[derive(Serializer)]
12802
12803struct SerializerContainerAttrs {
12804    model: syn::Path,
12805    /// `#[serializer(validate = "fn_name")]` on the struct — the
12806    /// cross-field validation hook (#436). The named inherent method
12807    /// must take `&self` and return
12808    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12809    /// `validate()` runs every per-field validator first; then calls
12810    /// the cross-field method if declared; aggregates all errors into
12811    /// one `FormErrors`.
12812    cross_validate: Option<syn::Ident>,
12813}
12814
12815#[derive(Default)]
12816struct SerializerFieldAttrs {
12817    read_only: bool,
12818    write_only: bool,
12819    source: Option<String>,
12820    skip: bool,
12821    /// `#[serializer(method = "fn_name")]` — computed field. The macro
12822    /// emits a `from_model` initializer that calls
12823    /// `Self::fn_name(&model)` and stores the return value.
12824    method: Option<String>,
12825    /// `#[serializer(validate = "fn_name")]` — per-field validator
12826    /// callable run by `Self::validate(&self)`. Must return
12827    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12828    /// the field name.
12829    validate: Option<String>,
12830    /// `#[serializer(nested)]` on a field whose type is another
12831    /// `Serializer` — the macro emits `from_model` initializer that
12832    /// reads the parent via `model.<source>.value()` then calls the
12833    /// child serializer's `from_model(parent)`. When the FK is
12834    /// unloaded the field falls back to `Default::default()` (does
12835    /// NOT panic) so a missing prefetch in prod degrades gracefully.
12836    /// Source field on the model defaults to the field name; override
12837    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12838    /// panic-on-unloaded behavior for tests.
12839    nested: bool,
12840    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12841    /// strict behavior: panic when the FK isn't loaded. Useful in
12842    /// test code where forgetting select_related must trip a hard
12843    /// failure rather than render a blank nested object.
12844    nested_strict: bool,
12845    /// `#[serializer(many = TagSerializer)]` — declare the field as
12846    /// a list of nested serializers. Field type must be `Vec<S>`
12847    /// where `S` is the inner serializer. The macro initializes the
12848    /// field to `Vec::new()` in `from_model` and emits a typed
12849    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12850    /// maps each model row through `S::from_model`. Auto-load isn't
12851    /// possible (the M2M / one-to-many accessor is async); callers
12852    /// fetch the children + call the setter post-from_model.
12853    many: Option<syn::Type>,
12854    /// `#[serializer(slug = "name")]` — render a relation as one of
12855    /// its fields.
12856    /// Source field on the model must be a `ForeignKey<T>`; the
12857    /// macro emits `from_model` glue that walks
12858    /// `model.<source>.value()?.<slug>` and clones it. Field type on
12859    /// the serializer is typically `String` (whatever type the slug
12860    /// column has). When the FK is unloaded the field falls back to
12861    /// `Default::default()`, same graceful-degrade contract as
12862    /// `nested`. Source defaults to the field name; override with
12863    /// `source = "..."`. v0.44.
12864    slug: Option<String>,
12865    /// `#[serializer(max_length = N)]` — caps
12866    /// the character count of a string field on write. Overrides the
12867    /// model's `max_length`; when absent the model value is inherited.
12868    max_length: Option<u64>,
12869    /// `#[serializer(min_length = N)]` — minimum character count.
12870    /// Serializer-only (the model has no `min_length` column).
12871    min_length: Option<u64>,
12872    /// `#[serializer(min = N)]` — inclusive
12873    /// integer lower bound; overrides the model's `min` when given.
12874    min: Option<i64>,
12875    /// `#[serializer(max = N)]` — inclusive
12876    /// integer upper bound; overrides the model's `max` when given.
12877    max: Option<i64>,
12878}
12879
12880fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12881    let mut model: Option<syn::Path> = None;
12882    let mut cross_validate: Option<syn::Ident> = None;
12883    for attr in &input.attrs {
12884        if !attr.path().is_ident("serializer") {
12885            continue;
12886        }
12887        attr.parse_nested_meta(|meta| {
12888            if meta.path.is_ident("model") {
12889                let _eq: syn::Token![=] = meta.input.parse()?;
12890                model = Some(meta.input.parse()?);
12891                return Ok(());
12892            }
12893            if meta.path.is_ident("validate") {
12894                // #436 — container-level `validate = "fn_name"` for
12895                // cross-field validation. Field-level
12896                // `#[serializer(validate = "...")]` on a field is
12897                // parsed separately in `parse_serializer_field_attrs`.
12898                let s: LitStr = meta.value()?.parse()?;
12899                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12900                return Ok(());
12901            }
12902            Err(meta.error(
12903                "unknown serializer container attribute \
12904                 (supported: `model`, `validate`)",
12905            ))
12906        })?;
12907    }
12908    let model = model.ok_or_else(|| {
12909        syn::Error::new_spanned(
12910            &input.ident,
12911            "`#[serializer(model = SomeModel)]` is required",
12912        )
12913    })?;
12914    Ok(SerializerContainerAttrs {
12915        model,
12916        cross_validate,
12917    })
12918}
12919
12920fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12921    let mut out = SerializerFieldAttrs::default();
12922    for attr in &field.attrs {
12923        if !attr.path().is_ident("serializer") {
12924            continue;
12925        }
12926        attr.parse_nested_meta(|meta| {
12927            if meta.path.is_ident("read_only") {
12928                out.read_only = true;
12929                return Ok(());
12930            }
12931            if meta.path.is_ident("write_only") {
12932                out.write_only = true;
12933                return Ok(());
12934            }
12935            if meta.path.is_ident("skip") {
12936                out.skip = true;
12937                return Ok(());
12938            }
12939            if meta.path.is_ident("source") {
12940                let s: LitStr = meta.value()?.parse()?;
12941                out.source = Some(s.value());
12942                return Ok(());
12943            }
12944            if meta.path.is_ident("method") {
12945                let s: LitStr = meta.value()?.parse()?;
12946                out.method = Some(s.value());
12947                return Ok(());
12948            }
12949            if meta.path.is_ident("validate") {
12950                let s: LitStr = meta.value()?.parse()?;
12951                out.validate = Some(s.value());
12952                return Ok(());
12953            }
12954            if meta.path.is_ident("many") {
12955                let _eq: syn::Token![=] = meta.input.parse()?;
12956                out.many = Some(meta.input.parse()?);
12957                return Ok(());
12958            }
12959            if meta.path.is_ident("nested") {
12960                out.nested = true;
12961                // Optional strict flag inside parentheses:
12962                //   #[serializer(nested(strict))]
12963                if meta.input.peek(syn::token::Paren) {
12964                    meta.parse_nested_meta(|inner| {
12965                        if inner.path.is_ident("strict") {
12966                            out.nested_strict = true;
12967                            return Ok(());
12968                        }
12969                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12970                    })?;
12971                }
12972                return Ok(());
12973            }
12974            if meta.path.is_ident("slug") {
12975                let s: LitStr = meta.value()?.parse()?;
12976                out.slug = Some(s.value());
12977                return Ok(());
12978            }
12979            if meta.path.is_ident("max_length") {
12980                let lit: syn::LitInt = meta.value()?.parse()?;
12981                out.max_length = Some(lit.base10_parse::<u64>()?);
12982                return Ok(());
12983            }
12984            if meta.path.is_ident("min_length") {
12985                let lit: syn::LitInt = meta.value()?.parse()?;
12986                out.min_length = Some(lit.base10_parse::<u64>()?);
12987                return Ok(());
12988            }
12989            if meta.path.is_ident("min") {
12990                let lit: syn::LitInt = meta.value()?.parse()?;
12991                out.min = Some(lit.base10_parse::<i64>()?);
12992                return Ok(());
12993            }
12994            if meta.path.is_ident("max") {
12995                let lit: syn::LitInt = meta.value()?.parse()?;
12996                out.max = Some(lit.base10_parse::<i64>()?);
12997                return Ok(());
12998            }
12999            Err(meta.error(
13000                "unknown serializer field attribute (supported: \
13001                 `read_only`, `write_only`, `source`, `skip`, `method`, \
13002                 `validate`, `nested`, `many`, `slug`, `max_length`, \
13003                 `min_length`, `min`, `max`)",
13004            ))
13005        })?;
13006    }
13007    // Validate: read_only + write_only is nonsensical
13008    if out.read_only && out.write_only {
13009        return Err(syn::Error::new_spanned(
13010            field,
13011            "a field cannot be both `read_only` and `write_only`",
13012        ));
13013    }
13014    if out.method.is_some() && out.source.is_some() {
13015        return Err(syn::Error::new_spanned(
13016            field,
13017            "`method` and `source` are mutually exclusive — `method` computes \
13018             the value from a method, `source` reads it from a different model field",
13019        ));
13020    }
13021    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13022        return Err(syn::Error::new_spanned(
13023            field,
13024            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13025             — pick one strategy for populating the field",
13026        ));
13027    }
13028    Ok(out)
13029}
13030
13031fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13032    let root = rustango_root();
13033    let struct_name = &input.ident;
13034    let struct_name_lit = struct_name.to_string();
13035
13036    let Data::Struct(data) = &input.data else {
13037        return Err(syn::Error::new_spanned(
13038            struct_name,
13039            "Serializer can only be derived on structs",
13040        ));
13041    };
13042    let Fields::Named(named) = &data.fields else {
13043        return Err(syn::Error::new_spanned(
13044            struct_name,
13045            "Serializer requires a struct with named fields",
13046        ));
13047    };
13048
13049    let container = parse_serializer_container_attrs(input)?;
13050    let model_path = &container.model;
13051
13052    // Classify each field. `ty` is only consumed by the
13053    // `#[cfg(feature = "openapi")]` block below, but we always
13054    // capture it to keep the field-info build a single pass.
13055    #[allow(dead_code)]
13056    struct FieldInfo {
13057        ident: syn::Ident,
13058        ty: syn::Type,
13059        attrs: SerializerFieldAttrs,
13060    }
13061    let mut fields_info: Vec<FieldInfo> = Vec::new();
13062    for field in &named.named {
13063        let ident = field.ident.clone().expect("named field has ident");
13064        let attrs = parse_serializer_field_attrs(field)?;
13065        fields_info.push(FieldInfo {
13066            ident,
13067            ty: field.ty.clone(),
13068            attrs,
13069        });
13070    }
13071
13072    // Generate from_model body: struct literal with each field assigned.
13073    let from_model_fields = fields_info.iter().map(|fi| {
13074        let ident = &fi.ident;
13075        let ty = &fi.ty;
13076        if let Some(_inner) = &fi.attrs.many {
13077            // Many — collection field. Initialize empty; caller
13078            // populates via the macro-emitted set_<field> helper
13079            // after fetching the M2M children.
13080            quote! { #ident: ::std::vec::Vec::new() }
13081        } else if let Some(method) = &fi.attrs.method {
13082            // SerializerMethodField: call Self::<method>(&model) to
13083            // compute the value. Method signature must be
13084            // `fn <method>(model: &T) -> <field type>`.
13085            let method_ident = syn::Ident::new(method, ident.span());
13086            quote! { #ident: Self::#method_ident(model) }
13087        } else if let Some(slug_field) = &fi.attrs.slug {
13088            // v0.44 — SlugRelatedField. Source defaults to the field
13089            // name on this struct; override via `source = "..."`. The
13090            // source field on the model is expected to be a
13091            // `ForeignKey<T>`; the slug field on the parent is named
13092            // by the attribute value. When the FK is unloaded the
13093            // field falls back to `Default::default()` — same
13094            // graceful-degrade contract as `nested`.
13095            let src_name = fi
13096                .attrs
13097                .source
13098                .as_deref()
13099                .unwrap_or(&ident_name(&fi.ident))
13100                .to_owned();
13101            let src_ident = ident_from_name(&src_name, ident.span());
13102            let slug_ident = syn::Ident::new(slug_field, ident.span());
13103            quote! {
13104                #ident: match model.#src_ident.value() {
13105                    ::core::option::Option::Some(__loaded) =>
13106                        ::core::clone::Clone::clone(&__loaded.#slug_ident),
13107                    ::core::option::Option::None =>
13108                        ::core::default::Default::default(),
13109                }
13110            }
13111        } else if fi.attrs.nested {
13112            // Nested serializer. Source defaults to the field name on
13113            // this struct; override via `source = "..."`. The source
13114            // field on the model is expected to be a `ForeignKey<T>`
13115            // whose `.value()` returns `Option<&T>` after lazy-load.
13116            //
13117            // Behavior matrix (tweakable per-field):
13118            //   * FK loaded   → nested object materializes via
13119            //                   ChildSerializer::from_model(parent).
13120            //   * FK unloaded → fall back to ChildSerializer::default()
13121            //                   (so prod doesn't crash on a missing
13122            //                   prefetch — just renders a blank nested
13123            //                   object). Add `#[serializer(nested,
13124            //                   strict)]` to keep the v0.18.1
13125            //                   panic-on-unloaded behavior for tests
13126            //                   that want hard guardrails.
13127            let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13128            let src_ident = ident_from_name(&src_name, ident.span());
13129            if fi.attrs.nested_strict {
13130                let panic_msg = format!(
13131                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13132                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13133                );
13134                quote! {
13135                    #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13136                        model.#src_ident.value().expect(#panic_msg),
13137                    )
13138                }
13139            } else {
13140                quote! {
13141                    #ident: match model.#src_ident.value() {
13142                        ::core::option::Option::Some(__loaded) =>
13143                            <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13144                        ::core::option::Option::None =>
13145                            ::core::default::Default::default(),
13146                    }
13147                }
13148            }
13149        } else if fi.attrs.write_only || fi.attrs.skip {
13150            // Not read from model — use default
13151            quote! { #ident: ::core::default::Default::default() }
13152        } else if let Some(src) = &fi.attrs.source {
13153            let src_ident = ident_from_name(src, ident.span());
13154            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13155        } else {
13156            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13157        }
13158    });
13159
13160    // is_writable predicate (also used by writable_lits /
13161    // writable_source_lits below).
13162    let is_writable = |fi: &&FieldInfo| {
13163        !fi.attrs.read_only
13164            && !fi.attrs.skip
13165            && fi.attrs.method.is_none()
13166            && !fi.attrs.nested
13167            && fi.attrs.many.is_none()
13168            && fi.attrs.slug.is_none()
13169    };
13170
13171    // Declarative field constraints: one block
13172    // per writable field, run inside `validate()`. Each resolves its
13173    // bounds as the serializer attr when given (`#[serializer(max_length
13174    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13175    // `max` / `choices`), then dispatches on the field's JSON value via
13176    // `forms::validators::check_value` (string → length/choices, integer
13177    // → min/max).
13178    let opt_usize = |v: Option<u64>| match v {
13179        Some(n) => {
13180            let n = n as usize;
13181            quote!(::core::option::Option::Some(#n))
13182        }
13183        None => quote!(::core::option::Option::None),
13184    };
13185    let opt_i64 = |v: Option<i64>| match v {
13186        Some(n) => quote!(::core::option::Option::Some(#n)),
13187        None => quote!(::core::option::Option::None),
13188    };
13189    let constraint_blocks: Vec<_> = fields_info
13190        .iter()
13191        .filter(is_writable)
13192        .map(|fi| {
13193            let ident = &fi.ident;
13194            let fname = ident_name(ident);
13195            let mname = fi
13196                .attrs
13197                .source
13198                .clone()
13199                .unwrap_or_else(|| ident_name(&fi.ident));
13200            let attr_max_len = opt_usize(fi.attrs.max_length);
13201            let attr_min_len = opt_usize(fi.attrs.min_length);
13202            let attr_min = opt_i64(fi.attrs.min);
13203            let attr_max = opt_i64(fi.attrs.max);
13204            quote! {
13205                if __sent(#fname) {
13206                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13207                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13208                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13209                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13210                    let __min: ::core::option::Option<i64> =
13211                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13212                    let __max: ::core::option::Option<i64> =
13213                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13214                    let __choices = __sf.and_then(|__f| __f.choices);
13215                    let __v = #root::__serde_json::to_value(&self.#ident)
13216                        .unwrap_or(#root::__serde_json::Value::Null);
13217                    #root::forms::validators::check_value(
13218                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13219                        &mut __errors,
13220                    );
13221                }
13222            }
13223        })
13224        .collect();
13225    let has_constraints = !constraint_blocks.is_empty();
13226
13227    // Per-field validators. Emit a
13228    // `validate(&self)` method that runs each user-defined validator
13229    // and aggregates errors into `FormErrors`.
13230    let validator_calls: Vec<_> = fields_info
13231        .iter()
13232        .filter_map(|fi| {
13233            let ident = &fi.ident;
13234            let name_lit = ident_name(ident);
13235            let method = fi.attrs.validate.as_ref()?;
13236            let method_ident = syn::Ident::new(method, ident.span());
13237            Some(quote! {
13238                if __sent(#name_lit) {
13239                    if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13240                        __errors.add(#name_lit.to_owned(), __e);
13241                    }
13242                }
13243            })
13244        })
13245        .collect();
13246    // #436 — cross-field `validate(&self)`. If the
13247    // container declared `#[serializer(validate = "fn_name")]`,
13248    // the macro-generated `validate(&self)` runs every per-field
13249    // validator first, then calls the user's cross-field method,
13250    // merging its `FormErrors` into the per-field errors. Either
13251    // alone is enough to emit the wrapper.
13252    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13253        quote! {
13254            // Merge cross-field errors into the per-field bucket so
13255            // a single .validate() call surfaces both layers.
13256            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13257                __errors.merge(__cross);
13258            }
13259        }
13260    });
13261    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13262    // Anything to run? Declarative constraints (for any writable field) +
13263    // per-field validators + cross-field hook.
13264    let has_run_validations = has_validators || has_constraints;
13265    // Shared body: declarative field constraints first (length / range /
13266    // choices, serializer-attr-or-model), then per-field validators, then
13267    // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13268    let checks = quote! {
13269        let mut __errors = #root::forms::FormErrors::default();
13270        #( #constraint_blocks )*
13271        #( #validator_calls )*
13272        #cross_validate_call
13273        if __errors.is_empty() {
13274            ::core::result::Result::Ok(())
13275        } else {
13276            ::core::result::Result::Err(__errors)
13277        }
13278    };
13279    let validate_body = quote! {
13280        let __sent = |_: &str| true;
13281        #checks
13282    };
13283    // PATCH: only the keys the body carries.
13284    let validate_patch_body = quote! {
13285        let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13286        #checks
13287    };
13288    // Inherent `validate(&self)` — kept for back-compat with direct
13289    // `serializer.validate()` calls that don't import `ModelSerializer`.
13290    // Emitted only when the serializer declares per-field/cross-field
13291    // validators (unchanged rule) so it never collides with a
13292    // hand-written inherent `validate`.
13293    let validate_method = if has_validators {
13294        quote! {
13295            impl #struct_name {
13296                /// Run the declarative field constraints, every
13297                /// `#[serializer(validate = "...")]` per-field validator,
13298                /// and (when declared) the container-level cross-field
13299                /// validator. Aggregates errors into `FormErrors` keyed by
13300                /// the field name. Returns `Ok(())` when all pass.
13301                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13302                    #validate_body
13303                }
13304            }
13305        }
13306    } else {
13307        quote! {}
13308    };
13309    // Trait-method override so the type-erased ViewSet write path can
13310    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13311    // there's anything to run (declarative constraints inherited from the
13312    // model count); otherwise the trait's default no-op is used.
13313    let trait_validate_override = if has_run_validations {
13314        quote! {
13315            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13316                #validate_body
13317            }
13318        }
13319    } else {
13320        quote! {}
13321    };
13322
13323    // For every `#[serializer(many = S)]` field, emit a
13324    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13325    // helper that maps the parents through `S::from_model`.
13326    let many_setters: Vec<_> = fields_info
13327        .iter()
13328        .filter_map(|fi| {
13329            let many_ty = fi.attrs.many.as_ref()?;
13330            let ident = &fi.ident;
13331            let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13332            Some(quote! {
13333                /// Populate this `many` field by mapping each parent model
13334                /// through the inner serializer's `from_model`. Call after
13335                /// fetching the M2M / one-to-many children since
13336                /// `from_model` itself can't await an SQL query.
13337                pub fn #setter(
13338                    &mut self,
13339                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13340                ) -> &mut Self {
13341                    self.#ident = models.iter()
13342                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13343                        .collect();
13344                    self
13345                }
13346            })
13347        })
13348        .collect();
13349    let many_setters_impl = if many_setters.is_empty() {
13350        quote! {}
13351    } else {
13352        quote! {
13353            impl #struct_name {
13354                #( #many_setters )*
13355            }
13356        }
13357    };
13358
13359    // Generate custom Serialize: skip write_only fields
13360    let output_fields: Vec<_> = fields_info
13361        .iter()
13362        .filter(|fi| !fi.attrs.write_only)
13363        .collect();
13364    let output_field_count = output_fields.len();
13365    let serialize_fields = output_fields.iter().map(|fi| {
13366        let ident = &fi.ident;
13367        let name_lit = ident_name(ident);
13368        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13369    });
13370
13371    // writable_fields: normal + write_only.
13372    // Exclude:
13373    //   - `read_only` — server-computed.
13374    //   - `skip` — caller sets manually post-from_model.
13375    //   - `method` — computed from a Self::fn(&model) call; accepting
13376    //     it on write is meaningless.
13377    //   - `nested` / `many` — populated from related-model data, not
13378    //     from a field on the wire body.
13379    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13380    // `many` in `writable_fields()`, which made the ViewSet write
13381    // path accept those fields from the JSON body and try to bind
13382    // them to the SQL UPDATE — a silent no-op at best, a type
13383    // mismatch at worst.
13384    // (`is_writable` is defined above, near the constraint codegen.)
13385    let writable_lits: Vec<_> = fields_info
13386        .iter()
13387        .filter(is_writable)
13388        .map(|fi| ident_name(&fi.ident))
13389        .collect();
13390
13391    // `writable_source_fields`: the MODEL field names of writable
13392    // serializer fields (source-resolved). The ViewSet write path skips
13393    // every model column NOT in this set, so `read_only` / `method` /
13394    // computed fields a client posts are ignored instead of written.
13395    let writable_source_lits: Vec<String> = fields_info
13396        .iter()
13397        .filter(is_writable)
13398        .map(|fi| {
13399            fi.attrs
13400                .source
13401                .clone()
13402                .unwrap_or_else(|| ident_name(&fi.ident))
13403        })
13404        .collect();
13405
13406    // `readable_source_fields`: model columns the JSON output copies
13407    // straight from the model, so `?ordering=` cannot sort on a column
13408    // the API never shows (#1845).
13409    let readable_source_lits: Vec<String> = fields_info
13410        .iter()
13411        .filter(|fi| {
13412            !fi.attrs.write_only
13413                && !fi.attrs.skip
13414                && fi.attrs.method.is_none()
13415                && !fi.attrs.nested
13416                && fi.attrs.many.is_none()
13417                && fi.attrs.slug.is_none()
13418        })
13419        .map(|fi| {
13420            fi.attrs
13421                .source
13422                .clone()
13423                .unwrap_or_else(|| ident_name(&fi.ident))
13424        })
13425        .collect();
13426
13427    // `from_writable_json`: build a partial instance for input
13428    // validation. Writable fields are parsed from the JSON body (keyed
13429    // by serializer field name); every other field defaults. Per-field
13430    // type errors collect into `FormErrors` keyed by the field name.
13431    // Construction is field-by-field (not `Self::default()`) so it works
13432    // for serializers regardless of a struct-level `Default`.
13433    let from_writable_json_inits: Vec<_> = fields_info
13434        .iter()
13435        .map(|fi| {
13436            let ident = &fi.ident;
13437            let fname = ident_name(ident);
13438            let ty = &fi.ty;
13439            if is_writable(&fi) {
13440                quote! {
13441                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13442                        ::core::option::Option::Some(__v) => {
13443                            match #root::__serde_json::from_value::<#ty>(
13444                                ::core::clone::Clone::clone(__v),
13445                            ) {
13446                                ::core::result::Result::Ok(__x) => __x,
13447                                ::core::result::Result::Err(__e) => {
13448                                    __errors.add(#fname.to_owned(), __e.to_string());
13449                                    ::core::default::Default::default()
13450                                }
13451                            }
13452                        }
13453                        ::core::option::Option::None => ::core::default::Default::default(),
13454                    }
13455                }
13456            } else {
13457                quote! { #ident: ::core::default::Default::default() }
13458            }
13459        })
13460        .collect();
13461    // `validate_patch`: overlay each sent writable field on the stored row.
13462    let patch_overlays: Vec<_> = fields_info
13463        .iter()
13464        .filter(is_writable)
13465        .map(|fi| {
13466            let ident = &fi.ident;
13467            let fname = ident_name(ident);
13468            let ty = &fi.ty;
13469            quote! {
13470                if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
13471                    match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
13472                        ::core::result::Result::Ok(__x) => __merged.#ident = __x,
13473                        ::core::result::Result::Err(__e) => {
13474                            __errors.add(#fname.to_owned(), __e.to_string());
13475                        }
13476                    }
13477                }
13478            }
13479        })
13480        .collect();
13481    let patch_validate_call = if has_run_validations {
13482        quote! { __merged.__rustango_validate_patch(__obj) }
13483    } else {
13484        quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
13485    };
13486    let patch_validate_helper = if has_run_validations {
13487        quote! {
13488            impl #struct_name {
13489                #[doc(hidden)]
13490                fn __rustango_validate_patch(
13491                    &self,
13492                    __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
13493                ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13494                    #validate_patch_body
13495                }
13496            }
13497        }
13498    } else {
13499        quote! {}
13500    };
13501
13502    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13503    // Only includes fields shown in JSON output (skips write_only). For each
13504    // `Option<T>` field, omit from `required` and add `.nullable()`.
13505    let openapi_impl = {
13506        #[cfg(feature = "openapi")]
13507        {
13508            let property_calls = output_fields.iter().map(|fi| {
13509                let ident = &fi.ident;
13510                let name_lit = ident_name(ident);
13511                let ty = &fi.ty;
13512                let nullable_call = if is_option(ty) {
13513                    quote! { .nullable() }
13514                } else {
13515                    quote! {}
13516                };
13517                quote! {
13518                    .property(
13519                        #name_lit,
13520                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13521                            #nullable_call,
13522                    )
13523                }
13524            });
13525            let required_lits: Vec<_> = output_fields
13526                .iter()
13527                .filter(|fi| !is_option(&fi.ty))
13528                .map(|fi| ident_name(&fi.ident))
13529                .collect();
13530            quote! {
13531                impl #root::openapi::OpenApiSchema for #struct_name {
13532                    fn openapi_schema() -> #root::openapi::Schema {
13533                        #root::openapi::Schema::object()
13534                            #( #property_calls )*
13535                            .required([ #( #required_lits ),* ])
13536                    }
13537                }
13538            }
13539        }
13540        #[cfg(not(feature = "openapi"))]
13541        {
13542            quote! {}
13543        }
13544    };
13545
13546    Ok(quote! {
13547        impl #root::serializer::ModelSerializer for #struct_name {
13548            type Model = #model_path;
13549
13550            fn from_model(model: &Self::Model) -> Self {
13551                Self {
13552                    #( #from_model_fields ),*
13553                }
13554            }
13555
13556            fn writable_fields() -> &'static [&'static str] {
13557                &[ #( #writable_lits ),* ]
13558            }
13559
13560            fn writable_source_fields() -> &'static [&'static str] {
13561                &[ #( #writable_source_lits ),* ]
13562            }
13563
13564            fn readable_source_fields() -> &'static [&'static str] {
13565                &[ #( #readable_source_lits ),* ]
13566            }
13567
13568            fn from_writable_json(
13569                __body: &#root::__serde_json::Value,
13570            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13571                let mut __errors = #root::forms::FormErrors::default();
13572                let __obj = __body.as_object();
13573                let __out = Self {
13574                    #( #from_writable_json_inits ),*
13575                };
13576                if __errors.is_empty() {
13577                    ::core::result::Result::Ok(__out)
13578                } else {
13579                    ::core::result::Result::Err(__errors)
13580                }
13581            }
13582
13583            fn validate_patch(
13584                __stored: &Self::Model,
13585                __body: &#root::__serde_json::Value,
13586            ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13587                let mut __errors = #root::forms::FormErrors::default();
13588                let __obj = __body.as_object();
13589                #[allow(unused_mut)]
13590                let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
13591                #( #patch_overlays )*
13592                if !__errors.is_empty() {
13593                    return ::core::result::Result::Err(__errors);
13594                }
13595                #patch_validate_call
13596            }
13597
13598            #trait_validate_override
13599        }
13600
13601        #patch_validate_helper
13602
13603        impl #root::__serde::Serialize for #struct_name {
13604            fn serialize<S>(&self, serializer: S)
13605                -> ::core::result::Result<S::Ok, S::Error>
13606            where
13607                S: #root::__serde::Serializer,
13608            {
13609                use #root::__serde::ser::SerializeStruct;
13610                let mut __state = serializer.serialize_struct(
13611                    #struct_name_lit,
13612                    #output_field_count,
13613                )?;
13614                #( #serialize_fields )*
13615                __state.end()
13616            }
13617        }
13618
13619        #openapi_impl
13620
13621        #validate_method
13622
13623        #many_setters_impl
13624    })
13625}
13626
13627/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13628/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13629/// when the feature is off.
13630#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13631fn is_option(ty: &syn::Type) -> bool {
13632    if let syn::Type::Path(p) = ty {
13633        if let Some(last) = p.path.segments.last() {
13634            return last.ident == "Option";
13635        }
13636    }
13637    false
13638}
13639
13640#[cfg(test)]
13641mod main_attr_tests {
13642    use super::*;
13643
13644    fn expand(args: &str) -> String {
13645        let body = quote! { async fn main() { run().await } };
13646        expand_main(args.parse().unwrap(), body)
13647            .expect("expansion failed")
13648            .to_string()
13649    }
13650
13651    /// Asserts on the expansion rather than on `parse_logging`,
13652    /// because the defect was a subscriber reaching the output, not
13653    /// an argument being misread (#1465).
13654    #[test]
13655    fn logging_false_emits_no_subscriber() {
13656        let out = expand("logging = false");
13657        assert!(
13658            !out.contains("logging :: Setup"),
13659            "`logging = false` must not install a subscriber:\n{out}",
13660        );
13661        assert!(
13662            out.contains("block_on"),
13663            "the runtime must still be built:\n{out}",
13664        );
13665    }
13666
13667    #[test]
13668    fn default_still_installs_a_subscriber() {
13669        // The control. Without it, an `expand_main` that dropped the
13670        // prologue unconditionally would pass the test above while
13671        // leaving every default project with no logging at all.
13672        let out = expand("");
13673        assert!(
13674            out.contains("logging :: Setup"),
13675            "the default entrypoint must still install one:\n{out}",
13676        );
13677    }
13678
13679    #[test]
13680    fn logging_false_composes_with_flavor() {
13681        let out = expand("flavor = \"current_thread\", logging = false");
13682        assert!(!out.contains("logging :: Setup"), "{out}");
13683        assert!(out.contains("new_current_thread"), "{out}");
13684    }
13685
13686    #[test]
13687    fn logging_true_is_the_default_shape() {
13688        // `logging = true` is accepted and means the default, so the
13689        // match must not fire on the substring `logging`.
13690        assert!(parse_logging(&"logging = true".parse().unwrap()));
13691        assert!(!parse_logging(&"logging=false".parse().unwrap()));
13692    }
13693}
13694
13695#[cfg(test)]
13696mod field_attr_tests {
13697    use super::*;
13698
13699    fn error_for(field: syn::Field) -> Option<String> {
13700        let attrs = parse_field_attrs(&field).unwrap();
13701        let kind = detect_type(&field.ty).unwrap().kind;
13702        check_bound_compatibility(&field, &attrs, kind)
13703            .err()
13704            .map(|e| e.to_string())
13705    }
13706
13707    /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
13708    #[test]
13709    fn type_specific_attrs_reject_other_types() {
13710        let f: syn::FieldsNamed = syn::parse_quote!({
13711            #[rustango(citext)] a: i64,
13712            #[rustango(vector(dims = 3))] b: String,
13713            #[rustango(geometry(srid = 4326))] c: String,
13714            #[rustango(citext)] d: String,
13715        });
13716        let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
13717        assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
13718        assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
13719        assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
13720        assert_eq!(errs[3], None);
13721    }
13722
13723    /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
13724    #[test]
13725    fn manager_ext_is_refused() {
13726        let input: DeriveInput = syn::parse_quote! {
13727            #[rustango(table = "p", manager(ext = "PostManagerExt"))]
13728            struct Post { id: i64 }
13729        };
13730        let err = parse_container_attrs(&input).err().expect("refused");
13731        assert!(err.to_string().contains("#2132"), "{err}");
13732    }
13733
13734    /// A raw ident derives names without `r#`; this used to panic (#1937).
13735    #[test]
13736    fn raw_idents_derive_plain_names() {
13737        let ident: syn::Ident = syn::parse_quote!(r#type);
13738        assert_eq!(column_type_ident(&ident).to_string(), "type_col");
13739        assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
13740    }
13741}