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 (`ForeignKey<T>` or `Option<ForeignKey<T>>`) as one of its fields: clones the loaded parent's `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. Cargo does not watch
200/// directory listings, so a new migration file is missed until the
201/// crate rebuilds. Add a `build.rs` that tracks the directory:
202///
203/// ```ignore
204/// fn main() { println!("cargo:rerun-if-changed=migrations"); }
205/// ```
206///
207/// A missing directory embeds nothing and emits a deprecation warning.
208#[proc_macro]
209pub fn embed_migrations(input: TokenStream) -> TokenStream {
210    expand_embed_migrations(input.into())
211        .unwrap_or_else(syn::Error::into_compile_error)
212        .into()
213}
214
215/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
216/// compile time against typed columns. Issue #269 / T1.7.
217///
218/// Each invocation lowers to the equivalent typed-column method call:
219///
220/// ```ignore
221/// // These expand identically:
222/// Q!(User.email__icontains = "alice")
223/// User::email.ilike("%alice%")
224/// ```
225///
226/// Field-name typos fail the build (the macro emits `User::no_such_field`
227/// which doesn't exist) — the headline ergonomic win of this slice over
228/// filters keyed by a plain string.
229///
230/// # Supported lookup suffixes
231///
232/// * bare `=` / `__exact` → `.eq(value)`
233/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
234/// * `__ne` → `.ne(value)`
235/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
236/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
237/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
238/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
239/// * `__in` → `.is_in(iterable)`
240/// * `__not_in` → `.not_in(iterable)`
241/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
242/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
243/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
244///
245/// Unknown suffixes fail the build with a `compile_error!` pointing at
246/// the lookup token.
247///
248/// # Combine
249///
250/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
251/// `.and()` / `.or()` / `.not()` methods:
252///
253/// ```ignore
254/// User::objects()
255///     .where_(
256///         Q!(User.active = true)
257///             .and(Q!(User.email__icontains = "alice"))
258///     )
259///     .fetch(&pool).await?;
260/// ```
261///
262/// All emitted code routes through existing per-dialect writers — no new
263/// SQL emission machinery. Tri-dialect support is inherent.
264#[allow(non_snake_case)]
265#[proc_macro]
266pub fn Q(input: TokenStream) -> TokenStream {
267    expand_q(input.into())
268        .unwrap_or_else(syn::Error::into_compile_error)
269        .into()
270}
271
272/// `#[rustango::main]` — the runserver entrypoint. Wraps
273/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
274/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
275/// functions are zero-boilerplate. `RUST_LOG` and
276/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
277/// environment, else from `./.env` (only those keys are read):
278///
279/// ```ignore
280/// #[rustango::main]
281/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
282///     rustango::server::Builder::from_env().await?
283///         .migrate("migrations").await?
284///         .api(my_app::urls::api())
285///         .seed_with(my_app::seed::run).await?
286///         .serve("0.0.0.0:8080").await
287/// }
288/// ```
289///
290/// Optional `flavor = "current_thread"` passes through to
291/// `#[tokio::main]`; default is the multi-threaded runtime.
292///
293/// `logging = false` suppresses the default subscriber, for apps
294/// that install their own. Needed whenever the body reaches
295/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
296/// macro installs gets there first, and `tracing` keeps the first
297/// one, so the `[logging]` section would otherwise be read and
298/// discarded (#1465).
299///
300/// ```ignore
301/// #[rustango::main(logging = false)]
302/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
303///     rustango::manage::Cli::new()
304///         .with_settings_from_env()
305///         .with_logging()          // now the first installer
306///         .run().await
307/// }
308/// ```
309///
310/// Pulls `tracing-subscriber` into the rustango crate behind the
311/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
312/// out get plain `#[tokio::main]` ergonomics without the dependency.
313#[proc_macro_attribute]
314pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
315    expand_main(args.into(), item.into())
316        .unwrap_or_else(syn::Error::into_compile_error)
317        .into()
318}
319
320fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
321    let mut input: syn::ItemFn = syn::parse2(item)?;
322    if input.sig.asyncness.is_none() {
323        return Err(syn::Error::new(
324            input.sig.ident.span(),
325            "`#[rustango::main]` must wrap an `async fn`",
326        ));
327    }
328
329    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
330    // to `#[tokio::main]`. Tokio's proc-macro internally emits
331    // `::tokio::*` paths that resolve against the user crate's deps,
332    // so calling it through the rustango re-export still requires the
333    // user to add tokio to their own Cargo.toml. Building the
334    // runtime ourselves keeps the dep transitive through the
335    // `runtime` feature on rustango.
336    //
337    // Parse optional `flavor = "current_thread"` / `flavor =
338    // "multi_thread"` from the attribute args. Unknown args are
339    // tolerated (forward-compat with tokio's own arg surface).
340    let root = rustango_root();
341    let flavor = parse_flavor(&args);
342    let builder_call = match flavor {
343        Flavor::CurrentThread => quote! {
344            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
345        },
346        Flavor::MultiThread => quote! {
347            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
348        },
349    };
350
351    // Detach the user body and rewrite `main` as a sync fn that
352    // builds the runtime and blocks on the async body.
353    let user_body = input.block.clone();
354    input.sig.asyncness = None;
355
356    // `logging = false` drops the block entirely, so a later
357    // `Cli::with_logging()` is the first installer and its
358    // `[logging]` settings actually take effect (#1465).
359    let logging_prologue = if parse_logging(&args) {
360        quote! {
361            // `Setup`'s defaults, not a copy of them: filter, format and
362            // the `Color::Auto` rule live in one place (#1493).
363            // `logging` and this macro share the `runtime` gate.
364            // The real `RUST_LOG` still wins over `./.env`'s (#2204).
365            let _ = #root::logging::Setup::new()
366                .with_default_env_filter(#root::__private_runtime::main_default_filter())
367                .with_format(#root::__private_runtime::main_format())
368                .install();
369        }
370    } else {
371        quote! {}
372    };
373
374    input.block = syn::parse2(quote! {{
375        { #logging_prologue }
376        let __rt = #builder_call
377            .enable_all()
378            .build()
379            .expect("failed to build tokio runtime");
380        __rt.block_on(async move #user_body)
381    }})?;
382
383    Ok(quote! {
384        #input
385    })
386}
387
388enum Flavor {
389    MultiThread,
390    CurrentThread,
391}
392
393fn parse_flavor(args: &TokenStream2) -> Flavor {
394    // Cheap parser: look for the literal token sequence
395    // `flavor = "current_thread"`. Everything else (including
396    // bare `multi_thread` or no args) defaults to multi-thread.
397    let s = args.to_string();
398    if s.contains("current_thread") {
399        Flavor::CurrentThread
400    } else {
401        Flavor::MultiThread
402    }
403}
404
405/// `false` when the attribute carries `logging = false` — the caller
406/// installs their own subscriber, usually via `Cli::with_logging`
407/// (#1465). Same cheap token match as `parse_flavor`.
408fn parse_logging(args: &TokenStream2) -> bool {
409    !args.to_string().replace(' ', "").contains("logging=false")
410}
411
412/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
413struct QInput {
414    base_path: syn::Path,
415    field: syn::Ident,
416    value: syn::Expr,
417}
418
419impl syn::parse::Parse for QInput {
420    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
421        let base_path: syn::Path = input.parse()?;
422        input.parse::<syn::Token![.]>()?;
423        let field: syn::Ident = input.parse()?;
424        input.parse::<syn::Token![=]>()?;
425        let value: syn::Expr = input.parse()?;
426        Ok(QInput {
427            base_path,
428            field,
429            value,
430        })
431    }
432}
433
434fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
435    let q: QInput = syn::parse2(input)?;
436    let root = rustango_root();
437    let field_str = ident_name(&q.field);
438    let field_span = q.field.span();
439    let (base, suffix) = match field_str.find("__") {
440        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
441        None => (field_str.as_str(), ""),
442    };
443    if base.is_empty() {
444        return Err(syn::Error::new(
445            field_span,
446            "Q!(): field name is empty before `__` suffix",
447        ));
448    }
449    let base_ident = ident_from_name(base, field_span);
450    let value = &q.value;
451    let path = &q.base_path;
452
453    // Most suffixes map directly to a Column method with the value
454    // forwarded unchanged. Some need value-shape massaging (wildcards
455    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
456    // ISNULL). Unknown suffixes fail the build.
457    let expanded = match suffix {
458        "" | "exact" => quote! {
459            #root::core::Column::eq(#path::#base_ident, #value)
460        },
461        "ne" => quote! {
462            #root::core::Column::ne(#path::#base_ident, #value)
463        },
464        "gt" => quote! {
465            #root::core::Column::gt(#path::#base_ident, #value)
466        },
467        "gte" => quote! {
468            #root::core::Column::gte(#path::#base_ident, #value)
469        },
470        "lt" => quote! {
471            #root::core::Column::lt(#path::#base_ident, #value)
472        },
473        "lte" => quote! {
474            #root::core::Column::lte(#path::#base_ident, #value)
475        },
476        // The escaped-LIKE lookups delegate to the typed Column
477        // methods, which escape the value and pair it with the
478        // *Escaped ops so `%` / `_` match literally on every dialect
479        // (#1257) — the macro no longer owns any pattern wrapping.
480        "iexact" => quote! {
481            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
482        },
483        "contains" => quote! {
484            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
485        },
486        "icontains" => quote! {
487            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
488        },
489        "startswith" => quote! {
490            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
491        },
492        "istartswith" => quote! {
493            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
494        },
495        "endswith" => quote! {
496            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
497        },
498        "iendswith" => quote! {
499            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
500        },
501        "in" => quote! {
502            #root::core::Column::is_in(#path::#base_ident, #value)
503        },
504        "not_in" => quote! {
505            #root::core::Column::not_in(#path::#base_ident, #value)
506        },
507        "isnull" => {
508            // Must be a bool literal at macro time so we can route to
509            // is_null vs is_not_null without a runtime branch.
510            let b = match value {
511                syn::Expr::Lit(syn::ExprLit {
512                    lit: syn::Lit::Bool(b),
513                    ..
514                }) => b.value(),
515                _ => {
516                    return Err(syn::Error::new_spanned(
517                        value,
518                        "Q!(): `__isnull` requires a `true` or `false` literal",
519                    ));
520                }
521            };
522            if b {
523                quote! { #root::core::Column::is_null(#path::#base_ident) }
524            } else {
525                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
526            }
527        }
528        "between" => {
529            // Accept a tuple literal `(lo, hi)`.
530            let tuple = match value {
531                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
532                _ => {
533                    return Err(syn::Error::new_spanned(
534                        value,
535                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
536                    ));
537                }
538            };
539            let lo = &tuple.elems[0];
540            let hi = &tuple.elems[1];
541            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
542        }
543        "regex" => quote! {
544            #root::core::Column::regex(#path::#base_ident, #value)
545        },
546        "iregex" => quote! {
547            #root::core::Column::iregex(#path::#base_ident, #value)
548        },
549        _ => {
550            return Err(syn::Error::new(
551                field_span,
552                format!(
553                    "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",
554                    suffix
555                ),
556            ));
557        }
558    };
559    Ok(expanded)
560}
561
562fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
563    // Default to "./migrations" if invoked without args.
564    let path_str = if input.is_empty() {
565        "./migrations".to_string()
566    } else {
567        let lit: LitStr = syn::parse2(input)?;
568        lit.value()
569    };
570
571    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
572        syn::Error::new(
573            proc_macro2::Span::call_site(),
574            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
575        )
576    })?;
577    let abs = std::path::Path::new(&manifest).join(&path_str);
578
579    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
580    if abs.is_dir() {
581        let read = std::fs::read_dir(&abs).map_err(|e| {
582            syn::Error::new(
583                proc_macro2::Span::call_site(),
584                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
585            )
586        })?;
587        for entry in read.flatten() {
588            let path = entry.path();
589            if !path.is_file() {
590                continue;
591            }
592            if path.extension().and_then(|s| s.to_str()) != Some("json") {
593                continue;
594            }
595            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
596                continue;
597            };
598            entries.push((stem.to_owned(), path));
599        }
600    }
601    entries.sort_by(|a, b| a.0.cmp(&b.0));
602
603    // Compile-time chain validation: read each migration's JSON,
604    // pull `name` and `prev` (file-stem-keyed for the chain check),
605    // and verify every `prev` points to another migration in the
606    // slice. Mismatches between the file stem and the embedded
607    // `name` field — and broken `prev` chains — fail at MACRO
608    // EXPANSION time so a misshapen migration set never compiles.
609    //
610    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
611    // migrations + a Rust proc-macro that reads them is a combo no
612    // other Rust web framework matches (Cot's are imperative Rust
613    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
614    // a static chain to validate).
615    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
616    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
617    for (stem, path) in &entries {
618        let raw = std::fs::read_to_string(path).map_err(|e| {
619            syn::Error::new(
620                proc_macro2::Span::call_site(),
621                format!(
622                    "embed_migrations!: cannot read {} for chain validation: {e}",
623                    path.display()
624                ),
625            )
626        })?;
627        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
628            syn::Error::new(
629                proc_macro2::Span::call_site(),
630                format!(
631                    "embed_migrations!: {} is not valid JSON: {e}",
632                    path.display()
633                ),
634            )
635        })?;
636        let name = json
637            .get("name")
638            .and_then(|v| v.as_str())
639            .ok_or_else(|| {
640                syn::Error::new(
641                    proc_macro2::Span::call_site(),
642                    format!(
643                        "embed_migrations!: {} is missing the `name` field",
644                        path.display()
645                    ),
646                )
647            })?
648            .to_owned();
649        if name != *stem {
650            return Err(syn::Error::new(
651                proc_macro2::Span::call_site(),
652                format!(
653                    "embed_migrations!: file stem `{stem}` does not match the migration's \
654                     `name` field `{name}` — rename the file or fix the JSON",
655                ),
656            ));
657        }
658        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
659        chain_names.push(name.clone());
660        prev_refs.push((name, prev));
661    }
662
663    let name_set: std::collections::HashSet<&str> =
664        chain_names.iter().map(String::as_str).collect();
665    for (name, prev) in &prev_refs {
666        if let Some(p) = prev {
667            if !name_set.contains(p.as_str()) {
668                return Err(syn::Error::new(
669                    proc_macro2::Span::call_site(),
670                    format!(
671                        "embed_migrations!: broken migration chain — `{name}` declares \
672                         prev=`{p}` but no migration with that name exists in {}",
673                        abs.display()
674                    ),
675                ));
676            }
677        }
678    }
679
680    let pairs: Vec<TokenStream2> = entries
681        .iter()
682        .map(|(name, path)| {
683            let path_lit = path.display().to_string();
684            quote! { (#name, ::core::include_str!(#path_lit)) }
685        })
686        .collect();
687
688    // A wrong path used to embed nothing silently; a deprecation use is
689    // the only stable way for a proc macro to warn without breaking builds.
690    let missing_warning = if abs.is_dir() {
691        quote! {}
692    } else {
693        let note = format!(
694            "embed_migrations!: directory {} does not exist; no migrations are embedded",
695            abs.display()
696        );
697        quote! {
698            #[deprecated(note = #note)]
699            const __RUSTANGO_MIGRATIONS_DIR_MISSING: () = ();
700            #[allow(clippy::let_unit_value)]
701            let _ = __RUSTANGO_MIGRATIONS_DIR_MISSING;
702        }
703    };
704
705    Ok(quote! {
706        {
707            #missing_warning
708            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
709            __RUSTANGO_EMBEDDED
710        }
711    })
712}
713
714fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
715    let root = rustango_root();
716    let struct_name = &input.ident;
717
718    let Data::Struct(data) = &input.data else {
719        return Err(syn::Error::new_spanned(
720            struct_name,
721            "Model can only be derived on structs",
722        ));
723    };
724    let Fields::Named(named) = &data.fields else {
725        return Err(syn::Error::new_spanned(
726            struct_name,
727            "Model requires a struct with named fields",
728        ));
729    };
730
731    let container = parse_container_attrs(input)?;
732    let table = container
733        .table
734        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
735    let model_name = struct_name.to_string();
736
737    let collected = collect_fields(named, &table)?;
738
739    // Validate that #[rustango(display = "…")] names a real field.
740    if let Some((ref display, span)) = container.display {
741        if !collected.field_names.iter().any(|n| n == display) {
742            return Err(syn::Error::new(
743                span,
744                format!("`display = \"{display}\"` does not match any field on this struct"),
745            ));
746        }
747    }
748    let display = container.display.map(|(name, _)| name);
749    let app_label = container.app.clone();
750
751    // Validate admin field-name lists against declared field names.
752    // Note: `list_display` is intentionally NOT validated here. As of
753    // v0.32 it may also reference inventory-registered computed
754    // fields (via `register_admin_computed!`) whose existence the
755    // macro can't see at compile time — they're submitted from any
756    // crate that depends on rustango. The runtime list-view resolves
757    // unknown names against the inventory + silently drops the
758    // truly-bogus ones, which is the cheaper trade-off versus
759    // forcing a per-Model attr to opt out.
760    if let Some(admin) = &container.admin {
761        for (label, list) in [
762            ("search_fields", &admin.search_fields),
763            ("readonly_fields", &admin.readonly_fields),
764            ("list_filter", &admin.list_filter),
765        ] {
766            if let Some((names, span)) = list {
767                for name in names {
768                    if !collected.field_names.iter().any(|n| n == name) {
769                        return Err(syn::Error::new(
770                            *span,
771                            format!(
772                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
773                            ),
774                        ));
775                    }
776                }
777            }
778        }
779        if let Some((pairs, span)) = &admin.ordering {
780            for (name, _) in pairs {
781                if !collected.field_names.iter().any(|n| n == name) {
782                    return Err(syn::Error::new(
783                        *span,
784                        format!(
785                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
786                        ),
787                    ));
788                }
789            }
790        }
791        if let Some((groups, span)) = &admin.fieldsets {
792            for (_, fields) in groups {
793                for name in fields {
794                    if !collected.field_names.iter().any(|n| n == name) {
795                        return Err(syn::Error::new(
796                            *span,
797                            format!(
798                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
799                            ),
800                        ));
801                    }
802                }
803            }
804        }
805    }
806    if let Some(audit) = &container.audit {
807        if let Some((names, span)) = &audit.track {
808            for name in names {
809                if !collected.field_names.iter().any(|n| n == name) {
810                    return Err(syn::Error::new(
811                        *span,
812                        format!(
813                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
814                        ),
815                    ));
816                }
817            }
818        }
819    }
820
821    // Issue #291 / T2.5 — validate each `default_order` column name
822    // against the model's collected fields. Typos fail at macro-expand
823    // time, not at the database.
824    for (col, _desc, span) in &container.default_order {
825        if !collected.field_names.iter().any(|n| n == col) {
826            return Err(syn::Error::new(
827                *span,
828                format!(
829                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
830                ),
831            ));
832        }
833    }
834
835    // Build the audit_track list for ModelSchema: None when no audit attr,
836    // Some(empty) when audit present without track, Some(names) when explicit.
837    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
838        audit
839            .track
840            .as_ref()
841            .map(|(names, _)| names.clone())
842            .unwrap_or_default()
843    });
844
845    // The upsert conflict target: a container-level composite unique,
846    // never a field `index(unique)` or a partial index PG can't target.
847    let upsert_unique: Option<Vec<String>> = container
848        .indexes
849        .iter()
850        .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
851        .map(|i| i.columns.clone());
852
853    // Merge field-level indexes into the container's index list.
854    let mut all_indexes: Vec<IndexAttr> = container.indexes;
855    for field in &named.named {
856        let ident = field.ident.as_ref().expect("named");
857        // The same default column as `process_field` (#1937).
858        let col = ident_name(ident);
859        // Re-parse field attrs to check for index flag
860        if let Ok(fa) = parse_field_attrs(field) {
861            if fa.index {
862                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
863                let auto_name = capped_index_name(if fa.index_unique {
864                    format!("{table}_{col_name}_uq_idx")
865                } else {
866                    format!("{table}_{col_name}_idx")
867                });
868                all_indexes.push(IndexAttr {
869                    name: fa.index_name.or(Some(auto_name)),
870                    columns: vec![col_name],
871                    unique: fa.index_unique,
872                    method: fa.index_method,
873                    where_clause: None,
874                    include: Vec::new(),
875                });
876            }
877        }
878    }
879    // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
880    for field in &named.named {
881        let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
882            continue;
883        };
884        if det.kind != DetectedKind::String || fa.max_length.is_some() {
885            continue;
886        }
887        let col = fa
888            .column
889            .clone()
890            .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
891        // MySQL indexes every foreign-key column too.
892        let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
893        if fa.primary_key
894            || fa.unique
895            || relation
896            || all_indexes.iter().any(|i| i.columns.contains(&col))
897        {
898            return Err(syn::Error::new_spanned(
899                field,
900                "a primary-key, unique, indexed or foreign-key `String` needs \
901                 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
902            ));
903        }
904    }
905
906    let model_impl = model_impl_tokens(
907        struct_name,
908        &model_name,
909        &table,
910        display.as_deref(),
911        app_label.as_deref(),
912        container.admin.as_ref(),
913        &container.default_order,
914        &collected.field_schemas,
915        collected.soft_delete_column.as_deref(),
916        container.permissions,
917        audit_track_names.as_deref(),
918        &container.m2m,
919        &all_indexes,
920        &container.checks,
921        &container.excludes,
922        &container.composite_fks,
923        &container.generic_fks,
924        container.scope.as_deref(),
925        container.is_view,
926        container.verbose_name.as_deref(),
927        container.verbose_name_plural.as_deref(),
928        container.managed,
929        container.base_manager_name.as_deref(),
930        container.order_with_respect_to.as_deref(),
931        container.proxy,
932        &container.required_db_features,
933        container.required_db_vendor.as_deref(),
934        container.default_related_name.as_deref(),
935        container.db_table_comment.as_deref(),
936        container
937            .get_latest_by
938            .as_ref()
939            .map(|(c, d)| (c.as_str(), *d)),
940        &container.extra_permissions,
941        &container.default_permissions,
942        &container.global_scopes,
943        &container.reverse_has_relations,
944        &container.generic_has_relations,
945    );
946    let module_ident = column_module_ident(struct_name);
947    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
948    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
949        let track_set: Option<std::collections::HashSet<&str>> = audit
950            .track
951            .as_ref()
952            .map(|(names, _)| names.iter().map(String::as_str).collect());
953        collected
954            .column_entries
955            .iter()
956            .filter(|c| {
957                track_set
958                    .as_ref()
959                    .map_or(true, |s| s.contains(c.name.as_str()))
960            })
961            .collect()
962    });
963    let inherent_impl = inherent_impl_tokens(
964        struct_name,
965        &collected,
966        collected.primary_key.as_ref(),
967        &column_consts,
968        audited_fields.as_deref(),
969        upsert_unique.as_deref(),
970        &container.manager_fns,
971    );
972    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
973    let from_row_impl = from_row_impl_tokens(
974        struct_name,
975        &collected.from_row_inits,
976        &collected.from_row_inits_my,
977    );
978    let reverse_helpers = reverse_helper_tokens(
979        struct_name,
980        &collected.fk_relations,
981        container.default_related_name.as_deref(),
982    )?;
983    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
984    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
985    // Issue #817 — `#[rustango(through(...))]` accessors.
986    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
987    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
988    let reverse_has_accessors =
989        reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
990    let generic_fk_accessors = generic_fk_accessor_tokens(
991        struct_name,
992        &container.generic_fks,
993        &collected.column_entries,
994    );
995
996    Ok(quote! {
997        #model_impl
998        #inherent_impl
999        #from_row_impl
1000        #column_module
1001        #reverse_helpers
1002        #m2m_accessors
1003        #generic_m2m_accessors
1004        #through_accessors
1005        #reverse_has_accessors
1006        #generic_fk_accessors
1007
1008        #root::core::inventory::submit! {
1009            // `module_path!()` evaluates at the registration site,
1010            // so a Model declared in `crate::blog::models` records
1011            // `"<crate>::blog::models"` and `resolved_app_label()`
1012            // can infer "blog" without an explicit attribute.
1013            #root::core::ModelEntry::new(
1014                <#struct_name as #root::core::Model>::SCHEMA,
1015                ::core::module_path!(),
1016            )
1017            .with_audited(
1018                <#struct_name as #root::core::Model>::__rustango_audited_update,
1019                <#struct_name as #root::core::Model>::__rustango_audited_delete,
1020            )
1021            .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1022            .with_audited_update_record(
1023                <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1024            )
1025        }
1026    })
1027}
1028
1029/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1030/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1031fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1032    let root = rustango_root();
1033    let parent_ty = &rel.parent_type;
1034    let field_ident = &rel.field_ident;
1035    let null_fk = if rel.nullable {
1036        quote! {
1037            if #receiver.#field_ident.is_none() {
1038                return ::core::result::Result::Ok(false);
1039            }
1040        }
1041    } else {
1042        quote! {}
1043    };
1044    quote! {
1045        #null_fk
1046        #root::sql::__rustango_require_join(
1047            row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1048        )?;
1049    }
1050}
1051
1052/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1053/// reused, and the deeper chain stitched into it; re-decoding it would
1054/// drop what that chain loaded.
1055fn loaded_reuse_tokens(
1056    rel: &FkRelation,
1057    receiver: &TokenStream2,
1058    load: &TokenStream2,
1059) -> TokenStream2 {
1060    let root = rustango_root();
1061    let field_ident = &rel.field_ident;
1062    let pattern = if rel.nullable {
1063        quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1064    } else {
1065        quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1066    };
1067    quote! {
1068        if let #pattern = &mut #receiver.#field_ident {
1069            if let ::core::option::Option::Some(__r) = __rest {
1070                let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1071            }
1072            return ::core::result::Result::Ok(true);
1073        }
1074    }
1075}
1076
1077/// Store the decoded `_parent` on the FK field, keeping the key the row
1078/// holds: with `on = <unique column>` it is not the parent's PK.
1079fn loaded_assign_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1080    let root = rustango_root();
1081    let field_ident = &rel.field_ident;
1082    if rel.nullable {
1083        quote! {
1084            if let ::core::option::Option::Some(__fk) = &mut #receiver.#field_ident {
1085                let _pk = ::core::clone::Clone::clone(__fk.pk_ref());
1086                *__fk = #root::sql::ForeignKey::loaded(_pk, _parent);
1087            }
1088        }
1089    } else {
1090        quote! {
1091            let _pk = ::core::clone::Clone::clone(#receiver.#field_ident.pk_ref());
1092            #receiver.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1093        }
1094    }
1095}
1096
1097/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1098/// matches `field_name` against the model's FK fields and, for a
1099/// match, decodes the FK target via the parent's macro-generated
1100/// `__rustango_from_aliased_row`, and stores `ForeignKey::Loaded` on
1101/// `self` under the key the row already holds.
1102///
1103/// Always emitted (with empty arms for FK-less models, which
1104/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1105/// trait bound on `fetch_on` is universally satisfied — users
1106/// never have to think about implementing it.
1107fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1108    let root = rustango_root();
1109    let arms = fk_relations.iter().map(|rel| {
1110        let parent_ty = &rel.parent_type;
1111        let field_ident = &rel.field_ident;
1112        let fk_name = ident_name(field_ident);
1113        let fk_col = fk_name.as_str();
1114        let guard = join_guard_tokens(rel, &quote!(self));
1115        let reuse = loaded_reuse_tokens(
1116            rel,
1117            &quote!(self),
1118            &quote!(#root::sql::LoadRelated::__rustango_load_related),
1119        );
1120        let assign = loaded_assign_tokens(rel, &quote!(self));
1121        quote! {
1122            #fk_col => {
1123                #guard
1124                #reuse
1125                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1126                // Audit #451 — multi-hop `select_related("a__b__c")`:
1127                // stitch the deeper relation onto this parent first,
1128                // decoding it at the accumulated `__next_alias`. The
1129                // parent type also impls `LoadRelated`, so this recurses
1130                // the FK chain to arbitrary depth.
1131                if let ::core::option::Option::Some(__r) = __rest {
1132                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1133                        &mut _parent, row, __r, &__next_alias,
1134                    )?;
1135                }
1136                #assign
1137                ::core::result::Result::Ok(true)
1138            }
1139        }
1140    });
1141    quote! {
1142        #[cfg(feature = "postgres")]
1143        impl #root::sql::LoadRelated for #struct_name {
1144            #[allow(unused_variables)]
1145            fn __rustango_load_related(
1146                &mut self,
1147                row: &#root::sql::sqlx::postgres::PgRow,
1148                field_name: &str,
1149                alias: &str,
1150            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1151                // Audit #451 — split the multi-hop path: `base` is the FK
1152                // on THIS model, `__rest` (if any) is the remaining chain
1153                // to stitch onto the loaded parent. `__next_alias` is the
1154                // accumulated join alias the parent's columns live under
1155                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1156                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1157                    match field_name.split_once("__") {
1158                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1159                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1160                    };
1161                let __next_alias: ::std::string::String = match __rest {
1162                    ::core::option::Option::Some(__r) => {
1163                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1164                        ::std::format!("{}__{}", alias, __rb)
1165                    }
1166                    ::core::option::Option::None => ::std::string::String::new(),
1167                };
1168                match __base {
1169                    #( #arms )*
1170                    _ => ::core::result::Result::Ok(false),
1171                }
1172            }
1173        }
1174    }
1175}
1176
1177/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1178/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1179/// which expands to a `LoadRelatedMy` impl when rustango is built with
1180/// the `mysql` feature, and to nothing otherwise. The decoded parent
1181/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1182/// decoder, also batch8) so the dual emission is symmetric across
1183/// backends.
1184fn load_related_impl_my_tokens(
1185    struct_name: &syn::Ident,
1186    fk_relations: &[FkRelation],
1187) -> TokenStream2 {
1188    let root = rustango_root();
1189    let arms = fk_relations.iter().map(|rel| {
1190        let parent_ty = &rel.parent_type;
1191        let field_ident = &rel.field_ident;
1192        let fk_name = ident_name(field_ident);
1193        let fk_col = fk_name.as_str();
1194        let guard = join_guard_tokens(rel, &quote!(__self));
1195        let reuse = loaded_reuse_tokens(
1196            rel,
1197            &quote!(__self),
1198            &quote!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1199        );
1200        let assign = loaded_assign_tokens(rel, &quote!(__self));
1201        // `self` IS hygiene-tracked through macro_rules — emitted from
1202        // a different context than the `&mut self` parameter inside
1203        // the macro_rules-expanded fn. Pass it through as `__self`
1204        // and let the macro_rules rebind it to the receiver.
1205        quote! {
1206            #fk_col => {
1207                #guard
1208                #reuse
1209                let mut _parent: #parent_ty =
1210                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1211                // Audit #451 — multi-hop: stitch the deeper relation onto
1212                // the parent at the accumulated alias (see PG twin).
1213                if let ::core::option::Option::Some(__r) = __rest {
1214                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1215                        &mut _parent, row, __r, &__next_alias,
1216                    )?;
1217                }
1218                #assign
1219                ::core::result::Result::Ok(true)
1220            }
1221        }
1222    });
1223    quote! {
1224        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1225            #( #arms )*
1226        });
1227    }
1228}
1229
1230/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1231/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1232/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1233fn load_related_impl_sqlite_tokens(
1234    struct_name: &syn::Ident,
1235    fk_relations: &[FkRelation],
1236) -> TokenStream2 {
1237    let root = rustango_root();
1238    let arms = fk_relations.iter().map(|rel| {
1239        let parent_ty = &rel.parent_type;
1240        let field_ident = &rel.field_ident;
1241        let fk_name = ident_name(field_ident);
1242        let fk_col = fk_name.as_str();
1243        let guard = join_guard_tokens(rel, &quote!(__self));
1244        let reuse = loaded_reuse_tokens(
1245            rel,
1246            &quote!(__self),
1247            &quote!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1248        );
1249        let assign = loaded_assign_tokens(rel, &quote!(__self));
1250        quote! {
1251            #fk_col => {
1252                #guard
1253                #reuse
1254                let mut _parent: #parent_ty =
1255                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1256                // Audit #451 — multi-hop: stitch the deeper relation onto
1257                // the parent at the accumulated alias (see PG twin).
1258                if let ::core::option::Option::Some(__r) = __rest {
1259                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1260                        &mut _parent, row, __r, &__next_alias,
1261                    )?;
1262                }
1263                #assign
1264                ::core::result::Result::Ok(true)
1265            }
1266        }
1267    });
1268    quote! {
1269        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1270            #( #arms )*
1271        });
1272    }
1273}
1274
1275/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1276/// matches `field_name` against the model's FK fields and returns
1277/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1278/// group children by parent PK.
1279///
1280/// Always emitted (with `_ => None` for FK-less models) so the
1281/// trait bound on `fetch_with_prefetch` is universally satisfied.
1282fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1283    let root = rustango_root();
1284    let arms = fk_relations.iter().map(|rel| {
1285        let field_ident = &rel.field_ident;
1286        let fk_name = ident_name(field_ident);
1287        let fk_col = fk_name.as_str();
1288        if rel.pk_kind == DetectedKind::I64 {
1289            // i64 FK — return the stored PK so prefetch_related can
1290            // group children by it. Nullable variant unwraps via
1291            // `as_ref().map(...)`: an unset (NULL) FK column yields
1292            // `None` and that child sits out of the grouping (correct
1293            // semantics — it has no parent to attach to).
1294            if rel.nullable {
1295                quote! {
1296                    #fk_col => self.#field_ident
1297                        .as_ref()
1298                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1299                }
1300            } else {
1301                quote! {
1302                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1303                }
1304            }
1305        } else {
1306            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1307            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1308            // i64-keyed grouping path — the trait signature is
1309            // `Option<i64>` and a non-i64 PK can't lower into it.
1310            // The FK still works for everything else (CRUD, lazy
1311            // load via `.get()`, select_related JOINs); only the
1312            // bulk prefetch grouper needs the integer key.
1313            quote! {
1314                #fk_col => ::core::option::Option::None,
1315            }
1316        }
1317    });
1318    // PK-type-agnostic version: every FK arm emits an
1319    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1320    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1321    // opt OUT of the legacy i64 method (it returns None) but opt IN
1322    // here.
1323    let value_arms = fk_relations.iter().map(|rel| {
1324        let field_ident = &rel.field_ident;
1325        let fk_name = ident_name(field_ident);
1326        let fk_col = fk_name.as_str();
1327        if rel.nullable {
1328            quote! {
1329                #fk_col => self.#field_ident
1330                    .as_ref()
1331                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1332                        #root::sql::ForeignKey::pk(fk)
1333                    )),
1334            }
1335        } else {
1336            quote! {
1337                #fk_col => ::core::option::Option::Some(
1338                    ::core::convert::Into::<#root::core::SqlValue>::into(
1339                        self.#field_ident.pk()
1340                    )
1341                ),
1342            }
1343        }
1344    });
1345    quote! {
1346        impl #root::sql::FkPkAccess for #struct_name {
1347            #[allow(unused_variables)]
1348            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1349                match field_name {
1350                    #( #arms )*
1351                    _ => ::core::option::Option::None,
1352                }
1353            }
1354            #[allow(unused_variables)]
1355            fn __rustango_fk_pk_value(
1356                &self,
1357                field_name: &str,
1358            ) -> ::core::option::Option<#root::core::SqlValue> {
1359                match field_name {
1360                    #( #value_arms )*
1361                    _ => ::core::option::Option::None,
1362                }
1363            }
1364        }
1365    }
1366}
1367
1368/// `related_name = "+"`: the FK gets no reverse accessor on its parent.
1369const NO_REVERSE_ACCESSOR: &str = "+";
1370
1371/// For every `ForeignKey<Parent>` field on `Child`, emit
1372/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1373/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1374/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1375/// — the canonical reverse-FK fetch. One round trip, no N+1.
1376///
1377/// **PG-only emission**: the accessor is bounded on
1378/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1379/// both of which are gated behind the `postgres` cargo feature. The
1380/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1381/// model derive itself compiles on tri-dialect / sqlite-only
1382/// downstream builds — the accessor just isn't materialised. A tri-
1383/// dialect `_set_pool` variant is a separate follow-up.
1384fn reverse_helper_tokens(
1385    child_ident: &syn::Ident,
1386    fk_relations: &[FkRelation],
1387    default_related_name: Option<&str>,
1388) -> syn::Result<TokenStream2> {
1389    let root = rustango_root();
1390    // `related_name = "+"`: no accessor, so a parent from another crate works.
1391    let fk_relations: Vec<&FkRelation> = fk_relations
1392        .iter()
1393        .filter(|rel| rel.related_name.as_deref() != Some(NO_REVERSE_ACCESSOR))
1394        .collect();
1395    if fk_relations.is_empty() {
1396        return Ok(TokenStream2::new());
1397    }
1398    // Method-name resolution per FK (issue #816 + follow-up):
1399    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1400    //      itself — wins over everything else.
1401    //   2. Container-level `default_related_name = "..."` on the
1402    //      child. Applies to every FK on this model that didn't
1403    //      override.
1404    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1405    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1406    //      edge cases.
1407    //
1408    // The PG-on-executor variant keeps the resolved name; the
1409    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1410    // framework's convention for the `&Pool` flavor of every helper).
1411    let default_pg_suffix = default_related_name
1412        .map(str::to_owned)
1413        .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1414    let name_of = |rel: &FkRelation| {
1415        rel.related_name
1416            .clone()
1417            .unwrap_or_else(|| default_pg_suffix.clone())
1418    };
1419    // Two FKs to one parent would define the same method twice (E0592).
1420    for (i, rel) in fk_relations.iter().enumerate() {
1421        let parent_ty = &rel.parent_type;
1422        let parent = quote!(#parent_ty).to_string();
1423        let name = name_of(rel);
1424        if let Some(first) = fk_relations[..i].iter().find(|r| {
1425            let other = &r.parent_type;
1426            quote!(#other).to_string() == parent && name_of(r) == name
1427        }) {
1428            return Err(syn::Error::new_spanned(
1429                &rel.field_ident,
1430                format!(
1431                    "`{}` and `{}` both point at `{parent}` and would both add `{parent}::{name}`; \
1432                     give one `#[rustango(related_name = \"...\")]`, or `related_name = \"+\"` for none",
1433                    first.field_ident, rel.field_ident
1434                ),
1435            ));
1436        }
1437    }
1438    let impls = fk_relations.iter().map(|rel| {
1439        let pg_suffix = name_of(rel);
1440        let pool_suffix = format!("{}_pool", pg_suffix);
1441        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1442        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1443        let parent_ty = &rel.parent_type;
1444        let fk_name = ident_name(&rel.field_ident);
1445        let fk_col = fk_name.as_str();
1446        let doc = format!(
1447            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1448             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1449             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1450             further `{child_ident}::objects()` filters via direct queryset use."
1451        );
1452        let pool_doc = format!(
1453            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1454             [`#root::sql::Pool`] and dispatches per backend so the \
1455             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1456             Use this from framework code that holds a `&Pool` (admin, \
1457             tenancy resolver, viewset handlers); reach for the executor- \
1458             bound variant when you already have a typed `sqlx::Executor`."
1459        );
1460        quote! {
1461            #[cfg(feature = "postgres")]
1462            impl #parent_ty {
1463                #[doc = #doc]
1464                ///
1465                /// # Errors
1466                /// Returns [`#root::sql::ExecError`] for SQL-writing
1467                /// or driver failures.
1468                pub async fn #pg_method_ident<'_c, _E>(
1469                    &self,
1470                    _executor: _E,
1471                ) -> ::core::result::Result<
1472                    ::std::vec::Vec<#child_ident>,
1473                    #root::sql::ExecError,
1474                >
1475                where
1476                    _E: #root::sql::sqlx::Executor<
1477                        '_c,
1478                        Database = #root::sql::sqlx::Postgres,
1479                    >,
1480                {
1481                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1482                    #root::query::QuerySet::<#child_ident>::new()
1483                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1484                        .fetch_on(_executor)
1485                        .await
1486                }
1487            }
1488
1489            impl #parent_ty {
1490                #[doc = #pool_doc]
1491                ///
1492                /// # Errors
1493                /// Returns [`#root::sql::ExecError`] for SQL-writing
1494                /// or driver failures.
1495                pub async fn #pool_method_ident(
1496                    &self,
1497                    pool: &#root::sql::Pool,
1498                ) -> ::core::result::Result<
1499                    ::std::vec::Vec<#child_ident>,
1500                    #root::sql::ExecError,
1501                > {
1502                    use #root::sql::FetcherPool as _;
1503                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1504                    #root::query::QuerySet::<#child_ident>::new()
1505                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1506                        .fetch(pool)
1507                        .await
1508                }
1509            }
1510        }
1511    });
1512    Ok(quote! { #( #impls )* })
1513}
1514
1515/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1516/// relation declared on the model.
1517/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1518/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1519///
1520/// Closes #239 + #240 — `comment.content_object` /
1521/// `comment.content_object = post` ergonomics on top of the existing
1522/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1523///
1524/// `column_entries` is passed so we can resolve each `ct_column` /
1525/// `pk_column` SQL name back to its Rust field ident — the macro
1526/// only sees the column-side strings in the attribute, but the
1527/// emitted accessor needs to read the actual struct field.
1528fn generic_fk_accessor_tokens(
1529    struct_name: &syn::Ident,
1530    generic_fks: &[GenericFkAttr],
1531    column_entries: &[ColumnEntry],
1532) -> TokenStream2 {
1533    let root = rustango_root();
1534    if generic_fks.is_empty() {
1535        return TokenStream2::new();
1536    }
1537    let methods = generic_fks.iter().filter_map(|gfk| {
1538        // Resolve `ct_column` + `pk_column` to the struct's Rust
1539        // field idents. A typo (column name doesn't match any field)
1540        // emits no method for that registration — the user will see
1541        // the compiler reject the SCHEMA literal anyway, so there's
1542        // a clear error path without us double-reporting.
1543        let ct_ident = column_entries
1544            .iter()
1545            .find(|c| c.column == gfk.ct_column)
1546            .map(|c| c.ident.clone())?;
1547        let pk_ident = column_entries
1548            .iter()
1549            .find(|c| c.column == gfk.pk_column)
1550            .map(|c| c.ident.clone())?;
1551
1552        let accessor_ident =
1553            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1554        let setter_ident =
1555            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1556        let name_literal = gfk.name.as_str();
1557
1558        Some(quote! {
1559            #[doc = concat!(
1560                "Resolve the polymorphic `",
1561                #name_literal,
1562                "` relation. Reads `self.",
1563                stringify!(#ct_ident),
1564                "` + `self.",
1565                stringify!(#pk_ident),
1566                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1567                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1568                #name_literal,
1569                "\", ...))]`."
1570            )]
1571            pub async fn #accessor_ident(
1572                &self,
1573                pool: &#root::sql::Pool,
1574            ) -> ::core::result::Result<
1575                ::core::option::Option<#root::__serde_json::Value>,
1576                #root::sql::ExecError,
1577            > {
1578                let gfk = #root::contenttypes::GenericForeignKey::new(
1579                    self.#ct_ident as i64,
1580                    self.#pk_ident as i64,
1581                );
1582                gfk.get_object(pool).await
1583            }
1584
1585            #[doc = concat!(
1586                "Set the polymorphic `",
1587                #name_literal,
1588                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1589                stringify!(#ct_ident),
1590                "` and `self.",
1591                stringify!(#pk_ident),
1592                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1593                #name_literal,
1594                "\", ...))]`."
1595            )]
1596            pub async fn #setter_ident<T: #root::core::Model>(
1597                &mut self,
1598                pool: &#root::sql::Pool,
1599                target_pk: i64,
1600            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1601                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1602                    pool,
1603                    target_pk,
1604                ).await?;
1605                self.#ct_ident = gfk.content_type_id as _;
1606                self.#pk_ident = gfk.object_pk as _;
1607                ::core::result::Result::Ok(())
1608            }
1609        })
1610    });
1611    quote! {
1612        impl #struct_name {
1613            #( #methods )*
1614        }
1615    }
1616}
1617
1618fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1619    let root = rustango_root();
1620    if m2m_relations.is_empty() {
1621        return TokenStream2::new();
1622    }
1623    let methods = m2m_relations.iter().map(|rel| {
1624        let method_name = format!("{}_m2m", rel.name);
1625        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1626        let through = rel.through.as_str();
1627        let src_col = rel.src.as_str();
1628        let dst_col = rel.dst.as_str();
1629        quote! {
1630            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1631                #root::sql::M2MManager {
1632                    src_pk: self.__rustango_pk_value(),
1633                    through: #through,
1634                    src_col: #src_col,
1635                    dst_col: #dst_col,
1636                }
1637            }
1638        }
1639    });
1640    quote! {
1641        impl #struct_name {
1642            #( #methods )*
1643        }
1644    }
1645}
1646
1647/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1648/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1649fn generic_m2m_accessor_tokens(
1650    struct_name: &syn::Ident,
1651    relations: &[GenericM2MAttr],
1652) -> TokenStream2 {
1653    let root = rustango_root();
1654    if relations.is_empty() {
1655        return TokenStream2::new();
1656    }
1657    let methods = relations.iter().map(|rel| {
1658        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1659        let through = rel.through.as_str();
1660        let pk_col = rel.pk_column.as_str();
1661        let ct_col = rel.ct_column.as_str();
1662        let related_col = rel.related_column.as_str();
1663        quote! {
1664            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1665                #root::sql::GenericM2MManager {
1666                    src_pk: self.__rustango_pk_value(),
1667                    src_schema: <Self as #root::core::Model>::SCHEMA,
1668                    through: #through,
1669                    pk_col: #pk_col,
1670                    ct_col: #ct_col,
1671                    dst_col: #related_col,
1672                }
1673            }
1674        }
1675    });
1676    // Lets an owner delete find and clear its junction rows.
1677    let entries = relations.iter().map(|rel| {
1678        let through = rel.through.as_str();
1679        let pk_col = rel.pk_column.as_str();
1680        let ct_col = rel.ct_column.as_str();
1681        quote! {
1682            #root::core::inventory::submit! {
1683                #root::core::GenericM2MEntry::new(
1684                    <#struct_name as #root::core::Model>::SCHEMA,
1685                    #through,
1686                    #pk_col,
1687                    #ct_col,
1688                )
1689            }
1690        }
1691    });
1692    quote! {
1693        impl #struct_name {
1694            #( #methods )*
1695        }
1696        #( #entries )*
1697    }
1698}
1699
1700/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1701/// associated functions for each `#[rustango(reverse_has(...))]`
1702/// attribute. Issue #830.
1703///
1704/// The two emitted functions return ready-to-use `WhereExpr` nodes
1705/// that downstream callers drop into
1706/// `QuerySet::<Self>::where_raw(...)`:
1707///
1708/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1709///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1710///   `whereHas` parity (without the closure-style sub-predicate
1711///   refinement — that's a follow-up; users can layer additional
1712///   predicates by constructing the SelectQuery themselves and
1713///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1714/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1715///   `whereDoesntHave` parity.
1716///
1717/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1718/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1719/// machinery threads the outer-table reference through automatically
1720/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1721fn reverse_has_accessor_tokens(
1722    struct_name: &syn::Ident,
1723    reverse_has_relations: &[ReverseHasAttr],
1724) -> TokenStream2 {
1725    let root = rustango_root();
1726    if reverse_has_relations.is_empty() {
1727        return TokenStream2::new();
1728    }
1729    let methods = reverse_has_relations.iter().map(|rel| {
1730        let exists_name = format!("{}_exists_expr", rel.name);
1731        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1732        let count_name = format!("{}_count", rel.name);
1733        let fetch_name = format!("{}_fetch", rel.name);
1734        let first_name = format!("{}_first", rel.name);
1735        let pluck_name = format!("{}_pluck", rel.name);
1736        let accessor_name = rel.name.as_str();
1737        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1738        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1739        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1740        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1741        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1742        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1743        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1744        let child = &rel.child;
1745        let child_fk_column = rel.child_fk_column.as_str();
1746        let self_pk_column = rel.self_pk_column.as_str();
1747        let exists_doc = format!(
1748            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1749             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1750             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1751             filter to {struct_name}s with at least one matching child.",
1752        );
1753        let not_exists_doc = format!(
1754            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1755             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1756             <outer>.{self_pk_column})`. Drop into \
1757             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1758             {struct_name}s with **no** matching child.",
1759        );
1760        let count_doc = format!(
1761            "Eloquent `$model->{name}->count()` analog — returns \
1762             the number of `{child}` rows whose `{child_fk_column}` \
1763             matches this `{struct_name}` instance's primary key. \
1764             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1765             {child_fk_column} = <self.pk>`.",
1766            name = rel.name,
1767        );
1768        let accessor_doc = format!(
1769            "Eloquent `$model->{name}` accessor — returns a \
1770             `QuerySet<{child}>` filtered to rows whose \
1771             `{child_fk_column}` matches this `{struct_name}` \
1772             instance's primary key. **Chainable**: compose `.filter()` \
1773             / `.order_by()` / `.limit()` etc. on top, then call \
1774             `.fetch(&pool)` (the QuerySet trait method) when \
1775             done. For the simple \"fetch all\" hot path with no \
1776             further composition, prefer the bare-name \
1777             `{name}_fetch(&pool)` companion.",
1778            name = rel.name,
1779        );
1780        let fetch_doc = format!(
1781            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1782             over `{name}(&self).fetch(&pool)`. Use this when \
1783             you don't need further `.filter()` / `.order_by()` \
1784             composition; falls back to the chainable accessor when \
1785             you do. Avoids the `_pool` suffix on the most common \
1786             call-site shape.",
1787            name = rel.name,
1788        );
1789        quote! {
1790            #[doc = #accessor_doc]
1791            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1792                #root::query::QuerySet::<#child>::new()
1793                    .filter(#child_fk_column, self.__rustango_pk_value())
1794            }
1795
1796            #[doc = #fetch_doc]
1797            pub async fn #fetch_ident(
1798                &self,
1799                pool: &#root::sql::Pool,
1800            ) -> ::core::result::Result<
1801                ::std::vec::Vec<#child>,
1802                #root::sql::ExecError,
1803            > {
1804                use #root::sql::FetcherPool as _;
1805                self.#accessor_ident().fetch(pool).await
1806            }
1807
1808            /// Eloquent `$model->relation->first()` / `hasOne`
1809            /// semantics — bare-name shortcut over
1810            /// `self.<name>().first(&pool)`. Returns `None` when no
1811            /// child rows match. Useful when the relation is
1812            /// nominally many-to-one in shape but at most one row is
1813            /// expected (latest comment, primary tag, etc.).
1814            pub async fn #first_ident(
1815                &self,
1816                pool: &#root::sql::Pool,
1817            ) -> ::core::result::Result<
1818                ::core::option::Option<#child>,
1819                #root::sql::ExecError,
1820            > {
1821                self.#accessor_ident().first(pool).await
1822            }
1823
1824            /// Eloquent `$model->relation->pluck($col)` — project a
1825            /// single column from the child rows into a `Vec<U>`.
1826            /// Skips the typed `Child` decode, which is cheaper when
1827            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1828            pub async fn #pluck_ident<U>(
1829                &self,
1830                col: &'static str,
1831                pool: &#root::sql::Pool,
1832            ) -> ::core::result::Result<
1833                ::std::vec::Vec<U>,
1834                #root::sql::ExecError,
1835            >
1836            where
1837                U: #root::sql::FlatScalar,
1838            {
1839                self.#accessor_ident()
1840                    .values_list_flat(col)
1841                    .fetch::<U>(pool)
1842                    .await
1843            }
1844
1845            #[doc = #exists_doc]
1846            pub fn #exists_ident() -> #root::core::WhereExpr {
1847                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1848                let child_schema =
1849                    <#child as #root::core::Model>::SCHEMA;
1850                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1851                    lhs: Expr::Column(#child_fk_column),
1852                    op: Op::Eq,
1853                    rhs: Expr::OuterRef(#self_pk_column),
1854                });
1855                WhereExpr::Exists(::std::boxed::Box::new(inner))
1856            }
1857
1858            #[doc = #not_exists_doc]
1859            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1860                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1861                let child_schema =
1862                    <#child as #root::core::Model>::SCHEMA;
1863                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1864                    lhs: Expr::Column(#child_fk_column),
1865                    op: Op::Eq,
1866                    rhs: Expr::OuterRef(#self_pk_column),
1867                });
1868                WhereExpr::NotExists(::std::boxed::Box::new(inner))
1869            }
1870
1871            #[doc = #count_doc]
1872            pub async fn #count_ident(
1873                &self,
1874                pool: &#root::sql::Pool,
1875            ) -> ::core::result::Result<
1876                i64,
1877                #root::sql::ExecError,
1878            > {
1879                use #root::sql::CounterPool as _;
1880                #root::query::QuerySet::<#child>::new()
1881                    .filter(#child_fk_column, self.__rustango_pk_value())
1882                    .count(pool)
1883                    .await
1884            }
1885        }
1886    });
1887    quote! {
1888        impl #struct_name {
1889            #( #methods )*
1890        }
1891    }
1892}
1893
1894/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1895/// `#[rustango(through(...))]` attribute. Issue #817.
1896///
1897/// Each accessor builds a correlated subquery via
1898/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1899/// intermediate table, filters on the FK column pointing at this
1900/// model, and projects the intermediate PK. The outer queryset
1901/// filters the far model by its FK-to-intermediate column being in
1902/// that set.
1903///
1904/// The returned `QuerySet<Far>` is **chainable** — the subquery
1905/// lives inside a `where_raw` clause so the user's later
1906/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1907/// disturb it.
1908///
1909/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1910/// SQLite — no LATERAL or backend-specific syntax involved.
1911fn through_accessor_tokens(
1912    struct_name: &syn::Ident,
1913    through_relations: &[ThroughAttr],
1914) -> TokenStream2 {
1915    let root = rustango_root();
1916    if through_relations.is_empty() {
1917        return TokenStream2::new();
1918    }
1919    let methods = through_relations.iter().map(|rel| {
1920        let method_name = format!("{}_through", rel.name);
1921        let count_name = format!("{}_through_count", rel.name);
1922        let fetch_name = format!("{}_through_fetch", rel.name);
1923        let first_name = format!("{}_through_first", rel.name);
1924        let pluck_name = format!("{}_through_pluck", rel.name);
1925        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1926        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1927        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1928        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1929        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1930        let far = &rel.far;
1931        let intermediate = &rel.intermediate;
1932        let far_fk_column = rel.far_fk_column.as_str();
1933        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1934        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1935        let doc = format!(
1936            "Eloquent `hasManyThrough` accessor — returns a \
1937             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1938             instance through the intermediate `{intermediate}` table. \
1939             Generated SQL shape: \
1940             `… WHERE {far_fk_column} IN (SELECT \
1941             {intermediate_pk_column} FROM <{intermediate}> WHERE \
1942             {intermediate_fk_column} = self.pk)`. Chainable like any \
1943             other QuerySet — compose `.filter()` / `.order_by()` / \
1944             `.limit()` etc. on top.",
1945        );
1946        let count_doc = format!(
1947            "Eloquent `$model->{name}->count()` analog for the \
1948             through-relation — returns the number of `{far}` rows \
1949             reachable through `{intermediate}`. Equivalent to \
1950             `self.{name}_through().count(pool)` but spelled \
1951             as a bare instance method for parity with the \
1952             `reverse_has` `<name>_count` shape.",
1953            name = rel.name,
1954        );
1955        quote! {
1956            #[doc = #doc]
1957            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1958                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1959                let intermediate_schema =
1960                    <#intermediate as #root::core::Model>::SCHEMA;
1961                let sub = SelectQuery::new(intermediate_schema)
1962                    .where_clause(WhereExpr::Predicate(Filter::new(
1963                        #intermediate_fk_column,
1964                        Op::Eq,
1965                        self.__rustango_pk_value(),
1966                    )))
1967                    .projection(::std::vec![#intermediate_pk_column]);
1968                #root::query::QuerySet::<#far>::new().where_raw(
1969                    WhereExpr::InSubquery {
1970                        column: #far_fk_column,
1971                        negated: false,
1972                        subquery: ::std::boxed::Box::new(sub),
1973                    },
1974                )
1975            }
1976
1977            #[doc = #count_doc]
1978            pub async fn #count_ident(
1979                &self,
1980                pool: &#root::sql::Pool,
1981            ) -> ::core::result::Result<
1982                i64,
1983                #root::sql::ExecError,
1984            > {
1985                use #root::sql::CounterPool as _;
1986                self.#method_ident().count(pool).await
1987            }
1988
1989            /// Eloquent `$model->relation->get()` for the
1990            /// through-relation — bare-name hot-path over
1991            /// `self.<name>_through().fetch(&pool)`. Use this
1992            /// when you don't need further `.filter()` /
1993            /// `.order_by()` composition; falls back to the
1994            /// chainable accessor when you do. Avoids the `_pool`
1995            /// suffix on the most common call-site shape.
1996            pub async fn #fetch_ident(
1997                &self,
1998                pool: &#root::sql::Pool,
1999            ) -> ::core::result::Result<
2000                ::std::vec::Vec<#far>,
2001                #root::sql::ExecError,
2002            > {
2003                use #root::sql::FetcherPool as _;
2004                self.#method_ident().fetch(pool).await
2005            }
2006
2007            /// Eloquent `hasOneThrough` analog — bare-name shortcut
2008            /// over `self.<name>_through().first(&pool)`. Returns
2009            /// `None` when no far rows are reachable through the
2010            /// intermediate. Useful when at most one row is
2011            /// expected (latest comment by country, primary tag,
2012            /// etc.).
2013            pub async fn #first_ident(
2014                &self,
2015                pool: &#root::sql::Pool,
2016            ) -> ::core::result::Result<
2017                ::core::option::Option<#far>,
2018                #root::sql::ExecError,
2019            > {
2020                self.#method_ident().first(pool).await
2021            }
2022
2023            /// Pluck a single column from the far rows into `Vec<U>`
2024            /// — cheaper than the typed `<Far>` decode when only one
2025            /// scalar column is needed.
2026            pub async fn #pluck_ident<U>(
2027                &self,
2028                col: &'static str,
2029                pool: &#root::sql::Pool,
2030            ) -> ::core::result::Result<
2031                ::std::vec::Vec<U>,
2032                #root::sql::ExecError,
2033            >
2034            where
2035                U: #root::sql::FlatScalar,
2036            {
2037                self.#method_ident()
2038                    .values_list_flat(col)
2039                    .fetch::<U>(pool)
2040                    .await
2041            }
2042        }
2043    });
2044    quote! {
2045        impl #struct_name {
2046            #( #methods )*
2047        }
2048    }
2049}
2050
2051struct ColumnEntry {
2052    /// The struct field ident, used both for the inherent const name on
2053    /// the model and for the inner column type's name.
2054    ident: syn::Ident,
2055    /// The struct's field type, used as `Column::Value`.
2056    value_ty: Type,
2057    /// Rust-side field name (e.g. `"id"`).
2058    name: String,
2059    /// SQL-side column name (e.g. `"user_id"`).
2060    column: String,
2061    /// `#root::core::FieldType::I64` etc.
2062    field_type_tokens: TokenStream2,
2063    /// A bare or optional `Uuid` field; MySQL decodes it from text.
2064    uuid_column: bool,
2065}
2066
2067struct CollectedFields {
2068    field_schemas: Vec<TokenStream2>,
2069    from_row_inits: Vec<TokenStream2>,
2070    /// Aliased counterparts of `from_row_inits` — read columns via
2071    /// `format!("{prefix}__{col}")` aliases so a Model can be
2072    /// decoded from a JOINed row's projected target columns.
2073    from_aliased_row_inits: Vec<TokenStream2>,
2074    /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2075    from_row_inits_my: Vec<TokenStream2>,
2076    from_aliased_row_inits_my: Vec<TokenStream2>,
2077    /// Static column-name list — used by the simple insert path
2078    /// (no `Auto<T>` fields). Aligned with `insert_values`.
2079    insert_columns: Vec<TokenStream2>,
2080    /// Static `Into<SqlValue>` expressions, one per field. Aligned
2081    /// with `insert_columns`. Used by the simple insert path only.
2082    insert_values: Vec<TokenStream2>,
2083    /// Per-field push expressions for the dynamic (Auto-aware)
2084    /// insert path. Each statement either unconditionally pushes
2085    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2086    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2087    insert_pushes: Vec<TokenStream2>,
2088    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2089    /// when `has_auto == false`.
2090    returning_cols: Vec<TokenStream2>,
2091    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2092    /// field. Run after `insert_returning` to populate the model.
2093    auto_assigns: Vec<TokenStream2>,
2094    /// `(ident, column_literal, is_uuid)` for every Auto field. Used by
2095    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2096    /// instead of `self`.
2097    auto_field_idents: Vec<(syn::Ident, String, bool)>,
2098    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2099    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2100    /// back into the struct after insert (MySQL has no RETURNING → the
2101    /// field stays at its placeholder until the next read).
2102    generated_field_idents: Vec<(syn::Ident, String)>,
2103    /// First `Auto<T>` field the database fills, with its inner `T`: the
2104    /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2105    first_db_auto: Option<(syn::Ident, Type)>,
2106    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2107    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2108    /// `columns`).
2109    bulk_pushes_no_auto: Vec<TokenStream2>,
2110    /// Bulk-insert per-row pushes for **all fields including Auto**.
2111    /// Used by the all-Auto-Set bulk path (Auto col included with the
2112    /// caller-supplied value).
2113    bulk_pushes_all: Vec<TokenStream2>,
2114    /// Column-name literals for non-Auto fields only (paired with
2115    /// `bulk_pushes_no_auto`).
2116    bulk_columns_no_auto: Vec<TokenStream2>,
2117    /// Column-name literals for every field including Auto (paired
2118    /// with `bulk_pushes_all`).
2119    bulk_columns_all: Vec<TokenStream2>,
2120    /// Fills each `default_uuid_v7` field of `_row` in place, so a
2121    /// mutable bulk insert hands the PKs back (#1934).
2122    bulk_fill_in_place: Vec<TokenStream2>,
2123    /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2124    rust_filled_idents: Vec<syn::Ident>,
2125    /// `auto_now` fields: `upsert` stamps them before binding.
2126    auto_now_idents: Vec<syn::Ident>,
2127    /// `default_uuid_v7` fields: an `Unset` one gets a fresh id, so the
2128    /// upsert marks it `generated` for the MySQL key guard.
2129    uuid_v7_fields: Vec<(syn::Ident, String)>,
2130    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2131    /// + the loop that asserts every row matches. One pair per Auto
2132    /// field. Empty when `has_auto == false`.
2133    bulk_auto_uniformity: Vec<TokenStream2>,
2134    /// Identifier of the first Auto field, used as the witness for
2135    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2136    first_auto_ident: Option<syn::Ident>,
2137    /// `true` if any field on the struct is `Auto<T>`.
2138    has_auto: bool,
2139    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2140    /// Gates `save()` codegen — only Auto PKs let us infer
2141    /// insert-vs-update from the in-memory value.
2142    pk_is_auto: bool,
2143    /// `Assignment` constructors for every non-PK column. Drives the
2144    /// UPDATE branch of `save()`.
2145    update_assignments: Vec<TokenStream2>,
2146    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2147    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2148    upsert_update_columns: Vec<TokenStream2>,
2149    primary_key: Option<(syn::Ident, String)>,
2150    column_entries: Vec<ColumnEntry>,
2151    /// Rust-side field names, in declaration order. Used to validate
2152    /// container attributes like `display = "…"`.
2153    field_names: Vec<String>,
2154    /// FK fields on this child model. Drives the reverse-relation
2155    /// helper emit — for each FK, the macro adds an inherent
2156    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2157    /// method on the parent type.
2158    fk_relations: Vec<FkRelation>,
2159    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2160    /// the model has one. Drives emission of the `soft_delete_on` /
2161    /// `restore_on` inherent methods. At most one such column per
2162    /// model is allowed; collect_fields rejects duplicates.
2163    soft_delete_column: Option<String>,
2164    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2165    /// companion to `soft_delete_column` for emitting predicates
2166    /// that need to read the field off `&self` (e.g. `trashed()`).
2167    soft_delete_field_ident: Option<syn::Ident>,
2168}
2169
2170#[derive(Clone)]
2171struct FkRelation {
2172    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2173    /// helper is emitted as `impl <ParentType> { … }`.
2174    parent_type: Type,
2175    /// Rust field ident of the FK. Its name is the key for select_related,
2176    /// prefetch and filters; the SQL column may differ (#1936).
2177    field_ident: syn::Ident,
2178    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2179    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2180    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2181    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2182    pk_kind: DetectedKind,
2183    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2184    /// FK column). Drives the `Some(...)` wrapping in load_related
2185    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2186    /// the codegen matches the field's declared shape.
2187    nullable: bool,
2188    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2189    /// override. When set, the reverse helper picks this name instead
2190    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2191    /// #816 (issue's "Related" note re: per-FK override).
2192    related_name: Option<String>,
2193}
2194
2195fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2196    let root = rustango_root();
2197    let cap = named.named.len();
2198    let mut out = CollectedFields {
2199        field_schemas: Vec::with_capacity(cap),
2200        from_row_inits: Vec::with_capacity(cap),
2201        from_aliased_row_inits: Vec::with_capacity(cap),
2202        from_row_inits_my: Vec::with_capacity(cap),
2203        from_aliased_row_inits_my: Vec::with_capacity(cap),
2204        insert_columns: Vec::with_capacity(cap),
2205        insert_values: Vec::with_capacity(cap),
2206        insert_pushes: Vec::with_capacity(cap),
2207        returning_cols: Vec::new(),
2208        auto_assigns: Vec::new(),
2209        auto_field_idents: Vec::new(),
2210        generated_field_idents: Vec::new(),
2211        first_db_auto: None,
2212        bulk_pushes_no_auto: Vec::with_capacity(cap),
2213        bulk_pushes_all: Vec::with_capacity(cap),
2214        bulk_columns_no_auto: Vec::with_capacity(cap),
2215        bulk_columns_all: Vec::with_capacity(cap),
2216        bulk_fill_in_place: Vec::new(),
2217        rust_filled_idents: Vec::new(),
2218        auto_now_idents: Vec::new(),
2219        uuid_v7_fields: Vec::new(),
2220        bulk_auto_uniformity: Vec::new(),
2221        first_auto_ident: None,
2222        has_auto: false,
2223        pk_is_auto: false,
2224        update_assignments: Vec::with_capacity(cap),
2225        upsert_update_columns: Vec::with_capacity(cap),
2226        primary_key: None,
2227        column_entries: Vec::with_capacity(cap),
2228        field_names: Vec::with_capacity(cap),
2229        fk_relations: Vec::new(),
2230        soft_delete_column: None,
2231        soft_delete_field_ident: None,
2232    };
2233
2234    for field in &named.named {
2235        let info = process_field(field, table)?;
2236        out.field_names.push(ident_name(info.ident));
2237        out.field_schemas.push(info.schema);
2238        out.from_row_inits.push(info.from_row_init);
2239        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2240        out.from_row_inits_my.push(info.from_row_init_my);
2241        out.from_aliased_row_inits_my
2242            .push(info.from_aliased_row_init_my);
2243        if let Some(parent_ty) = info.fk_inner.clone() {
2244            out.fk_relations.push(FkRelation {
2245                parent_type: parent_ty,
2246                field_ident: info.ident.clone(),
2247                pk_kind: info.fk_pk_kind,
2248                nullable: info.nullable,
2249                related_name: info.related_name.clone(),
2250            });
2251        }
2252        if info.soft_delete {
2253            if out.soft_delete_column.is_some() {
2254                return Err(syn::Error::new_spanned(
2255                    field,
2256                    "only one field may be marked `#[rustango(soft_delete)]`",
2257                ));
2258            }
2259            out.soft_delete_column = Some(info.column.clone());
2260            out.soft_delete_field_ident = Some(info.ident.clone());
2261        }
2262        let column = info.column.as_str();
2263        let ident = info.ident;
2264        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2265        // skip every write path — Postgres recomputes the value
2266        // from EXPR. Push only the column-entry record (so typed
2267        // column constants still exist for filtering / projection)
2268        // and the schema literal (already pushed above) and move
2269        // on. No insert_columns/values, no insert_pushes, no
2270        // bulk_*, no update_assignments, no upsert_update_columns,
2271        // no returning_cols.
2272        if info.generated_as.is_some() {
2273            out.column_entries.push(ColumnEntry {
2274                ident: ident.clone(),
2275                value_ty: info.value_ty.clone(),
2276                name: ident_name(ident),
2277                column: info.column.clone(),
2278                field_type_tokens: info.field_type_tokens,
2279                uuid_column: info.uuid_column,
2280            });
2281            // #1028 — refresh the DB-computed value after insert on
2282            // RETURNING-capable backends. The column joins `returning_cols`
2283            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2284            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2285            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2286            // it keeps the placeholder until the next read.
2287            out.returning_cols.push(quote!(#column));
2288            out.generated_field_idents
2289                .push((ident.clone(), info.column.clone()));
2290            continue;
2291        }
2292        out.insert_columns.push(quote!(#column));
2293        out.insert_values.push(quote! {
2294            ::core::convert::Into::<#root::core::SqlValue>::into(
2295                ::core::clone::Clone::clone(&self.#ident)
2296            )
2297        });
2298        if info.auto {
2299            out.has_auto = true;
2300            if out.first_auto_ident.is_none() {
2301                out.first_auto_ident = Some(ident.clone());
2302            }
2303            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2304            // before binding, so the value is already in
2305            // `self.#ident` after the insert_push — RETURNING is
2306            // redundant. Skip adding this column to returning_cols /
2307            // auto_assigns / auto_field_idents to avoid (a) an
2308            // unnecessary RETURNING column on every dialect, and (b)
2309            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2310            // integer PK.
2311            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2312            // for the same reason: they are filled Rust-side below and
2313            // bound, so the value is already in `self` and RETURNING
2314            // would be a redundant column on every dialect.
2315            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2316                if out.first_db_auto.is_none() {
2317                    out.first_db_auto = auto_inner_type(info.value_ty)
2318                        .cloned()
2319                        .map(|ty| (ident.clone(), ty));
2320                }
2321                out.returning_cols.push(quote!(#column));
2322                out.auto_field_idents.push((
2323                    ident.clone(),
2324                    info.column.clone(),
2325                    info.fk_pk_kind == DetectedKind::Uuid,
2326                ));
2327                out.auto_assigns.push(quote! {
2328                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2329                });
2330            }
2331            if info.default_uuid_v7 {
2332                // Rust-side UUIDv7 generation (issue #823, Eloquent
2333                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2334                // and bind; Auto::Set → bind the user's value. The
2335                // column is ALWAYS present in the INSERT statement —
2336                // no RETURNING / no DB DEFAULT needed.
2337                out.insert_pushes.push(quote! {
2338                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2339                        self.#ident = #root::sql::Auto::Set(
2340                            #root::__uuid::Uuid::now_v7(),
2341                        );
2342                    }
2343                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2344                        _columns.push(#column);
2345                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2346                            ::core::clone::Clone::clone(_v)
2347                        ));
2348                    }
2349                });
2350            } else if info.auto_now_add || info.auto_now {
2351                // Rust-side write timestamp (#1464), mirroring
2352                // `default_uuid_v7` above. `auto_now` has always bound
2353                // `Utc::now()` on UPDATE but not on INSERT, so the
2354                // first write of an `updated_at` took the DB default
2355                // while every later one took the clock — the same
2356                // column, filled two different ways.
2357                //
2358                // The default is the problem. `CREATE TABLE IF NOT
2359                // EXISTS` leaves an upgraded table's
2360                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2361                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2362                // statement that changes it — so every insert relying
2363                // on that default wrote the legacy shape forever, while
2364                // the migrate sweep converted the rows around it. The
2365                // column went permanently mixed and `ORDER BY` inverted
2366                // the admin audit log (#1616 rework review,
2367                // correctness-001 / dialects-005 / security-002).
2368                //
2369                // Binding it here makes the stale default unreachable
2370                // rather than trying to migrate it: the timestamp comes
2371                // from the application, not the database.
2372                out.insert_pushes.push(quote! {
2373                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2374                        self.#ident = #root::sql::Auto::Set(
2375                            #root::__chrono::Utc::now(),
2376                        );
2377                    }
2378                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2379                        _columns.push(#column);
2380                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2381                            ::core::clone::Clone::clone(_v)
2382                        ));
2383                    }
2384                });
2385            } else {
2386                out.insert_pushes.push(quote! {
2387                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2388                        _columns.push(#column);
2389                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2390                            ::core::clone::Clone::clone(_v)
2391                        ));
2392                    }
2393                });
2394            }
2395            // …except the timestamp columns, which must appear in BOTH
2396            // paths (#1464). The path is chosen by the *first* Auto
2397            // field — in practice the PK — so a bulk insert of rows
2398            // with an unset PK took the no-auto branch and dropped the
2399            // timestamp column, falling back to the DB default that
2400            // this change exists to stop depending on.
2401            //
2402            // `rows` is borrowed immutably here, so this fills per row
2403            // at push time rather than writing back into the struct. An
2404            // explicitly-Set value is still honoured; only `Unset`
2405            // takes the clock.
2406            // `default_uuid_v7` likewise: its column has no DB default (#1934).
2407            if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2408                out.rust_filled_idents.push(ident.clone());
2409            }
2410            if info.auto_now {
2411                out.auto_now_idents.push(ident.clone());
2412            }
2413            if info.default_uuid_v7 {
2414                out.uuid_v7_fields
2415                    .push((ident.clone(), info.column.clone()));
2416            }
2417            let rust_fill = if info.default_uuid_v7 {
2418                Some(quote!(#root::__uuid::Uuid::now_v7()))
2419            } else if info.auto_now_add || info.auto_now {
2420                Some(quote!(#root::__chrono::Utc::now()))
2421            } else {
2422                None
2423            };
2424            // Bulk: Auto fields appear in the all-Set path. An unset filled
2425            // field takes its fill there too, not NULL (#1950).
2426            out.bulk_columns_all.push(quote!(#column));
2427            if let Some(fill) = &rust_fill {
2428                let push = quote! {
2429                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2430                        match &_row.#ident {
2431                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2432                            #root::sql::Auto::Unset => #fill,
2433                        }
2434                    ));
2435                };
2436                out.bulk_columns_no_auto.push(quote!(#column));
2437                out.bulk_pushes_no_auto.push(push.clone());
2438                out.bulk_pushes_all.push(push);
2439            } else {
2440                out.bulk_pushes_all.push(quote! {
2441                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2442                        ::core::clone::Clone::clone(&_row.#ident)
2443                    ));
2444                });
2445            }
2446            if info.default_uuid_v7 {
2447                out.bulk_fill_in_place.push(quote! {
2448                    if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2449                        _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2450                    }
2451                });
2452            }
2453            // Uniformity check: every row's Auto state must match the
2454            // first row's. Mixed Set/Unset within one bulk_insert is
2455            // rejected here so the column list stays consistent.
2456            // A filled field may be unset on any row, so it is exempt.
2457            let ident_clone = ident.clone();
2458            if rust_fill.is_none() {
2459                out.bulk_auto_uniformity.push(quote! {
2460                    for _r in rows.iter().skip(1) {
2461                        if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2462                            return ::core::result::Result::Err(
2463                                #root::sql::ExecError::Sql(
2464                                    #root::sql::SqlError::BulkAutoMixed
2465                                )
2466                            );
2467                        }
2468                    }
2469                });
2470            }
2471        } else {
2472            out.insert_pushes.push(quote! {
2473                _columns.push(#column);
2474                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2475                    ::core::clone::Clone::clone(&self.#ident)
2476                ));
2477            });
2478            // Bulk: non-Auto fields appear in BOTH paths.
2479            out.bulk_columns_no_auto.push(quote!(#column));
2480            out.bulk_columns_all.push(quote!(#column));
2481            let push_expr = quote! {
2482                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2483                    ::core::clone::Clone::clone(&_row.#ident)
2484                ));
2485            };
2486            out.bulk_pushes_no_auto.push(push_expr.clone());
2487            out.bulk_pushes_all.push(push_expr);
2488        }
2489        if info.primary_key {
2490            if out.primary_key.is_some() {
2491                return Err(syn::Error::new_spanned(
2492                    field,
2493                    "only one field may be marked `#[rustango(primary_key)]`",
2494                ));
2495            }
2496            out.primary_key = Some((ident.clone(), info.column.clone()));
2497            if info.auto {
2498                out.pk_is_auto = true;
2499            }
2500        } else if info.auto_now_add {
2501            // Immutable post-insert: skip from UPDATE entirely.
2502        } else if info.auto_now {
2503            // `auto_now` columns: bind `chrono::Utc::now()` on every
2504            // UPDATE, and write it back so `self` matches the row.
2505            out.update_assignments.push(quote! {
2506                #root::core::Assignment::new(
2507                    #column,
2508                    {
2509                        let _now = #root::__chrono::Utc::now();
2510                        self.#ident = #root::sql::Auto::Set(_now);
2511                        ::core::convert::Into::<#root::core::SqlValue>::into(_now)
2512                    },
2513                )
2514            });
2515            out.upsert_update_columns.push(quote!(#column));
2516        } else {
2517            out.update_assignments.push(quote! {
2518                #root::core::Assignment::new(
2519                    #column,
2520                    ::core::convert::Into::<#root::core::SqlValue>::into(
2521                        ::core::clone::Clone::clone(&self.#ident)
2522                    ),
2523                )
2524            });
2525            out.upsert_update_columns.push(quote!(#column));
2526        }
2527        out.column_entries.push(ColumnEntry {
2528            ident: ident.clone(),
2529            value_ty: info.value_ty.clone(),
2530            name: ident_name(ident),
2531            column: info.column.clone(),
2532            field_type_tokens: info.field_type_tokens,
2533            uuid_column: info.uuid_column,
2534        });
2535    }
2536    Ok(out)
2537}
2538
2539/// A derived index name within PG's 63 bytes (MySQL takes 64); a longer one
2540/// keeps a prefix plus a hash of the whole, so two long names stay distinct.
2541fn capped_index_name(name: String) -> String {
2542    const MAX: usize = 63;
2543    if name.len() <= MAX {
2544        return name;
2545    }
2546    // FNV-1a: stable across builds, unlike `DefaultHasher`.
2547    let hash = name.bytes().fold(0x811c_9dc5_u32, |h, b| {
2548        (h ^ u32::from(b)).wrapping_mul(0x0100_0193)
2549    });
2550    let mut cut = MAX - 9;
2551    while !name.is_char_boundary(cut) {
2552        cut -= 1;
2553    }
2554    format!("{}_{hash:08x}", &name[..cut])
2555}
2556
2557fn model_impl_tokens(
2558    struct_name: &syn::Ident,
2559    model_name: &str,
2560    table: &str,
2561    display: Option<&str>,
2562    app_label: Option<&str>,
2563    admin: Option<&AdminAttrs>,
2564    default_order: &[(String, bool, proc_macro2::Span)],
2565    field_schemas: &[TokenStream2],
2566    soft_delete_column: Option<&str>,
2567    permissions: bool,
2568    audit_track: Option<&[String]>,
2569    m2m_relations: &[M2MAttr],
2570    indexes: &[IndexAttr],
2571    checks: &[CheckAttr],
2572    excludes: &[ExcludeAttr],
2573    composite_fks: &[CompositeFkAttr],
2574    generic_fks: &[GenericFkAttr],
2575    scope: Option<&str>,
2576    is_view: bool,
2577    verbose_name: Option<&str>,
2578    verbose_name_plural: Option<&str>,
2579    managed: bool,
2580    base_manager_name: Option<&str>,
2581    order_with_respect_to: Option<&str>,
2582    proxy: bool,
2583    required_db_features: &[String],
2584    required_db_vendor: Option<&str>,
2585    default_related_name: Option<&str>,
2586    db_table_comment: Option<&str>,
2587    get_latest_by: Option<(&str, bool)>,
2588    extra_permissions: &[(String, String)],
2589    default_permissions: &[String],
2590    global_scopes: &[GlobalScopeAttr],
2591    reverse_has_relations: &[ReverseHasAttr],
2592    generic_has_relations: &[GenericHasAttr],
2593) -> TokenStream2 {
2594    let root = rustango_root();
2595    let display_tokens = if let Some(name) = display {
2596        quote!(::core::option::Option::Some(#name))
2597    } else {
2598        quote!(::core::option::Option::None)
2599    };
2600    let app_label_tokens = if let Some(name) = app_label {
2601        quote!(::core::option::Option::Some(#name))
2602    } else {
2603        quote!(::core::option::Option::None)
2604    };
2605    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2606        quote!(::core::option::Option::Some(#col))
2607    } else {
2608        quote!(::core::option::Option::None)
2609    };
2610    let audit_track_tokens = match audit_track {
2611        None => quote!(::core::option::Option::None),
2612        Some(names) => {
2613            let lits = names.iter().map(|n| n.as_str());
2614            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2615        }
2616    };
2617    let admin_tokens = admin_config_tokens(admin);
2618    // Default `tenant` so single-tenant projects (no `scope` attr
2619    // anywhere) keep the v0.24.x behavior. Container-attr parser
2620    // already validated the value is "registry" or "tenant".
2621    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2622        Some("registry") => quote!(#root::core::ModelScope::Registry),
2623        _ => quote!(#root::core::ModelScope::Tenant),
2624    };
2625    let verbose_name_tokens = optional_str(verbose_name);
2626    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2627    let base_manager_name_tokens = optional_str(base_manager_name);
2628    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2629    let required_db_features_lits: Vec<&str> =
2630        required_db_features.iter().map(String::as_str).collect();
2631    let required_db_vendor_tokens = optional_str(required_db_vendor);
2632    let default_related_name_tokens = optional_str(default_related_name);
2633    let db_table_comment_tokens = optional_str(db_table_comment);
2634    let get_latest_by_tokens = match get_latest_by {
2635        Some((col, desc)) => {
2636            quote!(::core::option::Option::Some((#col, #desc)))
2637        }
2638        None => quote!(::core::option::Option::None),
2639    };
2640    let extra_permission_tokens: Vec<_> = extra_permissions
2641        .iter()
2642        .map(|(c, l)| quote!((#c, #l)))
2643        .collect();
2644    let default_permission_tokens: Vec<_> = default_permissions
2645        .iter()
2646        .map(|action| quote!(#action))
2647        .collect();
2648    let indexes_tokens = indexes.iter().map(|idx| {
2649        // When no explicit `name = "..."` was given, derive a stable,
2650        // collision-free name from the table + columns
2651        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2652        // would clash the moment a model declares two unnamed indexes.
2653        let derived_name = idx
2654            .name
2655            .clone()
2656            .unwrap_or_else(|| capped_index_name(format!("{table}_{}_idx", idx.columns.join("_"))));
2657        let name = derived_name.as_str();
2658        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2659        let unique = idx.unique;
2660        // Map the parsed method string onto the IndexMethod enum
2661        // variant — kept at the codegen layer so the IR doesn't
2662        // carry the string form.
2663        let method_variant = match idx.method.as_str() {
2664            "gin" => quote!(#root::core::IndexMethod::Gin),
2665            "gist" => quote!(#root::core::IndexMethod::Gist),
2666            "brin" => quote!(#root::core::IndexMethod::Brin),
2667            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2668            "hash" => quote!(#root::core::IndexMethod::Hash),
2669            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2670            _ => quote!(#root::core::IndexMethod::BTree),
2671        };
2672        let where_clause = match &idx.where_clause {
2673            Some(s) => quote!(::core::option::Option::Some(#s)),
2674            None => quote!(::core::option::Option::None),
2675        };
2676        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2677        quote! {{
2678            let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2679            i.unique = #unique;
2680            i.method = #method_variant;
2681            i.where_clause = #where_clause;
2682            i.include = &[ #(#include_lits),* ];
2683            i
2684        }}
2685    });
2686    let checks_tokens = checks.iter().map(|c| {
2687        let name = c.name.as_str();
2688        let expr = c.expr.as_str();
2689        quote!(#root::core::CheckConstraint::new(#name, #expr))
2690    });
2691    let excludes_tokens = excludes.iter().map(|e| {
2692        let name = e.name.as_str();
2693        let using = e.using.as_str();
2694        let element_tokens = e.elements.iter().map(|(col, op)| {
2695            let col_s = col.as_str();
2696            let op_s = op.as_str();
2697            quote!((#col_s, #op_s))
2698        });
2699        let where_tokens = match e.where_clause.as_deref() {
2700            Some(w) => quote!(::core::option::Option::Some(#w)),
2701            None => quote!(::core::option::Option::None),
2702        };
2703        quote! {{
2704            let mut e = #root::core::ExclusionConstraint::new(
2705                #name,
2706                #using,
2707                &[ #(#element_tokens),* ],
2708            );
2709            e.where_clause = #where_tokens;
2710            e
2711        }}
2712    });
2713    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2714        let name = rel.name.as_str();
2715        let to = rel.to.as_str();
2716        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2717        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2718        quote! {
2719            #root::core::CompositeFkRelation::new(
2720                #name,
2721                #to,
2722                &[ #(#from_cols),* ],
2723                &[ #(#on_cols),* ],
2724            )
2725        }
2726    });
2727    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2728        let name = rel.name.as_str();
2729        let ct_col = rel.ct_column.as_str();
2730        let pk_col = rel.pk_column.as_str();
2731        quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2732    });
2733    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2734    // no `#[rustango(default_order = "...")]` attribute was supplied.
2735    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2736        let col_lit = col.as_str();
2737        quote! { (#col_lit, #desc) }
2738    });
2739
2740    // Issue #820 — `global_scopes` slice literal. Empty when no
2741    // `#[rustango(global_scope(...))]` attribute was supplied. The
2742    // `apply` path is re-emitted verbatim so it resolves in the
2743    // consumer's scope; the name is stored as a string literal.
2744    let global_scope_tokens = global_scopes.iter().map(|s| {
2745        let name = s.name.as_str();
2746        // The slice sits in a nested `const`, where `Self` does not resolve.
2747        let mut apply = s.apply.clone();
2748        if let Some(first) = apply.segments.first_mut() {
2749            if first.ident == "Self" {
2750                first.ident = struct_name.clone();
2751            }
2752        }
2753        quote!(#root::core::GlobalScope::new(#name, #apply))
2754    });
2755
2756    let m2m_tokens = m2m_relations.iter().map(|rel| {
2757        let name = rel.name.as_str();
2758        let to = rel.to.as_str();
2759        let through = rel.through.as_str();
2760        let src = rel.src.as_str();
2761        let dst = rel.dst.as_str();
2762        let auto_create = rel.auto_create;
2763        quote! {{
2764            let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2765            m.auto_create = #auto_create;
2766            m
2767        }}
2768    });
2769    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2770    // when the model declares `#[rustango(reverse_has(...))]`. Each
2771    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2772    // a const expression. Models without reverse_has fall through to
2773    // the trait's empty default — no override emitted.
2774    let reverse_relations_override = if reverse_has_relations.is_empty() {
2775        quote!()
2776    } else {
2777        let entries = reverse_has_relations.iter().map(|rel| {
2778            let name = rel.name.as_str();
2779            let child = &rel.child;
2780            let child_fk_column = rel.child_fk_column.as_str();
2781            let self_pk_column = rel.self_pk_column.as_str();
2782            quote! {
2783                #root::core::ReverseRelation::new(
2784                    #name,
2785                    <#child as #root::core::Model>::SCHEMA,
2786                    #child_fk_column,
2787                    #self_pk_column,
2788                )
2789            }
2790        });
2791        quote! {
2792            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2793                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2794                RELS
2795            }
2796        }
2797    };
2798    // Issue #830 — `Model::generic_reverse_relations()` override for
2799    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2800    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2801        quote!()
2802    } else {
2803        let entries = generic_has_relations.iter().map(|rel| {
2804            let name = rel.name.as_str();
2805            let child = &rel.child;
2806            let ct_column = rel.ct_column.as_str();
2807            let pk_column = rel.pk_column.as_str();
2808            let self_pk_column = rel.self_pk_column.as_str();
2809            quote! {
2810                #root::core::GenericReverseRelation::new(
2811                    #name,
2812                    <#child as #root::core::Model>::SCHEMA,
2813                    #ct_column,
2814                    #pk_column,
2815                    #self_pk_column,
2816                )
2817            }
2818        });
2819        quote! {
2820            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2821                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2822                RELS
2823            }
2824        }
2825    };
2826    let fields_slice = const_slice(
2827        &quote!(#root::core::FieldSchema),
2828        field_schemas.iter().cloned(),
2829    );
2830    let m2m_slice = const_slice(&quote!(#root::core::M2MRelation), m2m_tokens);
2831    let indexes_slice = const_slice(&quote!(#root::core::IndexSchema), indexes_tokens);
2832    let checks_slice = const_slice(&quote!(#root::core::CheckConstraint), checks_tokens);
2833    let excludes_slice = const_slice(&quote!(#root::core::ExclusionConstraint), excludes_tokens);
2834    let composite_fk_slice = const_slice(
2835        &quote!(#root::core::CompositeFkRelation),
2836        composite_fk_tokens,
2837    );
2838    let generic_fk_slice = const_slice(&quote!(#root::core::GenericRelation), generic_fk_tokens);
2839    let audited_update_override = if audit_track.is_some() {
2840        quote! {
2841            fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2842                ::core::option::Option::Some(Self::__rustango_update_audited)
2843            }
2844            fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2845                ::core::option::Option::Some(Self::__rustango_delete_audited)
2846            }
2847            fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2848                ::core::option::Option::Some(Self::__rustango_create_audited)
2849            }
2850            fn __rustango_audited_update_record(
2851            ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
2852                ::core::option::Option::Some(Self::__rustango_update_record_audited)
2853            }
2854        }
2855    } else {
2856        quote!()
2857    };
2858    let global_scope_slice = const_slice(&quote!(#root::core::GlobalScope), global_scope_tokens);
2859    quote! {
2860        impl #root::core::Model for #struct_name {
2861            const SCHEMA: &'static #root::core::ModelSchema = &{
2862                #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2863                    name: #model_name,
2864                    table: #table,
2865                    fields: #fields_slice,
2866                    display: #display_tokens,
2867                    app_label: #app_label_tokens,
2868                    admin: #admin_tokens,
2869                    soft_delete_column: #soft_delete_tokens,
2870                    permissions: #permissions,
2871                    audit_track: #audit_track_tokens,
2872                    m2m: #m2m_slice,
2873                    indexes: #indexes_slice,
2874                    check_constraints: #checks_slice,
2875                    exclusion_constraints: #excludes_slice,
2876                    composite_relations: #composite_fk_slice,
2877                    generic_relations: #generic_fk_slice,
2878                    scope: #scope_tokens,
2879                    default_order: &[ #(#default_order_tokens),* ],
2880                    is_view: #is_view,
2881                    verbose_name: #verbose_name_tokens,
2882                    verbose_name_plural: #verbose_name_plural_tokens,
2883                    managed: #managed,
2884                    base_manager_name: #base_manager_name_tokens,
2885                    order_with_respect_to: #order_with_respect_to_tokens,
2886                    proxy: #proxy,
2887                    required_db_features: &[ #(#required_db_features_lits),* ],
2888                    required_db_vendor: #required_db_vendor_tokens,
2889                    default_related_name: #default_related_name_tokens,
2890                    db_table_comment: #db_table_comment_tokens,
2891                    get_latest_by: #get_latest_by_tokens,
2892                    extra_permissions: &[ #(#extra_permission_tokens),* ],
2893                    default_permissions: &[ #(#default_permission_tokens),* ],
2894                    global_scopes: #global_scope_slice,
2895                })
2896            };
2897
2898            #reverse_relations_override
2899            #generic_reverse_relations_override
2900            #audited_update_override
2901        }
2902    }
2903}
2904
2905/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2906/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2907/// otherwise a static reference to a populated `AdminConfig`.
2908fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2909    let root = rustango_root();
2910    let Some(admin) = admin else {
2911        return quote!(::core::option::Option::None);
2912    };
2913
2914    let list_display = admin
2915        .list_display
2916        .as_ref()
2917        .map(|(v, _)| v.as_slice())
2918        .unwrap_or(&[]);
2919    let list_display_lits = list_display.iter().map(|s| s.as_str());
2920
2921    let search_fields = admin
2922        .search_fields
2923        .as_ref()
2924        .map(|(v, _)| v.as_slice())
2925        .unwrap_or(&[]);
2926    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2927
2928    let readonly_fields = admin
2929        .readonly_fields
2930        .as_ref()
2931        .map(|(v, _)| v.as_slice())
2932        .unwrap_or(&[]);
2933    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2934
2935    let list_filter = admin
2936        .list_filter
2937        .as_ref()
2938        .map(|(v, _)| v.as_slice())
2939        .unwrap_or(&[]);
2940    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2941
2942    let actions = admin
2943        .actions
2944        .as_ref()
2945        .map(|(v, _)| v.as_slice())
2946        .unwrap_or(&[]);
2947    let actions_lits = actions.iter().map(|s| s.as_str());
2948
2949    let fieldsets = admin
2950        .fieldsets
2951        .as_ref()
2952        .map(|(v, _)| v.as_slice())
2953        .unwrap_or(&[]);
2954    let fieldset_tokens = const_slice(
2955        &quote!(#root::core::Fieldset),
2956        fieldsets.iter().map(|(title, fields)| {
2957            let title = title.as_str();
2958            let field_lits = fields.iter().map(String::as_str);
2959            quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
2960        }),
2961    );
2962
2963    let list_display_links = admin
2964        .list_display_links
2965        .as_ref()
2966        .map(|(v, _)| v.as_slice())
2967        .unwrap_or(&[]);
2968    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2969
2970    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2971    let actions_on_top = admin.actions_on_top.unwrap_or(true);
2972    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2973    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2974
2975    let prepopulated = admin
2976        .prepopulated_fields
2977        .as_ref()
2978        .map(|(v, _)| v.as_slice())
2979        .unwrap_or(&[]);
2980    let prepopulated_tokens = const_slice(
2981        &quote!(#root::core::PrepopulatedField),
2982        prepopulated.iter().map(|(target, sources)| {
2983            let target = target.as_str();
2984            let source_lits = sources.iter().map(String::as_str);
2985            quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
2986        }),
2987    );
2988
2989    let raw_id_fields = admin
2990        .raw_id_fields
2991        .as_ref()
2992        .map(|(v, _)| v.as_slice())
2993        .unwrap_or(&[]);
2994    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2995
2996    let autocomplete_fields = admin
2997        .autocomplete_fields
2998        .as_ref()
2999        .map(|(v, _)| v.as_slice())
3000        .unwrap_or(&[]);
3001    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
3002
3003    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
3004    let list_select_related_tokens = match admin.list_select_related.as_deref() {
3005        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
3006        Some("none") => quote!(#root::core::ListSelectRelated::None),
3007        Some(raw) => {
3008            let names: Vec<&str> = raw
3009                .split(',')
3010                .map(str::trim)
3011                .filter(|s| !s.is_empty())
3012                .collect();
3013            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
3014        }
3015    };
3016
3017    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
3018    // a Vec<(String, String)>. Empty / unset → no overrides.
3019    let formfield_pairs: Vec<(&str, &str)> = admin
3020        .formfield_overrides
3021        .as_ref()
3022        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
3023        .unwrap_or_default();
3024    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
3025        let field = *field;
3026        let widget = *widget;
3027        quote!((#field, #widget))
3028    });
3029
3030    let list_per_page = admin.list_per_page.unwrap_or(0);
3031
3032    let ordering_pairs = admin
3033        .ordering
3034        .as_ref()
3035        .map(|(v, _)| v.as_slice())
3036        .unwrap_or(&[]);
3037    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3038        let name = name.as_str();
3039        let desc = *desc;
3040        quote!((#name, #desc))
3041    });
3042
3043    quote! {
3044        ::core::option::Option::Some({
3045            const ADMIN: &#root::core::AdminConfig = &{
3046                #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3047                    list_display: &[ #( #list_display_lits ),* ],
3048                    search_fields: &[ #( #search_fields_lits ),* ],
3049                    list_per_page: #list_per_page,
3050                    ordering: &[ #( #ordering_tokens ),* ],
3051                    readonly_fields: &[ #( #readonly_fields_lits ),* ],
3052                    list_filter: &[ #( #list_filter_lits ),* ],
3053                    actions: &[ #( #actions_lits ),* ],
3054                    fieldsets: #fieldset_tokens,
3055                    list_display_links: &[ #( #list_display_links_lits ),* ],
3056                    search_help_text: #search_help_text,
3057                    actions_on_top: #actions_on_top,
3058                    actions_on_bottom: #actions_on_bottom,
3059                    date_hierarchy: #date_hierarchy,
3060                    prepopulated_fields: #prepopulated_tokens,
3061                    raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3062                    autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3063                    list_select_related: #list_select_related_tokens,
3064                    formfield_overrides: &[ #( #formfield_tokens ),* ],
3065                })
3066            };
3067            ADMIN
3068        })
3069    }
3070}
3071
3072fn inherent_impl_tokens(
3073    struct_name: &syn::Ident,
3074    fields: &CollectedFields,
3075    primary_key: Option<&(syn::Ident, String)>,
3076    column_consts: &TokenStream2,
3077    audited_fields: Option<&[&ColumnEntry]>,
3078    upsert_unique: Option<&[String]>,
3079    manager_fns: &[syn::Ident],
3080) -> TokenStream2 {
3081    let root = rustango_root();
3082    // Audit-emit fragments threaded into write paths. Non-empty only
3083    // when the model carries `#[rustango(audit(...))]`. They reborrow
3084    // `_executor` (a `&mut PgConnection` for audited models — the
3085    // macro switches the signature below) so the data write and the
3086    // audit INSERT both run on the same caller-supplied connection.
3087    let executor_passes_to_data_write = if audited_fields.is_some() {
3088        quote!(&mut *_executor)
3089    } else {
3090        quote!(_executor)
3091    };
3092    let executor_param = if audited_fields.is_some() {
3093        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
3094    } else {
3095        quote!(_executor: _E)
3096    };
3097    let executor_generics = if audited_fields.is_some() {
3098        quote!()
3099    } else {
3100        quote!(<'_c, _E>)
3101    };
3102    let executor_where = if audited_fields.is_some() {
3103        quote!()
3104    } else {
3105        quote! {
3106            where
3107                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
3108        }
3109    };
3110    // Audited `_on` methods take `&mut PgConnection`; `&PgPool` is an Executor for the rest.
3111    // One transaction: the before-image's FOR UPDATE must hold through
3112    // the UPDATE and its audit row.
3113    let pool_to_save_on = if audited_fields.is_some() {
3114        // Any failure, COMMIT included, rolls the INSERT back: put the pk and
3115        // stamps back too, or a retry takes the UPDATE path and writes nothing.
3116        let restored: Vec<&syn::Ident> = primary_key
3117            .map(|(pk, _)| pk)
3118            .into_iter()
3119            .chain(&fields.auto_now_idents)
3120            .collect();
3121        let prev: Vec<syn::Ident> = (0..restored.len())
3122            .map(|i| quote::format_ident!("__rustango_save_prev_{}", i))
3123            .collect();
3124        quote! {
3125            #( let #prev = ::core::clone::Clone::clone(&self.#restored); )*
3126            let _saved: ::core::result::Result<u64, #root::sql::ExecError> = async {
3127                let mut _tx = pool.begin().await?;
3128                let _affected = self.save_on(&mut *_tx).await?;
3129                _tx.commit().await?;
3130                ::core::result::Result::Ok(_affected)
3131            }
3132            .await;
3133            if _saved.is_err() {
3134                #( self.#restored = #prev; )*
3135            }
3136            _saved
3137        }
3138    } else {
3139        quote!(self.save_on(pool).await)
3140    };
3141    // One transaction so the write and its audit row commit together.
3142    let pool_to_insert_on = if audited_fields.is_some() {
3143        // A failed COMMIT leaves the RETURNING values on `self`; put them back.
3144        let assigned: Vec<syn::Ident> = fields
3145            .auto_field_idents
3146            .iter()
3147            .map(|(ident, _, _)| ident.clone())
3148            .collect();
3149        restore_auto_now_unless(
3150            &assigned,
3151            &quote!(::core::result::Result<(), #root::sql::ExecError>),
3152            &quote!(_saved.is_ok()),
3153            &quote! {
3154                let mut _tx = pool.begin().await?;
3155                self.insert_on(&mut *_tx).await?;
3156                _tx.commit().await?;
3157                ::core::result::Result::Ok(())
3158            },
3159        )
3160    } else {
3161        quote!(self.insert_on(pool).await)
3162    };
3163    let pool_to_delete_on = if audited_fields.is_some() {
3164        quote! {
3165            let mut _tx = pool.begin().await?;
3166            let _affected = self.delete_on(&mut *_tx).await?;
3167            _tx.commit().await?;
3168            ::core::result::Result::Ok(_affected)
3169        }
3170    } else {
3171        quote!(self.delete_on(pool).await)
3172    };
3173    let pool_to_bulk_insert_on = if audited_fields.is_some() {
3174        quote! {
3175            let mut _tx = pool.begin().await?;
3176            Self::bulk_insert_on(rows, &mut *_tx).await?;
3177            _tx.commit().await?;
3178            ::core::result::Result::Ok(())
3179        }
3180    } else {
3181        quote!(Self::bulk_insert_on(rows, pool).await)
3182    };
3183    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
3184    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
3185    // directly, but `upsert_on` for audited models takes
3186    // `&mut PgConnection` (the audit emit needs a real connection).
3187    // Add the missing acquire shim to keep audited Auto-PK upsert
3188    // compiling.
3189    let pool_to_upsert_on = if audited_fields.is_some() {
3190        quote! {
3191            let mut _tx = pool.begin().await?;
3192            self.upsert_on(&mut *_tx).await?;
3193            _tx.commit().await?;
3194            ::core::result::Result::Ok(())
3195        }
3196    } else {
3197        quote!(self.upsert_on(pool).await)
3198    };
3199
3200    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3201    // (audit-on-connection over &Pool needs a bi-dialect transaction
3202    // helper, deferred). Two body shapes:
3203    // - has_auto: build InsertQuery skipping Auto::Unset columns,
3204    //   request Auto cols in `returning`, dispatch via
3205    //   `insert_returning_pool`, then on the returned `PgRow` /
3206    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
3207    //   row OR drop the single i64 into the first Auto field on MySQL
3208    //   (multi-Auto models on MySQL error at runtime since
3209    //   `LAST_INSERT_ID()` only reports one)
3210    // - non-Auto: build InsertQuery with explicit columns/values and
3211    //   call `insert_pool` (no returning needed)
3212    // pool_insert_method body for the audited Auto-PK case is moved
3213    // to after audit_pair_tokens / audit_pk_to_string (they live
3214    // ~150 lines below). This block keeps the non-audited and
3215    // non-Auto branches in place — the audited Auto-PK arm is
3216    // computed below and merged via the dispatch helper variable.
3217    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3218        // Audited models with explicit (non-Auto) PKs go through
3219        // the non-Auto insert path below — the audit emit is one
3220        // round-trip after the INSERT inside the same tx via
3221        // audit::save_one_with_audit? No, INSERT semantics
3222        // differ. For non-Auto PK + audited, route through a
3223        // dedicated insert + audit emit on the same tx, but defer
3224        // the macro emission to the audit-bundle-aware block below
3225        // — this `quote!()` placeholder gets overwritten there.
3226        quote!()
3227    } else if audited_fields.is_some() && fields.has_auto {
3228        // Audited Auto-PK insert_pool — assembled after the audit
3229        // bundles. Placeholder; real emission below.
3230        quote!()
3231    } else if fields.has_auto {
3232        let pushes = &fields.insert_pushes;
3233        let returning_cols = &fields.returning_cols;
3234        // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3235        let filled = &fields.rust_filled_idents;
3236        let was_unset: Vec<syn::Ident> = filled
3237            .iter()
3238            .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3239            .collect();
3240        let insert_or_ignore_call = quote! {
3241            #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3242            let mut _columns: ::std::vec::Vec<&'static str> =
3243                ::std::vec::Vec::new();
3244            let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3245                ::std::vec::Vec::new();
3246            #( #pushes )*
3247            let _query = #root::core::InsertQuery::new(
3248                <Self as #root::core::Model>::SCHEMA,
3249                _columns,
3250                _values,
3251            );
3252            let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3253            if !matches!(_result, ::core::result::Result::Ok(true)) {
3254                #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3255            }
3256            _result
3257        };
3258        // When every `Auto<T>` field is filled Rust-side
3259        // (`default_uuid_v7`, issue #823), there is no column to read
3260        // back from the database — `returning_cols` is empty. Route
3261        // through plain `insert_pool` instead of
3262        // `insert_returning_pool` to skip the redundant RETURNING /
3263        // LAST_INSERT_ID round-trip.
3264        if fields.returning_cols.is_empty() {
3265            quote! {
3266                /// Insert this row against either backend. Every
3267                /// `Auto<T>` PK on this model is filled Rust-side
3268                /// (e.g. `default_uuid_v7`) before binding, so no
3269                /// RETURNING round-trip is needed.
3270                ///
3271                /// # Errors
3272                /// As [`Self::insert`].
3273                pub async fn insert_pool(
3274                    &mut self,
3275                    pool: &#root::sql::Pool,
3276                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3277                    let mut _columns: ::std::vec::Vec<&'static str> =
3278                        ::std::vec::Vec::new();
3279                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3280                        ::std::vec::Vec::new();
3281                    #( #pushes )*
3282                    let _query = #root::core::InsertQuery::new(
3283                        <Self as #root::core::Model>::SCHEMA,
3284                        _columns,
3285                        _values,
3286                    );
3287                    #root::sql::insert_pool(pool, &_query).await
3288                }
3289
3290                /// Eloquent `Model::insertOrIgnore()` — same shape
3291                /// as the auto-PK branch above. Returns `Ok(true)`
3292                /// when inserted, `Ok(false)` when a conflict caused
3293                /// the INSERT to silently skip.
3294                ///
3295                /// # Errors
3296                /// As [`Self::insert`].
3297                pub async fn insert_or_ignore(
3298                    &mut self,
3299                    pool: &#root::sql::Pool,
3300                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3301                    #insert_or_ignore_call
3302                }
3303            }
3304        } else {
3305            quote! {
3306                /// Insert this row against either backend, populating any
3307                /// `Auto<T>` PK from the auto-assigned value.
3308                ///
3309                /// # Errors
3310                /// As [`Self::insert`].
3311                pub async fn insert_pool(
3312                    &mut self,
3313                    pool: &#root::sql::Pool,
3314                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3315                    let mut _columns: ::std::vec::Vec<&'static str> =
3316                        ::std::vec::Vec::new();
3317                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3318                        ::std::vec::Vec::new();
3319                    #( #pushes )*
3320                    let _query = #root::core::InsertQuery::new(
3321                        <Self as #root::core::Model>::SCHEMA,
3322                        _columns,
3323                        _values,
3324                    )
3325                    .returning(::std::vec![ #( #returning_cols ),* ]);
3326                    let _result = #root::sql::insert_returning_pool(
3327                        pool, &_query,
3328                    ).await?;
3329                    #root::sql::apply_auto_pk(_result, self)
3330                }
3331
3332                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3333                /// row or silently skip on unique-constraint
3334                /// violation. Returns `Ok(true)` when a row was
3335                /// inserted, `Ok(false)` when a conflict caused the
3336                /// INSERT to silently skip.
3337                ///
3338                /// **Caveat on auto-PK models**: when the row is
3339                /// skipped (conflict), this instance's `Auto<T>`
3340                /// fields stay `Unset` — no PK is back-populated
3341                /// because the server didn't auto-assign one. For
3342                /// "insert then read back the PK or the existing
3343                /// row's PK", use the `upsert` family or
3344                /// `get_or_create`.
3345                ///
3346                /// # Errors
3347                /// As [`Self::insert`].
3348                pub async fn insert_or_ignore(
3349                    &mut self,
3350                    pool: &#root::sql::Pool,
3351                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3352                    #insert_or_ignore_call
3353                }
3354            }
3355        }
3356    } else {
3357        let insert_columns = &fields.insert_columns;
3358        let insert_values = &fields.insert_values;
3359        quote! {
3360            /// Insert this row into its table against either backend.
3361            /// Equivalent to [`Self::insert`] but takes
3362            /// [`#root::sql::Pool`].
3363            ///
3364            /// # Errors
3365            /// As [`Self::insert`].
3366            pub async fn insert_pool(
3367                &self,
3368                pool: &#root::sql::Pool,
3369            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3370                let _query = #root::core::InsertQuery::new(
3371                    <Self as #root::core::Model>::SCHEMA,
3372                    ::std::vec![ #( #insert_columns ),* ],
3373                    ::std::vec![ #( #insert_values ),* ],
3374                );
3375                #root::sql::insert_pool(pool, &_query).await
3376            }
3377
3378            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3379            /// or silently skip on unique-constraint violation. Maps
3380            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3381            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3382            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3383            /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3384            ///
3385            /// Returns `Ok(true)` when a row was inserted,
3386            /// `Ok(false)` when a conflict caused the INSERT to
3387            /// silently skip.
3388            ///
3389            /// Use for "create-if-absent" patterns where you don't
3390            /// need the row back. For "find-or-create with the row
3391            /// returned", use the queryset-level
3392            /// `crate::sql::get_or_create` free function.
3393            ///
3394            /// # Errors
3395            /// As [`Self::insert`], plus any dialect-specific
3396            /// translation error from the ConflictClause writer.
3397            pub async fn insert_or_ignore(
3398                &self,
3399                pool: &#root::sql::Pool,
3400            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3401                let _query = #root::core::InsertQuery::new(
3402                    <Self as #root::core::Model>::SCHEMA,
3403                    ::std::vec![ #( #insert_columns ),* ],
3404                    ::std::vec![ #( #insert_values ),* ],
3405                );
3406                #root::sql::insert_or_ignore(pool, &_query).await
3407            }
3408        }
3409    };
3410
3411    // The tracked (column, JSON value) pairs, spliced only into
3412    // `__rustango_audit_pairs`. Empty when the model isn't audited.
3413    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3414        .map(|tracked| {
3415            tracked
3416                .iter()
3417                .map(|c| {
3418                    let column_lit = c.column.as_str();
3419                    let ident = &c.ident;
3420                    quote! {
3421                        (
3422                            #column_lit,
3423                            #root::core::__json_value(&self.#ident),
3424                        )
3425                    }
3426                })
3427                .collect()
3428        })
3429        .unwrap_or_default();
3430    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3431        if fields.pk_is_auto {
3432            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3433        } else {
3434            quote!(::std::format!("{}", &self.#pk_ident))
3435        }
3436    } else {
3437        quote!(::std::string::String::new())
3438    };
3439    // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3440    // is built, so every audited write path records the same shape.
3441    let delete_audited_body = if primary_key.is_some() {
3442        quote! {
3443            ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3444                pool,
3445                query,
3446                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3447            ))
3448        }
3449    } else {
3450        quote! {
3451            let _ = (pool, query);
3452            ::std::boxed::Box::pin(async {
3453                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3454                    table: <Self as #root::core::Model>::SCHEMA.table,
3455                })
3456            })
3457        }
3458    };
3459    let update_audited_body = if primary_key.is_some() {
3460        quote! {
3461            ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3462                pool,
3463                query,
3464                move |_r: &Self| _r.__rustango_audit_entry(op),
3465            ))
3466        }
3467    } else {
3468        quote! {
3469            let _ = (pool, query, op);
3470            ::std::boxed::Box::pin(async {
3471                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3472                    table: <Self as #root::core::Model>::SCHEMA.table,
3473                })
3474            })
3475        }
3476    };
3477    let audit_entry_method = if audited_fields.is_some() {
3478        let pairs = audit_pair_tokens.iter();
3479        let pk_str = audit_pk_to_string.clone();
3480        quote! {
3481            /// This row's tracked `(column, value)` pairs.
3482            #[doc(hidden)]
3483            pub fn __rustango_audit_pairs(
3484                &self,
3485            ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3486                ::std::vec![ #( #pairs ),* ]
3487            }
3488
3489            /// This row's audit PK.
3490            #[doc(hidden)]
3491            pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3492                #pk_str
3493            }
3494
3495            /// Snapshot audit entry for `operation` on this row.
3496            #[doc(hidden)]
3497            pub fn __rustango_audit_entry(
3498                &self,
3499                operation: #root::audit::AuditOp,
3500            ) -> #root::audit::PendingEntry {
3501                #root::audit::PendingEntry {
3502                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3503                    entity_pk: self.__rustango_audit_pk(),
3504                    operation,
3505                    source: #root::audit::current_source(),
3506                    changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3507                }
3508            }
3509
3510            /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3511            #[doc(hidden)]
3512            pub fn __rustango_update_audited<'a>(
3513                pool: &'a #root::sql::Pool,
3514                query: &'a #root::core::UpdateQuery,
3515                op: #root::audit::AuditOp,
3516            ) -> ::std::pin::Pin<::std::boxed::Box<
3517                dyn ::core::future::Future<
3518                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3519                > + ::core::marker::Send + 'a,
3520            >> {
3521                #update_audited_body
3522            }
3523
3524            /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3525            #[doc(hidden)]
3526            pub fn __rustango_create_audited<'a, 't>(
3527                tx: &'a mut #root::sql::PoolTx<'t>,
3528                pk: #root::core::SqlValue,
3529            ) -> ::std::pin::Pin<::std::boxed::Box<
3530                dyn ::core::future::Future<
3531                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3532                > + ::core::marker::Send + 'a,
3533            >> {
3534                ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3535                    tx,
3536                    <Self as #root::core::Model>::SCHEMA,
3537                    pk,
3538                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3539                ))
3540            }
3541
3542            /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3543            #[doc(hidden)]
3544            pub fn __rustango_update_record_audited<'a, 't>(
3545                tx: &'a mut #root::sql::PoolTx<'t>,
3546                pk: #root::core::SqlValue,
3547            ) -> ::std::pin::Pin<::std::boxed::Box<
3548                dyn ::core::future::Future<
3549                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3550                > + ::core::marker::Send + 'a,
3551            >> {
3552                ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3553                    tx,
3554                    <Self as #root::core::Model>::SCHEMA,
3555                    pk,
3556                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3557                ))
3558            }
3559
3560            /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3561            #[doc(hidden)]
3562            pub fn __rustango_delete_audited<'a>(
3563                pool: &'a #root::sql::Pool,
3564                query: &'a #root::core::DeleteQuery,
3565            ) -> ::std::pin::Pin<::std::boxed::Box<
3566                dyn ::core::future::Future<
3567                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3568                > + ::core::marker::Send + 'a,
3569            >> {
3570                #delete_audited_body
3571            }
3572        }
3573    } else {
3574        quote!()
3575    };
3576    // Builds `_audit_entry`; `written` is the soft-delete column's new
3577    // value, recorded in place of the stale `self` field.
3578    let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3579        if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3580            quote! {
3581                let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3582                _audit_entry.set_tracked(#col, #v);
3583            }
3584        } else {
3585            quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3586        }
3587    };
3588    let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3589        .unwrap_or(#root::__serde_json::Value::Null));
3590    let restored_json = quote!(#root::__serde_json::Value::Null);
3591    // `_on` emit; `affected` paths skip the row when nothing changed.
3592    let make_op_emit =
3593        |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3594            if audited_fields.is_none() {
3595                return quote!();
3596            }
3597            let entry = make_entry(op_path, written);
3598            let emit = quote! {
3599                #entry
3600                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3601            };
3602            if affected {
3603                quote!(if _affected > 0 { #emit })
3604            } else {
3605                emit
3606            }
3607        };
3608    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3609    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3610    let audit_softdelete_emit = make_op_emit(
3611        quote!(#root::audit::AuditOp::SoftDelete),
3612        Some(soft_deleted_json.clone()),
3613        true,
3614    );
3615    let audit_restore_emit = make_op_emit(
3616        quote!(#root::audit::AuditOp::Restore),
3617        Some(restored_json.clone()),
3618        true,
3619    );
3620    // `&Pool` update runner for soft_delete / restore: one tx with the
3621    // audit row when audited, a plain UPDATE otherwise.
3622    let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3623        if audited_fields.is_some() {
3624            let entry = make_entry(op_path, Some(written));
3625            quote! {
3626                #entry
3627                #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3628            }
3629        } else {
3630            quote!(#root::sql::update_pool(pool, &_query).await)
3631        }
3632    };
3633    let pool_softdelete_run =
3634        make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3635    let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3636
3637    // `save_pool(&Pool)` — emitted for every model with a PK. The
3638    // audited `save_pool` is built below on the diff path; both share
3639    // `save_partial_methods`, which differ only in how they run `_query`.
3640    let save_ret = quote!(::core::result::Result<u64, #root::sql::ExecError>);
3641    let save_keep = quote!(matches!(_saved, ::core::result::Result::Ok(n) if n > 0));
3642    let save_partial_methods = |pk_ident: &syn::Ident,
3643                                pk_column_lit: &str,
3644                                run: TokenStream2|
3645     -> TokenStream2 {
3646        let assignments = &fields.update_assignments;
3647        // Pushed in step with `update_assignments`: one column each.
3648        let assignment_cols = &fields.upsert_update_columns;
3649        let write = restore_auto_now_unless(
3650            &fields.auto_now_idents,
3651            &save_ret,
3652            &save_keep,
3653            &quote! {
3654                // Build only the wanted assignments, so an unnamed
3655                // `auto_now` field is not stamped.
3656                let mut _filtered: ::std::vec::Vec<#root::core::Assignment> =
3657                    ::std::vec::Vec::with_capacity(_wanted_cols.len());
3658                #(
3659                    if _wanted_cols.contains(#assignment_cols) {
3660                        _filtered.push(#assignments);
3661                    }
3662                )*
3663                if _filtered.is_empty() {
3664                    // All field names valid, but they all map to
3665                    // non-assignable slots (PK column, computed/
3666                    // virtual fields, relations without an
3667                    // assignment). Same no-op as an empty list.
3668                    #root::__tracing::warn!(
3669                        target: "rustango::save_partial",
3670                        model = _schema.name,
3671                        "save_partial: every named field maps to a non-assignable column — no-op"
3672                    );
3673                    return ::core::result::Result::Ok(0);
3674                }
3675                let _query = #root::core::UpdateQuery::new(
3676                    _schema,
3677                    _filtered,
3678                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3679                        #pk_column_lit,
3680                        #root::core::Op::Eq,
3681                        ::core::convert::Into::<#root::core::SqlValue>::into(
3682                            ::core::clone::Clone::clone(&self.#pk_ident)
3683                        ),
3684                    )),
3685                );
3686                #run
3687            },
3688        );
3689        quote! {
3690            /// Save (UPDATE) only the listed Rust-side fields,
3691            /// leaving every other column untouched. Issue #66.
3692            ///
3693            /// `fields` are Rust-side struct field names; the macro
3694            /// resolves each to its SQL column. Unknown field
3695            /// names return [`#root::core::QueryError::UnknownField`]
3696            /// wrapped in `ExecError::Query`. An empty list is a
3697            /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3698            /// On an audited model the audit diff covers only these fields.
3699            ///
3700            /// Use this when:
3701            /// * you only mutated a couple of fields on a wide row
3702            ///   (avoid re-writing every column on every save), or
3703            /// * two writers diverged after their initial read and
3704            ///   you want to preserve the other writer's changes to
3705            ///   columns you didn't touch.
3706            ///
3707            /// Auto-PK models with an unset PK return
3708            /// [`#root::core::QueryError::UnknownField`] with
3709            /// field name `<pk>` — `save_partial` is an
3710            /// UPDATE-only path. Call [`Self::insert_pool`]
3711            /// (or [`Self::save_pool`] which dispatches based on
3712            /// PK state) for the INSERT case.
3713            ///
3714            /// # Errors
3715            /// As [`Self::save_pool`], plus `UnknownField` for
3716            /// unknown / empty / Auto-Unset cases.
3717            pub async fn save_partial(
3718                &mut self,
3719                fields: &[&str],
3720                pool: &#root::sql::Pool,
3721            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3722                if fields.is_empty() {
3723                    #root::__tracing::warn!(
3724                        target: "rustango::save_partial",
3725                        model = <Self as #root::core::Model>::SCHEMA.name,
3726                        "save_partial called with empty field list — no-op"
3727                    );
3728                    return ::core::result::Result::Ok(0);
3729                }
3730                let _schema = <Self as #root::core::Model>::SCHEMA;
3731                // Validate field names against the schema.
3732                let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3733                    ::std::collections::HashSet::with_capacity(fields.len());
3734                for f in fields {
3735                    match _schema.field(f) {
3736                        ::core::option::Option::Some(fs) => {
3737                            _wanted_cols.insert(fs.column);
3738                        }
3739                        ::core::option::Option::None => {
3740                            return ::core::result::Result::Err(
3741                                #root::sql::ExecError::Query(
3742                                    #root::core::QueryError::UnknownField {
3743                                        model: _schema.name,
3744                                        field: (*f).to_owned(),
3745                                    }
3746                                )
3747                            );
3748                        }
3749                    }
3750                }
3751                #write
3752            }
3753
3754            /// Typed-column counterpart of [`Self::save_partial`] —
3755            /// issue #67. `fields` is a tuple of [`Column`]
3756            /// constants whose `Model` matches `Self`; typos and
3757            /// model mismatches surface at *compile time*
3758            /// (`Author::name` inside a `Post::save_partial_typed`
3759            /// call is a type error, no runtime check).
3760            ///
3761            /// ```ignore
3762            /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3763            /// ```
3764            ///
3765            /// Lowers to [`Self::save_partial`] under the hood — the
3766            /// tuple is reduced to a `&[&str]` slice of Rust-side
3767            /// field names and forwarded.
3768            ///
3769            /// [`Column`]: #root::core::Column
3770            ///
3771            /// # Errors
3772            /// As [`Self::save_partial`].
3773            pub async fn save_partial_typed<
3774                L: #root::core::TypedFieldList<Self>,
3775            >(
3776                &mut self,
3777                fields: L,
3778                pool: &#root::sql::Pool,
3779            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3780                let _names = fields.rust_field_names();
3781                let _refs: ::std::vec::Vec<&str> =
3782                    _names.iter().copied().collect();
3783                self.save_partial(&_refs, pool).await
3784            }
3785        }
3786    };
3787    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3788        let pk_column_lit = pk_col.as_str();
3789        let assignments = &fields.update_assignments;
3790        if audited_fields.is_some() {
3791            quote!()
3792        } else {
3793            let partial = save_partial_methods(
3794                pk_ident,
3795                pk_column_lit,
3796                quote!(#root::sql::update_pool(pool, &_query).await),
3797            );
3798            let dispatch_unset = if fields.pk_is_auto {
3799                quote! {
3800                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3801                        return self.insert_pool(pool).await.map(|()| 1u64);
3802                    }
3803                }
3804            } else {
3805                quote!()
3806            };
3807            let write = restore_auto_now_unless(
3808                &fields.auto_now_idents,
3809                &save_ret,
3810                &save_keep,
3811                &quote! {
3812                    let _query = #root::core::UpdateQuery::new(
3813                        <Self as #root::core::Model>::SCHEMA,
3814                        ::std::vec![ #( #assignments ),* ],
3815                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3816                            #pk_column_lit,
3817                            #root::core::Op::Eq,
3818                            ::core::convert::Into::<#root::core::SqlValue>::into(
3819                                ::core::clone::Clone::clone(&self.#pk_ident)
3820                            ),
3821                        )),
3822                    );
3823                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3824                    ::core::result::Result::Ok(_affected)
3825                },
3826            );
3827            quote! {
3828                /// Save this row to its table against either backend.
3829                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3830                /// `UPDATE` keyed on the PK; global scopes do not apply.
3831                ///
3832                /// # Errors
3833                /// As [`Self::save`].
3834                pub async fn save_pool(
3835                    &mut self,
3836                    pool: &#root::sql::Pool,
3837                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3838                    #dispatch_unset
3839                    #write
3840                }
3841
3842                #partial
3843            }
3844        }
3845    } else {
3846        quote!()
3847    };
3848
3849    // Audited `insert_pool` (overrides the placeholder set higher up
3850    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3851    // audited models get insert_pool routing through
3852    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3853    // + auto-PK readback + audit emit). Snapshot-style audit (the
3854    // PendingEntry's `changes` carries post-write field values).
3855    let pool_insert_method = if audited_fields.is_some() {
3856        if let Some(_) = primary_key {
3857            let pushes = if fields.has_auto {
3858                fields.insert_pushes.clone()
3859            } else {
3860                // For non-Auto-PK models, the macro normally builds
3861                // {columns, values} from fields.insert_columns +
3862                // fields.insert_values rather than insert_pushes.
3863                // Map those into the pushes shape.
3864                fields
3865                    .insert_columns
3866                    .iter()
3867                    .zip(&fields.insert_values)
3868                    .map(|(col, val)| {
3869                        quote! {
3870                            _columns.push(#col);
3871                            _values.push(#val);
3872                        }
3873                    })
3874                    .collect()
3875            };
3876            // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
3877            let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
3878            {
3879                fields.returning_cols.clone()
3880            } else {
3881                // Non-Auto-PK: still need RETURNING something for the
3882                // audit helper's contract (it errors on empty
3883                // returning). Return the PK column so the audit row
3884                // can carry the assigned PK back. Some non-Auto PKs
3885                // are server-side-default (e.g. UUIDv4 default), so
3886                // RETURNING is genuinely useful.
3887                primary_key
3888                    .map(|(_, col)| {
3889                        let lit = col.as_str();
3890                        vec![quote!(#lit)]
3891                    })
3892                    .unwrap_or_default()
3893            };
3894            quote! {
3895                /// Insert this row against either backend with audit
3896                /// emission inside the same transaction. Bi-dialect
3897                /// counterpart of [`Self::insert`] for audited models.
3898                ///
3899                /// Snapshot-style audit (post-write field values).
3900                ///
3901                /// # Errors
3902                /// As [`Self::insert`].
3903                pub async fn insert_pool(
3904                    &mut self,
3905                    pool: &#root::sql::Pool,
3906                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3907                    let mut _columns: ::std::vec::Vec<&'static str> =
3908                        ::std::vec::Vec::new();
3909                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3910                        ::std::vec::Vec::new();
3911                    #( #pushes )*
3912                    let _query = #root::core::InsertQuery::new(
3913                        <Self as #root::core::Model>::SCHEMA,
3914                        _columns,
3915                        _values,
3916                    )
3917                    .returning(::std::vec![ #( #returning_cols ),* ]);
3918                    #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
3919                        _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
3920                    })
3921                    .await
3922                }
3923            }
3924        } else {
3925            quote!()
3926        }
3927    } else {
3928        // Keep the non-audited pool_insert_method we built earlier.
3929        pool_insert_method
3930    };
3931
3932    // Update audited save_pool: now that insert_pool is wired for
3933    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3934    // insert_pool. Non-audited save_pool already does this.
3935    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3936    // Replaces the snapshot-only emission with a per-backend transaction
3937    // body that:
3938    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3939    //     column), capturing BEFORE values
3940    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3941    //  3. builds AFTER pairs from &self
3942    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3943    //     AuditOp::Update + diff_changes(...) on the same tx connection
3944    //  5. commits
3945    //
3946    // Per-backend arms inline the SQL string + placeholder shape, then
3947    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3948    // is polymorphic over Row type — the same tokens work against
3949    // PgRow and MySqlRow as long as the field's Rust type implements
3950    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3951    // primitives + chrono/uuid/serde_json::Value all do).
3952    let pool_save_method = if let Some(tracked) = audited_fields {
3953        if let Some((pk_ident, pk_col)) = primary_key {
3954            let pk_column_lit = pk_col.as_str();
3955            let partial = save_partial_methods(
3956                pk_ident,
3957                pk_column_lit,
3958                quote! {
3959                    self.__rustango_save_with_diff(
3960                        pool,
3961                        &_query,
3962                        ::core::option::Option::Some(&_wanted_cols),
3963                    )
3964                    .await
3965                },
3966            );
3967            // Per-tracked-column BEFORE-pair token list. Each entry
3968            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3969            // The Row alias resolves to PgRow / MySqlRow per call site,
3970            // so the same template generates both the PG and MySQL bodies.
3971            let mk_before_pairs =
3972                |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
3973                    tracked
3974                        .iter()
3975                        .map(|c| {
3976                            let getter = getter(c);
3977                            let column_lit = c.column.as_str();
3978                            let value_ty = &c.value_ty;
3979                            quote! {
3980                                (
3981                                    #column_lit,
3982                                    match #getter::<#value_ty>(
3983                                        _audit_before_row, #column_lit,
3984                                    ) {
3985                                        ::core::result::Result::Ok(v) => {
3986                                            #root::core::__json_value(&v)
3987                                        }
3988                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3989                                    },
3990                                )
3991                            }
3992                        })
3993                        .collect()
3994                };
3995            let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
3996            let before_pairs_my = mk_before_pairs(&|c| {
3997                if c.uuid_column {
3998                    quote!(#root::sql::try_get_flat_my)
3999                } else {
4000                    quote!(#root::sql::try_get_returning_my)
4001                }
4002            });
4003            let before_pairs_sqlite =
4004                mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
4005            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
4006            let pk_value_for_bind = if fields.pk_is_auto {
4007                quote!(self.#pk_ident.get().copied().unwrap_or_default())
4008            } else {
4009                quote!(::core::clone::Clone::clone(&self.#pk_ident))
4010            };
4011            let assignments = &fields.update_assignments;
4012            let unset_dispatch = if fields.has_auto {
4013                quote! {
4014                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
4015                        return self.insert_pool(pool).await.map(|()| 1u64);
4016                    }
4017                }
4018            } else {
4019                quote!()
4020            };
4021            let write = restore_auto_now_unless(
4022                &fields.auto_now_idents,
4023                &save_ret,
4024                &save_keep,
4025                &quote! {
4026                    let _query = #root::core::UpdateQuery::new(
4027                        <Self as #root::core::Model>::SCHEMA,
4028                        ::std::vec![ #( #assignments ),* ],
4029                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4030                            #pk_column_lit,
4031                            #root::core::Op::Eq,
4032                            ::core::convert::Into::<#root::core::SqlValue>::into(
4033                                ::core::clone::Clone::clone(&self.#pk_ident)
4034                            ),
4035                        )),
4036                    );
4037                    self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
4038                        .await
4039                },
4040            );
4041            quote! {
4042                /// Save this row against either backend with audit
4043                /// emission (diff-style: BEFORE+AFTER) inside the
4044                /// same transaction. Auto::Unset PK routes to
4045                /// insert_pool. Bi-dialect counterpart of
4046                /// [`Self::save`] for audited models.
4047                ///
4048                /// The audit row's `changes` JSON contains one
4049                /// `{ "field": { "before": …, "after": … } }` entry
4050                /// per tracked column whose value actually changed
4051                /// — same shape as the existing &PgPool save() emits.
4052                /// Keyed on the PK; global scopes do not apply.
4053                ///
4054                /// # Errors
4055                /// As [`Self::save`].
4056                pub async fn save_pool(
4057                    &mut self,
4058                    pool: &#root::sql::Pool,
4059                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4060                    #unset_dispatch
4061                    #write
4062                }
4063
4064                /// Run `query` and write its diff entry, limited to `only` columns.
4065                #[doc(hidden)]
4066                async fn __rustango_save_with_diff(
4067                    &self,
4068                    pool: &#root::sql::Pool,
4069                    query: &#root::core::UpdateQuery,
4070                    only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
4071                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4072                    let mut _after_pairs = self.__rustango_audit_pairs();
4073                    if let ::core::option::Option::Some(only) = only {
4074                        _after_pairs.retain(|(col, _)| only.contains(col));
4075                    }
4076                    let _before_query = #root::audit::before_image_query(
4077                        <Self as #root::core::Model>::SCHEMA,
4078                        #pk_column_lit,
4079                        ::core::convert::Into::<#root::core::SqlValue>::into(
4080                            #pk_value_for_bind,
4081                        ),
4082                        &[ #( #tracked_cols ),* ],
4083                    );
4084                    #root::audit::save_one_with_diff(
4085                        pool,
4086                        query,
4087                        &_before_query,
4088                        <Self as #root::core::Model>::SCHEMA.table,
4089                        self.__rustango_audit_pk(),
4090                        _after_pairs,
4091                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4092                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4093                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4094                    ).await
4095                }
4096
4097                #partial
4098            }
4099        } else {
4100            quote!()
4101        }
4102    } else {
4103        pool_save_method
4104    };
4105
4106    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4107    // body shapes:
4108    // - non-audited: simple dispatch through `sql::delete_pool`
4109    // - audited: routes through `audit::delete_one_with_audit`,
4110    //   which opens a per-backend transaction wrapping DELETE +
4111    //   audit emit so the data write and audit row commit atomically.
4112    let pool_delete_method = {
4113        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4114        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4115        if let Some(pk_ident) = pk_ident_for_pool {
4116            if audited_fields.is_some() {
4117                quote! {
4118                    /// Delete this row against either backend with audit
4119                    /// emission inside the same transaction. Bi-dialect
4120                    /// counterpart of [`Self::delete`] for audited models.
4121                    /// Keyed on the PK; global scopes do not apply.
4122                    ///
4123                    /// # Errors
4124                    /// As [`Self::delete`].
4125                    pub async fn delete_pool(
4126                        &self,
4127                        pool: &#root::sql::Pool,
4128                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4129                        let _query = #root::core::DeleteQuery::by_pk(
4130                            <Self as #root::core::Model>::SCHEMA,
4131                            #pk_column_lit,
4132                            ::core::convert::Into::<#root::core::SqlValue>::into(
4133                                ::core::clone::Clone::clone(&self.#pk_ident)
4134                            ),
4135                        );
4136                        let _audit_entry =
4137                            self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4138                        #root::audit::delete_one_with_audit(
4139                            pool, &_query, &_audit_entry,
4140                        ).await
4141                    }
4142                }
4143            } else {
4144                quote! {
4145                    /// Delete the row identified by this instance's primary key
4146                    /// against either backend. Equivalent to [`Self::delete`] but
4147                    /// takes [`#root::sql::Pool`] and dispatches per backend.
4148                    /// Global scopes do not apply.
4149                    ///
4150                    /// # Errors
4151                    /// As [`Self::delete`].
4152                    pub async fn delete_pool(
4153                        &self,
4154                        pool: &#root::sql::Pool,
4155                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4156                        let _query = #root::core::DeleteQuery::by_pk(
4157                            <Self as #root::core::Model>::SCHEMA,
4158                            #pk_column_lit,
4159                            ::core::convert::Into::<#root::core::SqlValue>::into(
4160                                ::core::clone::Clone::clone(&self.#pk_ident)
4161                            ),
4162                        );
4163                        #root::sql::delete_pool(pool, &_query).await
4164                    }
4165                }
4166            }
4167        } else {
4168            quote!()
4169        }
4170    };
4171
4172    // Bulk writers on audited models write one audit row per affected
4173    // row, in the write's transaction (#1747).
4174    let (bulk_delete_run, bulk_update_run, truncate_run) = if audited_fields.is_some() {
4175        (
4176            quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4177                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4178            })
4179            .await),
4180            quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4181                _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4182            })
4183            .await),
4184            quote! {
4185                let _table = <Self as #root::core::Model>::SCHEMA.table;
4186                let _sql = #root::sql::truncate_sql(pool.dialect(), _table);
4187                #root::audit::truncate_with_audit(pool, _table, &_sql).await
4188            },
4189        )
4190    } else {
4191        (
4192            quote!(#root::sql::delete_pool(pool, &_query).await),
4193            quote!(#root::sql::update_pool(pool, &_query).await),
4194            quote!(#root::sql::truncate(pool, <Self as #root::core::Model>::SCHEMA).await),
4195        )
4196    };
4197
4198    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4199    // matching this instance's PK and overwrite the in-memory state
4200    // with the freshly-fetched columns.
4201    // Issue #825. Only emitted when the model declares a PK; non-PK
4202    // models can't address a specific row.
4203    //
4204    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4205    // every field from `self`; resets the PK to `Auto::Unset` when
4206    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4207    // a fresh autoincrement value. Non-Auto PKs preserve the source
4208    // PK — the caller must overwrite before insert. Pure-Rust, no
4209    // I/O, no dialect surface.
4210    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4211        let other_field_clones: Vec<TokenStream2> = fields
4212            .column_entries
4213            .iter()
4214            .filter(|c| &c.ident != pk_ident)
4215            .map(|c| {
4216                let ident = &c.ident;
4217                quote! {
4218                    #ident: ::core::clone::Clone::clone(&self.#ident)
4219                }
4220            })
4221            .collect();
4222        let pk_clone_token = if fields.pk_is_auto {
4223            quote! { #pk_ident: #root::sql::Auto::Unset }
4224        } else {
4225            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4226        };
4227        let replicate_doc = if fields.pk_is_auto {
4228            quote! {
4229                /// Eloquent-style `replicate()` — return a clone of this
4230                /// row with the primary key reset to [`Auto::Unset`] so
4231                /// the copy is ready for `insert_pool` / `save_pool` to
4232                /// allocate a fresh autoincrement value. Every other
4233                /// field is `Clone`d verbatim. Issue #825.
4234                ///
4235                /// `auto_now_add` / `auto_now` timestamp fields are
4236                /// **not** reset (Eloquent's `replicate` doesn't reset
4237                /// them either) — pass them through the normal insert
4238                /// path if you want fresh values, or assign them
4239                /// explicitly after the call.
4240            }
4241        } else {
4242            quote! {
4243                /// Eloquent-style `replicate()` — clone this row
4244                /// verbatim. Because the primary key is **not** an
4245                /// `Auto<T>`, the clone keeps the source PK; the
4246                /// caller must overwrite `copy.<pk>` before inserting
4247                /// to avoid a unique-key violation. Issue #825.
4248            }
4249        };
4250        // 2026-06-07 — field-name / shortcut collision guard.
4251        //
4252        // The macro emits both `pub const <field>: <field>_col = ...`
4253        // (per-field typed-column const, used by the typed-builder
4254        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4255        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4256        // model has a field named e.g. `count`, both items would land
4257        // in the same inherent impl with the same name, and the
4258        // compiler rejects the derive with "duplicate definitions".
4259        //
4260        // Drop the conflicting shortcut for that model. Callers can
4261        // still reach the same behavior via
4262        // `QuerySet::<Model>::default().count(&pool)`.
4263        let column_names: ::std::collections::HashSet<String> = fields
4264            .column_entries
4265            .iter()
4266            .map(|c| ident_name(&c.ident))
4267            .collect();
4268        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4269            if column_names.contains(name) {
4270                quote! {}
4271            } else {
4272                tokens
4273            }
4274        };
4275        let count_method = emit_if_no_field_collision(
4276            "count",
4277            quote! {
4278                /// Count rows of this model — `SELECT COUNT(*) FROM
4279                /// <table>`. Eloquent `Model::count()` parity.
4280                ///
4281                /// Skipped on models that already declare a field named
4282                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4283                /// in that case.
4284                ///
4285                /// # Errors
4286                /// As [`CounterPool::count`].
4287                ///
4288                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4289                pub async fn count(
4290                    pool: &#root::sql::Pool,
4291                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4292                    use #root::sql::CounterPool as _;
4293                    #root::query::QuerySet::<Self>::default()
4294                        .count(pool)
4295                        .await
4296                }
4297            },
4298        );
4299        let value_method = emit_if_no_field_collision(
4300            "value",
4301            quote! {
4302                /// Pluck a single scalar from the first row.
4303                /// Eloquent `Model::query()->value($col)` parity.
4304                ///
4305                /// Skipped on models that already declare a field named
4306                /// `value`. Drop into
4307                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4308                ///
4309                /// # Errors
4310                /// As `ValuesFlatQuerySet::first`.
4311                pub async fn value<U>(
4312                    col: &str,
4313                    pool: &#root::sql::Pool,
4314                ) -> ::core::result::Result<
4315                    ::core::option::Option<U>,
4316                    #root::sql::ExecError,
4317                >
4318                where
4319                    U: #root::sql::FlatScalar,
4320                {
4321                    let _col_static: &'static str = Self::__resolve_col(col)?;
4322                    #root::query::QuerySet::<Self>::default()
4323                        .values_list_flat(_col_static)
4324                        .first::<U>(pool)
4325                        .await
4326                }
4327            },
4328        );
4329        let sum_method = emit_if_no_field_collision(
4330            "sum",
4331            quote! {
4332                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4333                /// Skipped on models that already declare a field named
4334                /// `sum`.
4335                ///
4336                /// # Errors
4337                /// As [`#root::sql::fetch_aggregate_pool`].
4338                pub async fn sum<U>(
4339                    col: &str,
4340                    pool: &#root::sql::Pool,
4341                ) -> ::core::result::Result<
4342                    ::core::option::Option<U>,
4343                    #root::sql::ExecError,
4344                >
4345                where
4346                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4347                        + #root::sql::MaybeMyFromRow
4348                        + #root::sql::MaybeSqliteFromRow
4349                        + ::core::marker::Send
4350                        + ::core::marker::Unpin,
4351                {
4352                    #root::query::QuerySet::<Self>::default()
4353                        .sum::<U>(col, pool)
4354                        .await
4355                }
4356            },
4357        );
4358        let avg_method = emit_if_no_field_collision(
4359            "avg",
4360            quote! {
4361                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4362                /// models that already declare a field named `avg`.
4363                ///
4364                /// # Errors
4365                /// As [`#root::sql::fetch_aggregate_pool`].
4366                pub async fn avg<U>(
4367                    col: &str,
4368                    pool: &#root::sql::Pool,
4369                ) -> ::core::result::Result<
4370                    ::core::option::Option<U>,
4371                    #root::sql::ExecError,
4372                >
4373                where
4374                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4375                        + #root::sql::MaybeMyFromRow
4376                        + #root::sql::MaybeSqliteFromRow
4377                        + ::core::marker::Send
4378                        + ::core::marker::Unpin,
4379                {
4380                    #root::query::QuerySet::<Self>::default()
4381                        .avg::<U>(col, pool)
4382                        .await
4383                }
4384            },
4385        );
4386        let min_method = emit_if_no_field_collision(
4387            "min",
4388            quote! {
4389                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4390                /// models that already declare a field named `min`.
4391                ///
4392                /// # Errors
4393                /// As [`#root::sql::fetch_aggregate_pool`].
4394                pub async fn min<U>(
4395                    col: &str,
4396                    pool: &#root::sql::Pool,
4397                ) -> ::core::result::Result<
4398                    ::core::option::Option<U>,
4399                    #root::sql::ExecError,
4400                >
4401                where
4402                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4403                        + #root::sql::MaybeMyFromRow
4404                        + #root::sql::MaybeSqliteFromRow
4405                        + ::core::marker::Send
4406                        + ::core::marker::Unpin,
4407                {
4408                    #root::query::QuerySet::<Self>::default()
4409                        .min::<U>(col, pool)
4410                        .await
4411                }
4412            },
4413        );
4414        let max_method = emit_if_no_field_collision(
4415            "max",
4416            quote! {
4417                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4418                /// models that already declare a field named `max`.
4419                ///
4420                /// # Errors
4421                /// As [`#root::sql::fetch_aggregate_pool`].
4422                pub async fn max<U>(
4423                    col: &str,
4424                    pool: &#root::sql::Pool,
4425                ) -> ::core::result::Result<
4426                    ::core::option::Option<U>,
4427                    #root::sql::ExecError,
4428                >
4429                where
4430                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4431                        + #root::sql::MaybeMyFromRow
4432                        + #root::sql::MaybeSqliteFromRow
4433                        + ::core::marker::Send
4434                        + ::core::marker::Unpin,
4435                {
4436                    #root::query::QuerySet::<Self>::default()
4437                        .max::<U>(col, pool)
4438                        .await
4439                }
4440            },
4441        );
4442        let first_method = emit_if_no_field_collision(
4443            "first",
4444            quote! {
4445                /// First row of this model. Eloquent `Model::first()`.
4446                /// Skipped on models that already declare a field named
4447                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4448                ///
4449                /// # Errors
4450                /// As `QuerySet::first`.
4451                pub async fn first(
4452                    pool: &#root::sql::Pool,
4453                ) -> ::core::result::Result<
4454                    ::core::option::Option<Self>,
4455                    #root::sql::ExecError,
4456                > {
4457                    #root::query::QuerySet::<Self>::default()
4458                        .first(pool)
4459                        .await
4460                }
4461            },
4462        );
4463        let last_method = emit_if_no_field_collision(
4464            "last",
4465            quote! {
4466                /// Last row of this model by primary-key DESC.
4467                /// Eloquent `Model::query()->latest('id')->first()`
4468                /// parity — fetches the highest-PK row without
4469                /// requiring the caller to spell the PK column.
4470                /// Returns `None` on an empty table.
4471                ///
4472                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4473                /// Skipped on models that already declare a field
4474                /// named `last`.
4475                ///
4476                /// # Errors
4477                /// As `QuerySet::last`.
4478                pub async fn last(
4479                    pool: &#root::sql::Pool,
4480                ) -> ::core::result::Result<
4481                    ::core::option::Option<Self>,
4482                    #root::sql::ExecError,
4483                > {
4484                    #root::query::QuerySet::<Self>::default()
4485                        .last(pool)
4486                        .await
4487                }
4488            },
4489        );
4490        quote! {
4491            /// Re-SELECT this row by its primary key and overwrite
4492            /// every in-memory field with the freshly-fetched value.
4493            /// Issue #825.
4494            ///
4495            /// Use this when the row may have been modified by another
4496            /// process / connection / job since you read it — e.g. after
4497            /// a queued task callback, or to re-sync stale UI state
4498            /// before re-saving.
4499            ///
4500            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4501            /// when the primary key no longer matches any row (e.g.
4502            /// the row was deleted concurrently).
4503            ///
4504            /// # Errors
4505            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4506            /// the PK no longer exists.
4507            ///
4508            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4509            pub async fn refresh_from_db(
4510                &mut self,
4511                pool: &#root::sql::Pool,
4512            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4513                use #root::sql::FetcherPool as _;
4514                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4515                    ::core::clone::Clone::clone(&self.#pk_ident),
4516                );
4517                let mut _rows: ::std::vec::Vec<Self> =
4518                    #root::query::QuerySet::<Self>::default()
4519                        .filter(::core::stringify!(#pk_ident), _pk_val)
4520                        .limit(1)
4521                        .fetch(pool)
4522                        .await?;
4523                match _rows.into_iter().next() {
4524                    ::core::option::Option::Some(_fresh) => {
4525                        *self = _fresh;
4526                        ::core::result::Result::Ok(())
4527                    }
4528                    ::core::option::Option::None => ::core::result::Result::Err(
4529                        #root::sql::ExecError::Driver(
4530                            #root::sql::sqlx::Error::RowNotFound,
4531                        ),
4532                    ),
4533                }
4534            }
4535
4536            /// Atomically increment the integer column `col` by
4537            /// `by` for this row. Equivalent to
4538            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4539            ///
4540            /// **Doesn't mutate `self`** — the in-memory copy is now
4541            /// stale; call [`Self::refresh_from_db`] /
4542            /// [`Self::fresh`] to re-sync. Returns the rows-
4543            /// affected count (0 when the PK doesn't match any row,
4544            /// 1 on success).
4545            ///
4546            /// `col` is the Rust field name as a string; unknown
4547            /// fields surface as `UnknownField` at runtime. Negative
4548            /// `by` values atomically decrement (see also
4549            /// [`Self::decrement`]).
4550            ///
4551            /// # Errors
4552            /// As [`UpdaterPool::execute_pool`].
4553            ///
4554            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4555            pub async fn increment(
4556                &self,
4557                col: &str,
4558                by: i64,
4559                pool: &#root::sql::Pool,
4560            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4561                Self::__increment_one(self, col, by, pool).await
4562            }
4563
4564            /// Sibling of [`Self::increment`] — atomically
4565            /// decrement this row's `col` by `by`. Eloquent
4566            /// `$model->decrement($col, $by)` parity. Equivalent to
4567            /// `self.increment(col, -by, &pool)`; the separate name
4568            /// keeps call sites readable.
4569            ///
4570            /// # Errors
4571            /// As [`Self::increment`].
4572            pub async fn decrement(
4573                &self,
4574                col: &str,
4575                by: i64,
4576                pool: &#root::sql::Pool,
4577            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4578                Self::__increment_one(self, col, -by, pool).await
4579            }
4580
4581            /// Bulk-increment: add `by` to `col` on every row of the
4582            /// table. Eloquent `Model::query()->increment($col, $by)`
4583            /// parity. Use for counters, score adjustments, view
4584            /// rollups.
4585            ///
4586            /// # Errors
4587            /// As [`UpdaterPool::execute_pool`].
4588            ///
4589            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4590            pub async fn increment_each(
4591                col: &str,
4592                by: i64,
4593                pool: &#root::sql::Pool,
4594            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4595                Self::__increment_all(col, by, pool).await
4596            }
4597
4598            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4599            ///
4600            /// # Errors
4601            /// As [`Self::increment_each`].
4602            pub async fn decrement_each(
4603                col: &str,
4604                by: i64,
4605                pool: &#root::sql::Pool,
4606            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4607                Self::__increment_all(col, -by, pool).await
4608            }
4609
4610            /// Internal: forward to
4611            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4612            /// One-line wrapper kept as a per-Model method so the
4613            /// macro's emitted `increment` / `decrement` instance
4614            /// calls don't have to thread `Self` through manually.
4615            #[doc(hidden)]
4616            pub async fn __increment_one(
4617                this: &Self,
4618                col: &str,
4619                by: i64,
4620                pool: &#root::sql::Pool,
4621            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4622                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4623                    ::core::clone::Clone::clone(&this.#pk_ident),
4624                );
4625                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4626                    ::core::stringify!(#pk_ident),
4627                    _pk_val,
4628                    col,
4629                    by,
4630                    pool,
4631                )
4632                .await
4633            }
4634
4635            /// Internal: run
4636            /// [`#root::sql::model_shortcuts::increment_all_query`].
4637            #[doc(hidden)]
4638            pub async fn __increment_all(
4639                col: &str,
4640                by: i64,
4641                pool: &#root::sql::Pool,
4642            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4643                let _query =
4644                    #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4645                #bulk_update_run
4646            }
4647
4648            /// Internal: forward to
4649            /// [`#root::sql::model_shortcuts::resolve_col`].
4650            #[doc(hidden)]
4651            pub fn __resolve_col(
4652                col: &str,
4653            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4654                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4655            }
4656
4657            /// Internal: forward to
4658            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4659            #[doc(hidden)]
4660            #[must_use]
4661            pub fn __add_signed_expr(
4662                col_static: &'static str,
4663                signed_by: i64,
4664            ) -> #root::core::Expr {
4665                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4666            }
4667
4668            /// Re-SELECT this row by its primary key and return a
4669            /// **new** instance with the freshly-fetched fields.
4670            /// Eloquent `Model::fresh()` parity — non-mutating
4671            /// counterpart of [`Self::refresh_from_db`].
4672            ///
4673            /// Returns `Ok(None)` when the row was deleted
4674            /// concurrently — vs [`Self::refresh_from_db`]
4675            /// which surfaces that as `RowNotFound` because
4676            /// in-place mutation has nothing to write to.
4677            ///
4678            /// Useful when you want to compare the in-memory
4679            /// instance against the persisted state (audit-style
4680            /// diffs, conflict detection) without mutating the
4681            /// reference you already hold.
4682            ///
4683            /// # Errors
4684            /// As [`FetcherPool::fetch`].
4685            ///
4686            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4687            pub async fn fresh(
4688                &self,
4689                pool: &#root::sql::Pool,
4690            ) -> ::core::result::Result<
4691                ::core::option::Option<Self>,
4692                #root::sql::ExecError,
4693            > {
4694                use #root::sql::FetcherPool as _;
4695                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4696                    ::core::clone::Clone::clone(&self.#pk_ident),
4697                );
4698                let _rows: ::std::vec::Vec<Self> =
4699                    #root::query::QuerySet::<Self>::default()
4700                        .filter(::core::stringify!(#pk_ident), _pk_val)
4701                        .limit(1)
4702                        .fetch(pool)
4703                        .await?;
4704                ::core::result::Result::Ok(_rows.into_iter().next())
4705            }
4706
4707            #replicate_doc
4708            #[must_use]
4709            pub fn replicate(&self) -> Self {
4710                Self {
4711                    #pk_clone_token,
4712                    #( #other_field_clones, )*
4713                }
4714            }
4715
4716            #first_method
4717
4718            #last_method
4719
4720            /// Throwing counterpart of [`Self::first`] —
4721            /// errors with `RowNotFound` when the table is empty.
4722            /// Eloquent `Model::firstOrFail()` parity.
4723            ///
4724            /// # Errors
4725            /// As [`Self::first`]; additionally
4726            /// [`sqlx::Error::RowNotFound`] on empty tables.
4727            ///
4728            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4729            pub async fn first_or_fail(
4730                pool: &#root::sql::Pool,
4731            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4732                // Route through the queryset rather than `Self::first`,
4733                // which is suppressed on models with a field named
4734                // `first` (field/shortcut collision guard).
4735                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4736                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4737                    ::core::option::Option::None => ::core::result::Result::Err(
4738                        #root::sql::ExecError::Driver(
4739                            #root::sql::sqlx::Error::RowNotFound,
4740                        ),
4741                    ),
4742                }
4743            }
4744
4745            /// Single-column projection — `SELECT <col> FROM
4746            /// <table>`. Returns `Vec<U>` where each element is the
4747            /// decoded value of the column. Eloquent
4748            /// `Model::pluck($column)` parity.
4749            ///
4750            /// Thin wrapper over `QuerySet::<Self>::default()
4751            /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4752            /// `FlatScalar`; use `Option<_>` for a nullable column.
4753            ///
4754            /// # Errors
4755            /// As `ValuesFlatQuerySet::fetch`.
4756            pub async fn pluck<U>(
4757                col: &'static str,
4758                pool: &#root::sql::Pool,
4759            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4760            where
4761                U: #root::sql::FlatScalar,
4762            {
4763                #root::query::QuerySet::<Self>::default()
4764                    .values_list_flat(col)
4765                    .fetch::<U>(pool)
4766                    .await
4767            }
4768
4769            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4770            /// stream every row of this model in batches of `n`,
4771            /// invoking the callback once per batch. Stable PK-ASC
4772            /// ordering so the LIMIT/OFFSET pagination is
4773            /// deterministic across drivers.
4774            ///
4775            /// The callback is async — it can do further DB work
4776            /// (writes, related-row lookups, queue dispatch) per
4777            /// batch. Return `Err(...)` from the callback to abort
4778            /// the iteration early; the error bubbles up.
4779            ///
4780            /// **When to use**: bulk processing flows that can't
4781            /// fit the whole table in memory — sending newsletters,
4782            /// running data migrations, computing summary
4783            /// statistics. For small / known-bounded tables, plain
4784            /// `Self::all(&pool)` is simpler.
4785            ///
4786            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4787            /// large tables. For multi-million-row scans prefer
4788            /// keyset-by-PK (the standard "WHERE id > last_seen"
4789            /// shape) over `chunk(...)`.
4790            ///
4791            /// Skipped on models without a primary key — chunking
4792            /// needs a stable order to avoid skipping / repeating
4793            /// rows across batches.
4794            pub async fn chunk<F, Fut>(
4795                n: i64,
4796                pool: &#root::sql::Pool,
4797                mut cb: F,
4798            ) -> ::core::result::Result<(), #root::sql::ExecError>
4799            where
4800                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4801                Fut: ::core::future::Future<
4802                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4803                >,
4804            {
4805                use #root::sql::FetcherPool as _;
4806                let pk_col = match Self::primary_key_column() {
4807                    ::core::option::Option::Some(c) => c,
4808                    ::core::option::Option::None => {
4809                        return ::core::result::Result::Ok(());
4810                    }
4811                };
4812                let mut offset: i64 = 0;
4813                loop {
4814                    let rows: ::std::vec::Vec<Self> =
4815                        #root::query::QuerySet::<Self>::default()
4816                            .order_by(&[(pk_col, false)])
4817                            .limit(n)
4818                            .offset(offset)
4819                            .fetch(pool)
4820                            .await?;
4821                    if rows.is_empty() {
4822                        return ::core::result::Result::Ok(());
4823                    }
4824                    let len = rows.len() as i64;
4825                    cb(rows).await?;
4826                    if len < n {
4827                        return ::core::result::Result::Ok(());
4828                    }
4829                    offset += n;
4830                }
4831            }
4832
4833            /// Eloquent `Model::chunkById($n, fn (...))` — same
4834            /// per-batch callback shape as [`Self::chunk`], but uses
4835            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4836            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4837            /// — the right choice for multi-million-row sweeps.
4838            ///
4839            /// Requires the primary key to be a signed integer type
4840            /// (`i64` / `i32`); the keyset comparison rides on
4841            /// `__rustango_pk_value()` lowering through
4842            /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4843            /// non-integer PKs (UUID, String) — those should use
4844            /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4845            /// keyset loop.
4846            ///
4847            /// Callback errors abort iteration; the error bubbles up
4848            /// unchanged. Empty table → callback invoked zero times.
4849            pub async fn chunk_by_id<F, Fut>(
4850                n: i64,
4851                pool: &#root::sql::Pool,
4852                mut cb: F,
4853            ) -> ::core::result::Result<(), #root::sql::ExecError>
4854            where
4855                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4856                Fut: ::core::future::Future<
4857                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4858                >,
4859            {
4860                use #root::sql::FetcherPool as _;
4861                let pk_col = match Self::primary_key_column() {
4862                    ::core::option::Option::Some(c) => c,
4863                    ::core::option::Option::None => {
4864                        return ::core::result::Result::Ok(());
4865                    }
4866                };
4867                // Track the largest PK seen so the next batch picks
4868                // up from there. `i64::MIN` as the sentinel — the
4869                // very first iteration's `> MIN` matches every row,
4870                // so the loop entry is uniform with subsequent
4871                // iterations.
4872                let mut last_seen: i64 = i64::MIN;
4873                loop {
4874                    let key = ::std::format!("{}__gt", pk_col);
4875                    let rows: ::std::vec::Vec<Self> =
4876                        #root::query::QuerySet::<Self>::default()
4877                            .filter(key.as_str(), last_seen)
4878                            .order_by(&[(pk_col, false)])
4879                            .limit(n)
4880                            .fetch(pool)
4881                            .await?;
4882                    if rows.is_empty() {
4883                        return ::core::result::Result::Ok(());
4884                    }
4885                    let len = rows.len() as i64;
4886                    // Capture the last row's PK BEFORE moving rows
4887                    // into the callback.
4888                    let max_pk = match rows
4889                        .last()
4890                        .map(|r| r.__rustango_pk_value())
4891                    {
4892                        ::core::option::Option::Some(
4893                            #root::core::SqlValue::I64(v),
4894                        ) => v,
4895                        ::core::option::Option::Some(
4896                            #root::core::SqlValue::I32(v),
4897                        ) => i64::from(v),
4898                        _ => return ::core::result::Result::Ok(()),
4899                    };
4900                    cb(rows).await?;
4901                    if len < n {
4902                        return ::core::result::Result::Ok(());
4903                    }
4904                    last_seen = max_pk;
4905                }
4906            }
4907
4908            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4909            /// per-row callback companion to [`Self::chunk`].
4910            /// Streams every row in keyset-paginated batches of
4911            /// `batch` size, calling `cb` once per row.
4912            ///
4913            /// Inherits the keyset-paginated scan of
4914            /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4915            /// non-integer PKs are silently a no-op).
4916            ///
4917            /// ```ignore
4918            /// Post::each(500, &pool, |p| async move {
4919            ///     reindex(p).await?;
4920            ///     Ok(())
4921            /// }).await?;
4922            /// ```
4923            ///
4924            /// Return `Err(...)` from the callback to abort
4925            /// iteration; the error bubbles up unchanged.
4926            pub async fn each<F, Fut>(
4927                batch: i64,
4928                pool: &#root::sql::Pool,
4929                mut cb: F,
4930            ) -> ::core::result::Result<(), #root::sql::ExecError>
4931            where
4932                F: ::core::ops::FnMut(Self) -> Fut,
4933                Fut: ::core::future::Future<
4934                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4935                >,
4936            {
4937                use #root::sql::FetcherPool as _;
4938                let pk_col = match Self::primary_key_column() {
4939                    ::core::option::Option::Some(c) => c,
4940                    ::core::option::Option::None => {
4941                        return ::core::result::Result::Ok(());
4942                    }
4943                };
4944                let mut last_seen: i64 = i64::MIN;
4945                loop {
4946                    let key = ::std::format!("{}__gt", pk_col);
4947                    let rows: ::std::vec::Vec<Self> =
4948                        #root::query::QuerySet::<Self>::default()
4949                            .filter(key.as_str(), last_seen)
4950                            .order_by(&[(pk_col, false)])
4951                            .limit(batch)
4952                            .fetch(pool)
4953                            .await?;
4954                    if rows.is_empty() {
4955                        return ::core::result::Result::Ok(());
4956                    }
4957                    let len = rows.len() as i64;
4958                    let max_pk = match rows
4959                        .last()
4960                        .map(|r| r.__rustango_pk_value())
4961                    {
4962                        ::core::option::Option::Some(
4963                            #root::core::SqlValue::I64(v),
4964                        ) => v,
4965                        ::core::option::Option::Some(
4966                            #root::core::SqlValue::I32(v),
4967                        ) => i64::from(v),
4968                        _ => return ::core::result::Result::Ok(()),
4969                    };
4970                    for row in rows {
4971                        cb(row).await?;
4972                    }
4973                    if len < batch {
4974                        return ::core::result::Result::Ok(());
4975                    }
4976                    last_seen = max_pk;
4977                }
4978            }
4979
4980            /// Delete every row of this model — `TRUNCATE TABLE
4981            /// <table> RESTART IDENTITY CASCADE` on Postgres,
4982            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4983            /// support `TRUNCATE` inside foreign-key constraints
4984            /// or — for SQLite — at all). Eloquent `Model::truncate()`
4985            /// parity.
4986            ///
4987            /// **Use only in tests / fixture-reset flows.** Production
4988            /// writes through this would silently bypass the
4989            /// `pre_delete` / `post_delete` signals (no per-row hooks
4990            /// fire on a TRUNCATE / bulk DELETE FROM). An audited model
4991            /// gets one bulk audit entry, not one per row. Global scopes
4992            /// do not apply: every row goes, generic M2M links too.
4993            ///
4994            /// # Errors
4995            /// As [`truncate`].
4996            ///
4997            /// [`truncate`]: rustango::sql::truncate
4998            pub async fn truncate(
4999                pool: &#root::sql::Pool,
5000            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5001                #truncate_run
5002            }
5003
5004            /// Bulk-delete every row whose primary key is in
5005            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
5006            /// Returns the affected row count.
5007            ///
5008            /// Eloquent `Model::destroy([1, 2, 3])` parity.
5009            /// Empty `pks` is a no-op (returns 0).
5010            ///
5011            /// Accepts any iterable whose elements are
5012            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
5013            /// `[i64; N]`, etc.
5014            ///
5015            /// # Errors
5016            /// As `delete_pool`.
5017            pub async fn destroy<V>(
5018                pks: impl ::core::iter::IntoIterator<Item = V>,
5019                pool: &#root::sql::Pool,
5020            ) -> ::core::result::Result<u64, #root::sql::ExecError>
5021            where
5022                V: ::core::convert::Into<#root::core::SqlValue>,
5023            {
5024                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5025                    pks.into_iter().map(::core::convert::Into::into).collect();
5026                if _values.is_empty() {
5027                    return ::core::result::Result::Ok(0);
5028                }
5029                let _pk = <Self as #root::core::Model>::SCHEMA
5030                    .primary_key()
5031                    .ok_or_else(|| {
5032                        #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
5033                    })?;
5034                let _query = #root::query::QuerySet::<Self>::default()
5035                    .filter_op(
5036                        _pk.name,
5037                        #root::core::Op::In,
5038                        #root::core::SqlValue::List(_values),
5039                    )
5040                    .compile_delete()?;
5041                #bulk_delete_run
5042            }
5043
5044            /// Fetch every row where `<col> = <val>`. Eloquent
5045            /// `Model::where($col, $val)->get()` parity.
5046            ///
5047            /// Thin wrapper over `QuerySet::<Self>::default()
5048            /// .filter(col, val).fetch(pool)`. For one row,
5049            /// use [`Self::first_where`]; for a chain that
5050            /// needs further `.filter()` / `.order_by()` /
5051            /// `.limit()`, drop down to `Self::query().filter(...)`
5052            /// directly.
5053            ///
5054            /// `val` accepts any value `Into<SqlValue>` so plain
5055            /// strings, ints, UUIDs, etc. all work.
5056            ///
5057            /// # Errors
5058            /// As [`FetcherPool::fetch`].
5059            ///
5060            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5061            pub async fn where_(
5062                col: &str,
5063                val: impl ::core::convert::Into<#root::core::SqlValue>,
5064                pool: &#root::sql::Pool,
5065            ) -> ::core::result::Result<
5066                ::std::vec::Vec<Self>,
5067                #root::sql::ExecError,
5068            > {
5069                use #root::sql::FetcherPool as _;
5070                #root::query::QuerySet::<Self>::default()
5071                    .filter(col, val)
5072                    .fetch(pool)
5073                    .await
5074            }
5075
5076            /// Fetch every row where `<col> IN (vals)`. Eloquent
5077            /// `Model::whereIn($col, $vals)->get()` parity. Empty
5078            /// `vals` returns no rows (matches SQL's empty-IN
5079            /// semantics).
5080            ///
5081            /// `vals` accepts any iterable whose items are
5082            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5083            /// etc.
5084            ///
5085            /// # Errors
5086            /// As [`FetcherPool::fetch`].
5087            ///
5088            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5089            pub async fn where_in<V>(
5090                col: &str,
5091                vals: impl ::core::iter::IntoIterator<Item = V>,
5092                pool: &#root::sql::Pool,
5093            ) -> ::core::result::Result<
5094                ::std::vec::Vec<Self>,
5095                #root::sql::ExecError,
5096            >
5097            where
5098                V: ::core::convert::Into<#root::core::SqlValue>,
5099            {
5100                use #root::sql::FetcherPool as _;
5101                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5102                    vals.into_iter().map(::core::convert::Into::into).collect();
5103                if _values.is_empty() {
5104                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5105                }
5106                let _key = ::std::format!("{}__in", col);
5107                #root::query::QuerySet::<Self>::default()
5108                    .filter(&_key, #root::core::SqlValue::List(_values))
5109                    .fetch(pool)
5110                    .await
5111            }
5112
5113            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5114            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5115            /// `vals` returns every row (matches SQL's empty-NOT-IN
5116            /// semantics — vacuously true for every row).
5117            ///
5118            /// # Errors
5119            /// As [`FetcherPool::fetch`].
5120            ///
5121            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5122            pub async fn where_not_in<V>(
5123                col: &str,
5124                vals: impl ::core::iter::IntoIterator<Item = V>,
5125                pool: &#root::sql::Pool,
5126            ) -> ::core::result::Result<
5127                ::std::vec::Vec<Self>,
5128                #root::sql::ExecError,
5129            >
5130            where
5131                V: ::core::convert::Into<#root::core::SqlValue>,
5132            {
5133                use #root::sql::FetcherPool as _;
5134                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5135                    vals.into_iter().map(::core::convert::Into::into).collect();
5136                if _values.is_empty() {
5137                    return #root::query::QuerySet::<Self>::default()
5138                        .fetch(pool)
5139                        .await;
5140                }
5141                let _key = ::std::format!("{}__not_in", col);
5142                #root::query::QuerySet::<Self>::default()
5143                    .filter(&_key, #root::core::SqlValue::List(_values))
5144                    .fetch(pool)
5145                    .await
5146            }
5147
5148            /// Fetch every row where `<col> IS NULL`. Eloquent
5149            /// `Model::whereNull($col)->get()` parity.
5150            ///
5151            /// # Errors
5152            /// As [`FetcherPool::fetch`].
5153            ///
5154            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5155            pub async fn where_null(
5156                col: &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                let _key = ::std::format!("{}__isnull", col);
5164                #root::query::QuerySet::<Self>::default()
5165                    .filter(&_key, #root::core::SqlValue::Bool(true))
5166                    .fetch(pool)
5167                    .await
5168            }
5169
5170            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5171            /// `Model::whereNotNull($col)->get()` parity.
5172            ///
5173            /// # Errors
5174            /// As [`FetcherPool::fetch`].
5175            ///
5176            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5177            pub async fn where_not_null(
5178                col: &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                let _key = ::std::format!("{}__isnull", col);
5186                #root::query::QuerySet::<Self>::default()
5187                    .filter(&_key, #root::core::SqlValue::Bool(false))
5188                    .fetch(pool)
5189                    .await
5190            }
5191
5192            /// Fetch up to `n` rows in random order. Eloquent
5193            /// `Model::inRandomOrder()->limit($n)->get()` /
5194            /// `Model::query()->inRandomOrder()->get()->take($n)`
5195            /// parity. **Performance caveat**: random ordering
5196            /// forces a full table scan + per-row random key sort;
5197            /// the optimizer cannot use an index. Prefer a
5198            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5199            ///
5200            /// # Errors
5201            /// As [`FetcherPool::fetch`].
5202            ///
5203            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5204            pub async fn random_n(
5205                n: i64,
5206                pool: &#root::sql::Pool,
5207            ) -> ::core::result::Result<
5208                ::std::vec::Vec<Self>,
5209                #root::sql::ExecError,
5210            > {
5211                use #root::sql::FetcherPool as _;
5212                #root::query::QuerySet::<Self>::default()
5213                    .order_random()
5214                    .limit(n)
5215                    .fetch(pool)
5216                    .await
5217            }
5218
5219            /// Fetch one row in random order. Eloquent
5220            /// `Model::inRandomOrder()->first()` parity. Same
5221            /// performance caveat as [`Self::random_n`].
5222            ///
5223            /// # Errors
5224            /// As [`FetcherPool::fetch`].
5225            ///
5226            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5227            pub async fn random(
5228                pool: &#root::sql::Pool,
5229            ) -> ::core::result::Result<
5230                ::core::option::Option<Self>,
5231                #root::sql::ExecError,
5232            > {
5233                ::core::result::Result::Ok(
5234                    Self::random_n(1, pool).await?.into_iter().next(),
5235                )
5236            }
5237
5238            /// Fetch every row ordered ASC by `field`. Eloquent
5239            /// `Model::oldest($field)->get()` parity — the multi-row
5240            /// counterpart of [`Self::earliest`].
5241            ///
5242            /// # Errors
5243            /// As [`FetcherPool::fetch`].
5244            ///
5245            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5246            pub async fn oldest(
5247                field: &str,
5248                pool: &#root::sql::Pool,
5249            ) -> ::core::result::Result<
5250                ::std::vec::Vec<Self>,
5251                #root::sql::ExecError,
5252            > {
5253                use #root::sql::FetcherPool as _;
5254                #root::query::QuerySet::<Self>::default()
5255                    .order_by(&[(field, false)])
5256                    .fetch(pool)
5257                    .await
5258            }
5259
5260            /// Fetch every row ordered DESC by `field`. Eloquent
5261            /// `Model::latest($field)->get()` parity — the multi-row
5262            /// counterpart of [`Self::latest`].
5263            ///
5264            /// # Errors
5265            /// As [`FetcherPool::fetch`].
5266            ///
5267            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5268            pub async fn newest(
5269                field: &str,
5270                pool: &#root::sql::Pool,
5271            ) -> ::core::result::Result<
5272                ::std::vec::Vec<Self>,
5273                #root::sql::ExecError,
5274            > {
5275                use #root::sql::FetcherPool as _;
5276                #root::query::QuerySet::<Self>::default()
5277                    .order_by(&[(field, true)])
5278                    .fetch(pool)
5279                    .await
5280            }
5281
5282            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5283            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5284            /// Routes through the existing `__year` lookup suffix
5285            /// (issue #829).
5286            ///
5287            /// # Errors
5288            /// As [`FetcherPool::fetch`].
5289            ///
5290            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5291            pub async fn where_year(
5292                col: &str,
5293                year: i64,
5294                pool: &#root::sql::Pool,
5295            ) -> ::core::result::Result<
5296                ::std::vec::Vec<Self>,
5297                #root::sql::ExecError,
5298            > {
5299                use #root::sql::FetcherPool as _;
5300                let _key = ::std::format!("{}__year", col);
5301                #root::query::QuerySet::<Self>::default()
5302                    .filter(&_key, #root::core::SqlValue::I64(year))
5303                    .fetch(pool)
5304                    .await
5305            }
5306
5307            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5308            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5309            /// `month` is 1–12.
5310            ///
5311            /// # Errors
5312            /// As [`FetcherPool::fetch`].
5313            ///
5314            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5315            pub async fn where_month(
5316                col: &str,
5317                month: i64,
5318                pool: &#root::sql::Pool,
5319            ) -> ::core::result::Result<
5320                ::std::vec::Vec<Self>,
5321                #root::sql::ExecError,
5322            > {
5323                use #root::sql::FetcherPool as _;
5324                let _key = ::std::format!("{}__month", col);
5325                #root::query::QuerySet::<Self>::default()
5326                    .filter(&_key, #root::core::SqlValue::I64(month))
5327                    .fetch(pool)
5328                    .await
5329            }
5330
5331            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5332            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5333            /// `day` is 1–31.
5334            ///
5335            /// # Errors
5336            /// As [`FetcherPool::fetch`].
5337            ///
5338            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5339            pub async fn where_day(
5340                col: &str,
5341                day: i64,
5342                pool: &#root::sql::Pool,
5343            ) -> ::core::result::Result<
5344                ::std::vec::Vec<Self>,
5345                #root::sql::ExecError,
5346            > {
5347                use #root::sql::FetcherPool as _;
5348                let _key = ::std::format!("{}__day", col);
5349                #root::query::QuerySet::<Self>::default()
5350                    .filter(&_key, #root::core::SqlValue::I64(day))
5351                    .fetch(pool)
5352                    .await
5353            }
5354
5355            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5356            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5357            /// `hour` is 0–23.
5358            ///
5359            /// # Errors
5360            /// As [`FetcherPool::fetch`].
5361            ///
5362            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5363            pub async fn where_hour(
5364                col: &str,
5365                hour: i64,
5366                pool: &#root::sql::Pool,
5367            ) -> ::core::result::Result<
5368                ::std::vec::Vec<Self>,
5369                #root::sql::ExecError,
5370            > {
5371                use #root::sql::FetcherPool as _;
5372                let _key = ::std::format!("{}__hour", col);
5373                #root::query::QuerySet::<Self>::default()
5374                    .filter(&_key, #root::core::SqlValue::I64(hour))
5375                    .fetch(pool)
5376                    .await
5377            }
5378
5379            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5380            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5381            /// `minute` is 0–59.
5382            ///
5383            /// # Errors
5384            /// As [`FetcherPool::fetch`].
5385            ///
5386            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5387            pub async fn where_minute(
5388                col: &str,
5389                minute: i64,
5390                pool: &#root::sql::Pool,
5391            ) -> ::core::result::Result<
5392                ::std::vec::Vec<Self>,
5393                #root::sql::ExecError,
5394            > {
5395                use #root::sql::FetcherPool as _;
5396                let _key = ::std::format!("{}__minute", col);
5397                #root::query::QuerySet::<Self>::default()
5398                    .filter(&_key, #root::core::SqlValue::I64(minute))
5399                    .fetch(pool)
5400                    .await
5401            }
5402
5403            /// Fetch every row where `<col> LIKE <pattern>` —
5404            /// caller-supplied pattern (must include `%` / `_`
5405            /// wildcards manually). Eloquent `Model::whereLike`
5406            /// parity.
5407            ///
5408            /// # Errors
5409            /// As [`FetcherPool::fetch`].
5410            ///
5411            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5412            pub async fn where_like(
5413                col: &str,
5414                pattern: impl ::core::convert::Into<::std::string::String>,
5415                pool: &#root::sql::Pool,
5416            ) -> ::core::result::Result<
5417                ::std::vec::Vec<Self>,
5418                #root::sql::ExecError,
5419            > {
5420                use #root::sql::FetcherPool as _;
5421                let _key = ::std::format!("{}__like", col);
5422                #root::query::QuerySet::<Self>::default()
5423                    .filter(
5424                        &_key,
5425                        #root::core::SqlValue::String(pattern.into()),
5426                    )
5427                    .fetch(pool)
5428                    .await
5429            }
5430
5431            /// Fetch every row where `<col> ILIKE <pattern>` —
5432            /// case-insensitive LIKE (PG native, MySQL/SQLite
5433            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5434            ///
5435            /// # Errors
5436            /// As [`FetcherPool::fetch`].
5437            ///
5438            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5439            pub async fn where_ilike(
5440                col: &str,
5441                pattern: impl ::core::convert::Into<::std::string::String>,
5442                pool: &#root::sql::Pool,
5443            ) -> ::core::result::Result<
5444                ::std::vec::Vec<Self>,
5445                #root::sql::ExecError,
5446            > {
5447                use #root::sql::FetcherPool as _;
5448                let _key = ::std::format!("{}__ilike", col);
5449                #root::query::QuerySet::<Self>::default()
5450                    .filter(
5451                        &_key,
5452                        #root::core::SqlValue::String(pattern.into()),
5453                    )
5454                    .fetch(pool)
5455                    .await
5456            }
5457
5458            /// Fetch every row where `<col>` starts with `prefix`
5459            /// (auto-appends `%`). Eloquent
5460            /// `whereLike("col", "$prefix%")` parity.
5461            ///
5462            /// # Errors
5463            /// As [`FetcherPool::fetch`].
5464            ///
5465            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5466            pub async fn where_starts_with(
5467                col: &str,
5468                prefix: impl ::core::convert::Into<::std::string::String>,
5469                pool: &#root::sql::Pool,
5470            ) -> ::core::result::Result<
5471                ::std::vec::Vec<Self>,
5472                #root::sql::ExecError,
5473            > {
5474                use #root::sql::FetcherPool as _;
5475                let _key = ::std::format!("{}__startswith", col);
5476                #root::query::QuerySet::<Self>::default()
5477                    .filter(
5478                        &_key,
5479                        #root::core::SqlValue::String(prefix.into()),
5480                    )
5481                    .fetch(pool)
5482                    .await
5483            }
5484
5485            /// Fetch every row where `<col>` ends with `suffix`
5486            /// (auto-prepends `%`). Eloquent
5487            /// `whereLike("col", "%$suffix")` parity.
5488            ///
5489            /// # Errors
5490            /// As [`FetcherPool::fetch`].
5491            ///
5492            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5493            pub async fn where_ends_with(
5494                col: &str,
5495                suffix: impl ::core::convert::Into<::std::string::String>,
5496                pool: &#root::sql::Pool,
5497            ) -> ::core::result::Result<
5498                ::std::vec::Vec<Self>,
5499                #root::sql::ExecError,
5500            > {
5501                use #root::sql::FetcherPool as _;
5502                let _key = ::std::format!("{}__endswith", col);
5503                #root::query::QuerySet::<Self>::default()
5504                    .filter(
5505                        &_key,
5506                        #root::core::SqlValue::String(suffix.into()),
5507                    )
5508                    .fetch(pool)
5509                    .await
5510            }
5511
5512            /// Fetch every row where `<col>` contains `substr`
5513            /// (auto-wraps with `%`).
5514            /// Eloquent `whereLike("col", "%$substr%")` parity.
5515            ///
5516            /// # Errors
5517            /// As [`FetcherPool::fetch`].
5518            ///
5519            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5520            pub async fn where_contains(
5521                col: &str,
5522                substr: impl ::core::convert::Into<::std::string::String>,
5523                pool: &#root::sql::Pool,
5524            ) -> ::core::result::Result<
5525                ::std::vec::Vec<Self>,
5526                #root::sql::ExecError,
5527            > {
5528                use #root::sql::FetcherPool as _;
5529                let _key = ::std::format!("{}__contains", col);
5530                #root::query::QuerySet::<Self>::default()
5531                    .filter(
5532                        &_key,
5533                        #root::core::SqlValue::String(substr.into()),
5534                    )
5535                    .fetch(pool)
5536                    .await
5537            }
5538
5539            /// Fetch every row where `<col> > val`. Eloquent
5540            /// `Model::where($col, ">", $val)->get()` parity.
5541            ///
5542            /// # Errors
5543            /// As [`FetcherPool::fetch`].
5544            ///
5545            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5546            pub async fn where_gt(
5547                col: &str,
5548                val: impl ::core::convert::Into<#root::core::SqlValue>,
5549                pool: &#root::sql::Pool,
5550            ) -> ::core::result::Result<
5551                ::std::vec::Vec<Self>,
5552                #root::sql::ExecError,
5553            > {
5554                use #root::sql::FetcherPool as _;
5555                let _key = ::std::format!("{}__gt", col);
5556                #root::query::QuerySet::<Self>::default()
5557                    .filter(&_key, val)
5558                    .fetch(pool)
5559                    .await
5560            }
5561
5562            /// Fetch every row where `<col> >= val`. Eloquent
5563            /// `Model::where($col, ">=", $val)->get()` parity.
5564            ///
5565            /// # Errors
5566            /// As [`FetcherPool::fetch`].
5567            ///
5568            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5569            pub async fn where_gte(
5570                col: &str,
5571                val: impl ::core::convert::Into<#root::core::SqlValue>,
5572                pool: &#root::sql::Pool,
5573            ) -> ::core::result::Result<
5574                ::std::vec::Vec<Self>,
5575                #root::sql::ExecError,
5576            > {
5577                use #root::sql::FetcherPool as _;
5578                let _key = ::std::format!("{}__gte", col);
5579                #root::query::QuerySet::<Self>::default()
5580                    .filter(&_key, val)
5581                    .fetch(pool)
5582                    .await
5583            }
5584
5585            /// Fetch every row where `<col> < val`. Eloquent
5586            /// `Model::where($col, "<", $val)->get()` parity.
5587            ///
5588            /// # Errors
5589            /// As [`FetcherPool::fetch`].
5590            ///
5591            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5592            pub async fn where_lt(
5593                col: &str,
5594                val: impl ::core::convert::Into<#root::core::SqlValue>,
5595                pool: &#root::sql::Pool,
5596            ) -> ::core::result::Result<
5597                ::std::vec::Vec<Self>,
5598                #root::sql::ExecError,
5599            > {
5600                use #root::sql::FetcherPool as _;
5601                let _key = ::std::format!("{}__lt", col);
5602                #root::query::QuerySet::<Self>::default()
5603                    .filter(&_key, val)
5604                    .fetch(pool)
5605                    .await
5606            }
5607
5608            /// Fetch every row where `<col> <= val`. Eloquent
5609            /// `Model::where($col, "<=", $val)->get()` parity.
5610            ///
5611            /// # Errors
5612            /// As [`FetcherPool::fetch`].
5613            ///
5614            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5615            pub async fn where_lte(
5616                col: &str,
5617                val: impl ::core::convert::Into<#root::core::SqlValue>,
5618                pool: &#root::sql::Pool,
5619            ) -> ::core::result::Result<
5620                ::std::vec::Vec<Self>,
5621                #root::sql::ExecError,
5622            > {
5623                use #root::sql::FetcherPool as _;
5624                let _key = ::std::format!("{}__lte", col);
5625                #root::query::QuerySet::<Self>::default()
5626                    .filter(&_key, val)
5627                    .fetch(pool)
5628                    .await
5629            }
5630
5631            /// Fetch every row where `<col> <> val`. Eloquent
5632            /// `Model::where($col, "!=", $val)->get()` parity.
5633            ///
5634            /// # Errors
5635            /// As [`FetcherPool::fetch`].
5636            ///
5637            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5638            pub async fn where_ne(
5639                col: &str,
5640                val: impl ::core::convert::Into<#root::core::SqlValue>,
5641                pool: &#root::sql::Pool,
5642            ) -> ::core::result::Result<
5643                ::std::vec::Vec<Self>,
5644                #root::sql::ExecError,
5645            > {
5646                use #root::sql::FetcherPool as _;
5647                let _key = ::std::format!("{}__ne", col);
5648                #root::query::QuerySet::<Self>::default()
5649                    .filter(&_key, val)
5650                    .fetch(pool)
5651                    .await
5652            }
5653
5654            /// Fetch every row matching `<col> = val` for ANY of the
5655            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5656            /// parity. Empty `cols` returns no rows.
5657            ///
5658            /// Resolves each `&str` column to its SCHEMA-registered
5659            /// `&'static str` once and builds a single OR-composed
5660            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5661            ///
5662            /// # Errors
5663            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5664            /// when any column is not declared on the model.
5665            ///
5666            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5667            pub async fn where_any(
5668                cols: &[&str],
5669                val: impl ::core::convert::Into<#root::core::SqlValue>,
5670                pool: &#root::sql::Pool,
5671            ) -> ::core::result::Result<
5672                ::std::vec::Vec<Self>,
5673                #root::sql::ExecError,
5674            > {
5675                Self::__where_multi(cols, val, false, pool).await
5676            }
5677
5678            /// Fetch every row matching `<col> = val` for ALL listed
5679            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5680            /// Empty `cols` returns every row (vacuous AND).
5681            ///
5682            /// # Errors
5683            /// As [`Self::where_any`].
5684            pub async fn where_all(
5685                cols: &[&str],
5686                val: impl ::core::convert::Into<#root::core::SqlValue>,
5687                pool: &#root::sql::Pool,
5688            ) -> ::core::result::Result<
5689                ::std::vec::Vec<Self>,
5690                #root::sql::ExecError,
5691            > {
5692                Self::__where_multi(cols, val, true, pool).await
5693            }
5694
5695            /// Internal: build a Q expression composing `cols` via
5696            /// AND (`all`) or OR (`!all`), then fetch. Backs
5697            /// `where_any` / `where_all`.
5698            #[doc(hidden)]
5699            pub async fn __where_multi(
5700                cols: &[&str],
5701                val: impl ::core::convert::Into<#root::core::SqlValue>,
5702                all: bool,
5703                pool: &#root::sql::Pool,
5704            ) -> ::core::result::Result<
5705                ::std::vec::Vec<Self>,
5706                #root::sql::ExecError,
5707            > {
5708                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5709                    .await
5710            }
5711
5712            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5713            /// parity. PK-ordered
5714            /// is NOT guaranteed without an explicit `order_by` —
5715            /// drop into `Self::query()` for that.
5716            ///
5717            /// # Errors
5718            /// As [`FetcherPool::fetch`].
5719            ///
5720            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5721            pub async fn take(
5722                n: i64,
5723                pool: &#root::sql::Pool,
5724            ) -> ::core::result::Result<
5725                ::std::vec::Vec<Self>,
5726                #root::sql::ExecError,
5727            > {
5728                use #root::sql::FetcherPool as _;
5729                #root::query::QuerySet::<Self>::default()
5730                    .limit(n)
5731                    .fetch(pool)
5732                    .await
5733            }
5734
5735            /// Fetch the page-th window of `per_page` rows
5736            /// (1-indexed). Eloquent
5737            /// `Model::query()->forPage($page, $perPage)->get()`
5738            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5739            /// LIMIT per_page`; for large offsets this is O(N) —
5740            /// prefer keyset pagination via PK on hot paths.
5741            ///
5742            /// # Errors
5743            /// As [`FetcherPool::fetch`].
5744            ///
5745            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5746            pub async fn for_page(
5747                page: i64,
5748                per_page: i64,
5749                pool: &#root::sql::Pool,
5750            ) -> ::core::result::Result<
5751                ::std::vec::Vec<Self>,
5752                #root::sql::ExecError,
5753            > {
5754                use #root::sql::FetcherPool as _;
5755                let _offset = #root::list_params::page_offset(page, per_page);
5756                #root::query::QuerySet::<Self>::default()
5757                    .limit(per_page)
5758                    .offset(_offset)
5759                    .fetch(pool)
5760                    .await
5761            }
5762
5763            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5764            /// one page of rows AND the total row count in one
5765            /// call. Returns `(rows, total)`. Two queries: a
5766            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5767            /// for the total.
5768            ///
5769            /// Useful for paginated UIs that need both the visible
5770            /// rows AND a "Page X of Y" / total-count widget. For
5771            /// large tables prefer keyset pagination + cached count;
5772            /// every call to `paginate` re-counts the full table.
5773            ///
5774            /// Same 1-indexed `page` convention as [`Self::for_page`].
5775            ///
5776            /// # Errors
5777            /// As [`Self::for_page`] and [`Self::count`].
5778            pub async fn paginate(
5779                page: i64,
5780                per_page: i64,
5781                pool: &#root::sql::Pool,
5782            ) -> ::core::result::Result<
5783                (::std::vec::Vec<Self>, i64),
5784                #root::sql::ExecError,
5785            > {
5786                // Route through the queryset rather than `Self::count`:
5787                // the `count` inherent method is suppressed on models
5788                // that declare a field named `count` (field/shortcut
5789                // collision guard), and `paginate` must still compile
5790                // for those models.
5791                let total = {
5792                    use #root::sql::CounterPool as _;
5793                    #root::query::QuerySet::<Self>::default()
5794                        .count(pool)
5795                        .await?
5796                };
5797                let rows = Self::for_page(page, per_page, pool).await?;
5798                ::core::result::Result::Ok((rows, total))
5799            }
5800
5801            /// Bulk-update — set `set_col = set_val` on every row
5802            /// matching `where_col = where_val`. Returns affected row
5803            /// count. Eloquent
5804            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5805            /// parity.
5806            ///
5807            /// For multi-column updates drop into the queryset
5808            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5809            ///
5810            /// # Errors
5811            /// As [`UpdaterPool::execute_pool`].
5812            ///
5813            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5814            pub async fn update_where(
5815                where_col: &str,
5816                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5817                set_col: &str,
5818                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5819                pool: &#root::sql::Pool,
5820            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5821                let _query = #root::query::QuerySet::<Self>::default()
5822                    .filter(where_col, where_val)
5823                    .update()
5824                    .set(set_col, set_val)
5825                    .compile()?;
5826                #bulk_update_run
5827            }
5828
5829            /// Bulk-delete — remove every row matching
5830            /// `where_col = where_val`. Returns affected row count.
5831            /// Eloquent
5832            /// `Model::where($where_col, $where_val)->delete()` parity.
5833            ///
5834            /// # Errors
5835            /// As [`#root::sql::delete_pool`].
5836            pub async fn delete_where(
5837                where_col: &str,
5838                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5839                pool: &#root::sql::Pool,
5840            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5841                let _query = #root::query::QuerySet::<Self>::default()
5842                    .filter_op(where_col, #root::core::Op::Eq, where_val)
5843                    .compile_delete()?;
5844                #bulk_delete_run
5845            }
5846
5847            /// Bulk-update — set `set_col = set_val` on EVERY row of
5848            /// this model's table. **No WHERE clause** — use with
5849            /// care. Eloquent `Model::query()->update([$col => $val])`
5850            /// parity.
5851            ///
5852            /// Use for backfills, one-shot reset flows, etc. The
5853            /// returned count is rows affected.
5854            ///
5855            /// # Errors
5856            /// As [`UpdaterPool::execute_pool`].
5857            ///
5858            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5859            pub async fn update_all(
5860                set_col: &str,
5861                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5862                pool: &#root::sql::Pool,
5863            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5864                let _query = #root::query::QuerySet::<Self>::default()
5865                    .update()
5866                    .set(set_col, set_val)
5867                    .compile()?;
5868                #bulk_update_run
5869            }
5870
5871            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5872            /// Eloquent `Model::whereNotLike` parity. Pattern is
5873            /// passed verbatim — caller controls `%` / `_`.
5874            ///
5875            /// # Errors
5876            /// As [`FetcherPool::fetch`].
5877            ///
5878            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5879            pub async fn where_not_like(
5880                col: &str,
5881                pattern: impl ::core::convert::Into<::std::string::String>,
5882                pool: &#root::sql::Pool,
5883            ) -> ::core::result::Result<
5884                ::std::vec::Vec<Self>,
5885                #root::sql::ExecError,
5886            > {
5887                use #root::sql::FetcherPool as _;
5888                let _key = ::std::format!("{}__not_like", col);
5889                #root::query::QuerySet::<Self>::default()
5890                    .filter(
5891                        &_key,
5892                        #root::core::SqlValue::String(pattern.into()),
5893                    )
5894                    .fetch(pool)
5895                    .await
5896            }
5897
5898            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5899            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5900            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5901            ///
5902            /// # Errors
5903            /// As [`FetcherPool::fetch`].
5904            ///
5905            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5906            pub async fn where_not_ilike(
5907                col: &str,
5908                pattern: impl ::core::convert::Into<::std::string::String>,
5909                pool: &#root::sql::Pool,
5910            ) -> ::core::result::Result<
5911                ::std::vec::Vec<Self>,
5912                #root::sql::ExecError,
5913            > {
5914                use #root::sql::FetcherPool as _;
5915                let _key = ::std::format!("{}__not_ilike", col);
5916                #root::query::QuerySet::<Self>::default()
5917                    .filter(
5918                        &_key,
5919                        #root::core::SqlValue::String(pattern.into()),
5920                    )
5921                    .fetch(pool)
5922                    .await
5923            }
5924
5925            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5926            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5927            /// parity.
5928            ///
5929            /// # Errors
5930            /// As [`FetcherPool::fetch`].
5931            ///
5932            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5933            pub async fn where_not_between(
5934                col: &str,
5935                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5936                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5937                pool: &#root::sql::Pool,
5938            ) -> ::core::result::Result<
5939                ::std::vec::Vec<Self>,
5940                #root::sql::ExecError,
5941            > {
5942                use #root::sql::FetcherPool as _;
5943                let _key = ::std::format!("{}__not_between", col);
5944                let _vals = #root::core::SqlValue::List(::std::vec![
5945                    ::core::convert::Into::into(lo),
5946                    ::core::convert::Into::into(hi),
5947                ]);
5948                #root::query::QuerySet::<Self>::default()
5949                    .filter(&_key, _vals)
5950                    .fetch(pool)
5951                    .await
5952            }
5953
5954            /// Returns the SQL table name for this model. Eloquent
5955            /// `$model->getTable()` parity.
5956            #[must_use]
5957            pub fn table_name() -> &'static str {
5958                <Self as #root::core::Model>::SCHEMA.table
5959            }
5960
5961            /// Returns the SQL column name of this model's primary
5962            /// key, or `None` when the model has no
5963            /// `#[rustango(primary_key)]`. Eloquent
5964            /// `$model->getKeyName()` parity.
5965            #[must_use]
5966            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5967                <Self as #root::core::Model>::SCHEMA
5968                    .primary_key()
5969                    .map(|f| f.column)
5970            }
5971
5972            /// Fetch every row where `<col> BETWEEN lo AND hi`
5973            /// (inclusive on both ends — same as SQL). Eloquent
5974            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5975            ///
5976            /// # Errors
5977            /// As [`FetcherPool::fetch`].
5978            ///
5979            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5980            pub async fn where_between(
5981                col: &str,
5982                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5983                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5984                pool: &#root::sql::Pool,
5985            ) -> ::core::result::Result<
5986                ::std::vec::Vec<Self>,
5987                #root::sql::ExecError,
5988            > {
5989                use #root::sql::FetcherPool as _;
5990                let _key = ::std::format!("{}__between", col);
5991                let _vals = #root::core::SqlValue::List(::std::vec![
5992                    ::core::convert::Into::into(lo),
5993                    ::core::convert::Into::into(hi),
5994                ]);
5995                #root::query::QuerySet::<Self>::default()
5996                    .filter(&_key, _vals)
5997                    .fetch(pool)
5998                    .await
5999            }
6000
6001            /// Fetch the first row where `<col> = <val>`. Returns
6002            /// `Ok(None)` when no row matches. Eloquent
6003            /// `Model::firstWhere($col, $val)` parity.
6004            ///
6005            /// Thin wrapper over `QuerySet::<Self>::default()
6006            /// .filter(col, val).first(pool)`. Use this when you
6007            /// want one row identified by a non-PK column (e.g.
6008            /// `User::first_where_pool("email", "x@y.com", &pool)`).
6009            ///
6010            /// `val` accepts any value `Into<SqlValue>` so plain
6011            /// strings, ints, UUIDs, etc. all work.
6012            ///
6013            /// # Errors
6014            /// As `QuerySet::first`.
6015            pub async fn first_where(
6016                col: &str,
6017                val: impl ::core::convert::Into<#root::core::SqlValue>,
6018                pool: &#root::sql::Pool,
6019            ) -> ::core::result::Result<
6020                ::core::option::Option<Self>,
6021                #root::sql::ExecError,
6022            > {
6023                #root::query::QuerySet::<Self>::default()
6024                    .filter(col, val)
6025                    .first(pool)
6026                    .await
6027            }
6028
6029            #value_method
6030
6031            /// Fetch the row with the largest `field` value —
6032            /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
6033            /// `Ok(None)` for an empty table. Eloquent
6034            /// `Model::latest($field)->first()` parity.
6035            /// Thin wrapper over `QuerySet::<Self>::default()
6036            /// .latest(field, pool)`.
6037            ///
6038            /// **Field name** is the Rust field ident as a string
6039            /// (not the SQL column). It is a runtime `&str`, so an
6040            /// unknown one compiles and surfaces as
6041            /// `ExecError::Query(QueryError::UnknownField)` when the
6042            /// query runs.
6043            ///
6044            /// # Errors
6045            /// As `QuerySet::latest`.
6046            pub async fn latest(
6047                field: &str,
6048                pool: &#root::sql::Pool,
6049            ) -> ::core::result::Result<
6050                ::core::option::Option<Self>,
6051                #root::sql::ExecError,
6052            > {
6053                #root::query::QuerySet::<Self>::default()
6054                    .latest(field, pool)
6055                    .await
6056            }
6057
6058            /// Sibling of [`Self::latest`] — fetches the row
6059            /// with the smallest `field` value (`ORDER BY <field>
6060            /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
6061            /// ->first()` parity.
6062            ///
6063            /// # Errors
6064            /// As [`Self::latest`].
6065            pub async fn earliest(
6066                field: &str,
6067                pool: &#root::sql::Pool,
6068            ) -> ::core::result::Result<
6069                ::core::option::Option<Self>,
6070                #root::sql::ExecError,
6071            > {
6072                #root::query::QuerySet::<Self>::default()
6073                    .earliest(field, pool)
6074                    .await
6075            }
6076
6077            #count_method
6078
6079            /// `true` when the table contains at least one row.
6080            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6081            /// over `QuerySet::<Self>::default().exists(pool)`.
6082            ///
6083            /// # Errors
6084            /// As [`ExistsPool::exists`].
6085            ///
6086            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6087            pub async fn exists(
6088                pool: &#root::sql::Pool,
6089            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6090                use #root::sql::ExistsPool as _;
6091                #root::query::QuerySet::<Self>::default()
6092                    .exists(pool)
6093                    .await
6094            }
6095
6096            /// Inverse of [`Self::exists`] — returns `true` when
6097            /// the table has zero rows. Eloquent
6098            /// `Model::doesntExist()` parity.
6099            ///
6100            /// # Errors
6101            /// As [`Self::exists`].
6102            pub async fn doesnt_exist(
6103                pool: &#root::sql::Pool,
6104            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6105                Self::exists(pool).await.map(|e| !e)
6106            }
6107
6108            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6109            /// `true` when a row with primary key `pk` exists in
6110            /// the table. Sugar over
6111            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6112            ///
6113            /// Differs from [`Self::exists`] (which checks "any row
6114            /// in the table") by checking a specific PK existence.
6115            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6116            /// because the row is never materialized — the SQL is
6117            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6118            ///
6119            /// # Errors
6120            /// As [`#root::sql::ExistsPool::contains_pk`].
6121            pub async fn contains_pk(
6122                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6123                pool: &#root::sql::Pool,
6124            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6125                use #root::sql::ExistsPool as _;
6126                #root::query::QuerySet::<Self>::default()
6127                    .contains_pk(pool, pk)
6128                    .await
6129            }
6130
6131            #sum_method
6132            #avg_method
6133            #min_method
6134            #max_method
6135
6136            /// Fetch every row of this model from `pool`. Eloquent
6137            /// `Model::all()` parity — a thin wrapper over
6138            /// `QuerySet::<Self>::default().fetch(pool)`.
6139            ///
6140            /// **Use with care on large tables** — there's no
6141            /// pagination or limit; the entire table is materialized
6142            /// into memory. For anything beyond fixture / lookup
6143            /// tables, page through `QuerySet::<Self>::default()
6144            /// .order_by(...).limit(N).offset(M).fetch(pool)`
6145            /// or stream via `.iterator(chunk_size)`.
6146            ///
6147            /// # Errors
6148            /// As [`FetcherPool::fetch`].
6149            ///
6150            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6151            pub async fn all(
6152                pool: &#root::sql::Pool,
6153            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6154            {
6155                use #root::sql::FetcherPool as _;
6156                #root::query::QuerySet::<Self>::default()
6157                    .fetch(pool)
6158                    .await
6159            }
6160
6161            /// Look up every row whose primary key is in `pks`.
6162            /// Returns the matching rows in **inventory** order — NOT
6163            /// the order of `pks`. Empty `pks` returns an empty
6164            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6165            /// with a list) parity.
6166            ///
6167            /// Thin wrapper over `QuerySet::<Self>::default()
6168            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6169            /// Caller-supplied PKs that don't match a row are
6170            /// silently skipped (the returned vec is shorter than
6171            /// the input list). For an order-preserving / "fail
6172            /// when any missing" variant, build the queryset
6173            /// explicitly with `in_bulk` instead.
6174            ///
6175            /// Accepts any iterable whose elements are
6176            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6177            /// `Vec<Uuid>`, etc.
6178            ///
6179            /// # Errors
6180            /// As [`FetcherPool::fetch`].
6181            ///
6182            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6183            pub async fn find_many<V>(
6184                pks: impl ::core::iter::IntoIterator<Item = V>,
6185                pool: &#root::sql::Pool,
6186            ) -> ::core::result::Result<
6187                ::std::vec::Vec<Self>,
6188                #root::sql::ExecError,
6189            >
6190            where
6191                V: ::core::convert::Into<#root::core::SqlValue>,
6192            {
6193                use #root::sql::FetcherPool as _;
6194                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6195                    pks.into_iter().map(::core::convert::Into::into).collect();
6196                if _values.is_empty() {
6197                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6198                }
6199                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6200                #root::query::QuerySet::<Self>::default()
6201                    .filter(&_key, #root::core::SqlValue::List(_values))
6202                    .fetch(pool)
6203                    .await
6204            }
6205
6206            /// Look up the row whose primary key equals `pk`. Returns
6207            /// `Ok(None)` when no row matches, rather than erroring.
6208            /// Eloquent `Model::find`
6209            /// shape — accepts any value `Into<SqlValue>`.
6210            ///
6211            /// One-liner shortcut for the common
6212            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6213            /// .limit(1).fetch(pool).await?.into_iter().next()`
6214            /// dance.
6215            ///
6216            /// # Errors
6217            /// As [`FetcherPool::fetch`].
6218            ///
6219            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6220            pub async fn find(
6221                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6222                pool: &#root::sql::Pool,
6223            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6224            {
6225                use #root::sql::FetcherPool as _;
6226                let _pk_val: #root::core::SqlValue = pk.into();
6227                let mut _rows: ::std::vec::Vec<Self> =
6228                    #root::query::QuerySet::<Self>::default()
6229                        .filter(::core::stringify!(#pk_ident), _pk_val)
6230                        .limit(1)
6231                        .fetch(pool)
6232                        .await?;
6233                ::core::result::Result::Ok(_rows.into_iter().next())
6234            }
6235
6236            /// Look up the row whose primary key equals `pk`. Errors
6237            /// when no row matches — the throwing counterpart of
6238            /// [`Self::find`]. Eloquent `Model::findOrFail`
6239            /// parity.
6240            ///
6241            /// Translates the miss into
6242            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6243            /// so callers can `?`-bubble straight through the typical
6244            /// `ExecError` error chain.
6245            ///
6246            /// # Errors
6247            /// As [`Self::find`]; additionally
6248            /// [`sqlx::Error::RowNotFound`] when no row matches.
6249            ///
6250            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6251            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6252            pub async fn find_or_fail(
6253                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6254                pool: &#root::sql::Pool,
6255            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6256                match Self::find(pk, pool).await? {
6257                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6258                    ::core::option::Option::None => ::core::result::Result::Err(
6259                        #root::sql::ExecError::Driver(
6260                            #root::sql::sqlx::Error::RowNotFound,
6261                        ),
6262                    ),
6263                }
6264            }
6265
6266            /// Look up multiple rows by primary key, **failing** if
6267            /// any requested PK is missing. Eloquent
6268            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6269            /// inventory order (NOT request order — same as
6270            /// [`Self::find_many`]).
6271            ///
6272            /// Empty `pks` returns an empty `Vec` (no rows requested
6273            /// → nothing to fail on).
6274            ///
6275            /// Differs from [`Self::find_many`] in that this method
6276            /// requires every PK to resolve. If even one is missing,
6277            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6278            /// in `ExecError::Driver`). Rows are deduped at the SQL
6279            /// layer, so passing the same PK twice in `pks` counts as
6280            /// one expected row.
6281            ///
6282            /// # Errors
6283            /// As [`Self::find_many`], plus `RowNotFound` when the
6284            /// returned row count is less than the count of distinct
6285            /// requested PKs.
6286            pub async fn find_many_or_fail<V>(
6287                pks: impl ::core::iter::IntoIterator<Item = V>,
6288                pool: &#root::sql::Pool,
6289            ) -> ::core::result::Result<
6290                ::std::vec::Vec<Self>,
6291                #root::sql::ExecError,
6292            >
6293            where
6294                V: ::core::convert::Into<#root::core::SqlValue>,
6295            {
6296                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6297                    pks.into_iter().map(::core::convert::Into::into).collect();
6298                if _values.is_empty() {
6299                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6300                }
6301                // Dedup the requested PK list before counting so
6302                // duplicate-PK requests don't false-fail (the SQL
6303                // `IN (...)` clause naturally dedups too).
6304                let mut _seen: ::std::collections::HashSet<
6305                    ::std::string::String,
6306                > = ::std::collections::HashSet::new();
6307                for v in &_values {
6308                    _seen.insert(v.to_display_string());
6309                }
6310                let _expected = _seen.len();
6311                let _rows = Self::find_many(_values, pool).await?;
6312                if _rows.len() < _expected {
6313                    return ::core::result::Result::Err(
6314                        #root::sql::ExecError::Driver(
6315                            #root::sql::sqlx::Error::RowNotFound,
6316                        ),
6317                    );
6318                }
6319                ::core::result::Result::Ok(_rows)
6320            }
6321
6322            /// Find by primary key or run `fallback` to produce a
6323            /// default row to return. Eloquent
6324            /// `Model::findOr($pk, fn() => …)` parity.
6325            ///
6326            /// Unlike [`Self::find_or_fail`] (which raises on
6327            /// miss), this is the "give me something sensible"
6328            /// branch: typical use is "fetch the user's row, else
6329            /// fall back to an anonymous/guest stub".
6330            ///
6331            /// `fallback` runs only when no row matches — the DB
6332            /// round-trip happens unconditionally.
6333            ///
6334            /// # Errors
6335            /// As [`Self::find`].
6336            pub async fn find_or<F>(
6337                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6338                pool: &#root::sql::Pool,
6339                fallback: F,
6340            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6341            where
6342                F: ::core::ops::FnOnce() -> Self,
6343            {
6344                ::core::result::Result::Ok(
6345                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6346                )
6347            }
6348
6349            /// Same as [`Self::find_or`] but also returns a `bool`
6350            /// indicating whether the row was found in the DB
6351            /// (`true`) or freshly built from `fallback` (`false`).
6352            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6353            /// in PHP the returned model also exposes `->exists`;
6354            /// here that's surfaced as the second tuple element.
6355            ///
6356            /// Useful for edit-or-create form handlers where the
6357            /// caller needs to know whether to PATCH or POST when
6358            /// the user submits the form.
6359            ///
6360            /// # Errors
6361            /// As [`Self::find`].
6362            pub async fn find_or_new<F>(
6363                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6364                pool: &#root::sql::Pool,
6365                fallback: F,
6366            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6367            where
6368                F: ::core::ops::FnOnce() -> Self,
6369            {
6370                match Self::find(pk, pool).await? {
6371                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6372                    ::core::option::Option::None => {
6373                        ::core::result::Result::Ok((fallback(), false))
6374                    }
6375                }
6376            }
6377
6378            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6379            /// [`Self::find_or_new`] but **persists** the new row
6380            /// when the PK isn't found. Returns `(row, exists: bool)`
6381            /// — `exists=true` when the PK was found,
6382            /// `false` when a fresh row was inserted.
6383            ///
6384            /// ```ignore
6385            /// let (post, found) = Post::find_or_insert(
6386            ///     pk,
6387            ///     &pool,
6388            ///     || Post { id: Auto::default(), title: "new".into() },
6389            /// ).await?;
6390            /// // `found` true → returned existing row;
6391            /// // `found` false → fallback was inserted, `post.id` populated.
6392            /// ```
6393            ///
6394            /// **Caveat**: two concurrent calls that both miss the
6395            /// find can both INSERT, violating uniqueness. Wrap in a
6396            /// transaction or rely on a UNIQUE constraint + handle
6397            /// the conflict error if you need race-free semantics.
6398            ///
6399            /// # Errors
6400            /// As [`Self::find`] and [`Self::save_pool`].
6401            pub async fn find_or_insert<F>(
6402                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6403                pool: &#root::sql::Pool,
6404                fallback: F,
6405            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6406            where
6407                F: ::core::ops::FnOnce() -> Self,
6408            {
6409                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6410                    return ::core::result::Result::Ok((_row, true));
6411                }
6412                let mut _new = fallback();
6413                _new.save_pool(pool).await?;
6414                ::core::result::Result::Ok((_new, false))
6415            }
6416
6417            /// Fetch the first row of the table, or run `fallback`
6418            /// when the table is empty. Eloquent
6419            /// `Model::firstOr(fn() => …)` parity.
6420            ///
6421            /// # Errors
6422            /// As [`Self::first`].
6423            pub async fn first_or<F>(
6424                pool: &#root::sql::Pool,
6425                fallback: F,
6426            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6427            where
6428                F: ::core::ops::FnOnce() -> Self,
6429            {
6430                // Route through the queryset rather than `Self::first`,
6431                // which is suppressed on models with a field named
6432                // `first` (field/shortcut collision guard).
6433                ::core::result::Result::Ok(
6434                    #root::query::QuerySet::<Self>::default()
6435                        .first(pool)
6436                        .await?
6437                        .unwrap_or_else(fallback),
6438                )
6439            }
6440
6441            /// Fetch exactly one row matching `<col> = <val>`. Errors
6442            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6443            /// or more than one matches
6444            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6445            /// Eloquent `Model::sole($col, $val)` parity.
6446            ///
6447            /// # Errors
6448            /// As [`Self::where_`] plus the explicit
6449            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6450            pub async fn sole(
6451                col: &str,
6452                val: impl ::core::convert::Into<#root::core::SqlValue>,
6453                pool: &#root::sql::Pool,
6454            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6455                let mut _rows = Self::where_(col, val, pool).await?;
6456                match _rows.len() {
6457                    0 => ::core::result::Result::Err(
6458                        #root::sql::ExecError::Driver(
6459                            #root::sql::sqlx::Error::RowNotFound,
6460                        ),
6461                    ),
6462                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6463                    n => ::core::result::Result::Err(
6464                        #root::sql::ExecError::MultipleRowsReturned {
6465                            op: "sole",
6466                            table: <Self as #root::core::Model>::SCHEMA.name,
6467                            count: n,
6468                        },
6469                    ),
6470                }
6471            }
6472        }
6473    } else {
6474        quote!()
6475    };
6476
6477    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6478    // the non-audited `_pool` methods but execute against an open
6479    // `PoolTx` so the writes participate in the caller's transaction.
6480    // Auditing inside TX is deferred; these always use the plain
6481    // executor primitives regardless of whether the model is audited.
6482    let tx_insert_method = if fields.has_auto {
6483        let pushes = &fields.insert_pushes;
6484        let returning_cols = &fields.returning_cols;
6485        quote! {
6486            /// Insert this row inside an open transaction, populating
6487            /// any `Auto<T>` PK from the auto-assigned value. Works
6488            /// against any backend that `tx` wraps.
6489            ///
6490            /// # Errors
6491            /// As [`Self::insert_pool`].
6492            pub async fn insert_tx(
6493                &mut self,
6494                tx: &mut #root::sql::PoolTx<'_>,
6495            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6496                let mut _columns: ::std::vec::Vec<&'static str> =
6497                    ::std::vec::Vec::new();
6498                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6499                    ::std::vec::Vec::new();
6500                #( #pushes )*
6501                let _query = #root::core::InsertQuery::new(
6502                    <Self as #root::core::Model>::SCHEMA,
6503                    _columns,
6504                    _values,
6505                )
6506                .returning(::std::vec![ #( #returning_cols ),* ]);
6507                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6508                #root::sql::apply_auto_pk(_result, self)
6509            }
6510        }
6511    } else {
6512        let insert_columns = &fields.insert_columns;
6513        let insert_values = &fields.insert_values;
6514        quote! {
6515            /// Insert this row inside an open transaction.
6516            ///
6517            /// # Errors
6518            /// As [`Self::insert_pool`].
6519            pub async fn insert_tx(
6520                &self,
6521                tx: &mut #root::sql::PoolTx<'_>,
6522            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6523                let _query = #root::core::InsertQuery::new(
6524                    <Self as #root::core::Model>::SCHEMA,
6525                    ::std::vec![ #( #insert_columns ),* ],
6526                    ::std::vec![ #( #insert_values ),* ],
6527                );
6528                #root::sql::insert_tx(tx, &_query).await
6529            }
6530        }
6531    };
6532
6533    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6534        let pk_column_lit = pk_col.as_str();
6535        let assignments = &fields.update_assignments;
6536        let dispatch_unset = if fields.pk_is_auto {
6537            quote! {
6538                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6539                    return self.insert_tx(tx).await.map(|()| 1u64);
6540                }
6541            }
6542        } else {
6543            quote!()
6544        };
6545        let write = restore_auto_now_unless(
6546            &fields.auto_now_idents,
6547            &save_ret,
6548            &save_keep,
6549            &quote! {
6550                let _query = #root::core::UpdateQuery::new(
6551                    <Self as #root::core::Model>::SCHEMA,
6552                    ::std::vec![ #( #assignments ),* ],
6553                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6554                        #pk_column_lit,
6555                        #root::core::Op::Eq,
6556                        ::core::convert::Into::<#root::core::SqlValue>::into(
6557                            ::core::clone::Clone::clone(&self.#pk_ident)
6558                        ),
6559                    )),
6560                );
6561                let _affected = #root::sql::update_tx(tx, &_query).await?;
6562                ::core::result::Result::Ok(_affected)
6563            },
6564        );
6565        quote! {
6566            /// Save this row inside an open transaction. `INSERT` when
6567            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6568            /// PK. Works against any backend that `tx` wraps.
6569            ///
6570            /// # Errors
6571            /// As [`Self::save_pool`].
6572            pub async fn save_tx(
6573                &mut self,
6574                tx: &mut #root::sql::PoolTx<'_>,
6575            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6576                #dispatch_unset
6577                #write
6578            }
6579        }
6580    } else {
6581        quote!()
6582    };
6583
6584    let tx_delete_method = {
6585        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6586        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6587        if let Some(pk_ident) = pk_ident_for_tx {
6588            quote! {
6589                /// Delete the row identified by this instance's PK
6590                /// inside an open transaction. Works against any backend
6591                /// that `tx` wraps.
6592                ///
6593                /// # Errors
6594                /// As [`Self::delete_pool`].
6595                pub async fn delete_tx(
6596                    &self,
6597                    tx: &mut #root::sql::PoolTx<'_>,
6598                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6599                    let _query = #root::core::DeleteQuery::by_pk(
6600                        <Self as #root::core::Model>::SCHEMA,
6601                        #pk_column_lit,
6602                        ::core::convert::Into::<#root::core::SqlValue>::into(
6603                            ::core::clone::Clone::clone(&self.#pk_ident)
6604                        ),
6605                    );
6606                    #root::sql::delete_tx(tx, &_query).await
6607                }
6608            }
6609        } else {
6610            quote!()
6611        }
6612    };
6613
6614    // Update emission captures both BEFORE and AFTER state — runs an
6615    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6616    // each tracked field's prior value, then after the UPDATE diffs
6617    // against the in-memory `&self`. `diff_changes` drops unchanged
6618    // columns so the JSON only contains the actual delta.
6619    //
6620    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6621    // and binds `_audit_before_pairs`; `audit_update_post` runs
6622    // after the UPDATE and emits the PendingEntry.
6623    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6624        audited_fields
6625    {
6626        if tracked.is_empty() {
6627            (quote!(), quote!())
6628        } else {
6629            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6630            let pk_column_lit_for_select =
6631                primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6632            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6633                if fields.pk_is_auto {
6634                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6635                } else {
6636                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6637                }
6638            } else {
6639                quote!(0_i64)
6640            };
6641            let before_pairs = tracked.iter().map(|c| {
6642                let column_lit = c.column.as_str();
6643                let value_ty = &c.value_ty;
6644                quote! {
6645                    (
6646                        #column_lit,
6647                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6648                            &_audit_before_row, #column_lit,
6649                        ) {
6650                            ::core::result::Result::Ok(v) => #root::core::__json_value(&v),
6651                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6652                        },
6653                    )
6654                }
6655            });
6656            let pre = quote! {
6657                let _audit_before_query = #root::audit::before_image_query(
6658                    <Self as #root::core::Model>::SCHEMA,
6659                    #pk_column_lit_for_select,
6660                    ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6661                    &[ #( #tracked_cols ),* ],
6662                );
6663                // A failed pre-read must not let the UPDATE run unaudited (#1907).
6664                let _audit_before_pairs:
6665                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6666                    #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6667                        .await?
6668                        .into_iter()
6669                        .next()
6670                        .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6671            };
6672            let post = quote! {
6673                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6674                    let _audit_after = self.__rustango_audit_pairs();
6675                    if let ::core::option::Option::Some(_audit_entry) =
6676                        #root::audit::PendingEntry::update_diff(
6677                            <Self as #root::core::Model>::SCHEMA.table,
6678                            self.__rustango_audit_pk(),
6679                            &_audit_before,
6680                            &_audit_after,
6681                        )
6682                    {
6683                        #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6684                    }
6685                }
6686            };
6687            (pre, post)
6688        }
6689    } else {
6690        (quote!(), quote!())
6691    };
6692
6693    // Bulk-insert audit: one `Create` entry per row once RETURNING has
6694    // set its PK, written in one batched INSERT.
6695    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6696        quote! {
6697            let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6698                .iter()
6699                .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6700                .collect();
6701            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6702        }
6703    } else {
6704        quote!()
6705    };
6706
6707    let save_method = if fields.pk_is_auto {
6708        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6709        let pk_column_lit = pk_column.as_str();
6710        let assignments = &fields.update_assignments;
6711        let upsert_cols = &fields.upsert_update_columns;
6712        let upsert_pushes = &fields.insert_pushes;
6713        // Read before the pushes, which fill each unset id.
6714        let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6715            fields.uuid_v7_fields.iter().cloned().unzip();
6716        let upsert_returning = &fields.returning_cols;
6717        let upsert_auto_assigns = &fields.auto_assigns;
6718        let auto_now_idents = &fields.auto_now_idents;
6719        // Conflict target: prefer the first declared `unique_together`
6720        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6721        // `BIGSERIAL` and never collide on insert, so a PK-only target
6722        // would silently turn `upsert()` into "always-insert" for
6723        // surrogate-PK models with composite UNIQUE constraints — see
6724        // `RolePermission` / `UserRole` / `UserPermission` in the
6725        // tenancy permission engine. When no `unique_together` is
6726        // declared we keep the PK target (the original behaviour).
6727        let upsert_target_columns: Vec<String> =
6728            upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6729        let upsert_target_lits = upsert_target_columns
6730            .iter()
6731            .map(String::as_str)
6732            .collect::<Vec<_>>();
6733        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6734            quote!(#root::core::ConflictClause::DoNothing)
6735        } else {
6736            quote!(#root::core::ConflictClause::DoUpdate {
6737                target: ::std::vec![ #( #upsert_target_lits ),* ],
6738                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6739            })
6740        };
6741        // Audited: the upsert reports whether it inserted or updated (#1795).
6742        let (upsert_run, audit_upsert_emit) = if audited_fields.is_some() {
6743            (
6744                quote! {
6745                    let (_returning_row_v, _audit_op) =
6746                        #root::audit::upsert_returning_on(&mut *_executor, &query).await?;
6747                },
6748                quote! {
6749                    let _audit_entry = self.__rustango_audit_entry(_audit_op);
6750                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6751                },
6752            )
6753        } else {
6754            (
6755                quote! {
6756                    let _returning_row_v =
6757                        #root::sql::__macro_internals::insert_returning_on(_executor, &query)
6758                            .await?;
6759                },
6760                quote!(),
6761            )
6762        };
6763        let save_on_write = restore_auto_now_unless(
6764            auto_now_idents,
6765            &save_ret,
6766            &save_keep,
6767            &quote! {
6768                #audit_update_pre
6769                let _query = #root::core::UpdateQuery::new(
6770                    <Self as #root::core::Model>::SCHEMA,
6771                    ::std::vec![ #( #assignments ),* ],
6772                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6773                        #pk_column_lit,
6774                        #root::core::Op::Eq,
6775                        ::core::convert::Into::<#root::core::SqlValue>::into(
6776                            ::core::clone::Clone::clone(&self.#pk_ident)
6777                        ),
6778                    )),
6779                );
6780                let _affected = #root::sql::__macro_internals::update_on(
6781                    #executor_passes_to_data_write,
6782                    &_query,
6783                ).await?;
6784                #audit_update_post
6785                ::core::result::Result::Ok(_affected)
6786            },
6787        );
6788        let upsert_write = restore_auto_now_unless(
6789            auto_now_idents,
6790            &quote!(::core::result::Result<(), #root::sql::ExecError>),
6791            &quote!(_saved.is_ok()),
6792            &quote! {
6793                // The UPDATE branch writes `auto_now` too, so stamp it like `save`.
6794                #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
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                .generated(_generated);
6804                #upsert_run
6805                let _returning_row = &_returning_row_v;
6806                #( #upsert_auto_assigns )*
6807                #audit_upsert_emit
6808                ::core::result::Result::Ok(())
6809            },
6810        );
6811        Some(quote! {
6812            /// Insert this row if its `Auto<T>` primary key is
6813            /// `Unset`, otherwise update the existing row matching the
6814            /// PK. The caller doesn't need to pick `insert` vs the
6815            /// bulk-update path manually.
6816            ///
6817            /// On the insert branch, populates the PK from `RETURNING`
6818            /// (same behavior as `insert`). On the update branch,
6819            /// writes every non-PK column back; if no row matches the
6820            /// PK, returns `Ok(())` silently.
6821            ///
6822            /// Only generated when the primary key is declared as
6823            /// `Auto<T>`. Models with a manually-managed PK must use
6824            /// `insert` or the QuerySet update builder.
6825            ///
6826            /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6827            /// driver-specific `PgPool`, so it does not exist at all on a
6828            /// build without the `postgres` feature: selecting `sqlite`
6829            /// makes the method vanish rather than fail with anything that
6830            /// names the cause.
6831            ///
6832            /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6833            /// all three backends. The naming is inverted — the short name
6834            /// is the narrow one — and that is tracked in #1293.
6835            ///
6836            /// # Errors
6837            /// Returns [`#root::sql::ExecError`] for SQL-writing
6838            /// or driver failures.
6839            #[cfg(feature = "postgres")]
6840            pub async fn save(
6841                &mut self,
6842                pool: &#root::sql::sqlx::PgPool,
6843            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6844                #pool_to_save_on
6845            }
6846
6847            /// Like [`Self::save`] but accepts any sqlx executor —
6848            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6849            /// escape hatch for tenant-scoped writes: schema-mode
6850            /// tenants share the registry pool but rely on a per-
6851            /// checkout `SET search_path`, so passing `&PgPool` would
6852            /// silently hit the wrong schema. Acquire a connection
6853            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6854            ///
6855            /// # Errors
6856            /// As [`Self::save`].
6857            #[cfg(feature = "postgres")]
6858            pub async fn save_on #executor_generics (
6859                &mut self,
6860                #executor_param,
6861            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6862            #executor_where
6863            {
6864                // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6865                // the rows-affected count (0 when the PK no longer exists).
6866                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6867                    return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6868                }
6869                #save_on_write
6870            }
6871
6872            /// Per-call override for the audit source. Runs
6873            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6874            /// scope so the resulting audit entry records `source`
6875            /// instead of the task-local default. Useful for seed
6876            /// scripts and one-off CLI tools that don't sit inside an
6877            /// admin handler. The override applies only to this call;
6878            /// no global state changes.
6879            ///
6880            /// # Errors
6881            /// As [`Self::save_on`].
6882            #[cfg(feature = "postgres")]
6883            pub async fn save_on_with #executor_generics (
6884                &mut self,
6885                #executor_param,
6886                source: #root::audit::AuditSource,
6887            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6888            #executor_where
6889            {
6890                #root::audit::with_source(source, self.save_on(_executor)).await
6891            }
6892
6893            /// Insert this row or update it in-place if the primary key already
6894            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6895            ///
6896            /// With `Auto::Unset` PK the server assigns a new key and no conflict
6897            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6898            /// inserted if absent or all non-PK columns are overwritten if present.
6899            ///
6900            /// # Errors
6901            /// As [`Self::insert_on`].
6902            #[cfg(feature = "postgres")]
6903            pub async fn upsert(
6904                &mut self,
6905                pool: &#root::sql::sqlx::PgPool,
6906            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6907                #pool_to_upsert_on
6908            }
6909
6910            /// Like [`Self::upsert`] but accepts any sqlx executor.
6911            /// See [`Self::save_on`] for tenancy-scoped rationale.
6912            ///
6913            /// # Errors
6914            /// As [`Self::upsert`].
6915            #[cfg(feature = "postgres")]
6916            pub async fn upsert_on #executor_generics (
6917                &mut self,
6918                #executor_param,
6919            ) -> ::core::result::Result<(), #root::sql::ExecError>
6920            #executor_where
6921            {
6922                let mut _columns: ::std::vec::Vec<&'static str> =
6923                    ::std::vec::Vec::new();
6924                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6925                    ::std::vec::Vec::new();
6926                #[allow(unused_mut)]
6927                let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
6928                #(
6929                    if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
6930                        _generated.push(#gen_cols);
6931                    }
6932                )*
6933                #upsert_write
6934            }
6935        })
6936    } else {
6937        None
6938    };
6939
6940    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6941        let pk_column_lit = pk_column.as_str();
6942        // Optional `soft_delete_on` / `restore_on` companions when the
6943        // model has a `#[rustango(soft_delete)]` column. They land
6944        // alongside the regular `delete_on` so callers have both
6945        // options — a hard delete (audit-tracked as a real DELETE) and
6946        // a logical delete (audit-tracked as an UPDATE setting the
6947        // deleted_at column to NOW()).
6948        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6949            let col_lit = col;
6950            let sd_field_ident = fields
6951                .soft_delete_field_ident
6952                .clone()
6953                .expect("soft_delete_column without ident");
6954            quote! {
6955                /// Soft-delete this row by setting its
6956                /// `#[rustango(soft_delete)]` column to `NOW()`.
6957                /// The row stays in the table; query helpers can
6958                /// filter it out by checking the column for `IS NOT
6959                /// NULL`.
6960                ///
6961                /// # Errors
6962                /// As [`Self::delete`].
6963                #[cfg(feature = "postgres")]
6964                pub async fn soft_delete_on #executor_generics (
6965                    &self,
6966                    #executor_param,
6967                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6968                #executor_where
6969                {
6970                    let _now = #root::__chrono::Utc::now();
6971                    let _query = #root::soft_delete::__mark_query(
6972                        <Self as #root::core::Model>::SCHEMA,
6973                        #col_lit,
6974                        #pk_column_lit,
6975                        ::core::convert::Into::<#root::core::SqlValue>::into(
6976                            ::core::clone::Clone::clone(&self.#pk_ident)
6977                        ),
6978                        ::core::option::Option::Some(_now),
6979                    );
6980                    let _affected = #root::sql::__macro_internals::update_on(
6981                        #executor_passes_to_data_write,
6982                        &_query,
6983                    ).await?;
6984                    #audit_softdelete_emit
6985                    ::core::result::Result::Ok(_affected)
6986                }
6987
6988                /// Inverse of [`Self::soft_delete_on`] — clears the
6989                /// soft-delete column back to NULL so the row is
6990                /// considered live again.
6991                ///
6992                /// # Errors
6993                /// As [`Self::delete`].
6994                #[cfg(feature = "postgres")]
6995                pub async fn restore_on #executor_generics (
6996                    &self,
6997                    #executor_param,
6998                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6999                #executor_where
7000                {
7001                    let _query = #root::soft_delete::__mark_query(
7002                        <Self as #root::core::Model>::SCHEMA,
7003                        #col_lit,
7004                        #pk_column_lit,
7005                        ::core::convert::Into::<#root::core::SqlValue>::into(
7006                            ::core::clone::Clone::clone(&self.#pk_ident)
7007                        ),
7008                        ::core::option::Option::None,
7009                    );
7010                    let _affected = #root::sql::__macro_internals::update_on(
7011                        #executor_passes_to_data_write,
7012                        &_query,
7013                    ).await?;
7014                    #audit_restore_emit
7015                    ::core::result::Result::Ok(_affected)
7016                }
7017
7018                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
7019                /// — takes [`#root::sql::Pool`] and dispatches per
7020                /// backend. Eloquent `Model::delete()` semantics on
7021                /// soft-delete-enabled models (closes #821 partial).
7022                ///
7023                /// Sets the `#[rustango(soft_delete)]` column to
7024                /// `NOW()` on every backend. Query helpers
7025                /// (`QuerySet::active()` / `only_trashed()`,
7026                /// `soft_delete::active_filter` /
7027                /// `compose_with_active`) filter trashed rows out by
7028                /// reading `IS NULL` on the same column. Keyed on the PK;
7029                /// global scopes do not apply.
7030                ///
7031                /// # Errors
7032                /// As [`#root::sql::update_pool`].
7033                pub async fn soft_delete(
7034                    &self,
7035                    pool: &#root::sql::Pool,
7036                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7037                    let _now = #root::__chrono::Utc::now();
7038                    let _query = #root::soft_delete::__mark_query(
7039                        <Self as #root::core::Model>::SCHEMA,
7040                        #col_lit,
7041                        #pk_column_lit,
7042                        ::core::convert::Into::<#root::core::SqlValue>::into(
7043                            ::core::clone::Clone::clone(&self.#pk_ident)
7044                        ),
7045                        ::core::option::Option::Some(_now),
7046                    );
7047                    #pool_softdelete_run
7048                }
7049
7050                /// Tri-dialect counterpart of [`Self::restore_on`].
7051                /// Clears the `#[rustango(soft_delete)]` column back
7052                /// to `NULL`, marking the row live again. Eloquent
7053                /// `Model::restore()` parity. Keyed on the PK; global
7054                /// scopes do not apply.
7055                ///
7056                /// # Errors
7057                /// As [`#root::sql::update_pool`].
7058                pub async fn restore(
7059                    &self,
7060                    pool: &#root::sql::Pool,
7061                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7062                    let _query = #root::soft_delete::__mark_query(
7063                        <Self as #root::core::Model>::SCHEMA,
7064                        #col_lit,
7065                        #pk_column_lit,
7066                        ::core::convert::Into::<#root::core::SqlValue>::into(
7067                            ::core::clone::Clone::clone(&self.#pk_ident)
7068                        ),
7069                        ::core::option::Option::None,
7070                    );
7071                    #pool_restore_run
7072                }
7073
7074                /// Hard-delete this row, ignoring the soft-delete
7075                /// column. Eloquent `Model::forceDelete()` parity —
7076                /// the escape hatch when you need to actually purge
7077                /// data (GDPR, fixture cleanup, etc.).
7078                ///
7079                /// Equivalent to [`Self::delete_pool`] (the framework's
7080                /// non-soft delete) — exposed under the Eloquent name
7081                /// for muscle-memory + so soft-delete-enabled models
7082                /// have all three operations (soft / restore / force)
7083                /// in one place.
7084                ///
7085                /// # Errors
7086                /// As [`Self::delete_pool`].
7087                pub async fn force_delete(
7088                    &self,
7089                    pool: &#root::sql::Pool,
7090                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7091                    Self::delete_pool(self, pool).await
7092                }
7093
7094                /// Returns `true` when this row is soft-deleted (its
7095                /// `#[rustango(soft_delete)]` column is currently
7096                /// set — Eloquent `$model->trashed()` parity).
7097                ///
7098                /// Pure in-memory predicate over `&self`; does not
7099                /// hit the database. Useful in admin/template code
7100                /// like `{% if post.trashed() %}…{% endif %}` and in
7101                /// guard clauses on restore/force-delete flows.
7102                pub fn trashed(&self) -> bool {
7103                    ::core::option::Option::is_some(&self.#sd_field_ident)
7104                }
7105
7106                /// Fetch every row whose `#[rustango(soft_delete)]`
7107                /// column is `NULL` (a.k.a. the live, non-trashed
7108                /// rows). Eloquent's default `Model::all()` behavior on
7109                /// a soft-delete model (Eloquent auto-scopes trashed
7110                /// rows out; rustango doesn't have auto-scoping yet —
7111                /// see issue #820 — so this is the explicit shortcut).
7112                ///
7113                /// One-liner over `QuerySet::<Self>::default()
7114                /// .active().fetch(pool)`. Closes #821 partial.
7115                ///
7116                /// # Errors
7117                /// As [`#root::sql::FetcherPool::fetch`].
7118                pub async fn active(
7119                    pool: &#root::sql::Pool,
7120                ) -> ::core::result::Result<
7121                    ::std::vec::Vec<Self>,
7122                    #root::sql::ExecError,
7123                > {
7124                    use #root::sql::FetcherPool as _;
7125                    #root::query::QuerySet::<Self>::default()
7126                        .active()
7127                        .fetch(pool)
7128                        .await
7129                }
7130
7131                /// Fetch ONLY soft-deleted rows. Eloquent
7132                /// `Model::onlyTrashed()->get()` parity — drives the
7133                /// admin "Trash" page, restore flows, GDPR purge
7134                /// scans, etc.
7135                ///
7136                /// One-liner over `QuerySet::<Self>::default()
7137                /// .only_trashed().fetch(pool)`. Closes #821
7138                /// partial.
7139                ///
7140                /// # Errors
7141                /// As [`#root::sql::FetcherPool::fetch`].
7142                pub async fn only_trashed(
7143                    pool: &#root::sql::Pool,
7144                ) -> ::core::result::Result<
7145                    ::std::vec::Vec<Self>,
7146                    #root::sql::ExecError,
7147                > {
7148                    use #root::sql::FetcherPool as _;
7149                    #root::query::QuerySet::<Self>::default()
7150                        .only_trashed()
7151                        .fetch(pool)
7152                        .await
7153                }
7154
7155                /// Fetch every row, both live and soft-deleted.
7156                /// Eloquent `Model::withTrashed()->get()` parity.
7157                ///
7158                /// Today every queryset already includes trashed rows
7159                /// (rustango has no global-scope tracking yet — issue
7160                /// #820), so this is functionally equivalent to
7161                /// [`Self::all`]. Exposed as a named shortcut so
7162                /// soft-delete-aware code reads `Model::with_trashed_pool`
7163                /// rather than `Model::all_pool` — keeps intent visible
7164                /// in callers and stays correct when auto-scoping lands.
7165                ///
7166                /// Closes #821 partial.
7167                ///
7168                /// # Errors
7169                /// As [`#root::sql::FetcherPool::fetch`].
7170                pub async fn with_trashed(
7171                    pool: &#root::sql::Pool,
7172                ) -> ::core::result::Result<
7173                    ::std::vec::Vec<Self>,
7174                    #root::sql::ExecError,
7175                > {
7176                    use #root::sql::FetcherPool as _;
7177                    #root::query::QuerySet::<Self>::default()
7178                        .with_trashed()
7179                        .fetch(pool)
7180                        .await
7181                }
7182
7183            }
7184        } else {
7185            quote!()
7186        };
7187        quote! {
7188            /// Delete the row identified by this instance's primary key.
7189            ///
7190            /// Returns the number of rows affected (0 or 1).
7191            ///
7192            /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7193            /// a driver-specific `PgPool`, so it does not exist on a build
7194            /// without the `postgres` feature. [`Self::delete_pool`] takes
7195            /// `rustango::sql::Pool` and works on all three. The naming is
7196            /// inverted; tracked in #1293.
7197            ///
7198            /// # Errors
7199            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7200            /// driver failures.
7201            #[cfg(feature = "postgres")]
7202            pub async fn delete(
7203                &self,
7204                pool: &#root::sql::sqlx::PgPool,
7205            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7206                #pool_to_delete_on
7207            }
7208
7209            /// Like [`Self::delete`] but accepts any sqlx executor —
7210            /// for tenant-scoped deletes against an explicitly-acquired
7211            /// connection. See [`Self::save_on`] for the rationale.
7212            ///
7213            /// # Errors
7214            /// As [`Self::delete`].
7215            #[cfg(feature = "postgres")]
7216            pub async fn delete_on #executor_generics (
7217                &self,
7218                #executor_param,
7219            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7220            #executor_where
7221            {
7222                let query = #root::core::DeleteQuery::by_pk(
7223                    <Self as #root::core::Model>::SCHEMA,
7224                    #pk_column_lit,
7225                    ::core::convert::Into::<#root::core::SqlValue>::into(
7226                        ::core::clone::Clone::clone(&self.#pk_ident)
7227                    ),
7228                );
7229                let _affected = #root::sql::__macro_internals::delete_on(
7230                    #executor_passes_to_data_write,
7231                    &query,
7232                ).await?;
7233                #audit_delete_emit
7234                ::core::result::Result::Ok(_affected)
7235            }
7236
7237            /// Per-call audit-source override for [`Self::delete_on`].
7238            /// See [`Self::save_on_with`] for shape rationale.
7239            ///
7240            /// # Errors
7241            /// As [`Self::delete_on`].
7242            #[cfg(feature = "postgres")]
7243            pub async fn delete_on_with #executor_generics (
7244                &self,
7245                #executor_param,
7246                source: #root::audit::AuditSource,
7247            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7248            #executor_where
7249            {
7250                #root::audit::with_source(source, self.delete_on(_executor)).await
7251            }
7252            #pool_delete_method
7253            #pool_insert_method
7254            #audit_entry_method
7255            #pool_save_method
7256            #refresh_replicate_methods
7257            #tx_delete_method
7258            #tx_insert_method
7259            #tx_save_method
7260            #soft_delete_methods
7261
7262            /// Returns `true` when `other` represents the same DB
7263            /// row as `self` — i.e. their primary keys compare
7264            /// equal. Eloquent `$model->is($other)` parity.
7265            ///
7266            /// Because both arguments are typed `&Self`, the
7267            /// model/table check is automatic — `Post::is` cannot
7268            /// be invoked against a `Comment` at compile time. Only
7269            /// the PK has to be compared at runtime.
7270            pub fn is(&self, other: &Self) -> bool {
7271                self.#pk_ident == other.#pk_ident
7272            }
7273
7274            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7275            /// parity.
7276            pub fn is_not(&self, other: &Self) -> bool {
7277                self.#pk_ident != other.#pk_ident
7278            }
7279
7280            /// Returns this row's primary-key value as an
7281            /// [`#root::core::SqlValue`]. Eloquent
7282            /// `$model->getKey()` parity.
7283            ///
7284            /// Useful when you need to thread the PK through a
7285            /// generic `Into<SqlValue>`-bound API without knowing the
7286            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7287            #[must_use]
7288            pub fn get_key(&self) -> #root::core::SqlValue {
7289                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7290            }
7291
7292        }
7293    });
7294
7295    let insert_method = if fields.has_auto {
7296        let pushes = &fields.insert_pushes;
7297        let returning_cols = &fields.returning_cols;
7298        let auto_assigns = &fields.auto_assigns;
7299        quote! {
7300            /// Insert this row into its table. Skips columns whose
7301            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7302            /// sequence fills them in, then reads each `Auto` column
7303            /// back via `RETURNING` and stores it on `self`.
7304            ///
7305            /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7306            /// a driver-specific `PgPool`, so it does not exist on a build
7307            /// without the `postgres` feature. [`Self::insert_pool`] takes
7308            /// `rustango::sql::Pool` and works on all three. The naming is
7309            /// inverted; tracked in #1293.
7310            ///
7311            /// # Errors
7312            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7313            /// driver failures.
7314            #[cfg(feature = "postgres")]
7315            pub async fn insert(
7316                &mut self,
7317                pool: &#root::sql::sqlx::PgPool,
7318            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7319                #pool_to_insert_on
7320            }
7321
7322            /// Like [`Self::insert`] but accepts any sqlx executor.
7323            /// See [`Self::save_on`] for tenancy-scoped rationale.
7324            ///
7325            /// # Errors
7326            /// As [`Self::insert`].
7327            #[cfg(feature = "postgres")]
7328            pub async fn insert_on #executor_generics (
7329                &mut self,
7330                #executor_param,
7331            ) -> ::core::result::Result<(), #root::sql::ExecError>
7332            #executor_where
7333            {
7334                let mut _columns: ::std::vec::Vec<&'static str> =
7335                    ::std::vec::Vec::new();
7336                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7337                    ::std::vec::Vec::new();
7338                #( #pushes )*
7339                let query = #root::core::InsertQuery::new(
7340                    <Self as #root::core::Model>::SCHEMA,
7341                    _columns,
7342                    _values,
7343                )
7344                .returning(::std::vec![ #( #returning_cols ),* ]);
7345                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7346                    #executor_passes_to_data_write,
7347                    &query,
7348                ).await?;
7349                let _returning_row = &_returning_row_v;
7350                #( #auto_assigns )*
7351                #audit_insert_emit
7352                ::core::result::Result::Ok(())
7353            }
7354
7355            /// Per-call audit-source override for [`Self::insert_on`].
7356            /// See [`Self::save_on_with`] for shape rationale.
7357            ///
7358            /// # Errors
7359            /// As [`Self::insert_on`].
7360            #[cfg(feature = "postgres")]
7361            pub async fn insert_on_with #executor_generics (
7362                &mut self,
7363                #executor_param,
7364                source: #root::audit::AuditSource,
7365            ) -> ::core::result::Result<(), #root::sql::ExecError>
7366            #executor_where
7367            {
7368                #root::audit::with_source(source, self.insert_on(_executor)).await
7369            }
7370        }
7371    } else {
7372        let insert_columns = &fields.insert_columns;
7373        let insert_values = &fields.insert_values;
7374        quote! {
7375            /// Insert this row into its table.
7376            ///
7377            /// # Errors
7378            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7379            /// driver failures.
7380            #[cfg(feature = "postgres")]
7381            pub async fn insert(
7382                &self,
7383                pool: &#root::sql::sqlx::PgPool,
7384            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7385                self.insert_on(pool).await
7386            }
7387
7388            /// Like [`Self::insert`] but accepts any sqlx executor.
7389            /// See [`Self::save_on`] for tenancy-scoped rationale.
7390            ///
7391            /// # Errors
7392            /// As [`Self::insert`].
7393            #[cfg(feature = "postgres")]
7394            pub async fn insert_on<'_c, _E>(
7395                &self,
7396                _executor: _E,
7397            ) -> ::core::result::Result<(), #root::sql::ExecError>
7398            where
7399                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7400            {
7401                let query = #root::core::InsertQuery::new(
7402                    <Self as #root::core::Model>::SCHEMA,
7403                    ::std::vec![ #( #insert_columns ),* ],
7404                    ::std::vec![ #( #insert_values ),* ],
7405                );
7406                #root::sql::__macro_internals::insert_on(_executor, &query).await
7407            }
7408        }
7409    };
7410
7411    let bulk_insert_method = if fields.has_auto {
7412        let cols_no_auto = &fields.bulk_columns_no_auto;
7413        let cols_all = &fields.bulk_columns_all;
7414        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7415        let pushes_all = &fields.bulk_pushes_all;
7416        let returning_cols = &fields.returning_cols;
7417        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7418        let uniformity = &fields.bulk_auto_uniformity;
7419        let fill_in_place = &fields.bulk_fill_in_place;
7420        // Nothing to read back when every Auto field is filled Rust-side (#1934).
7421        let read_back = if fields.returning_cols.is_empty() {
7422            quote!()
7423        } else {
7424            quote! {
7425                if _returned.len() != rows.len() {
7426                    return ::core::result::Result::Err(
7427                        #root::sql::ExecError::Sql(
7428                            #root::sql::SqlError::BulkInsertReturningMismatch {
7429                                expected: rows.len(),
7430                                actual: _returned.len(),
7431                            }
7432                        )
7433                    );
7434                }
7435                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7436                    #auto_assigns_for_row
7437                }
7438            }
7439        };
7440        // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7441        let first_auto_ident = fields
7442            .first_db_auto
7443            .as_ref()
7444            .map(|(ident, _)| ident)
7445            .or(fields.first_auto_ident.as_ref())
7446            .expect("has_auto implies first_auto_ident is Some");
7447        quote! {
7448            /// Bulk-insert `rows` in a single round-trip. Every row's
7449            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7450            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7451            /// (caller-supplied values). Mixed Set/Unset is rejected
7452            /// — call `insert` per row for that case.
7453            ///
7454            /// Empty slice is a no-op. Each row's `Auto` fields are
7455            /// populated from the `RETURNING` clause in input order
7456            /// before this returns.
7457            ///
7458            /// # Errors
7459            /// Returns [`#root::sql::ExecError`] for validation,
7460            /// SQL-writing, mixed-Auto rejection, or driver failures.
7461            #[cfg(feature = "postgres")]
7462            pub async fn bulk_insert(
7463                rows: &mut [Self],
7464                pool: &#root::sql::sqlx::PgPool,
7465            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7466                #pool_to_bulk_insert_on
7467            }
7468
7469            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7470            /// See [`Self::save_on`] for tenancy-scoped rationale.
7471            ///
7472            /// # Errors
7473            /// As [`Self::bulk_insert`].
7474            #[cfg(feature = "postgres")]
7475            pub async fn bulk_insert_on #executor_generics (
7476                rows: &mut [Self],
7477                #executor_param,
7478            ) -> ::core::result::Result<(), #root::sql::ExecError>
7479            #executor_where
7480            {
7481                if rows.is_empty() {
7482                    return ::core::result::Result::Ok(());
7483                }
7484                let _first_unset = matches!(
7485                    rows[0].#first_auto_ident,
7486                    #root::sql::Auto::Unset
7487                );
7488                #( #uniformity )*
7489                for _row in rows.iter_mut() {
7490                    #( #fill_in_place )*
7491                }
7492
7493                let mut _all_rows: ::std::vec::Vec<
7494                    ::std::vec::Vec<#root::core::SqlValue>,
7495                > = ::std::vec::Vec::with_capacity(rows.len());
7496                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7497                    for _row in rows.iter() {
7498                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7499                            ::std::vec::Vec::new();
7500                        #( #pushes_no_auto )*
7501                        _all_rows.push(_row_vals);
7502                    }
7503                    ::std::vec![ #( #cols_no_auto ),* ]
7504                } else {
7505                    for _row in rows.iter() {
7506                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7507                            ::std::vec::Vec::new();
7508                        #( #pushes_all )*
7509                        _all_rows.push(_row_vals);
7510                    }
7511                    ::std::vec![ #( #cols_all ),* ]
7512                };
7513
7514                let _query = #root::core::BulkInsertQuery::new(
7515                    <Self as #root::core::Model>::SCHEMA,
7516                    _columns,
7517                    _all_rows,
7518                )
7519                .returning(::std::vec![ #( #returning_cols ),* ]);
7520                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7521                    #executor_passes_to_data_write,
7522                    &_query,
7523                ).await?;
7524                #read_back
7525                #audit_bulk_insert_emit
7526                ::core::result::Result::Ok(())
7527            }
7528        }
7529    } else {
7530        let cols_all = &fields.bulk_columns_all;
7531        let pushes_all = &fields.bulk_pushes_all;
7532        quote! {
7533            /// Bulk-insert `rows` in a single round-trip. Every row's
7534            /// fields are written verbatim — there are no `Auto<T>`
7535            /// fields on this model.
7536            ///
7537            /// Empty slice is a no-op.
7538            ///
7539            /// # Errors
7540            /// Returns [`#root::sql::ExecError`] for validation,
7541            /// SQL-writing, or driver failures.
7542            #[cfg(feature = "postgres")]
7543            pub async fn bulk_insert(
7544                rows: &[Self],
7545                pool: &#root::sql::sqlx::PgPool,
7546            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7547                #pool_to_bulk_insert_on
7548            }
7549
7550            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7551            /// See [`Self::save_on`] for tenancy-scoped rationale.
7552            ///
7553            /// # Errors
7554            /// As [`Self::bulk_insert`].
7555            #[cfg(feature = "postgres")]
7556            pub async fn bulk_insert_on #executor_generics (
7557                rows: &[Self],
7558                #executor_param,
7559            ) -> ::core::result::Result<(), #root::sql::ExecError>
7560            #executor_where
7561            {
7562                if rows.is_empty() {
7563                    return ::core::result::Result::Ok(());
7564                }
7565                let mut _all_rows: ::std::vec::Vec<
7566                    ::std::vec::Vec<#root::core::SqlValue>,
7567                > = ::std::vec::Vec::with_capacity(rows.len());
7568                for _row in rows.iter() {
7569                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7570                        ::std::vec::Vec::new();
7571                    #( #pushes_all )*
7572                    _all_rows.push(_row_vals);
7573                }
7574                let _query = #root::core::BulkInsertQuery::new(
7575                    <Self as #root::core::Model>::SCHEMA,
7576                    ::std::vec![ #( #cols_all ),* ],
7577                    _all_rows,
7578                );
7579                let _ = #root::sql::__macro_internals::bulk_insert_on(
7580                    #executor_passes_to_data_write,
7581                    &_query,
7582                ).await?;
7583                #audit_bulk_insert_emit
7584                ::core::result::Result::Ok(())
7585            }
7586        }
7587    };
7588
7589    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7590    // (no postgres-feature gate); routes through the existing
7591    // `bulk_insert_pool` + per-dialect conflict writer.
7592    //
7593    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7594    // sequence picks the PK for fresh inserts; the UPDATE branch never
7595    // touches the Auto column.
7596    // Audited models audit each written row; MySQL, with no RETURNING,
7597    // is refused there (#1747, #1795).
7598    let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7599        (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7600        (true, true) => quote! {
7601            #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7602                _r.__rustango_audit_entry(_op)
7603            })
7604            .await
7605        },
7606        (true, false) => quote! {
7607            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7608                table: <Self as #root::core::Model>::SCHEMA.table,
7609            })
7610        },
7611    };
7612    let bulk_upsert_pool_method = {
7613        let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7614            fields.uuid_v7_fields.iter().cloned().unzip();
7615        // Pick the "no Auto" columns when the model has Auto fields,
7616        // else every column.
7617        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7618            (
7619                fields.bulk_columns_no_auto.clone(),
7620                fields.bulk_pushes_no_auto.clone(),
7621            )
7622        } else {
7623            (
7624                fields.bulk_columns_all.clone(),
7625                fields.bulk_pushes_all.clone(),
7626            )
7627        };
7628        quote! {
7629            /// Tri-dialect bulk insert that updates on conflict — the
7630            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7631            ///
7632            /// Per-row values are extracted and lowered into a
7633            /// [`#root::core::BulkInsertQuery`] with
7634            /// `on_conflict = DoUpdate { target, update_columns }`. The
7635            /// writer dispatches per-dialect:
7636            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7637            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7638            ///   `target` is not the only unique key, since MySQL matches every UNIQUE index
7639            ///
7640            /// `target` names the column(s) whose unique constraint
7641            /// defines the conflict (typically a `unique` or
7642            /// `unique_together` natural-key column, NOT the `Auto<T>`
7643            /// PK). `update_cols` names the columns to overwrite on
7644            /// conflict — every other column is left untouched on the
7645            /// existing row.
7646            ///
7647            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7648            /// picks the PK on insert; the update path never touches
7649            /// the Auto column). Auto-set rows trigger a hard error.
7650            /// Empty slice is a no-op. On an audited model each written
7651            /// row gets a `create` or `update` audit row; MySQL refuses it.
7652            ///
7653            /// # Errors
7654            /// Returns [`#root::sql::ExecError`] for validation,
7655            /// SQL-writing, or driver failures.
7656            pub async fn bulk_upsert_pool(
7657                rows: &[Self],
7658                target: &[&'static str],
7659                update_cols: &[&'static str],
7660                pool: &#root::sql::Pool,
7661            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7662                if rows.is_empty() {
7663                    return ::core::result::Result::Ok(());
7664                }
7665                let mut _all_rows: ::std::vec::Vec<
7666                    ::std::vec::Vec<#root::core::SqlValue>,
7667                > = ::std::vec::Vec::with_capacity(rows.len());
7668                for _row in rows.iter() {
7669                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7670                        ::std::vec::Vec::new();
7671                    #( #upsert_pushes )*
7672                    _all_rows.push(_row_vals);
7673                }
7674                #[allow(unused_mut)]
7675                let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7676                #(
7677                    if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7678                        _generated.push(#gen_cols);
7679                    }
7680                )*
7681                let _query = #root::core::BulkInsertQuery::new(
7682                    <Self as #root::core::Model>::SCHEMA,
7683                    ::std::vec![ #( #upsert_cols ),* ],
7684                    _all_rows,
7685                )
7686                .on_conflict_do_update(target, update_cols)
7687                .generated(_generated);
7688                #run_conflict_bulk_insert
7689            }
7690
7691            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7692            /// skip rows that would violate a unique constraint. Issue
7693            /// #267 / T1.5. Same per-dialect dispatch as
7694            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7695            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7696            /// <pivot> = <pivot>` (MySQL no-op write).
7697            ///
7698            /// # Errors
7699            /// As [`Self::bulk_upsert_pool`].
7700            pub async fn bulk_insert_or_ignore_pool(
7701                rows: &[Self],
7702                pool: &#root::sql::Pool,
7703            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7704                if rows.is_empty() {
7705                    return ::core::result::Result::Ok(());
7706                }
7707                let mut _all_rows: ::std::vec::Vec<
7708                    ::std::vec::Vec<#root::core::SqlValue>,
7709                > = ::std::vec::Vec::with_capacity(rows.len());
7710                for _row in rows.iter() {
7711                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7712                        ::std::vec::Vec::new();
7713                    #( #upsert_pushes )*
7714                    _all_rows.push(_row_vals);
7715                }
7716                let _query = #root::core::BulkInsertQuery::new(
7717                    <Self as #root::core::Model>::SCHEMA,
7718                    ::std::vec![ #( #upsert_cols ),* ],
7719                    _all_rows,
7720                )
7721                .on_conflict_do_nothing();
7722                #run_conflict_bulk_insert
7723            }
7724        }
7725    };
7726
7727    // Ergonomic `Model::bulk_update(objs, fields)`.
7728    // The SQL/IR/executor stack
7729    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7730    // `write_bulk_update_*` writers) already existed; what was missing
7731    // was the per-model constructor that maps `&[Self]` + a runtime
7732    // column list into rows of `[pk, col_vals…]` so callers don't
7733    // hand-build the IR. Emitted only when the model has a primary key
7734    // (the PK is the join key and can't itself be updated).
7735    let bulk_update_rows_run = if audited_fields.is_some() {
7736        quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7737            _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7738        })
7739        .await)
7740    } else {
7741        quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7742    };
7743    let bulk_update_method = match &fields.primary_key {
7744        None => quote! {},
7745        Some((pk_ident, pk_col)) => {
7746            // One pair of match arms per non-PK column: a validation arm
7747            // resolving the runtime name to its `&'static str` column,
7748            // and a value arm pushing that field off the row.
7749            let mut col_arms: Vec<TokenStream2> = Vec::new();
7750            let mut val_arms: Vec<TokenStream2> = Vec::new();
7751            for entry in &fields.column_entries {
7752                if &entry.column == pk_col {
7753                    continue;
7754                }
7755                let col = &entry.column;
7756                let ident = &entry.ident;
7757                col_arms.push(quote! { #col => #col, });
7758                val_arms.push(quote! {
7759                    #col => _row_vals.push(
7760                        ::core::convert::Into::<#root::core::SqlValue>::into(
7761                            ::core::clone::Clone::clone(&_o.#ident)
7762                        )
7763                    ),
7764                });
7765            }
7766            quote! {
7767                /// Write per-row-different values for the named `fields`
7768                /// across every object in `objs`, matched by primary key.
7769                /// Past the backend's bind cap the rows go in batches inside
7770                /// one transaction, so the update is all or none.
7771                ///
7772                /// `fields` names the **columns** to update. The primary
7773                /// key identifies each row and cannot itself be updated
7774                /// (pass it and you get
7775                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7776                /// Empty `objs` or `fields` is a no-op returning `0`.
7777                /// Objects whose PK matches no row are simply not updated.
7778                /// Returns the number of rows affected.
7779                ///
7780                /// Tri-dialect: lowers to a
7781                /// [`#root::core::BulkUpdateQuery`] and dispatches
7782                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7783                /// a CTE + correlated subquery on SQLite, an inner
7784                /// `JOIN (VALUES …)` on MySQL.
7785                ///
7786                /// # Errors
7787                /// [`#root::core::QueryError::UnknownField`] for a name
7788                /// that isn't a column on this model,
7789                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7790                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7791                /// SQL-writing / driver failures.
7792                pub async fn bulk_update(
7793                    objs: &[Self],
7794                    fields: &[&str],
7795                    pool: &#root::sql::Pool,
7796                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7797                    if objs.is_empty() || fields.is_empty() {
7798                        return ::core::result::Result::Ok(0);
7799                    }
7800                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7801                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7802                        ::std::vec::Vec::with_capacity(fields.len());
7803                    for &_f in fields {
7804                        let _col: &'static str = match _f {
7805                            #pk_col => {
7806                                return ::core::result::Result::Err(
7807                                    ::core::convert::Into::into(
7808                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7809                                            model: _model_name,
7810                                            field: ::std::string::ToString::to_string(_f),
7811                                        }
7812                                    )
7813                                );
7814                            }
7815                            #( #col_arms )*
7816                            _ => {
7817                                return ::core::result::Result::Err(
7818                                    ::core::convert::Into::into(
7819                                        #root::core::QueryError::UnknownField {
7820                                            model: _model_name,
7821                                            field: ::std::string::ToString::to_string(_f),
7822                                        }
7823                                    )
7824                                );
7825                            }
7826                        };
7827                        // A model whose only field is the primary key emits no
7828                        // non-PK arms, so every arm above diverges and this push
7829                        // is genuinely unreachable — correct, since such a model
7830                        // has nothing to bulk-update. Scoped to the statement
7831                        // rather than the fn so a real unreachability elsewhere
7832                        // in this body still warns. Without it every PK-only
7833                        // model warns in the *user's* crate, which they cannot
7834                        // act on (#1290).
7835                        #[allow(unreachable_code)]
7836                        _update_columns.push(_col);
7837                    }
7838                    let mut _rows: ::std::vec::Vec<
7839                        ::std::vec::Vec<#root::core::SqlValue>,
7840                    > = ::std::vec::Vec::with_capacity(objs.len());
7841                    for _o in objs.iter() {
7842                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7843                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7844                        // PK first — the writers expect `[pk, …update cols]`.
7845                        _row_vals.push(
7846                            ::core::convert::Into::<#root::core::SqlValue>::into(
7847                                ::core::clone::Clone::clone(&_o.#pk_ident)
7848                            )
7849                        );
7850                        for &_f in fields {
7851                            match _f {
7852                                #( #val_arms )*
7853                                // Unreachable: every name was validated
7854                                // against the same arm set above.
7855                                _ => {}
7856                            }
7857                        }
7858                        _rows.push(_row_vals);
7859                    }
7860                    let _query = #root::core::BulkUpdateQuery::new(
7861                        <Self as #root::core::Model>::SCHEMA,
7862                        _update_columns,
7863                        _rows,
7864                    );
7865                    #bulk_update_rows_run
7866                }
7867            }
7868        }
7869    };
7870
7871    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7872        quote! {
7873            /// Hidden runtime accessor for the primary-key value as a
7874            /// [`SqlValue`]. Used by reverse-relation helpers
7875            /// (`<parent>::<child>_set`) emitted from sibling models'
7876            /// FK fields. Not part of the public API.
7877            #[doc(hidden)]
7878            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7879                ::core::convert::Into::<#root::core::SqlValue>::into(
7880                    ::core::clone::Clone::clone(&self.#pk_ident)
7881                )
7882            }
7883        }
7884    });
7885
7886    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7887        quote! {
7888            impl #root::sql::HasPkValue for #struct_name {
7889                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7890                    ::core::convert::Into::<#root::core::SqlValue>::into(
7891                        ::core::clone::Clone::clone(&self.#pk_ident)
7892                    )
7893                }
7894            }
7895        }
7896    });
7897
7898    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7899
7900    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7901    // dispatch to the right per-backend body without the macro emitting
7902    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7903    // so audited models with non-Auto PKs (which still go through
7904    // `insert_one_with_audit` → `apply_auto_pk`) link.
7905    let assign_auto_pk_pool_impl = {
7906        let auto_assigns = &fields.auto_assigns;
7907        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7908        // so the body is structurally identical to `auto_assigns` —
7909        // only the helper name swaps from `try_get_returning` to
7910        // `try_get_returning_sqlite` so the closure typechecks against
7911        // a `SqliteRow` instead of a `PgRow`.
7912        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7913            .auto_field_idents
7914            .iter()
7915            .map(|(ident, column, uuid)| {
7916                // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
7917                if *uuid {
7918                    return quote! {
7919                        self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
7920                            _returning_row, #column
7921                        )?);
7922                    };
7923                }
7924                quote! {
7925                    self.#ident = #root::sql::try_get_returning_sqlite(
7926                        _returning_row, #column
7927                    )?;
7928                }
7929            })
7930            .collect();
7931        // #1028 — decode each `generated_as` column from the same
7932        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7933        // own type drives the decode (no `Auto<T>` wrapper).
7934        let generated_assigns: Vec<TokenStream2> = fields
7935            .generated_field_idents
7936            .iter()
7937            .map(|(ident, column)| {
7938                quote! {
7939                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7940                }
7941            })
7942            .collect();
7943        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7944            .generated_field_idents
7945            .iter()
7946            .map(|(ident, column)| {
7947                quote! {
7948                    self.#ident = #root::sql::try_get_returning_sqlite(
7949                        _returning_row, #column
7950                    )?;
7951                }
7952            })
7953            .collect();
7954        let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
7955            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7956            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7957            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7958            // those models can't use AUTO_INCREMENT). The trait is only
7959            // touched on the MySQL arm at runtime, so PG-only consumers
7960            // never see the bound failure.
7961            //
7962            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7963            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7964            // runtime with "multi-column RETURNING". MySQL has no
7965            // multi-column RETURNING semantic and a follow-up SELECT
7966            // would need cross-trait plumbing. Pragmatic shape: succeed
7967            // with the FIRST Auto field populated from LAST_INSERT_ID();
7968            // any other Auto fields stay `Auto::Unset`. Callers that
7969            // need the DB-defaulted timestamp / UUID can re-fetch the
7970            // row by PK after `save_pool`. Fixes the cookbook chapter
7971            // 12 dialect divergence.
7972            quote! {
7973                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7974                    ::rustango_from_mysql_auto_id(_id)?;
7975                self.#first = #root::sql::Auto::Set(_converted);
7976                ::core::result::Result::Ok(())
7977            }
7978        } else {
7979            quote! {
7980                let _ = _id;
7981                ::core::result::Result::Ok(())
7982            }
7983        };
7984        quote! {
7985            impl #root::sql::AssignAutoPkPool for #struct_name {
7986                fn __rustango_assign_from_pg_row(
7987                    &mut self,
7988                    _returning_row: &#root::sql::PgReturningRow,
7989                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7990                    #( #auto_assigns )*
7991                    #( #generated_assigns )*
7992                    ::core::result::Result::Ok(())
7993                }
7994                fn __rustango_assign_from_mysql_id(
7995                    &mut self,
7996                    _id: i64,
7997                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7998                    #mysql_body
7999                }
8000                fn __rustango_assign_from_sqlite_row(
8001                    &mut self,
8002                    _returning_row: &#root::sql::SqliteReturningRow,
8003                ) -> ::core::result::Result<(), #root::sql::ExecError> {
8004                    #( #auto_assigns_sqlite )*
8005                    #( #generated_assigns_sqlite )*
8006                    ::core::result::Result::Ok(())
8007                }
8008            }
8009        }
8010    };
8011
8012    let from_aliased_row_inits = &fields.from_aliased_row_inits;
8013    let aliased_row_helper = quote! {
8014        /// Decode a row's aliased target columns (produced by
8015        /// `select_related`'s LEFT JOIN) into a fresh instance of
8016        /// this model. Reads each column via
8017        /// `format!("{prefix}__{col}")`, matching the alias the
8018        /// SELECT writer emitted. Slice 9.0d.
8019        #[doc(hidden)]
8020        #[cfg(feature = "postgres")]
8021        pub fn __rustango_from_aliased_row(
8022            row: &#root::sql::sqlx::postgres::PgRow,
8023            prefix: &str,
8024        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8025            ::core::result::Result::Ok(Self {
8026                #( #from_aliased_row_inits ),*
8027            })
8028        }
8029    };
8030    // v0.23.0-batch8 — MySQL counterpart, gated through the
8031    // cfg-aware macro_rules so PG-only builds expand to nothing.
8032    let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
8033    let aliased_row_helper_my = quote! {
8034        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
8035            #( #from_aliased_row_inits_my ),*
8036        });
8037    };
8038
8039    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
8040    // pattern + cfg gate on the `sqlite` feature.
8041    let aliased_row_helper_sqlite = quote! {
8042        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
8043            #( #from_aliased_row_inits ),*
8044        });
8045    };
8046
8047    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
8048    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
8049    let load_related_impl_sqlite =
8050        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
8051
8052    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
8053    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
8054    // default `Self::objects()`. Each accessor returns a fresh
8055    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
8056    // methods the user defined.
8057    let extra_manager_fns: Vec<TokenStream2> = manager_fns
8058        .iter()
8059        .map(|fn_ident| {
8060            let model_name_str = struct_name.to_string();
8061            let fn_name_str = fn_ident.to_string();
8062            let doc = format!(
8063                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
8064                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
8065                 equivalent to `Self::objects()`. Chains with any \
8066                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
8067                 extension methods."
8068            );
8069            quote! {
8070                #[doc = #doc]
8071                #[must_use]
8072                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
8073                    #root::query::QuerySet::new()
8074                }
8075            }
8076        })
8077        .collect();
8078
8079    quote! {
8080        impl #struct_name {
8081            /// Start a new `QuerySet` over this model.
8082            #[must_use]
8083            pub fn objects() -> #root::query::QuerySet<#struct_name> {
8084                #root::query::QuerySet::new()
8085            }
8086
8087            /// Eloquent-shape alias of [`Self::objects`]. Returns
8088            /// a fresh `QuerySet<Self>` ready for `.filter()` /
8089            /// `.where_()` / etc. Matches Laravel muscle-memory:
8090            ///
8091            /// ```ignore
8092            /// // Eloquent:    Post::query()->where('published', true)
8093            /// // rustango:    Post::query().filter("published", true)
8094            /// //         or:  Post::objects().filter("published", true)
8095            /// ```
8096            ///
8097            /// Both names point at the same underlying constructor;
8098            /// neither is preferred.
8099            #[must_use]
8100            pub fn query() -> #root::query::QuerySet<#struct_name> {
8101                #root::query::QuerySet::new()
8102            }
8103
8104            #( #extra_manager_fns )*
8105
8106            #insert_method
8107
8108            #bulk_insert_method
8109
8110            #bulk_upsert_pool_method
8111
8112            #bulk_update_method
8113
8114            #save_method
8115
8116            #pk_methods
8117
8118            #pk_value_helper
8119
8120            #aliased_row_helper
8121
8122            #column_consts
8123        }
8124
8125        #aliased_row_helper_my
8126
8127        #aliased_row_helper_sqlite
8128
8129        #load_related_impl
8130
8131        #load_related_impl_my
8132
8133        #load_related_impl_sqlite
8134
8135        #has_pk_value_impl
8136
8137        #fk_pk_access_impl
8138
8139        #assign_auto_pk_pool_impl
8140    }
8141}
8142
8143/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8144/// `auto_assigns` but reading from `_returning_row` and writing to
8145/// `_row_mut` instead of `self`.
8146fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8147    let root = rustango_root();
8148    let lines = fields.auto_field_idents.iter().map(|(ident, column, _)| {
8149        let col_lit = column.as_str();
8150        quote! {
8151            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8152                _returning_row,
8153                #col_lit,
8154            )?;
8155        }
8156    });
8157    quote! { #( #lines )* }
8158}
8159
8160/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8161fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8162    let lines = entries.iter().map(|e| {
8163        let ident = &e.ident;
8164        let col_ty = column_type_ident(ident);
8165        quote! {
8166            #[allow(non_upper_case_globals)]
8167            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8168        }
8169    });
8170    quote! { #(#lines)* }
8171}
8172
8173/// Emit a hidden per-model module carrying one zero-sized type per field,
8174/// each with a `Column` impl pointing back at the model.
8175fn column_module_tokens(
8176    module_ident: &syn::Ident,
8177    struct_name: &syn::Ident,
8178    entries: &[ColumnEntry],
8179) -> TokenStream2 {
8180    let root = rustango_root();
8181    let items = entries.iter().map(|e| {
8182        let col_ty = column_type_ident(&e.ident);
8183        let value_ty = &e.value_ty;
8184        let name = &e.name;
8185        let column = &e.column;
8186        let field_type_tokens = &e.field_type_tokens;
8187        quote! {
8188            #[derive(::core::clone::Clone, ::core::marker::Copy)]
8189            pub struct #col_ty;
8190
8191            impl #root::core::Column for #col_ty {
8192                type Model = super::#struct_name;
8193                type Value = #value_ty;
8194                const NAME: &'static str = #name;
8195                const COLUMN: &'static str = #column;
8196                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8197            }
8198        }
8199    });
8200    quote! {
8201        #[doc(hidden)]
8202        #[allow(non_camel_case_types, non_snake_case)]
8203        pub mod #module_ident {
8204            // Re-import the parent scope so field types referencing
8205            // sibling models (e.g. `ForeignKey<Author>`) resolve
8206            // inside this submodule. Without this we'd hit
8207            // `proc_macro_derive_resolution_fallback` warnings.
8208            #[allow(unused_imports)]
8209            use super::*;
8210            #(#items)*
8211        }
8212    }
8213}
8214
8215fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8216    let name = ident_name(field_ident);
8217    syn::Ident::new(&format!("{name}_col"), field_ident.span())
8218}
8219
8220/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8221/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8222fn restore_auto_now_unless(
8223    idents: &[syn::Ident],
8224    ret: &TokenStream2,
8225    keep: &TokenStream2,
8226    body: &TokenStream2,
8227) -> TokenStream2 {
8228    if idents.is_empty() {
8229        return body.clone();
8230    }
8231    let prev: Vec<syn::Ident> = (0..idents.len())
8232        .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8233        .collect();
8234    quote! {
8235        #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8236        let _saved: #ret = async { #body }.await;
8237        if !(#keep) {
8238            #( self.#idents = #prev; )*
8239        }
8240        _saved
8241    }
8242}
8243
8244/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8245fn ident_name(ident: &syn::Ident) -> String {
8246    syn::ext::IdentExt::unraw(ident).to_string()
8247}
8248
8249/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8250fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8251    match syn::parse_str::<syn::Ident>(name) {
8252        Ok(mut ident) => {
8253            ident.set_span(span);
8254            ident
8255        }
8256        Err(_) => syn::Ident::new_raw(name, span),
8257    }
8258}
8259
8260fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8261    syn::Ident::new(
8262        &format!("__rustango_cols_{struct_name}"),
8263        struct_name.span(),
8264    )
8265}
8266
8267fn from_row_impl_tokens(
8268    struct_name: &syn::Ident,
8269    from_row_inits: &[TokenStream2],
8270    from_row_inits_my: &[TokenStream2],
8271) -> TokenStream2 {
8272    let root = rustango_root();
8273    // The Postgres impl is always emitted — every rustango build pulls in
8274    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8275    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8276    // macro_rules whose body collapses to nothing when rustango is built
8277    // without the `mysql` feature. No user-facing feature shim required.
8278    //
8279    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8280    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8281    // back into a comma-separated list inside square brackets. Since each
8282    // entry is already in `field: expr` shape, the existing tokens slot in.
8283    quote! {
8284        #[cfg(feature = "postgres")]
8285        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8286            for #struct_name
8287        {
8288            fn from_row(
8289                row: &'r #root::sql::sqlx::postgres::PgRow,
8290            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8291                ::core::result::Result::Ok(Self {
8292                    #( #from_row_inits ),*
8293                })
8294            }
8295        }
8296
8297        #root::__impl_my_from_row!(#struct_name, |row| {
8298            #( #from_row_inits_my ),*
8299        });
8300
8301        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8302            #( #from_row_inits ),*
8303        });
8304    }
8305}
8306
8307struct ContainerAttrs {
8308    table: Option<String>,
8309    display: Option<(String, proc_macro2::Span)>,
8310    /// Explicit app label from `#[rustango(app = "blog")]`.
8311    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8312    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8313    /// at runtime — this attribute is the override for cases where
8314    /// the inference is wrong (e.g. a model that conceptually belongs
8315    /// to one app but is physically in another module).
8316    app: Option<String>,
8317    /// Per-model admin knobs from
8318    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8319    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8320    /// admin code falls back to `AdminConfig::DEFAULT`.
8321    admin: Option<AdminAttrs>,
8322    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8323    /// `None` when the model isn't audited — write paths emit no
8324    /// audit entries. When present, single-row writes capture
8325    /// before/after for the listed fields and bulk writes batch
8326    /// snapshots into one INSERT into `rustango_audit_log`.
8327    audit: Option<AuditAttrs>,
8328    /// `true` when `#[rustango(permissions)]` is present. Signals that
8329    /// `auto_create_permissions` should seed the four CRUD codenames for
8330    /// this model.
8331    permissions: bool,
8332    /// Many-to-many relations declared via
8333    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8334    ///                 src = "post_id", dst = "tag_id"))]`.
8335    m2m: Vec<M2MAttr>,
8336    /// Polymorphic M2M relations declared via
8337    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8338    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8339    ///   related_column = "tag_id"))]` (issue #818).
8340    generic_m2m: Vec<GenericM2MAttr>,
8341    /// Composite indexes declared via
8342    /// `#[rustango(index("col1, col2"))]` or
8343    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8344    /// Single-column indexes from `#[rustango(index)]` on fields are
8345    /// accumulated here during field collection.
8346    indexes: Vec<IndexAttr>,
8347    /// Table-level CHECK constraints declared via
8348    /// `#[rustango(check(name = "…", expr = "…"))]`.
8349    checks: Vec<CheckAttr>,
8350    /// Table-level PG `EXCLUDE` constraints declared via
8351    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8352    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8353    /// migration writer renders them on Postgres and skips with a
8354    /// warning on MySQL/SQLite. Issue #319.
8355    excludes: Vec<ExcludeAttr>,
8356    /// Composite (multi-column) FKs declared via
8357    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8358    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8359    composite_fks: Vec<CompositeFkAttr>,
8360    /// Generic ("any model") FKs declared via
8361    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8362    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8363    generic_fks: Vec<GenericFkAttr>,
8364    /// Where this model lives in a tenancy deployment, declared via
8365    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8366    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8367    /// to partition diff output between registry-scoped and
8368    /// tenant-scoped migration files.
8369    scope: Option<String>,
8370    /// Extra QuerySet accessor names from
8371    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8372    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8373    /// the default `Self::objects()`. Multiple attributes allowed.
8374    manager_fns: Vec<syn::Ident>,
8375    /// Default ordering declared via `#[rustango(default_order =
8376    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8377    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8378    /// `-` prefix means descending; the `+` prefix or no prefix means
8379    /// ascending.
8380    default_order: Vec<(String, bool, proc_macro2::Span)>,
8381    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8382    /// Routes the emitted schema's `is_view = true` so the migration
8383    /// snapshot skips this model (its underlying SQL view is operator-
8384    /// managed, not rustango-managed).
8385    is_view: bool,
8386    /// `#[rustango(managed = false)]` — who owns the table.
8387    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8388    /// the migration snapshot skips this model so `makemigrations` /
8389    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8390    /// TABLE` against it (operator-managed schema).
8391    managed: bool,
8392    /// Name of the model's base manager, from
8393    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8394    /// `ModelSchema::base_manager_name`. Declarative-only today.
8395    base_manager_name: Option<String>,
8396    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8397    /// field this model's instances are ordered relative to.
8398    /// Declarative-only today; threaded onto
8399    /// `ModelSchema::order_with_respect_to`.
8400    order_with_respect_to: Option<String>,
8401    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8402    /// Marks the model as a proxy that
8403    /// shares its DB table with another struct. Threaded into
8404    /// `ModelSchema::proxy` so future codegen can skip table-owning
8405    /// behavior for proxies.
8406    proxy: bool,
8407    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8408    /// Each comma-separated capability token surfaces on
8409    /// `ModelSchema::required_db_features` so `manage check --deploy`
8410    /// can warn when the active dialect lacks one.
8411    required_db_features: Vec<String>,
8412    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8413    /// Normalized to the dialect name `manage check --deploy`
8414    /// compares against `Settings.database.backend`. Aliases
8415    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8416    /// stored under the canonical name.
8417    required_db_vendor: Option<String>,
8418    /// `#[rustango(default_related_name = "...")]`. Threaded into
8419    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8420    /// name to use when an FK / M2M field doesn't override it.
8421    /// Today rustango doesn't auto-emit reverse managers; the
8422    /// metadata is the foundation for that work.
8423    default_related_name: Option<String>,
8424    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8425    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8426    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8427    /// inline `COMMENT='...'`, SQLite: no-op).
8428    db_table_comment: Option<String>,
8429    /// `#[rustango(get_latest_by = "created_at")]` /
8430    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8431    /// `(column, descending)` where `descending = true` when the
8432    /// attribute value starts with `-`. Threaded into
8433    /// `ModelSchema::get_latest_by`.
8434    get_latest_by: Option<(String, bool)>,
8435    /// Extra `(codename, label)` permissions
8436    /// from `#[rustango(extra_permissions = "approve:Can approve,
8437    /// archive:Can archive")]`. Comma-separated `codename:label`
8438    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8439    extra_permissions: Vec<(String, String)>,
8440    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8441    /// `"view"`) the framework
8442    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8443    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8444    /// Validated at parse time; unknown actions fail with a span-pointing
8445    /// error.
8446    default_permissions: Vec<String>,
8447    /// `#[rustango(verbose_name = "blog post")]` — the
8448    /// human-readable singular label for the model. Threaded into
8449    /// `ModelSchema::verbose_name` so admin section headers /
8450    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8451    /// over the Rust struct identifier.
8452    verbose_name: Option<String>,
8453    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8454    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8455    /// When unset, `display_label_plural()` auto-suffixes `s`.
8456    verbose_name_plural: Option<String>,
8457    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8458    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8459    /// a name (used by `QuerySet::without_global_scope`) with a
8460    /// `fn() -> WhereExpr` path that the macro emits into
8461    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8462    global_scopes: Vec<GlobalScopeAttr>,
8463    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8464    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8465    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8466    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8467    /// entry emits an inherent `<name>_through(&self)` accessor that
8468    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8469    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8470    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8471    through_relations: Vec<ThroughAttr>,
8472    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8473    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8474    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8475    /// Each entry emits two associated functions on the parent —
8476    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8477    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8478    /// over a correlated subquery against the child table. Users
8479    /// drop the result into `QuerySet::where_raw(...)`.
8480    reverse_has_relations: Vec<ReverseHasAttr>,
8481    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8482    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8483    /// entry so the relation-existence family resolves polymorphic
8484    /// children by name.
8485    generic_has_relations: Vec<GenericHasAttr>,
8486}
8487
8488/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8489/// declaration. Each entry becomes one `core::GlobalScope` in the
8490/// emitted schema literal; `apply` resolves at macro-expand time
8491/// against the consumer's scope so the function must be in scope at
8492/// the use site. Issue #820.
8493struct GlobalScopeAttr {
8494    name: String,
8495    apply: syn::Path,
8496}
8497
8498/// Parsed `#[rustango(through(...))]` declaration. Issue
8499/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8500/// `hasManyThrough` / `hasOneThrough` parity.
8501///
8502/// `Country hasManyThrough Post via User` declares as:
8503///
8504/// ```ignore
8505/// #[rustango(through(
8506///     name                   = "posts",
8507///     far                    = "Post",
8508///     far_fk_column          = "author_id",
8509///     intermediate           = "User",
8510///     intermediate_fk_column = "country_id",
8511/// ))]
8512/// struct Country { ... }
8513/// ```
8514///
8515/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8516/// which returns a queryset filtered via a correlated subquery —
8517/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8518/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8519/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8520/// because the subquery lives inside a `WhereExpr::InSubquery` node
8521/// and the outer queryset's pending list stays empty.
8522///
8523/// All four required arguments use **SQL column / table names**
8524/// (not Rust field names) to sidestep the multi-hop-filter substrate
8525/// gap. Once that substrate lands, a higher-level Rust-field-name
8526/// shorthand can be added without breaking this surface.
8527struct ThroughAttr {
8528    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8529    name: String,
8530    /// Far model type identifier. `far = "Post"` → returns
8531    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8532    /// the derive expands.
8533    far: syn::Ident,
8534    /// SQL column on the far model's table that references the
8535    /// intermediate model's primary key. For `Post`'s
8536    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8537    /// default `<field>_id` convention) or whatever the user
8538    /// declared via `#[rustango(db_column = "...")]`.
8539    far_fk_column: String,
8540    /// Intermediate model type identifier. Needed to look up its
8541    /// `SCHEMA` so the subquery's `FROM` clause points at the
8542    /// intermediate table. `intermediate = "User"`.
8543    intermediate: syn::Ident,
8544    /// SQL column on the intermediate's table that references the
8545    /// source (this) model's primary key. For `User`'s
8546    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8547    intermediate_fk_column: String,
8548    /// SQL primary-key column on the intermediate's table — the
8549    /// column the subquery projects. Optional; defaults to `"id"`
8550    /// (rustango's default PK column name). Override when the
8551    /// intermediate declares a custom PK column.
8552    intermediate_pk_column: String,
8553}
8554
8555/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8556/// child_fk_column = "..."))]` declaration. Issue
8557/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8558/// `whereHas` / `whereDoesntHave` parity.
8559///
8560/// `Post hasMany Comment` declares as:
8561///
8562/// ```ignore
8563/// #[rustango(reverse_has(
8564///     name             = "comments",
8565///     child            = "Comment",
8566///     child_fk_column  = "post_id",
8567/// ))]
8568/// struct Post { ... }
8569/// ```
8570///
8571/// The macro emits two associated functions on `Post`:
8572///
8573/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8574///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8575///   <outer>.<self_pk_column>)`.
8576/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8577///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8578///
8579/// User code:
8580///
8581/// ```ignore
8582/// // Posts with at least one comment:
8583/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8584/// // Posts with no comments:
8585/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8586/// ```
8587///
8588/// As with #817, all column / table identifiers are **SQL names**
8589/// (not Rust field names) so the substrate is independent of the
8590/// outstanding multi-hop filter resolver gap. The emitted
8591/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8592/// SQL-emit time via the writer's scope stack.
8593struct ReverseHasAttr {
8594    /// Accessor name. `name = "comments"` → emits
8595    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8596    name: String,
8597    /// Child model type identifier. `child = "Comment"` — needed to
8598    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8599    /// points at the child table.
8600    child: syn::Ident,
8601    /// SQL column on the child's table that references this model's
8602    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8603    /// column is `"post_id"`.
8604    child_fk_column: String,
8605    /// SQL primary-key column on **this** model's table — the column
8606    /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8607    /// (rustango's default PK column name).
8608    self_pk_column: String,
8609}
8610
8611/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8612/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8613/// relation-existence family (issue #830). The child is a polymorphic,
8614/// content-type-discriminated model; emits a `Model::
8615/// generic_reverse_relations()` entry the queryset resolves by name.
8616struct GenericHasAttr {
8617    /// Accessor name, e.g. `name = "tags"`.
8618    name: String,
8619    /// Child model type identifier (`child = "Tag"`) — looked up for its
8620    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8621    child: syn::Ident,
8622    /// Column on the child table holding the parent's content-type id.
8623    /// Optional; defaults to `"content_type_id"`.
8624    ct_column: String,
8625    /// Column on the child table holding the parent's PK value.
8626    /// Optional; defaults to `"object_pk"`.
8627    pk_column: String,
8628    /// SQL primary-key column on **this** (parent) model's table.
8629    /// Optional; defaults to `"id"`.
8630    self_pk_column: String,
8631}
8632
8633/// Parsed form of one index declaration (field-level or container-level).
8634struct IndexAttr {
8635    /// Index name; auto-derived when `None` at parse time.
8636    name: Option<String>,
8637    /// Column names in the index.
8638    columns: Vec<String>,
8639    /// `true` for `CREATE UNIQUE INDEX`.
8640    unique: bool,
8641    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8642    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8643    /// `"btree"` when the attribute is absent — the DDL writer omits
8644    /// the `USING` clause and the backend uses its own default
8645    /// (btree on every supported dialect).
8646    method: String,
8647    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8648    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8649    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8650    where_clause: Option<String>,
8651    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8652    /// Empty `Vec` (the default) means "no covering columns".
8653    include: Vec<String>,
8654}
8655
8656/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8657struct CheckAttr {
8658    name: String,
8659    expr: String,
8660}
8661
8662/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8663/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8664/// PG-only — surfaced on every backend in the macro emit; the migration
8665/// writer skips the op on MySQL/SQLite. Issue #319.
8666struct ExcludeAttr {
8667    /// Constraint name (free-form Rust identifier).
8668    name: String,
8669    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8670    using: String,
8671    /// Comma-separated `(column, operator)` pairs, in declaration
8672    /// order. Parsed from `"col WITH op, col WITH op"`.
8673    elements: Vec<(String, String)>,
8674    /// Optional `WHERE` predicate (raw SQL).
8675    where_clause: Option<String>,
8676}
8677
8678/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8679/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8680/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8681/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8682/// the model, not the field.
8683struct CompositeFkAttr {
8684    /// Logical relation name (free-form Rust identifier).
8685    name: String,
8686    /// SQL table name of the target.
8687    to: String,
8688    /// Source-side column names, in declaration order.
8689    from: Vec<String>,
8690    /// Target-side column names, same length / order as `from`.
8691    on: Vec<String>,
8692}
8693
8694/// Parsed form of one `#[rustango(generic_fk(name = "target",
8695/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8696/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8697/// generic ("any model") FKs live on the model, not the field.
8698struct GenericFkAttr {
8699    /// Logical relation name (free-form Rust identifier).
8700    name: String,
8701    /// Source-side column carrying the `content_type_id` value.
8702    ct_column: String,
8703    /// Source-side column carrying the target row's primary key.
8704    pk_column: String,
8705}
8706
8707/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8708struct M2MAttr {
8709    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8710    name: String,
8711    /// Target table (e.g. `"app_tags"`).
8712    to: String,
8713    /// Junction table (e.g. `"post_tags"`).
8714    through: String,
8715    /// Source FK column in the junction table (e.g. `"post_id"`).
8716    src: String,
8717    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8718    dst: String,
8719    /// Whether the migration writer should auto-create the junction
8720    /// table. Default `true`. Set `auto_create = false` (#324) when
8721    /// the operator declares the through table as its own
8722    /// `#[derive(Model)]` struct with extra columns.
8723    auto_create: bool,
8724}
8725
8726/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8727/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8728/// junction carries a ContentType discriminator so unrelated models
8729/// share one pivot + related set.
8730struct GenericM2MAttr {
8731    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8732    name: String,
8733    /// Polymorphic junction table (e.g. `"taggables"`).
8734    through: String,
8735    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8736    pk_column: String,
8737    /// Junction column holding the owning model's `content_type_id`
8738    /// discriminator (e.g. `"taggable_type"`).
8739    ct_column: String,
8740    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8741    related_column: String,
8742}
8743
8744/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8745/// "user"))]`. `track` is a comma-separated list of field names whose
8746/// before/after values land in the JSONB `changes` column. `source`
8747/// is informational only — it pins a default source when the model
8748/// is written outside any `audit::with_source(...)` scope (rare).
8749#[derive(Default)]
8750struct AuditAttrs {
8751    /// Field names to capture in the `changes` JSONB. Validated
8752    /// against declared scalar fields at compile time. Empty means
8753    /// "track every scalar field".
8754    track: Option<(Vec<String>, proc_macro2::Span)>,
8755}
8756
8757/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8758/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8759/// comma-separated strings; we validate each ident against the model's
8760/// declared fields at compile time.
8761#[derive(Default)]
8762struct AdminAttrs {
8763    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8764    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8765    list_per_page: Option<usize>,
8766    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8767    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8768    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8769    /// Bulk action names. No field-validation against model fields —
8770    /// these are action handlers, not column references.
8771    actions: Option<(Vec<String>, proc_macro2::Span)>,
8772    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8773    /// sections, comma-separated fields per section, optional
8774    /// `Title:` prefix. Empty title omits the `<legend>`.
8775    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8776    /// `admin(list_display_links = "title")`. Names
8777    /// from `list_display` whose cells should link to detail/edit.
8778    /// Issue #350.
8779    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8780    /// `admin(search_help_text = "...")`. Short
8781    /// caption rendered beside the admin list view's search box.
8782    /// Issue #353.
8783    search_help_text: Option<String>,
8784    /// `admin(actions_on_top = false)`. Hides the
8785    /// action-bar above the table. Default `true`. Issue #354.
8786    actions_on_top: Option<bool>,
8787    /// `admin(actions_on_bottom = true)`. Renders an
8788    /// additional action-bar below the table. Default `false`.
8789    /// Issue #354.
8790    actions_on_bottom: Option<bool>,
8791    /// `admin(date_hierarchy = "created_at")`. Name of
8792    /// a date / datetime field whose values render as a clickable
8793    /// year / month / day drill-down strip above the list table.
8794    /// Empty / unset disables the strip. Issue #355.
8795    date_hierarchy: Option<String>,
8796    /// `admin(prepopulated_fields = "slug:title")`.
8797    /// Each entry is `target:source[+source2]`; multiple entries are
8798    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8799    /// The admin change-form emits JS that slugifies the source values
8800    /// into the target field on every keystroke. Issue #356.
8801    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8802    /// `admin(raw_id_fields = "parent, owner")`. Names
8803    /// of FK fields whose change-form widget renders a lookup link
8804    /// next to the input. Issue #357.
8805    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8806    /// `admin(autocomplete_fields = "author_id")`.
8807    /// Names of FK fields whose change-form widget renders an
8808    /// Ajax-driven typeahead populated from a `__autocomplete`
8809    /// endpoint on the target model. Issue #358.
8810    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8811    /// `admin(list_select_related = "all" | "none" | "author, …")`
8812    /// — tunes the admin list view's FK auto-JOIN
8813    /// policy. Default `"all"` matches rustango's join-everything
8814    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8815    list_select_related: Option<String>,
8816    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8817    /// Each entry is `field_name:widget_name`; multiple
8818    /// entries comma-separated. Empty / unset → no overrides. The
8819    /// list of widget names supported is documented on
8820    /// `AdminConfig::formfield_overrides`. Issue #359.
8821    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8822}
8823
8824fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8825    let mut out = ContainerAttrs {
8826        table: None,
8827        display: None,
8828        app: None,
8829        admin: None,
8830        audit: None,
8831        // Default `permissions = true` so every `#[derive(Model)]`
8832        // gets the four CRUD codenames seeded by `auto_create_permissions`
8833        // and is visible to non-superusers in the tenant admin without
8834        // manual per-model annotation. Models that intentionally don't
8835        // want permission rows (registry-internal types, framework
8836        // tables operators shouldn't manage directly) opt out via
8837        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8838        // out-of-the-box admin invisibility regression (#62).
8839        permissions: true,
8840        m2m: Vec::new(),
8841        generic_m2m: Vec::new(),
8842        indexes: Vec::new(),
8843        checks: Vec::new(),
8844        excludes: Vec::new(),
8845        composite_fks: Vec::new(),
8846        generic_fks: Vec::new(),
8847        scope: None,
8848        manager_fns: Vec::new(),
8849        default_order: Vec::new(),
8850        is_view: false,
8851        managed: true,
8852        verbose_name: None,
8853        verbose_name_plural: None,
8854        base_manager_name: None,
8855        order_with_respect_to: None,
8856        proxy: false,
8857        required_db_features: Vec::new(),
8858        required_db_vendor: None,
8859        default_related_name: None,
8860        db_table_comment: None,
8861        get_latest_by: None,
8862        extra_permissions: Vec::new(),
8863        default_permissions: Vec::new(),
8864        global_scopes: Vec::new(),
8865        through_relations: Vec::new(),
8866        reverse_has_relations: Vec::new(),
8867        generic_has_relations: Vec::new(),
8868    };
8869    for attr in &input.attrs {
8870        if !attr.path().is_ident("rustango") {
8871            continue;
8872        }
8873        attr.parse_nested_meta(|meta| {
8874            if meta.path.is_ident("table") {
8875                let s: LitStr = meta.value()?.parse()?;
8876                let name = s.value();
8877                // v0.27.3 (#65) — macro-time guard against table names
8878                // that compile but break SQL downstream. Hyphens are
8879                // the common footgun: PostgreSQL accepts a quoted
8880                // `"intermediate-region"` in CREATE TABLE, but the
8881                // FK / index name derivation in `migrate::ddl`
8882                // emits `intermediate-region_field_fkey` unquoted,
8883                // which then fails the SQL parser. Same shape rule
8884                // as Postgres regular identifiers so the safe path
8885                // is the only path.
8886                validate_table_name(&name, s.span())?;
8887                out.table = Some(name);
8888                return Ok(());
8889            }
8890            if meta.path.is_ident("display") {
8891                let s: LitStr = meta.value()?.parse()?;
8892                out.display = Some((s.value(), s.span()));
8893                return Ok(());
8894            }
8895            if meta.path.is_ident("app") {
8896                let s: LitStr = meta.value()?.parse()?;
8897                out.app = Some(s.value());
8898                return Ok(());
8899            }
8900            if meta.path.is_ident("scope") {
8901                let s: LitStr = meta.value()?.parse()?;
8902                let val = s.value();
8903                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8904                    return Err(meta.error(format!(
8905                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8906                    )));
8907                }
8908                out.scope = Some(val);
8909                return Ok(());
8910            }
8911            if meta.path.is_ident("admin") {
8912                let mut admin = AdminAttrs::default();
8913                meta.parse_nested_meta(|inner| {
8914                    if inner.path.is_ident("list_display") {
8915                        let s: LitStr = inner.value()?.parse()?;
8916                        admin.list_display =
8917                            Some((split_field_list(&s.value()), s.span()));
8918                        return Ok(());
8919                    }
8920                    if inner.path.is_ident("search_fields") {
8921                        let s: LitStr = inner.value()?.parse()?;
8922                        admin.search_fields =
8923                            Some((split_field_list(&s.value()), s.span()));
8924                        return Ok(());
8925                    }
8926                    if inner.path.is_ident("readonly_fields") {
8927                        let s: LitStr = inner.value()?.parse()?;
8928                        admin.readonly_fields =
8929                            Some((split_field_list(&s.value()), s.span()));
8930                        return Ok(());
8931                    }
8932                    if inner.path.is_ident("list_per_page") {
8933                        let lit: syn::LitInt = inner.value()?.parse()?;
8934                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8935                        return Ok(());
8936                    }
8937                    if inner.path.is_ident("ordering") {
8938                        let s: LitStr = inner.value()?.parse()?;
8939                        admin.ordering = Some((
8940                            parse_ordering_list(&s.value()),
8941                            s.span(),
8942                        ));
8943                        return Ok(());
8944                    }
8945                    if inner.path.is_ident("list_filter") {
8946                        let s: LitStr = inner.value()?.parse()?;
8947                        admin.list_filter =
8948                            Some((split_field_list(&s.value()), s.span()));
8949                        return Ok(());
8950                    }
8951                    if inner.path.is_ident("actions") {
8952                        let s: LitStr = inner.value()?.parse()?;
8953                        admin.actions =
8954                            Some((split_field_list(&s.value()), s.span()));
8955                        return Ok(());
8956                    }
8957                    if inner.path.is_ident("fieldsets") {
8958                        let s: LitStr = inner.value()?.parse()?;
8959                        admin.fieldsets =
8960                            Some((parse_fieldset_list(&s.value()), s.span()));
8961                        return Ok(());
8962                    }
8963                    if inner.path.is_ident("list_display_links") {
8964                        let s: LitStr = inner.value()?.parse()?;
8965                        admin.list_display_links =
8966                            Some((split_field_list(&s.value()), s.span()));
8967                        return Ok(());
8968                    }
8969                    if inner.path.is_ident("search_help_text") {
8970                        let s: LitStr = inner.value()?.parse()?;
8971                        admin.search_help_text = Some(s.value());
8972                        return Ok(());
8973                    }
8974                    if inner.path.is_ident("actions_on_top") {
8975                        let lit: syn::LitBool = inner.value()?.parse()?;
8976                        admin.actions_on_top = Some(lit.value);
8977                        return Ok(());
8978                    }
8979                    if inner.path.is_ident("actions_on_bottom") {
8980                        let lit: syn::LitBool = inner.value()?.parse()?;
8981                        admin.actions_on_bottom = Some(lit.value);
8982                        return Ok(());
8983                    }
8984                    if inner.path.is_ident("date_hierarchy") {
8985                        let s: LitStr = inner.value()?.parse()?;
8986                        admin.date_hierarchy = Some(s.value());
8987                        return Ok(());
8988                    }
8989                    if inner.path.is_ident("prepopulated_fields") {
8990                        let s: LitStr = inner.value()?.parse()?;
8991                        admin.prepopulated_fields =
8992                            Some((parse_prepopulated_list(&s.value()), s.span()));
8993                        return Ok(());
8994                    }
8995                    if inner.path.is_ident("raw_id_fields") {
8996                        let s: LitStr = inner.value()?.parse()?;
8997                        admin.raw_id_fields =
8998                            Some((split_field_list(&s.value()), s.span()));
8999                        return Ok(());
9000                    }
9001                    if inner.path.is_ident("autocomplete_fields") {
9002                        let s: LitStr = inner.value()?.parse()?;
9003                        admin.autocomplete_fields =
9004                            Some((split_field_list(&s.value()), s.span()));
9005                        return Ok(());
9006                    }
9007                    if inner.path.is_ident("list_select_related") {
9008                        let s: LitStr = inner.value()?.parse()?;
9009                        admin.list_select_related = Some(s.value());
9010                        return Ok(());
9011                    }
9012                    if inner.path.is_ident("formfield_overrides") {
9013                        let s: LitStr = inner.value()?.parse()?;
9014                        admin.formfield_overrides =
9015                            Some((parse_formfield_overrides(&s.value()), s.span()));
9016                        return Ok(());
9017                    }
9018                    Err(inner.error(
9019                        "unknown admin attribute (supported: \
9020                         `list_display`, `list_display_links`, \
9021                         `search_fields`, `search_help_text`, \
9022                         `readonly_fields`, \
9023                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
9024                         `actions_on_top`, `actions_on_bottom`, \
9025                         `date_hierarchy`, \
9026                         `prepopulated_fields`, \
9027                         `raw_id_fields`, \
9028                         `autocomplete_fields`, \
9029                         `list_select_related`, \
9030                         `formfield_overrides`, \
9031                         `fieldsets`)",
9032                    ))
9033                })?;
9034                out.admin = Some(admin);
9035                return Ok(());
9036            }
9037            if meta.path.is_ident("manager") {
9038                // Its empty trait could not take methods (#2132).
9039                return Err(meta.error(
9040                    "`manager(ext = ...)` was removed (#2132): declare your own \
9041                     `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
9042                ));
9043            }
9044            if meta.path.is_ident("manager_fn") {
9045                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
9046                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
9047                // next to the default `Self::objects()`. Multiple
9048                // attributes accumulate.
9049                let s: LitStr = meta.value()?.parse()?;
9050                let name = s.value();
9051                if name.is_empty() {
9052                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
9053                }
9054                if name == "objects" {
9055                    return Err(meta.error(
9056                        "`manager_fn = \"objects\"` collides with the default \
9057                         accessor — pick a different name",
9058                    ));
9059                }
9060                let ident = syn::Ident::new(&name, s.span());
9061                if out.manager_fns.iter().any(|prev| *prev == ident) {
9062                    return Err(meta.error(format!(
9063                        "duplicate `manager_fn = \"{name}\"`"
9064                    )));
9065                }
9066                out.manager_fns.push(ident);
9067                return Ok(());
9068            }
9069            if meta.path.is_ident("default_order") {
9070                // `#[rustango(default_order = "-created_at, status")]`
9071                // — issue #291 / T2.5. Comma-separated list; `-prefix`
9072                // means descending, `+prefix` or bare name means ascending.
9073                // Per-query opt-in via `QuerySet::with_default_order()`.
9074                let s: LitStr = meta.value()?.parse()?;
9075                let raw = s.value();
9076                let span = s.span();
9077                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9078                    Vec::new();
9079                for entry in raw.split(',') {
9080                    let trimmed = entry.trim();
9081                    if trimmed.is_empty() {
9082                        return Err(syn::Error::new(
9083                            span,
9084                            "`default_order = \"...\"` has an empty entry — \
9085                             check for a stray comma",
9086                        ));
9087                    }
9088                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9089                        (true, rest.trim().to_owned())
9090                    } else if let Some(rest) = trimmed.strip_prefix('+') {
9091                        (false, rest.trim().to_owned())
9092                    } else {
9093                        (false, trimmed.to_owned())
9094                    };
9095                    if name.is_empty() {
9096                        return Err(syn::Error::new(
9097                            span,
9098                            "`default_order` entry has no column name after the prefix",
9099                        ));
9100                    }
9101                    if parsed.iter().any(|(n, _, _)| *n == name) {
9102                        return Err(syn::Error::new(
9103                            span,
9104                            format!("duplicate column `{name}` in `default_order`"),
9105                        ));
9106                    }
9107                    parsed.push((name, desc, span));
9108                }
9109                if parsed.is_empty() {
9110                    return Err(syn::Error::new(
9111                        span,
9112                        "`default_order = \"...\"` cannot be empty",
9113                    ));
9114                }
9115                out.default_order = parsed;
9116                return Ok(());
9117            }
9118            if meta.path.is_ident("global_scope") {
9119                // `#[rustango(global_scope(name = "active", apply =
9120                //  path::to::fn))]` — issue #820. The apply function
9121                // path resolves at macro-expand time in the consumer's
9122                // scope; the macro re-emits it verbatim into the
9123                // `ModelSchema::global_scopes` slice literal.
9124                let span = meta.path.span();
9125                let mut scope_name: Option<String> = None;
9126                let mut apply_path: Option<syn::Path> = None;
9127                meta.parse_nested_meta(|inner| {
9128                    if inner.path.is_ident("name") {
9129                        let s: LitStr = inner.value()?.parse()?;
9130                        let raw = s.value();
9131                        if raw.trim().is_empty() {
9132                            return Err(syn::Error::new(
9133                                s.span(),
9134                                "`global_scope(name = \"...\")` must not be empty",
9135                            ));
9136                        }
9137                        scope_name = Some(raw);
9138                        return Ok(());
9139                    }
9140                    if inner.path.is_ident("apply") {
9141                        let p: syn::Path = inner.value()?.parse()?;
9142                        apply_path = Some(p);
9143                        return Ok(());
9144                    }
9145                    Err(inner.error(
9146                        "unknown `global_scope` attribute (supported: \
9147                         `name`, `apply`)",
9148                    ))
9149                })?;
9150                let Some(name) = scope_name else {
9151                    return Err(syn::Error::new(
9152                        span,
9153                        "`global_scope` requires `name = \"...\"`",
9154                    ));
9155                };
9156                let Some(apply) = apply_path else {
9157                    return Err(syn::Error::new(
9158                        span,
9159                        "`global_scope` requires `apply = fn_path`",
9160                    ));
9161                };
9162                if out.global_scopes.iter().any(|s| s.name == name) {
9163                    return Err(syn::Error::new(
9164                        span,
9165                        format!(
9166                            "duplicate global scope name `{name}` — \
9167                             pick a unique identifier so \
9168                             `QuerySet::without_global_scope(\"{name}\")` \
9169                             is unambiguous"
9170                        ),
9171                    ));
9172                }
9173                out.global_scopes.push(GlobalScopeAttr { name, apply });
9174                return Ok(());
9175            }
9176            if meta.path.is_ident("through") {
9177                // `#[rustango(through(name = "posts", far = "Post",
9178                //  far_fk_column = "author_id", intermediate = "User",
9179                //  intermediate_fk_column = "country_id"
9180                //  [, intermediate_pk_column = "id"]))]` — issue #817.
9181                let span = meta.path.span();
9182                let mut accessor_name: Option<String> = None;
9183                let mut far_ident: Option<syn::Ident> = None;
9184                let mut far_fk_column: Option<String> = None;
9185                let mut intermediate_ident: Option<syn::Ident> = None;
9186                let mut intermediate_fk_column: Option<String> = None;
9187                let mut intermediate_pk_column: Option<String> = None;
9188                fn parse_nonempty_string(
9189                    inner: &syn::meta::ParseNestedMeta<'_>,
9190                    field: &str,
9191                ) -> syn::Result<String> {
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                            format!("`through({field} = \"...\")` must not be empty"),
9199                        ));
9200                    }
9201                    Ok(trimmed.to_owned())
9202                }
9203                meta.parse_nested_meta(|inner| {
9204                    if inner.path.is_ident("name") {
9205                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9206                        return Ok(());
9207                    }
9208                    if inner.path.is_ident("far") {
9209                        // Far model name accepted as a string literal
9210                        // so the attribute fits inside the existing
9211                        // `parse_nested_meta` shape; parsed back into a
9212                        // `syn::Ident` so the emitted accessor can
9213                        // reference the type directly.
9214                        let s: LitStr = inner.value()?.parse()?;
9215                        let raw = s.value();
9216                        let trimmed = raw.trim();
9217                        if trimmed.is_empty() {
9218                            return Err(syn::Error::new(
9219                                s.span(),
9220                                "`through(far = \"...\")` must not be empty",
9221                            ));
9222                        }
9223                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9224                        return Ok(());
9225                    }
9226                    if inner.path.is_ident("far_fk_column") {
9227                        far_fk_column =
9228                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9229                        return Ok(());
9230                    }
9231                    if inner.path.is_ident("intermediate") {
9232                        let s: LitStr = inner.value()?.parse()?;
9233                        let raw = s.value();
9234                        let trimmed = raw.trim();
9235                        if trimmed.is_empty() {
9236                            return Err(syn::Error::new(
9237                                s.span(),
9238                                "`through(intermediate = \"...\")` must not be empty",
9239                            ));
9240                        }
9241                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9242                        return Ok(());
9243                    }
9244                    if inner.path.is_ident("intermediate_fk_column") {
9245                        intermediate_fk_column =
9246                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9247                        return Ok(());
9248                    }
9249                    if inner.path.is_ident("intermediate_pk_column") {
9250                        intermediate_pk_column =
9251                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9252                        return Ok(());
9253                    }
9254                    Err(inner.error(
9255                        "unknown `through` attribute (supported: \
9256                         `name`, `far`, `far_fk_column`, \
9257                         `intermediate`, `intermediate_fk_column`, \
9258                         `intermediate_pk_column`)",
9259                    ))
9260                })?;
9261                let Some(name) = accessor_name else {
9262                    return Err(syn::Error::new(
9263                        span,
9264                        "`through` requires `name = \"...\"`",
9265                    ));
9266                };
9267                let Some(far) = far_ident else {
9268                    return Err(syn::Error::new(
9269                        span,
9270                        "`through` requires `far = \"FarModelType\"`",
9271                    ));
9272                };
9273                let Some(far_fk_column) = far_fk_column else {
9274                    return Err(syn::Error::new(
9275                        span,
9276                        "`through` requires `far_fk_column = \"<column>\"`",
9277                    ));
9278                };
9279                let Some(intermediate) = intermediate_ident else {
9280                    return Err(syn::Error::new(
9281                        span,
9282                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9283                    ));
9284                };
9285                let Some(intermediate_fk_column) = intermediate_fk_column else {
9286                    return Err(syn::Error::new(
9287                        span,
9288                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9289                    ));
9290                };
9291                let intermediate_pk_column =
9292                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9293                if out.through_relations.iter().any(|t| t.name == name) {
9294                    return Err(syn::Error::new(
9295                        span,
9296                        format!(
9297                            "duplicate `through(name = \"{name}\")` — \
9298                             pick a unique accessor name"
9299                        ),
9300                    ));
9301                }
9302                out.through_relations.push(ThroughAttr {
9303                    name,
9304                    far,
9305                    far_fk_column,
9306                    intermediate,
9307                    intermediate_fk_column,
9308                    intermediate_pk_column,
9309                });
9310                return Ok(());
9311            }
9312            if meta.path.is_ident("reverse_has") {
9313                // `#[rustango(reverse_has(name = "comments",
9314                //  child = "Comment", child_fk_column = "post_id"
9315                //  [, self_pk_column = "id"]))]` — issue #830.
9316                let span = meta.path.span();
9317                let mut accessor_name: Option<String> = None;
9318                let mut child_ident: Option<syn::Ident> = None;
9319                let mut child_fk_column: Option<String> = None;
9320                let mut self_pk_column: Option<String> = None;
9321                meta.parse_nested_meta(|inner| {
9322                    if inner.path.is_ident("name") {
9323                        let s: LitStr = inner.value()?.parse()?;
9324                        let raw = s.value();
9325                        if raw.trim().is_empty() {
9326                            return Err(syn::Error::new(
9327                                s.span(),
9328                                "`reverse_has(name = \"...\")` must not be empty",
9329                            ));
9330                        }
9331                        accessor_name = Some(raw);
9332                        return Ok(());
9333                    }
9334                    if inner.path.is_ident("child") {
9335                        let s: LitStr = inner.value()?.parse()?;
9336                        let raw = s.value();
9337                        let trimmed = raw.trim();
9338                        if trimmed.is_empty() {
9339                            return Err(syn::Error::new(
9340                                s.span(),
9341                                "`reverse_has(child = \"...\")` must not be empty",
9342                            ));
9343                        }
9344                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9345                        return Ok(());
9346                    }
9347                    if inner.path.is_ident("child_fk_column") {
9348                        let s: LitStr = inner.value()?.parse()?;
9349                        let raw = s.value();
9350                        let trimmed = raw.trim();
9351                        if trimmed.is_empty() {
9352                            return Err(syn::Error::new(
9353                                s.span(),
9354                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9355                            ));
9356                        }
9357                        child_fk_column = Some(trimmed.to_owned());
9358                        return Ok(());
9359                    }
9360                    if inner.path.is_ident("self_pk_column") {
9361                        let s: LitStr = inner.value()?.parse()?;
9362                        let raw = s.value();
9363                        let trimmed = raw.trim();
9364                        if trimmed.is_empty() {
9365                            return Err(syn::Error::new(
9366                                s.span(),
9367                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9368                            ));
9369                        }
9370                        self_pk_column = Some(trimmed.to_owned());
9371                        return Ok(());
9372                    }
9373                    Err(inner.error(
9374                        "unknown `reverse_has` attribute (supported: \
9375                         `name`, `child`, `child_fk_column`, \
9376                         `self_pk_column`)",
9377                    ))
9378                })?;
9379                let Some(name) = accessor_name else {
9380                    return Err(syn::Error::new(
9381                        span,
9382                        "`reverse_has` requires `name = \"...\"`",
9383                    ));
9384                };
9385                let Some(child) = child_ident else {
9386                    return Err(syn::Error::new(
9387                        span,
9388                        "`reverse_has` requires `child = \"ChildModelType\"`",
9389                    ));
9390                };
9391                let Some(child_fk_column) = child_fk_column else {
9392                    return Err(syn::Error::new(
9393                        span,
9394                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9395                    ));
9396                };
9397                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9398                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9399                    return Err(syn::Error::new(
9400                        span,
9401                        format!(
9402                            "duplicate `reverse_has(name = \"{name}\")` — \
9403                             pick a unique accessor name"
9404                        ),
9405                    ));
9406                }
9407                out.reverse_has_relations.push(ReverseHasAttr {
9408                    name,
9409                    child,
9410                    child_fk_column,
9411                    self_pk_column,
9412                });
9413                return Ok(());
9414            }
9415            if meta.path.is_ident("generic_has") {
9416                // `#[rustango(generic_has(name = "tags",
9417                //  child = "Tag", ct_column = "content_type_id",
9418                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9419                //  — issue #830, the reverse generic-FK (GFK) arm of the
9420                //  relation-existence family.
9421                let span = meta.path.span();
9422                let mut accessor_name: Option<String> = None;
9423                let mut child_ident: Option<syn::Ident> = None;
9424                let mut ct_column: Option<String> = None;
9425                let mut pk_column: Option<String> = None;
9426                let mut self_pk_column: Option<String> = None;
9427                meta.parse_nested_meta(|inner| {
9428                    if inner.path.is_ident("name") {
9429                        let s: LitStr = inner.value()?.parse()?;
9430                        let raw = s.value();
9431                        if raw.trim().is_empty() {
9432                            return Err(syn::Error::new(
9433                                s.span(),
9434                                "`generic_has(name = \"...\")` must not be empty",
9435                            ));
9436                        }
9437                        accessor_name = Some(raw);
9438                        return Ok(());
9439                    }
9440                    if inner.path.is_ident("child") {
9441                        let s: LitStr = inner.value()?.parse()?;
9442                        let trimmed = s.value().trim().to_owned();
9443                        if trimmed.is_empty() {
9444                            return Err(syn::Error::new(
9445                                s.span(),
9446                                "`generic_has(child = \"...\")` must not be empty",
9447                            ));
9448                        }
9449                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9450                        return Ok(());
9451                    }
9452                    if inner.path.is_ident("ct_column") {
9453                        let s: LitStr = inner.value()?.parse()?;
9454                        let trimmed = s.value().trim().to_owned();
9455                        if trimmed.is_empty() {
9456                            return Err(syn::Error::new(
9457                                s.span(),
9458                                "`generic_has(ct_column = \"...\")` must not be empty",
9459                            ));
9460                        }
9461                        ct_column = Some(trimmed);
9462                        return Ok(());
9463                    }
9464                    if inner.path.is_ident("pk_column") {
9465                        let s: LitStr = inner.value()?.parse()?;
9466                        let trimmed = s.value().trim().to_owned();
9467                        if trimmed.is_empty() {
9468                            return Err(syn::Error::new(
9469                                s.span(),
9470                                "`generic_has(pk_column = \"...\")` must not be empty",
9471                            ));
9472                        }
9473                        pk_column = Some(trimmed);
9474                        return Ok(());
9475                    }
9476                    if inner.path.is_ident("self_pk_column") {
9477                        let s: LitStr = inner.value()?.parse()?;
9478                        let trimmed = s.value().trim().to_owned();
9479                        if trimmed.is_empty() {
9480                            return Err(syn::Error::new(
9481                                s.span(),
9482                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9483                            ));
9484                        }
9485                        self_pk_column = Some(trimmed);
9486                        return Ok(());
9487                    }
9488                    Err(inner.error(
9489                        "unknown `generic_has` attribute (supported: \
9490                         `name`, `child`, `ct_column`, `pk_column`, \
9491                         `self_pk_column`)",
9492                    ))
9493                })?;
9494                let Some(name) = accessor_name else {
9495                    return Err(syn::Error::new(
9496                        span,
9497                        "`generic_has` requires `name = \"...\"`",
9498                    ));
9499                };
9500                let Some(child) = child_ident else {
9501                    return Err(syn::Error::new(
9502                        span,
9503                        "`generic_has` requires `child = \"ChildModelType\"`",
9504                    ));
9505                };
9506                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9507                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9508                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9509                if out.generic_has_relations.iter().any(|r| r.name == name) {
9510                    return Err(syn::Error::new(
9511                        span,
9512                        format!(
9513                            "duplicate `generic_has(name = \"{name}\")` — \
9514                             pick a unique accessor name"
9515                        ),
9516                    ));
9517                }
9518                out.generic_has_relations.push(GenericHasAttr {
9519                    name,
9520                    child,
9521                    ct_column,
9522                    pk_column,
9523                    self_pk_column,
9524                });
9525                return Ok(());
9526            }
9527            if meta.path.is_ident("audit") {
9528                let mut audit = AuditAttrs::default();
9529                meta.parse_nested_meta(|inner| {
9530                    if inner.path.is_ident("track") {
9531                        let s: LitStr = inner.value()?.parse()?;
9532                        audit.track =
9533                            Some((split_field_list(&s.value()), s.span()));
9534                        return Ok(());
9535                    }
9536                    Err(inner.error(
9537                        "unknown audit attribute (supported: `track`)",
9538                    ))
9539                })?;
9540                out.audit = Some(audit);
9541                return Ok(());
9542            }
9543            if meta.path.is_ident("permissions") {
9544                // Two forms accepted:
9545                //   #[rustango(permissions)]          — flag form, true
9546                //   #[rustango(permissions = false)]  — explicit opt-out
9547                //   #[rustango(permissions = true)]   — explicit opt-in
9548                if let Ok(v) = meta.value() {
9549                    let lit: syn::LitBool = v.parse()?;
9550                    out.permissions = lit.value;
9551                } else {
9552                    out.permissions = true;
9553                }
9554                return Ok(());
9555            }
9556            if meta.path.is_ident("view") {
9557                // Issue #293 / T2.10. Two forms accepted, matching
9558                // the `permissions` flag pattern:
9559                //   #[rustango(view)]          — flag form, true
9560                //   #[rustango(view = false)]  — explicit opt-out
9561                //   #[rustango(view = true)]   — explicit opt-in
9562                if let Ok(v) = meta.value() {
9563                    let lit: syn::LitBool = v.parse()?;
9564                    out.is_view = lit.value;
9565                } else {
9566                    out.is_view = true;
9567                }
9568                return Ok(());
9569            }
9570            if meta.path.is_ident("managed") {
9571                // Who owns the table. Issue #321.
9572                //   #[rustango(managed = false)]  — operator-managed table
9573                //   #[rustango(managed = true)]   — rustango-managed (the default)
9574                // Bare-flag form is intentionally not accepted: writing
9575                // `#[rustango(managed)]` reads as "yes please manage it"
9576                // which is already the default. The opt-out is the only
9577                // useful state, so it must be explicit.
9578                let v = meta.value()?;
9579                let lit: syn::LitBool = v.parse()?;
9580                out.managed = lit.value;
9581                return Ok(());
9582            }
9583            if meta.path.is_ident("verbose_name") {
9584                let s: LitStr = meta.value()?.parse()?;
9585                out.verbose_name = Some(s.value());
9586                return Ok(());
9587            }
9588            if meta.path.is_ident("verbose_name_plural") {
9589                let s: LitStr = meta.value()?.parse()?;
9590                out.verbose_name_plural = Some(s.value());
9591                return Ok(());
9592            }
9593            if meta.path.is_ident("db_table_comment") {
9594                // Free-form table-level comment attached to the DB
9595                // catalog.
9596                let s: LitStr = meta.value()?.parse()?;
9597                out.db_table_comment = Some(s.value());
9598                return Ok(());
9599            }
9600            if meta.path.is_ident("proxy") {
9601                // Declarative flag
9602                // marking the struct as a proxy of another model that
9603                // shares its DB table. Stored on `ModelSchema::proxy`
9604                // so future codegen can skip `CreateTable` emission
9605                // for proxies (parent owns the table) and route
9606                // per-instance method resolution to the proxy class.
9607                //
9608                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9609                let value = if meta.input.peek(syn::Token![=]) {
9610                    meta.value()?.parse::<syn::LitBool>()?.value
9611                } else {
9612                    true
9613                };
9614                out.proxy = value;
9615                return Ok(());
9616            }
9617            if meta.path.is_ident("order_with_respect_to") {
9618                // `order_with_respect_to = "parent_fk"` — the model's
9619                // instances are intrinsically ordered relative to
9620                // their parent FK.
9621                //
9622                // rustango stores the FK field name on
9623                // `ModelSchema::order_with_respect_to`. Declarative-only
9624                // today: the migration writer + admin surfaces still
9625                // treat every model identically. Future codegen will
9626                // key off the metadata to auto-emit the `_order`
9627                // column and reorder helpers.
9628                //
9629                // Validated as a Rust-shape identifier so the macro
9630                // can reject typos at derive time.
9631                let s: LitStr = meta.value()?.parse()?;
9632                let raw = s.value();
9633                if raw.is_empty() {
9634                    return Err(syn::Error::new(
9635                        s.span(),
9636                        "`order_with_respect_to` must be a non-empty FK field name",
9637                    ));
9638                }
9639                let valid = raw
9640                    .chars()
9641                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9642                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9643                if !valid {
9644                    return Err(syn::Error::new(
9645                        s.span(),
9646                        format!(
9647                            "`order_with_respect_to` must be a valid Rust \
9648                             identifier (letters / digits / underscores, \
9649                             not starting with a digit); got `{raw}`"
9650                        ),
9651                    ));
9652                }
9653                out.order_with_respect_to = Some(raw);
9654                return Ok(());
9655            }
9656            if meta.path.is_ident("required_db_features") {
9657                // Capability tokens the model needs (e.g.
9658                // `"json_extract"`, `"window_functions"`,
9659                // `"row_security"`). Comma-separated.
9660                // `manage check --deploy` walks every model and warns
9661                // when the active backend doesn't advertise the
9662                // capability.
9663                //
9664                // rustango ships a small registry of capability tokens
9665                // each dialect supports — see `Dialect::supports`.
9666                // Unknown tokens still parse (they end up on the
9667                // schema and show up in the warning) so projects can
9668                // declare aspirational capabilities and the check
9669                // verb will keep nagging until the dialect implements
9670                // them.
9671                let s: LitStr = meta.value()?.parse()?;
9672                let raw = s.value();
9673                let features: Vec<String> = raw
9674                    .split(',')
9675                    .map(str::trim)
9676                    .filter(|s| !s.is_empty())
9677                    .map(str::to_owned)
9678                    .collect();
9679                if features.is_empty() {
9680                    return Err(syn::Error::new(
9681                        s.span(),
9682                        "`required_db_features` must list at least one \
9683                         comma-separated capability token",
9684                    ));
9685                }
9686                out.required_db_features = features;
9687                return Ok(());
9688            }
9689            if meta.path.is_ident("required_db_vendor") {
9690                // The model is only meant to run against the named DB
9691                // backend. `manage check --deploy` flags a mismatch so
9692                // ops catches "I forgot to switch DATABASE_URL" at
9693                // deploy time rather than runtime.
9694                //
9695                // Restricted to the three backends rustango ships
9696                // dialects for so the check verb can compare reliably.
9697                let s: LitStr = meta.value()?.parse()?;
9698                let raw = s.value().to_ascii_lowercase();
9699                match raw.as_str() {
9700                    "postgresql" | "postgres" | "pg" => {
9701                        out.required_db_vendor = Some("postgres".to_owned());
9702                    }
9703                    "mysql" | "mariadb" => {
9704                        out.required_db_vendor = Some("mysql".to_owned());
9705                    }
9706                    "sqlite" | "sqlite3" => {
9707                        out.required_db_vendor = Some("sqlite".to_owned());
9708                    }
9709                    _ => {
9710                        return Err(syn::Error::new(
9711                            s.span(),
9712                            format!(
9713                                "unknown required_db_vendor `{raw}` — \
9714                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9715                                 `mysql` (alias: `mariadb`), or `sqlite` \
9716                                 (alias: `sqlite3`)"
9717                            ),
9718                        ));
9719                    }
9720                }
9721                return Ok(());
9722            }
9723            if meta.path.is_ident("base_manager_name") {
9724                // Name of the manager that `<instance>.<relation>_set`
9725                // uses when resolving reverse-relation managers.
9726                // Distinct from `default_manager_name` (what
9727                // `Self::objects()` returns). Stored on
9728                // `ModelSchema::base_manager_name`.
9729                //
9730                // Validated as a Rust identifier so it stays safe to
9731                // re-emit as code in future reverse-manager codegen.
9732                let s: LitStr = meta.value()?.parse()?;
9733                let raw = s.value();
9734                if raw.is_empty() {
9735                    return Err(syn::Error::new(
9736                        s.span(),
9737                        "`base_manager_name` must be a non-empty string",
9738                    ));
9739                }
9740                let valid = raw
9741                    .chars()
9742                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9743                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9744                if !valid {
9745                    return Err(syn::Error::new(
9746                        s.span(),
9747                        format!(
9748                            "`base_manager_name` must be a valid Rust \
9749                             identifier (letters / digits / underscores, \
9750                             not starting with a digit); got `{raw}`"
9751                        ),
9752                    ));
9753                }
9754                out.base_manager_name = Some(raw);
9755                return Ok(());
9756            }
9757            if meta.path.is_ident("default_related_name") {
9758                // The name reverse-relation accessors use when callers
9759                // don't override `related_name = "..."` on the FK / M2M
9760                // field. Stored on `ModelSchema::default_related_name`
9761                // so future reverse-manager codegen / schema emit /
9762                // admin templates can pick the right accessor name
9763                // (today rustango doesn't auto-emit reverse managers;
9764                // the metadata is the foundation for that work).
9765                //
9766                // Enforced non-empty + ASCII identifier-shape so the
9767                // string is safe to use as a Rust ident later.
9768                let s: LitStr = meta.value()?.parse()?;
9769                let raw = s.value();
9770                if raw.is_empty() {
9771                    return Err(syn::Error::new(
9772                        s.span(),
9773                        "`default_related_name` must be a non-empty string",
9774                    ));
9775                }
9776                let valid = raw
9777                    .chars()
9778                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9779                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9780                if !valid {
9781                    return Err(syn::Error::new(
9782                        s.span(),
9783                        format!(
9784                            "`default_related_name` must be snake_case ASCII \
9785                             (lowercase letters / digits / underscores, not \
9786                             starting with a digit); got `{raw}`"
9787                        ),
9788                    ));
9789                }
9790                out.default_related_name = Some(raw);
9791                return Ok(());
9792            }
9793            if meta.path.is_ident("extra_permissions") {
9794                // Comma-separated `codename:label` pairs.
9795                let s: LitStr = meta.value()?.parse()?;
9796                let raw = s.value();
9797                let mut pairs = Vec::new();
9798                for entry in raw.split(',') {
9799                    let entry = entry.trim();
9800                    if entry.is_empty() {
9801                        continue;
9802                    }
9803                    let (codename, label) = match entry.split_once(':') {
9804                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9805                        None => (entry.to_owned(), entry.to_owned()),
9806                    };
9807                    if codename.is_empty() {
9808                        return Err(meta.error(
9809                            "`extra_permissions` entries must be `codename:label` pairs",
9810                        ));
9811                    }
9812                    pairs.push((codename, label));
9813                }
9814                if pairs.is_empty() {
9815                    return Err(meta
9816                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9817                }
9818                out.extra_permissions = pairs;
9819                return Ok(());
9820            }
9821            if meta.path.is_ident("default_permissions") {
9822                // Comma-separated subset of the CRUD action set.
9823                // Empty means all four (the framework default).
9824                let s: LitStr = meta.value()?.parse()?;
9825                let raw = s.value();
9826                let mut actions: Vec<String> = Vec::new();
9827                for entry in raw.split(',') {
9828                    let action = entry.trim().to_ascii_lowercase();
9829                    if action.is_empty() {
9830                        continue;
9831                    }
9832                    match action.as_str() {
9833                        "add" | "change" | "delete" | "view" => {}
9834                        other => {
9835                            return Err(syn::Error::new(
9836                                s.span(),
9837                                format!(
9838                                    "unknown default_permissions action `{other}` — \
9839                                     expected one of `add`, `change`, `delete`, `view`"
9840                                ),
9841                            ));
9842                        }
9843                    }
9844                    if !actions.contains(&action) {
9845                        actions.push(action);
9846                    }
9847                }
9848                if actions.is_empty() {
9849                    return Err(syn::Error::new(
9850                        s.span(),
9851                        "`default_permissions = \"…\"` must list at least one action; \
9852                         use `permissions = false` on the container if you want NO \
9853                         permissions seeded for this model.",
9854                    ));
9855                }
9856                out.default_permissions = actions;
9857                return Ok(());
9858            }
9859            if meta.path.is_ident("get_latest_by") {
9860                // The `-` prefix selects descending order.
9861                let s: LitStr = meta.value()?.parse()?;
9862                let raw = s.value();
9863                let trimmed = raw.trim();
9864                if trimmed.is_empty() {
9865                    return Err(meta.error("`get_latest_by` must name a column"));
9866                }
9867                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9868                    (stripped.to_owned(), true)
9869                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9870                    (stripped.to_owned(), false)
9871                } else {
9872                    (trimmed.to_owned(), false)
9873                };
9874                if col.is_empty() {
9875                    return Err(meta.error("`get_latest_by` must name a column"));
9876                }
9877                out.get_latest_by = Some((col, desc));
9878                return Ok(());
9879            }
9880            if meta.path.is_ident("unique_together") {
9881                // Composite UNIQUE index. Two syntaxes:
9882                //
9883                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9884                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9885                //
9886                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9887                // (col1, col2)`, where <name> defaults to
9888                // `<table>_<col1>_<col2>_uq` when not supplied.
9889                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9890                out.indexes.push(IndexAttr {
9891                    name,
9892                    columns,
9893                    unique: true,
9894                    method: "btree".to_owned(),
9895                    where_clause: None,
9896                include: Vec::new(),
9897                });
9898                return Ok(());
9899            }
9900            if meta.path.is_ident("index_together") {
9901                // Composite (non-unique) index. Two syntaxes
9902                // mirroring `unique_together`.
9903                //
9904                //   #[rustango(index_together = "created_at, status")]
9905                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9906                let (columns, name) = parse_together_attr(&meta, "index_together")?;
9907                out.indexes.push(IndexAttr {
9908                    name,
9909                    columns,
9910                    unique: false,
9911                    method: "btree".to_owned(),
9912                    where_clause: None,
9913                include: Vec::new(),
9914                });
9915                return Ok(());
9916            }
9917            if meta.path.is_ident("unique_when") {
9918                // Partial unique index. Issue #265 / T1.3.
9919                //
9920                //   #[rustango(unique_when(
9921                //       columns   = "email",
9922                //       condition = "deleted_at IS NULL",
9923                //       name      = "unique_active_email"
9924                //   ))]
9925                //
9926                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9927                // on PG / SQLite (both ship partial indexes natively).
9928                // MySQL falls back to a plain UNIQUE index — the
9929                // condition is lost; document the limitation in the
9930                // generated migration.
9931                let mut columns: Option<Vec<String>> = None;
9932                let mut condition: Option<String> = None;
9933                let mut name: Option<String> = None;
9934                let mut include: Vec<String> = Vec::new();
9935                meta.parse_nested_meta(|inner| {
9936                    if inner.path.is_ident("columns") {
9937                        let s: LitStr = inner.value()?.parse()?;
9938                        columns = Some(split_field_list(&s.value()));
9939                        return Ok(());
9940                    }
9941                    if inner.path.is_ident("condition") {
9942                        let s: LitStr = inner.value()?.parse()?;
9943                        condition = Some(s.value());
9944                        return Ok(());
9945                    }
9946                    if inner.path.is_ident("name") {
9947                        let s: LitStr = inner.value()?.parse()?;
9948                        name = Some(s.value());
9949                        return Ok(());
9950                    }
9951                    if inner.path.is_ident("include") {
9952                        // PG 11+ covering-index columns. Non-key columns
9953                        // travel with the index leaf for index-only
9954                        // scans. Dropped on MySQL/SQLite by the writer.
9955                        let s: LitStr = inner.value()?.parse()?;
9956                        include = split_field_list(&s.value());
9957                        return Ok(());
9958                    }
9959                    Err(inner.error(
9960                        "unknown unique_when attribute (supported: \
9961                         `columns = \"...\"`, `condition = \"...\"`, \
9962                         `name = \"...\"`, `include = \"...\"`)",
9963                    ))
9964                })?;
9965                let columns = columns.ok_or_else(|| {
9966                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9967                })?;
9968                let condition = condition.ok_or_else(|| {
9969                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9970                })?;
9971                if columns.is_empty() {
9972                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9973                }
9974                out.indexes.push(IndexAttr {
9975                    name,
9976                    columns,
9977                    unique: true,
9978                    method: "btree".to_owned(),
9979                    where_clause: Some(condition),
9980                    include,
9981                });
9982                return Ok(());
9983            }
9984            if meta.path.is_ident("index_when") {
9985                // Non-unique partial index. Sibling of `unique_when`
9986                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9987                //
9988                //   #[rustango(index_when(
9989                //       columns   = "status, created_at",
9990                //       condition = "deleted_at IS NULL",
9991                //       name      = "active_status_created_idx"
9992                //   ))]
9993                //
9994                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9995                // on PG / SQLite (both ship partial indexes natively).
9996                // MySQL has no native partial-index support — the writer
9997                // emits a plain CREATE INDEX and the condition is lost;
9998                // operators wanting that selectivity on MySQL should
9999                // declare a covering index plus an application-level
10000                // filter.
10001                let mut columns: Option<Vec<String>> = None;
10002                let mut condition: Option<String> = None;
10003                let mut name: Option<String> = None;
10004                let mut method: String = "btree".to_owned();
10005                let mut include: Vec<String> = Vec::new();
10006                meta.parse_nested_meta(|inner| {
10007                    if inner.path.is_ident("columns") {
10008                        let s: LitStr = inner.value()?.parse()?;
10009                        columns = Some(split_field_list(&s.value()));
10010                        return Ok(());
10011                    }
10012                    if inner.path.is_ident("condition") {
10013                        let s: LitStr = inner.value()?.parse()?;
10014                        condition = Some(s.value());
10015                        return Ok(());
10016                    }
10017                    if inner.path.is_ident("name") {
10018                        let s: LitStr = inner.value()?.parse()?;
10019                        name = Some(s.value());
10020                        return Ok(());
10021                    }
10022                    if inner.path.is_ident("method") {
10023                        let s: LitStr = inner.value()?.parse()?;
10024                        method = s.value();
10025                        return Ok(());
10026                    }
10027                    if inner.path.is_ident("include") {
10028                        // PG 11+ covering-index columns; non-key
10029                        // columns travel with the index leaf. Dropped
10030                        // on MySQL/SQLite.
10031                        let s: LitStr = inner.value()?.parse()?;
10032                        include = split_field_list(&s.value());
10033                        return Ok(());
10034                    }
10035                    Err(inner.error(
10036                        "unknown index_when attribute (supported: \
10037                         `columns = \"...\"`, `condition = \"...\"`, \
10038                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
10039                         `include = \"...\"`)",
10040                    ))
10041                })?;
10042                let columns = columns
10043                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
10044                let condition = condition.ok_or_else(|| {
10045                    meta.error("`index_when(...)` requires `condition = \"...\"`")
10046                })?;
10047                if columns.is_empty() {
10048                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
10049                }
10050                out.indexes.push(IndexAttr {
10051                    name,
10052                    columns,
10053                    unique: false,
10054                    method,
10055                    where_clause: Some(condition),
10056                    include,
10057                });
10058                return Ok(());
10059            }
10060            if meta.path.is_ident("index") {
10061                // Container-level composite index. Two syntaxes:
10062                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
10063                //   #[rustango(index("col1, col2"))]    — call form (same result)
10064                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
10065                // The call form takes the column list as a leading string
10066                // literal, then optional `unique` / `name = "..."` /
10067                // `method = "..."` flags (a leading literal can't compose
10068                // under `parse_nested_meta`, so the paren body is parsed by
10069                // hand). `unique_together` / `index_together` remain
10070                // aliases for the same feature.
10071                let cols_lit: LitStr;
10072                let mut unique = false;
10073                let mut name: Option<String> = None;
10074                let mut method = "btree".to_owned();
10075                if meta.input.peek(syn::token::Paren) {
10076                    let content;
10077                    syn::parenthesized!(content in meta.input);
10078                    cols_lit = content.parse()?;
10079                    while content.peek(syn::Token![,]) {
10080                        content.parse::<syn::Token![,]>()?;
10081                        if content.is_empty() {
10082                            break;
10083                        }
10084                        let flag: syn::Ident = content.parse()?;
10085                        if flag == "unique" {
10086                            unique = true;
10087                        } else if flag == "name" {
10088                            content.parse::<syn::Token![=]>()?;
10089                            let s: LitStr = content.parse()?;
10090                            name = Some(s.value());
10091                        } else if flag == "method" {
10092                            content.parse::<syn::Token![=]>()?;
10093                            let s: LitStr = content.parse()?;
10094                            let v = s.value();
10095                            match v.as_str() {
10096                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10097                                | "bloom" => method = v,
10098                                other => {
10099                                    return Err(syn::Error::new(
10100                                        s.span(),
10101                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10102                                    ));
10103                                }
10104                            }
10105                        } else {
10106                            return Err(syn::Error::new(
10107                                flag.span(),
10108                                "unknown index flag (supported: `unique`, `name`, `method`)",
10109                            ));
10110                        }
10111                    }
10112                } else {
10113                    cols_lit = meta.value()?.parse()?;
10114                }
10115                let columns = split_field_list(&cols_lit.value());
10116                out.indexes.push(IndexAttr {
10117                    name,
10118                    columns,
10119                    unique,
10120                    method,
10121                    where_clause: None,
10122                    include: Vec::new(),
10123                });
10124                return Ok(());
10125            }
10126            if meta.path.is_ident("check") {
10127                // #[rustango(check(name = "…", expr = "…"))]
10128                let mut name: Option<String> = None;
10129                let mut expr: Option<String> = None;
10130                meta.parse_nested_meta(|inner| {
10131                    if inner.path.is_ident("name") {
10132                        let s: LitStr = inner.value()?.parse()?;
10133                        name = Some(s.value());
10134                        return Ok(());
10135                    }
10136                    if inner.path.is_ident("expr") {
10137                        let s: LitStr = inner.value()?.parse()?;
10138                        expr = Some(s.value());
10139                        return Ok(());
10140                    }
10141                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10142                })?;
10143                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10144                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10145                out.checks.push(CheckAttr { name, expr });
10146                return Ok(());
10147            }
10148            if meta.path.is_ident("exclude") {
10149                // #[rustango(exclude(name = "…", using = "gist",
10150                //                    elements = "col WITH op, col WITH op",
10151                //                    where = "…"))]
10152                let mut name: Option<String> = None;
10153                let mut using: Option<String> = None;
10154                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10155                let mut where_clause: Option<String> = None;
10156                meta.parse_nested_meta(|inner| {
10157                    if inner.path.is_ident("name") {
10158                        let s: LitStr = inner.value()?.parse()?;
10159                        name = Some(s.value());
10160                        return Ok(());
10161                    }
10162                    if inner.path.is_ident("using") {
10163                        let s: LitStr = inner.value()?.parse()?;
10164                        using = Some(s.value());
10165                        return Ok(());
10166                    }
10167                    if inner.path.is_ident("elements") {
10168                        let s: LitStr = inner.value()?.parse()?;
10169                        elements_raw = Some((s.value(), s.span()));
10170                        return Ok(());
10171                    }
10172                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10173                        let s: LitStr = inner.value()?.parse()?;
10174                        where_clause = Some(s.value());
10175                        return Ok(());
10176                    }
10177                    Err(inner.error(
10178                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10179                    ))
10180                })?;
10181                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10182                let using = using.unwrap_or_else(|| "gist".to_owned());
10183                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10184                    meta.error(
10185                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
10186                    )
10187                })?;
10188                // Parse `col WITH op` pairs separated by commas.
10189                let mut elements: Vec<(String, String)> = Vec::new();
10190                for pair in elements_str.split(',') {
10191                    let pair = pair.trim();
10192                    if pair.is_empty() {
10193                        continue;
10194                    }
10195                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10196                    let col = split.next().unwrap_or("").trim();
10197                    let rest = split.next().unwrap_or("").trim();
10198                    // `WITH op` — case-insensitive on `WITH`, then op.
10199                    let rest_lc = rest.to_ascii_lowercase();
10200                    let op = rest_lc
10201                        .strip_prefix("with")
10202                        .map(|r| r.trim_start())
10203                        .filter(|r| !r.is_empty())
10204                        .map(|_| {
10205                            // Pull the original-case op from `rest` after the
10206                            // `WITH ` token (5 chars).
10207                            rest[4..].trim_start().to_owned()
10208                        });
10209                    let Some(op) = op else {
10210                        return Err(syn::Error::new(
10211                            elements_span,
10212                            format!(
10213                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10214                                 (e.g. `room_id WITH =` or `during WITH &&`)"
10215                            ),
10216                        ));
10217                    };
10218                    if col.is_empty() || op.is_empty() {
10219                        return Err(syn::Error::new(
10220                            elements_span,
10221                            format!(
10222                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10223                                 (both sides non-empty)"
10224                            ),
10225                        ));
10226                    }
10227                    elements.push((col.to_owned(), op));
10228                }
10229                if elements.is_empty() {
10230                    return Err(syn::Error::new(
10231                        elements_span,
10232                        "exclude requires at least one `col WITH op` element",
10233                    ));
10234                }
10235                out.excludes.push(ExcludeAttr {
10236                    name,
10237                    using,
10238                    elements,
10239                    where_clause,
10240                });
10241                return Ok(());
10242            }
10243            if meta.path.is_ident("generic_fk") {
10244                let mut gfk = GenericFkAttr {
10245                    name: String::new(),
10246                    ct_column: String::new(),
10247                    pk_column: String::new(),
10248                };
10249                meta.parse_nested_meta(|inner| {
10250                    if inner.path.is_ident("name") {
10251                        let s: LitStr = inner.value()?.parse()?;
10252                        gfk.name = s.value();
10253                        return Ok(());
10254                    }
10255                    if inner.path.is_ident("ct_column") {
10256                        let s: LitStr = inner.value()?.parse()?;
10257                        gfk.ct_column = s.value();
10258                        return Ok(());
10259                    }
10260                    if inner.path.is_ident("pk_column") {
10261                        let s: LitStr = inner.value()?.parse()?;
10262                        gfk.pk_column = s.value();
10263                        return Ok(());
10264                    }
10265                    Err(inner.error(
10266                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10267                    ))
10268                })?;
10269                if gfk.name.is_empty() {
10270                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10271                }
10272                if gfk.ct_column.is_empty() {
10273                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10274                }
10275                if gfk.pk_column.is_empty() {
10276                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10277                }
10278                out.generic_fks.push(gfk);
10279                return Ok(());
10280            }
10281            if meta.path.is_ident("fk_composite") {
10282                let mut fk = CompositeFkAttr {
10283                    name: String::new(),
10284                    to: String::new(),
10285                    from: Vec::new(),
10286                    on: Vec::new(),
10287                };
10288                meta.parse_nested_meta(|inner| {
10289                    if inner.path.is_ident("name") {
10290                        let s: LitStr = inner.value()?.parse()?;
10291                        fk.name = s.value();
10292                        return Ok(());
10293                    }
10294                    if inner.path.is_ident("to") {
10295                        let s: LitStr = inner.value()?.parse()?;
10296                        fk.to = s.value();
10297                        return Ok(());
10298                    }
10299                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10300                    // comma-list of string literals.
10301                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10302                        let value = inner.value()?;
10303                        let content;
10304                        syn::parenthesized!(content in value);
10305                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10306                            content.parse_terminated(
10307                                |p| p.parse::<syn::LitStr>(),
10308                                syn::Token![,],
10309                            )?;
10310                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10311                        if inner.path.is_ident("on") {
10312                            fk.on = cols;
10313                        } else {
10314                            fk.from = cols;
10315                        }
10316                        return Ok(());
10317                    }
10318                    Err(inner.error(
10319                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10320                    ))
10321                })?;
10322                if fk.name.is_empty() {
10323                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10324                }
10325                if fk.to.is_empty() {
10326                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10327                }
10328                if fk.from.is_empty() || fk.on.is_empty() {
10329                    return Err(meta.error(
10330                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10331                    ));
10332                }
10333                if fk.from.len() != fk.on.len() {
10334                    return Err(meta.error(format!(
10335                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10336                        fk.from.len(),
10337                        fk.on.len(),
10338                    )));
10339                }
10340                out.composite_fks.push(fk);
10341                return Ok(());
10342            }
10343            if meta.path.is_ident("m2m") {
10344                let mut m2m = M2MAttr {
10345                    name: String::new(),
10346                    to: String::new(),
10347                    through: String::new(),
10348                    src: String::new(),
10349                    dst: String::new(),
10350                    auto_create: true,
10351                };
10352                meta.parse_nested_meta(|inner| {
10353                    if inner.path.is_ident("name") {
10354                        let s: LitStr = inner.value()?.parse()?;
10355                        m2m.name = s.value();
10356                        return Ok(());
10357                    }
10358                    if inner.path.is_ident("to") {
10359                        let s: LitStr = inner.value()?.parse()?;
10360                        m2m.to = s.value();
10361                        return Ok(());
10362                    }
10363                    if inner.path.is_ident("through") {
10364                        let s: LitStr = inner.value()?.parse()?;
10365                        m2m.through = s.value();
10366                        return Ok(());
10367                    }
10368                    if inner.path.is_ident("src") {
10369                        let s: LitStr = inner.value()?.parse()?;
10370                        m2m.src = s.value();
10371                        return Ok(());
10372                    }
10373                    if inner.path.is_ident("dst") {
10374                        let s: LitStr = inner.value()?.parse()?;
10375                        m2m.dst = s.value();
10376                        return Ok(());
10377                    }
10378                    if inner.path.is_ident("auto_create") {
10379                        let lit: syn::LitBool = inner.value()?.parse()?;
10380                        m2m.auto_create = lit.value;
10381                        return Ok(());
10382                    }
10383                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10384                })?;
10385                if m2m.name.is_empty() {
10386                    return Err(meta.error("m2m requires `name = \"...\"`"));
10387                }
10388                if m2m.to.is_empty() {
10389                    return Err(meta.error("m2m requires `to = \"...\"`"));
10390                }
10391                if m2m.through.is_empty() {
10392                    return Err(meta.error("m2m requires `through = \"...\"`"));
10393                }
10394                if m2m.src.is_empty() {
10395                    return Err(meta.error("m2m requires `src = \"...\"`"));
10396                }
10397                if m2m.dst.is_empty() {
10398                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10399                }
10400                out.m2m.push(m2m);
10401                return Ok(());
10402            }
10403            if meta.path.is_ident("generic_m2m") {
10404                let mut gm = GenericM2MAttr {
10405                    name: String::new(),
10406                    through: String::new(),
10407                    pk_column: String::new(),
10408                    ct_column: String::new(),
10409                    related_column: String::new(),
10410                };
10411                meta.parse_nested_meta(|inner| {
10412                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10413                        let s: LitStr = inner.value()?.parse()?;
10414                        Ok(s.value())
10415                    };
10416                    if inner.path.is_ident("name") {
10417                        gm.name = field(&inner)?;
10418                        return Ok(());
10419                    }
10420                    if inner.path.is_ident("through") {
10421                        gm.through = field(&inner)?;
10422                        return Ok(());
10423                    }
10424                    if inner.path.is_ident("pk_column") {
10425                        gm.pk_column = field(&inner)?;
10426                        return Ok(());
10427                    }
10428                    if inner.path.is_ident("ct_column") {
10429                        gm.ct_column = field(&inner)?;
10430                        return Ok(());
10431                    }
10432                    if inner.path.is_ident("related_column") {
10433                        gm.related_column = field(&inner)?;
10434                        return Ok(());
10435                    }
10436                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10437                })?;
10438                for (val, label) in [
10439                    (&gm.name, "name"),
10440                    (&gm.through, "through"),
10441                    (&gm.pk_column, "pk_column"),
10442                    (&gm.ct_column, "ct_column"),
10443                    (&gm.related_column, "related_column"),
10444                ] {
10445                    if val.is_empty() {
10446                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10447                    }
10448                }
10449                out.generic_m2m.push(gm);
10450                return Ok(());
10451            }
10452            Err(meta.error("unknown rustango container attribute"))
10453        })?;
10454    }
10455    Ok(out)
10456}
10457
10458/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10459/// owned field names, trimming whitespace and skipping empty entries.
10460/// Field-name validation against the model is done by the caller.
10461fn split_field_list(raw: &str) -> Vec<String> {
10462    raw.split(',')
10463        .map(str::trim)
10464        .filter(|s| !s.is_empty())
10465        .map(str::to_owned)
10466        .collect()
10467}
10468
10469/// Shared parser for `unique_together` and `index_together` container
10470/// attrs. Accepts both shapes:
10471///
10472///   * `attr = "col1, col2"`              — auto-derived index name.
10473///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10474///
10475/// Returns `(columns, name)`.
10476fn parse_together_attr(
10477    meta: &syn::meta::ParseNestedMeta<'_>,
10478    attr: &str,
10479) -> syn::Result<(Vec<String>, Option<String>)> {
10480    // Disambiguate by whether the next token is `=` (key-value) or
10481    // `(` (parenthesized).
10482    if meta.input.peek(syn::Token![=]) {
10483        let cols_lit: LitStr = meta.value()?.parse()?;
10484        let columns = split_field_list(&cols_lit.value());
10485        check_together_columns(meta, attr, &columns)?;
10486        return Ok((columns, None));
10487    }
10488    let mut columns: Option<Vec<String>> = None;
10489    let mut name: Option<String> = None;
10490    meta.parse_nested_meta(|inner| {
10491        if inner.path.is_ident("columns") {
10492            let s: LitStr = inner.value()?.parse()?;
10493            columns = Some(split_field_list(&s.value()));
10494            return Ok(());
10495        }
10496        if inner.path.is_ident("name") {
10497            let s: LitStr = inner.value()?.parse()?;
10498            name = Some(s.value());
10499            return Ok(());
10500        }
10501        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10502    })?;
10503    let columns = columns.ok_or_else(|| {
10504        meta.error(format!(
10505            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10506        ))
10507    })?;
10508    check_together_columns(meta, attr, &columns)?;
10509    Ok((columns, name))
10510}
10511
10512fn check_together_columns(
10513    meta: &syn::meta::ParseNestedMeta<'_>,
10514    attr: &str,
10515    columns: &[String],
10516) -> syn::Result<()> {
10517    if columns.len() < 2 {
10518        let single = if attr == "unique_together" {
10519            "#[rustango(unique)] on the field"
10520        } else {
10521            "#[rustango(index)] on the field"
10522        };
10523        return Err(meta.error(format!(
10524            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10525        )));
10526    }
10527    Ok(())
10528}
10529
10530/// Parse the fieldsets DSL: pipe-separated sections, optional
10531/// `"Title:"` prefix on each, comma-separated field names after.
10532/// Examples:
10533/// * `"name, office"` → one untitled section with two fields
10534/// * `"Identity: name, office | Metadata: created_at"` → two titled
10535///   sections
10536///
10537/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10538fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10539    raw.split('|')
10540        .map(str::trim)
10541        .filter(|s| !s.is_empty())
10542        .map(|section| {
10543            // Split off an optional `Title:` prefix (first colon).
10544            let (title, rest) = match section.split_once(':') {
10545                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10546                _ => (String::new(), section),
10547            };
10548            let fields = split_field_list(rest);
10549            (title, fields)
10550        })
10551        .collect()
10552}
10553
10554/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10555/// comma-separated entry maps a target field to one or more source
10556/// fields joined with `+`. Whitespace around tokens is trimmed.
10557/// Entries missing `:` or with empty target/source lists are dropped.
10558fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10559    raw.split(',')
10560        .map(str::trim)
10561        .filter(|s| !s.is_empty())
10562        .filter_map(|entry| {
10563            let (target, sources_raw) = entry.split_once(':')?;
10564            let target = target.trim().to_owned();
10565            if target.is_empty() {
10566                return None;
10567            }
10568            let sources: Vec<String> = sources_raw
10569                .split('+')
10570                .map(|s| s.trim().to_owned())
10571                .filter(|s| !s.is_empty())
10572                .collect();
10573            if sources.is_empty() {
10574                return None;
10575            }
10576            Some((target, sources))
10577        })
10578        .collect()
10579}
10580
10581/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10582/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10583/// and empty halves drop silently — the macro layer only enforces shape,
10584/// not field-name vs. widget-name validity (those checks happen at
10585/// `AdminConfig` consumption time). Issue #359.
10586fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10587    raw.split(',')
10588        .map(str::trim)
10589        .filter(|s| !s.is_empty())
10590        .filter_map(|entry| {
10591            let (field, widget) = entry.split_once(':')?;
10592            let field = field.trim().to_owned();
10593            let widget = widget.trim().to_owned();
10594            if field.is_empty() || widget.is_empty() {
10595                return None;
10596            }
10597            Some((field, widget))
10598        })
10599        .collect()
10600}
10601
10602/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10603/// Returns `(field_name, desc)` pairs in the same order as the input.
10604fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10605    raw.split(',')
10606        .map(str::trim)
10607        .filter(|s| !s.is_empty())
10608        .map(|spec| {
10609            spec.strip_prefix('-')
10610                .map_or((spec.to_owned(), false), |rest| {
10611                    (rest.trim().to_owned(), true)
10612                })
10613        })
10614        .collect()
10615}
10616
10617struct FieldAttrs {
10618    column: Option<String>,
10619    primary_key: bool,
10620    fk: Option<String>,
10621    o2o: Option<String>,
10622    on: Option<String>,
10623    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10624    /// "set_default" | "no_action")]` — what happens to this row when
10625    /// the row it points at is deleted.
10626    /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10627    /// set; the macro errors at compile time if applied to a
10628    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10629    /// DDL writer renders `ON DELETE <action>` after the constraint
10630    /// clause when this is `Some`; `None` falls back to the database
10631    /// default (NO ACTION on every backend rustango supports).
10632    on_delete: Option<String>,
10633    /// `#[rustango(related_name = "...")]` — per-FK
10634    /// reverse-accessor override. When set, the derive emits
10635    /// `Parent::<related_name>[_pool]` instead of the container-level
10636    /// `default_related_name` or the `<child_snake>_set[_pool]`
10637    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10638    /// silently ignored on non-FK fields. Follow-up to #816.
10639    /// `"+"` emits none, for a parent defined in another crate.
10640    related_name: Option<String>,
10641    max_length: Option<u32>,
10642    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10643    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10644    /// unconstrained `vector` column.
10645    vector_dims: Option<u32>,
10646    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10647    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10648    /// unconstrained `geometry(Point)` column (SRID 0).
10649    geometry_srid: Option<u32>,
10650    min: Option<i64>,
10651    max: Option<i64>,
10652    default: Option<String>,
10653    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10654    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10655    /// "gen_random_uuid()"`. The Rust field type must be
10656    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10657    /// INSERTs so the DB DEFAULT fires.
10658    auto_uuid: bool,
10659    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10660    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10661    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10662    /// the field is `Auto::Unset`, then bound as a normal parameter
10663    /// rather than relying on a per-dialect DB function. Issue #823
10664    /// (Eloquent `HasUuids`).
10665    ///
10666    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10667    /// Composes with every backend (PG / MySQL / SQLite) — no
10668    /// `gen_random_uuid()` requirement on the database.
10669    default_uuid_v7: bool,
10670    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10671    /// Server-set on insert, immutable from app code afterwards.
10672    /// Implies `auto + default = "now()"`. Field type must be
10673    /// `DateTime<Utc>`.
10674    auto_now_add: bool,
10675    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10676    /// every insert AND every update. Implies `auto + default =
10677    /// "now()"`; the macro additionally rewrites `update_on` /
10678    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10679    /// field value.
10680    auto_now: bool,
10681    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10682    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10683    /// `soft_delete_on(executor)` and `restore_on(executor)`
10684    /// methods on the model.
10685    soft_delete: bool,
10686    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10687    /// the column in the generated DDL.
10688    unique: bool,
10689    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10690    /// generates a `CREATE INDEX` for this column. `unique` here means
10691    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10692    index: bool,
10693    index_unique: bool,
10694    index_name: Option<String>,
10695    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10696    /// `"btree"`. Issue #34.
10697    index_method: String,
10698    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10699    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10700    /// the macro skips this column from every INSERT and UPDATE
10701    /// path, so the database always recomputes the value from
10702    /// `EXPR`. Backlog item #35.
10703    generated_as: Option<String>,
10704    /// `#[rustango(help_text = "…")]` — help text
10705    /// rendered below the admin form's input. Threaded into
10706    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10707    /// layers can read it.
10708    help_text: Option<String>,
10709    /// `#[rustango(choices = "value:Label, value:Label")]` — the
10710    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10711    /// as a `&'static [(&'static str, &'static str)]` slice. When
10712    /// present, the admin form renders a `<select>` instead of `<input>`
10713    /// and the validator rejects values not in the list. Only meaningful
10714    /// for `FieldType::String`; the macro errors at compile time if
10715    /// applied to a non-string field.
10716    choices: Option<Vec<(String, String)>>,
10717    /// `#[rustango(db_comment = "…")]` — DB-side column
10718    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10719    /// the comment in CREATE TABLE; Postgres emits a separate
10720    /// `COMMENT ON COLUMN` statement after the table is created;
10721    /// SQLite silently drops the value (no native column comments).
10722    db_comment: Option<String>,
10723    /// `#[rustango(verbose_name = "…")]` — human-readable
10724    /// label for the field. Threaded into `FieldSchema::verbose_name`
10725    /// so admin column headers, form labels, and other display
10726    /// surfaces can prefer the friendly caption over the Rust
10727    /// identifier. `None` means renderers fall back to the field name.
10728    verbose_name: Option<String>,
10729    /// `#[rustango(editable = false)]` — opt out of
10730    /// auto-generated form rendering. Defaults to `true` so existing
10731    /// fields keep their current admin / form behavior; setting
10732    /// `false` removes the field from the admin change-form entirely
10733    /// (the value is still visible on detail / list views, just not
10734    /// editable).
10735    editable: bool,
10736    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10737    /// "form may submit empty even when DB is NOT NULL". Threaded into
10738    /// `FieldSchema::blank`. Defaults to `false`.
10739    blank: bool,
10740    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10741    /// case-insensitive text column. Threaded into
10742    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10743    /// fields; the macro errors at derive time if applied elsewhere.
10744    case_insensitive: bool,
10745    /// `#[rustango(validators = "email,url")]` — the
10746    /// model-level validator chain. Comma-separated names that
10747    /// dispatch to the `validators::*` family in `validate_value`.
10748    /// Empty by default; fires on every typed INSERT/UPDATE.
10749    validators: Vec<String>,
10750}
10751
10752fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10753    let mut out = FieldAttrs {
10754        column: None,
10755        primary_key: false,
10756        fk: None,
10757        o2o: None,
10758        on: None,
10759        on_delete: None,
10760        related_name: None,
10761        max_length: None,
10762        vector_dims: None,
10763        geometry_srid: None,
10764        min: None,
10765        max: None,
10766        default: None,
10767        auto_uuid: false,
10768        default_uuid_v7: false,
10769        auto_now_add: false,
10770        auto_now: false,
10771        soft_delete: false,
10772        unique: false,
10773        index: false,
10774        index_unique: false,
10775        index_name: None,
10776        index_method: "btree".to_owned(),
10777        generated_as: None,
10778        help_text: None,
10779        choices: None,
10780        db_comment: None,
10781        verbose_name: None,
10782        editable: true,
10783        blank: false,
10784        case_insensitive: false,
10785        validators: Vec::new(),
10786    };
10787    for attr in &field.attrs {
10788        if !attr.path().is_ident("rustango") {
10789            continue;
10790        }
10791        attr.parse_nested_meta(|meta| {
10792            if meta.path.is_ident("column") {
10793                let s: LitStr = meta.value()?.parse()?;
10794                let name = s.value();
10795                validate_sql_identifier(&name, "column", s.span())?;
10796                out.column = Some(name);
10797                return Ok(());
10798            }
10799            if meta.path.is_ident("primary_key") {
10800                out.primary_key = true;
10801                return Ok(());
10802            }
10803            if meta.path.is_ident("fk") {
10804                let s: LitStr = meta.value()?.parse()?;
10805                out.fk = Some(s.value());
10806                return Ok(());
10807            }
10808            if meta.path.is_ident("o2o") {
10809                let s: LitStr = meta.value()?.parse()?;
10810                out.o2o = Some(s.value());
10811                return Ok(());
10812            }
10813            if meta.path.is_ident("on") {
10814                let s: LitStr = meta.value()?.parse()?;
10815                out.on = Some(s.value());
10816                return Ok(());
10817            }
10818            if meta.path.is_ident("on_delete") {
10819                let s: LitStr = meta.value()?.parse()?;
10820                let raw = s.value();
10821                let normalized = raw.trim().to_ascii_lowercase();
10822                // Validate at parse time so the user gets a clear span
10823                // rather than a downstream compile error in the emit.
10824                match normalized.as_str() {
10825                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10826                    _ => {
10827                        return Err(syn::Error::new(
10828                            s.span(),
10829                            format!(
10830                                "unknown on_delete action `{raw}`; expected one of \
10831                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10832                            ),
10833                        ));
10834                    }
10835                }
10836                out.on_delete = Some(normalized);
10837                return Ok(());
10838            }
10839            if meta.path.is_ident("related_name") {
10840                let s: LitStr = meta.value()?.parse()?;
10841                let raw = s.value();
10842                if raw.trim().is_empty() {
10843                    return Err(syn::Error::new(
10844                        s.span(),
10845                        "`related_name` must be a non-empty identifier",
10846                    ));
10847                }
10848                // Validate as a Rust-ident shape — the value becomes
10849                // a method name on the parent type. Same rule as
10850                // `default_related_name` so the two surfaces match.
10851                // `"+"` emits no accessor at all.
10852                let snake = raw
10853                    .chars()
10854                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10855                    && !raw.starts_with(char::is_numeric);
10856                if !snake && raw != NO_REVERSE_ACCESSOR {
10857                    return Err(syn::Error::new(
10858                        s.span(),
10859                        "`related_name` must be snake_case ASCII (lowercase letters, \
10860                         digits, underscores; no leading digit), or \"+\" for none",
10861                    ));
10862                }
10863                out.related_name = Some(raw);
10864                return Ok(());
10865            }
10866            if meta.path.is_ident("max_length") {
10867                let lit: syn::LitInt = meta.value()?.parse()?;
10868                out.max_length = Some(lit.base10_parse::<u32>()?);
10869                return Ok(());
10870            }
10871            // `#[rustango(vector(dims = N))]` — pgvector column
10872            // dimension (#824). Nested-meta form so it reads like the
10873            // other typed-column attrs.
10874            if meta.path.is_ident("vector") {
10875                meta.parse_nested_meta(|inner| {
10876                    if inner.path.is_ident("dims") {
10877                        let lit: syn::LitInt = inner.value()?.parse()?;
10878                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10879                        return Ok(());
10880                    }
10881                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10882                })?;
10883                return Ok(());
10884            }
10885            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10886            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10887            if meta.path.is_ident("geometry") {
10888                meta.parse_nested_meta(|inner| {
10889                    if inner.path.is_ident("srid") {
10890                        let lit: syn::LitInt = inner.value()?.parse()?;
10891                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10892                        return Ok(());
10893                    }
10894                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10895                })?;
10896                return Ok(());
10897            }
10898            if meta.path.is_ident("min") {
10899                out.min = Some(parse_signed_i64(&meta)?);
10900                return Ok(());
10901            }
10902            if meta.path.is_ident("max") {
10903                out.max = Some(parse_signed_i64(&meta)?);
10904                return Ok(());
10905            }
10906            if meta.path.is_ident("default") {
10907                let s: LitStr = meta.value()?.parse()?;
10908                out.default = Some(s.value());
10909                return Ok(());
10910            }
10911            if meta.path.is_ident("generated_as") {
10912                let s: LitStr = meta.value()?.parse()?;
10913                out.generated_as = Some(s.value());
10914                return Ok(());
10915            }
10916            if meta.path.is_ident("help_text") {
10917                let s: LitStr = meta.value()?.parse()?;
10918                out.help_text = Some(s.value());
10919                return Ok(());
10920            }
10921            if meta.path.is_ident("choices") {
10922                let s: LitStr = meta.value()?.parse()?;
10923                let raw = s.value();
10924                let mut pairs: Vec<(String, String)> = Vec::new();
10925                for chunk in raw.split(',') {
10926                    let chunk = chunk.trim();
10927                    if chunk.is_empty() {
10928                        continue;
10929                    }
10930                    let (value, label) = match chunk.split_once(':') {
10931                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10932                        None => (chunk.to_owned(), chunk.to_owned()),
10933                    };
10934                    if value.is_empty() {
10935                        return Err(syn::Error::new(
10936                            s.span(),
10937                            "`choices` entry has empty value before `:`",
10938                        ));
10939                    }
10940                    pairs.push((value, label));
10941                }
10942                if pairs.is_empty() {
10943                    return Err(syn::Error::new(
10944                        s.span(),
10945                        "`choices = \"…\"` must contain at least one value",
10946                    ));
10947                }
10948                out.choices = Some(pairs);
10949                return Ok(());
10950            }
10951            if meta.path.is_ident("db_comment") {
10952                let s: LitStr = meta.value()?.parse()?;
10953                out.db_comment = Some(s.value());
10954                return Ok(());
10955            }
10956            if meta.path.is_ident("verbose_name") {
10957                let s: LitStr = meta.value()?.parse()?;
10958                out.verbose_name = Some(s.value());
10959                return Ok(());
10960            }
10961            if meta.path.is_ident("editable") {
10962                // Two forms accepted:
10963                //   #[rustango(editable = false)] / true — explicit
10964                //   #[rustango(editable)] — flag form (= true, the
10965                //   default, so harmless; included for symmetry)
10966                if let Ok(v) = meta.value() {
10967                    let lit: syn::LitBool = v.parse()?;
10968                    out.editable = lit.value;
10969                } else {
10970                    out.editable = true;
10971                }
10972                return Ok(());
10973            }
10974            if meta.path.is_ident("blank") {
10975                // Two forms accepted:
10976                //   #[rustango(blank)] — flag form, true
10977                //   #[rustango(blank = false)] / true — explicit
10978                if let Ok(v) = meta.value() {
10979                    let lit: syn::LitBool = v.parse()?;
10980                    out.blank = lit.value;
10981                } else {
10982                    out.blank = true;
10983                }
10984                return Ok(());
10985            }
10986            if meta.path.is_ident("citext") {
10987                // Case-insensitive text column (#344). Two forms:
10988                //   #[rustango(citext)]          — flag form, true
10989                //   #[rustango(citext = true)]   — explicit
10990                //   #[rustango(citext = false)]  — explicit opt-out
10991                // String-only validation lives in the field-type
10992                // emitter (the FieldType discriminant is computed in
10993                // `detect_type`); the macro records the flag and the
10994                // DDL writer emits dialect-specific COLLATE / CITEXT.
10995                if let Ok(v) = meta.value() {
10996                    let lit: syn::LitBool = v.parse()?;
10997                    out.case_insensitive = lit.value;
10998                } else {
10999                    out.case_insensitive = true;
11000                }
11001                return Ok(());
11002            }
11003            if meta.path.is_ident("validators") {
11004                let s: LitStr = meta.value()?.parse()?;
11005                let raw = s.value();
11006                out.validators = raw
11007                    .split(',')
11008                    .map(str::trim)
11009                    .filter(|s| !s.is_empty())
11010                    .map(str::to_owned)
11011                    .collect();
11012                if out.validators.is_empty() {
11013                    return Err(syn::Error::new(
11014                        s.span(),
11015                        "`validators = \"…\"` must list at least one name",
11016                    ));
11017                }
11018                return Ok(());
11019            }
11020            if meta.path.is_ident("auto_uuid") {
11021                out.auto_uuid = true;
11022                // Implied: PK + auto + DEFAULT gen_random_uuid().
11023                // Each is also explicitly settable; the explicit
11024                // value wins if conflicting.
11025                out.primary_key = true;
11026                if out.default.is_none() {
11027                    out.default = Some("gen_random_uuid()".into());
11028                }
11029                return Ok(());
11030            }
11031            if meta.path.is_ident("default_uuid_v7") {
11032                // Backend-neutral counterpart of `auto_uuid` — issue #823.
11033                // No SQL DEFAULT (the macro fills the value Rust-side
11034                // before binding); just mark the field as PK + Auto
11035                // so the insert path is the `Auto::Unset → generate`
11036                // branch.
11037                out.default_uuid_v7 = true;
11038                out.primary_key = true;
11039                return Ok(());
11040            }
11041            if meta.path.is_ident("auto_now_add") {
11042                out.auto_now_add = true;
11043                if out.default.is_none() {
11044                    out.default = Some("now()".into());
11045                }
11046                return Ok(());
11047            }
11048            if meta.path.is_ident("auto_now") {
11049                out.auto_now = true;
11050                if out.default.is_none() {
11051                    out.default = Some("now()".into());
11052                }
11053                return Ok(());
11054            }
11055            if meta.path.is_ident("soft_delete") {
11056                out.soft_delete = true;
11057                return Ok(());
11058            }
11059            if meta.path.is_ident("unique") {
11060                out.unique = true;
11061                return Ok(());
11062            }
11063            if meta.path.is_ident("index") {
11064                out.index = true;
11065                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11066                if meta.input.peek(syn::token::Paren) {
11067                    meta.parse_nested_meta(|inner| {
11068                        if inner.path.is_ident("unique") {
11069                            out.index_unique = true;
11070                            return Ok(());
11071                        }
11072                        if inner.path.is_ident("name") {
11073                            let s: LitStr = inner.value()?.parse()?;
11074                            out.index_name = Some(s.value());
11075                            return Ok(());
11076                        }
11077                        if inner.path.is_ident("method") {
11078                            let s: LitStr = inner.value()?.parse()?;
11079                            let v = s.value();
11080                            match v.as_str() {
11081                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11082                                    out.index_method = v;
11083                                }
11084                                other => {
11085                                    return Err(inner.error(format!(
11086                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11087                                    )));
11088                                }
11089                            }
11090                            return Ok(());
11091                        }
11092                        Err(inner.error(
11093                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11094                        ))
11095                    })?;
11096                }
11097                return Ok(());
11098            }
11099            Err(meta.error("unknown rustango field attribute"))
11100        })?;
11101    }
11102    Ok(out)
11103}
11104
11105/// Parse a signed integer literal, accepting optional leading `-`.
11106fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11107    let expr: syn::Expr = meta.value()?.parse()?;
11108    match expr {
11109        syn::Expr::Lit(syn::ExprLit {
11110            lit: syn::Lit::Int(lit),
11111            ..
11112        }) => lit.base10_parse::<i64>(),
11113        syn::Expr::Unary(syn::ExprUnary {
11114            op: syn::UnOp::Neg(_),
11115            expr,
11116            ..
11117        }) => {
11118            if let syn::Expr::Lit(syn::ExprLit {
11119                lit: syn::Lit::Int(lit),
11120                ..
11121            }) = *expr
11122            {
11123                let v: i64 = lit.base10_parse()?;
11124                Ok(-v)
11125            } else {
11126                Err(syn::Error::new_spanned(expr, "expected integer literal"))
11127            }
11128        }
11129        other => Err(syn::Error::new_spanned(
11130            other,
11131            "expected integer literal (signed)",
11132        )),
11133    }
11134}
11135
11136struct FieldInfo<'a> {
11137    ident: &'a syn::Ident,
11138    column: String,
11139    primary_key: bool,
11140    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11141    /// skip this column when `Auto::Unset` and emit it under
11142    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11143    auto: bool,
11144    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11145    /// the `Column::Value` associated type for typed-column tokens.
11146    value_ty: &'a Type,
11147    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11148    field_type_tokens: TokenStream2,
11149    schema: TokenStream2,
11150    from_row_init: TokenStream2,
11151    /// Variant of [`Self::from_row_init`] that reads the column via
11152    /// `format!("{prefix}__{col}")` so a model can be decoded out of
11153    /// the aliased columns of a JOINed row. Drives slice 9.0d's
11154    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11155    /// helper that `select_related` calls when stitching loaded FKs.
11156    from_aliased_row_init: TokenStream2,
11157    /// MySQL twins of the two inits above. They differ only for a UUID
11158    /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11159    from_row_init_my: TokenStream2,
11160    from_aliased_row_init_my: TokenStream2,
11161    /// A bare or optional `Uuid` field.
11162    uuid_column: bool,
11163    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11164    /// relation helper emit (`Author::<child>_set`) needs to know `T`
11165    /// to point the generated method at the right child model.
11166    fk_inner: Option<Type>,
11167    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11168    /// `kind` (since ForeignKey detection sets `kind` to K's
11169    /// underlying type) but stored separately for clarity at the
11170    /// `FkRelation` construction site, which only sees the FK's
11171    /// surface fields.
11172    fk_pk_kind: DetectedKind,
11173    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11174    /// the bare `ForeignKey<T, K>`. Routes the load_related and
11175    /// fk_pk_access emitters to wrap assignments / accessors in
11176    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11177    /// FK column compiles end-to-end. The DDL writer reads this off
11178    /// the field schema (`nullable` flag); the macro just needs to
11179    /// keep the Rust-side codegen consistent.
11180    nullable: bool,
11181    /// `true` when this column was marked `#[rustango(auto_now)]` —
11182    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11183    /// column instead of the user-supplied value, so `updated_at`
11184    /// always reflects the latest write without the caller having
11185    /// to remember to set it.
11186    auto_now: bool,
11187    /// `true` when this column was marked `#[rustango(auto_now_add)]`
11188    /// — the column is server-set on INSERT (DB DEFAULT) and
11189    /// **immutable** afterwards. `update_on` / `save_on` skip the
11190    /// column entirely so a stale `created_at` value in memory never
11191    /// rewrites the persisted timestamp.
11192    auto_now_add: bool,
11193    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11194    /// Triggers emission of `soft_delete_on(executor)` and
11195    /// `restore_on(executor)` on the model's inherent impl. There is
11196    /// at most one such column per model — emission asserts this.
11197    soft_delete: bool,
11198    /// `Some` when this column was marked
11199    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11200    /// every INSERT and UPDATE path; the database recomputes the
11201    /// value from `EXPR`. Backlog item #35.
11202    generated_as: Option<String>,
11203    /// `true` when this column was marked
11204    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11205    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11206    /// with `Uuid::now_v7()` before binding, so the PK is generated
11207    /// Rust-side and the column is always present in the INSERT
11208    /// statement (no DB DEFAULT requirement). Issue #823.
11209    default_uuid_v7: bool,
11210    /// `Some` when this FK field carried `#[rustango(related_name =
11211    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11212    /// consumed by [`reverse_helper_tokens`] to override the default
11213    /// `<child_snake>_set` accessor name. Follow-up to #816.
11214    related_name: Option<String>,
11215}
11216
11217/// Reject table names that won't survive SQL identifier
11218/// derivation downstream. Postgres' regular-identifier rule
11219/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11220/// through the framework's unquoted FK / index / constraint name
11221/// emission without surprises. We also disallow leading-digit and
11222/// the empty string for clarity.
11223///
11224/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11225/// here — those produce a runtime error from the SQL parser,
11226/// which is loud enough; statically enumerating reserved words
11227/// across the three supported dialects is more friction than help.
11228///
11229/// Backlog item #65.
11230fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11231    validate_sql_identifier(name, "table", span)
11232}
11233
11234/// Reject SQL identifiers that compile but break downstream SQL
11235/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11236/// `kind` is "table" / "column" — used for the error message so users
11237/// see which attribute caused the failure.
11238fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11239    if name.is_empty() {
11240        return Err(syn::Error::new(
11241            span,
11242            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11243        ));
11244    }
11245    let mut chars = name.chars();
11246    let first = chars.next().unwrap();
11247    if !(first.is_ascii_alphabetic() || first == '_') {
11248        return Err(syn::Error::new(
11249            span,
11250            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11251        ));
11252    }
11253    for c in chars {
11254        if !(c.is_ascii_alphanumeric() || c == '_') {
11255            return Err(syn::Error::new(
11256                span,
11257                format!(
11258                    "{kind} name `{name}` contains invalid character {c:?} — \
11259                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11260                     Hyphens in particular break FK / index name derivation \
11261                     downstream; use underscores instead (e.g. `{}`)",
11262                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11263                ),
11264            ));
11265        }
11266    }
11267    Ok(())
11268}
11269
11270fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11271    let root = rustango_root();
11272    let attrs = parse_field_attrs(field)?;
11273    let ident = field
11274        .ident
11275        .as_ref()
11276        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11277    let name = ident_name(ident);
11278    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11279    let primary_key = attrs.primary_key;
11280    let DetectedType {
11281        kind,
11282        nullable,
11283        auto: detected_auto,
11284        fk_inner,
11285    } = detect_type(&field.ty)?;
11286    check_bound_compatibility(field, &attrs, kind)?;
11287    let auto = detected_auto;
11288    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11289    // INSERT path: the user must wrap the field in `Auto<T>`, which
11290    // marks the column as DB-default-supplied. The mixin attrs then
11291    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11292    // for `auto_now`, force the value on UPDATE too.
11293    if attrs.auto_uuid {
11294        if kind != DetectedKind::Uuid {
11295            return Err(syn::Error::new_spanned(
11296                field,
11297                "`#[rustango(auto_uuid)]` requires the field type to be \
11298                 `Auto<uuid::Uuid>`",
11299            ));
11300        }
11301        if !detected_auto {
11302            return Err(syn::Error::new_spanned(
11303                field,
11304                "`#[rustango(auto_uuid)]` requires the field type to be \
11305                 wrapped in `Auto<...>` so the macro skips the column on \
11306                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11307            ));
11308        }
11309    }
11310    if attrs.default_uuid_v7 {
11311        if kind != DetectedKind::Uuid {
11312            return Err(syn::Error::new_spanned(
11313                field,
11314                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11315                 `Auto<uuid::Uuid>`",
11316            ));
11317        }
11318        if !detected_auto {
11319            return Err(syn::Error::new_spanned(
11320                field,
11321                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11322                 wrapped in `Auto<...>` so the macro can detect the \
11323                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11324            ));
11325        }
11326        if attrs.auto_uuid {
11327            return Err(syn::Error::new_spanned(
11328                field,
11329                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11330                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11331                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11332                 Pick one.",
11333            ));
11334        }
11335    }
11336    if attrs.auto_now_add || attrs.auto_now {
11337        if kind != DetectedKind::DateTime {
11338            return Err(syn::Error::new_spanned(
11339                field,
11340                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11341                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11342            ));
11343        }
11344        if !detected_auto {
11345            return Err(syn::Error::new_spanned(
11346                field,
11347                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11348                 the field type to be wrapped in `Auto<...>` so the macro skips \
11349                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11350            ));
11351        }
11352    }
11353    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11354        return Err(syn::Error::new_spanned(
11355            field,
11356            "`#[rustango(soft_delete)]` requires the field type to be \
11357             `Option<chrono::DateTime<chrono::Utc>>`",
11358        ));
11359    }
11360    let is_mixin_auto =
11361        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11362    if detected_auto && !primary_key && !is_mixin_auto {
11363        return Err(syn::Error::new_spanned(
11364            field,
11365            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11366             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11367             `auto_now`",
11368        ));
11369    }
11370    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11371        return Err(syn::Error::new_spanned(
11372            field,
11373            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11374             SERIAL / BIGSERIAL already supplies a default sequence.",
11375        ));
11376    }
11377    if fk_inner.is_some() && primary_key {
11378        return Err(syn::Error::new_spanned(
11379            field,
11380            "`ForeignKey<T>` is not allowed on a primary-key field — \
11381             a row's PK is its own identity, not a reference to a parent.",
11382        ));
11383    }
11384    if attrs.generated_as.is_some() {
11385        if primary_key {
11386            return Err(syn::Error::new_spanned(
11387                field,
11388                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11389                 primary-key field — a PK must be writable so the row \
11390                 has an identity at INSERT time.",
11391            ));
11392        }
11393        if attrs.default.is_some() {
11394            return Err(syn::Error::new_spanned(
11395                field,
11396                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11397                 `default = \"…\"` — Postgres rejects DEFAULT on \
11398                 generated columns. The expression IS the default.",
11399            ));
11400        }
11401        if detected_auto {
11402            return Err(syn::Error::new_spanned(
11403                field,
11404                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11405                 an `Auto<T>` field — generated columns are computed \
11406                 by the DB, not server-assigned via a sequence. Use a \
11407                 plain Rust type (e.g. `f64`).",
11408            ));
11409        }
11410        if fk_inner.is_some() {
11411            return Err(syn::Error::new_spanned(
11412                field,
11413                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11414                 ForeignKey field.",
11415            ));
11416        }
11417    }
11418    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11419    let column_lit = column.as_str();
11420    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11421    // dimension that the bare `Vector` Rust type can't carry, so emit
11422    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11423    // fallback of `Vector(0)`.
11424    let field_type_tokens = if kind == DetectedKind::Vector {
11425        let root = rustango_root();
11426        let dims = attrs.vector_dims.unwrap_or(0);
11427        quote!(#root::core::FieldType::Vector(#dims))
11428    } else if kind == DetectedKind::Geometry {
11429        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11430        // SRID that the bare `Point` Rust type can't carry, so emit
11431        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11432        let root = rustango_root();
11433        let srid = attrs.geometry_srid.unwrap_or(0);
11434        quote!(#root::core::FieldType::Geometry(#srid))
11435    } else {
11436        kind.variant_tokens()
11437    };
11438    let max_length = optional_u32(attrs.max_length);
11439    let min = optional_i64(attrs.min);
11440    let max = optional_i64(attrs.max);
11441    let default = optional_str(attrs.default.as_deref());
11442
11443    let unique = attrs.unique;
11444    let generated_as = optional_str(attrs.generated_as.as_deref());
11445    let help_text = optional_str(attrs.help_text.as_deref());
11446    let choices = optional_choices(attrs.choices.as_deref());
11447    let db_comment = optional_str(attrs.db_comment.as_deref());
11448    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11449    let editable = attrs.editable;
11450    let blank = attrs.blank;
11451    let case_insensitive = attrs.case_insensitive;
11452    let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11453    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11454    {
11455        return Err(syn::Error::new_spanned(
11456            field,
11457            "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11458             or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11459             has no meaning on a non-FK column.",
11460        ));
11461    }
11462    if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11463        return Err(syn::Error::new_spanned(
11464            field,
11465            "`on_delete = \"set_null\"` needs a nullable column: use \
11466             `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11467             alias for `Option` is not seen).",
11468        ));
11469    }
11470    let fk_on_delete = match attrs.on_delete.as_deref() {
11471        None => quote!(::core::option::Option::None),
11472        Some(action) => {
11473            let variant = match action {
11474                "cascade" => quote!(Cascade),
11475                "restrict" => quote!(Restrict),
11476                "set_null" => quote!(SetNull),
11477                "set_default" => quote!(SetDefault),
11478                "no_action" => quote!(NoAction),
11479                // parse_field_attrs already validated this — guard against future drift.
11480                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11481            };
11482            quote!(::core::option::Option::Some(
11483                #root::core::OnDeleteAction::#variant
11484            ))
11485        }
11486    };
11487    let auto_now_flag = attrs.auto_now;
11488    let schema = quote! {{
11489        #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11490            name: #name,
11491            column: #column_lit,
11492            ty: #field_type_tokens,
11493            nullable: #nullable,
11494            primary_key: #primary_key,
11495            relation: #relation,
11496            max_length: #max_length,
11497            min: #min,
11498            max: #max,
11499            default: #default,
11500            auto: #auto,
11501            auto_now: #auto_now_flag,
11502            unique: #unique,
11503            generated_as: #generated_as,
11504            help_text: #help_text,
11505            choices: #choices,
11506            db_comment: #db_comment,
11507            verbose_name: #verbose_name,
11508            editable: #editable,
11509            blank: #blank,
11510            case_insensitive: #case_insensitive,
11511            fk_on_delete: #fk_on_delete,
11512            validators: &[ #(#validators_lits),* ],
11513        })
11514    }};
11515
11516    let from_row_init = quote! {
11517        #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11518    };
11519    let from_aliased_row_init = quote! {
11520        #ident: #root::sql::sqlx::Row::try_get(
11521            row,
11522            #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11523        )?
11524    };
11525    // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11526    // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11527    let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11528    let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11529        (
11530            quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11531            quote! {
11532                #ident: #root::sql::try_get_flat_my(
11533                    row,
11534                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11535                )?
11536            },
11537        )
11538    } else {
11539        (from_row_init.clone(), from_aliased_row_init.clone())
11540    };
11541
11542    Ok(FieldInfo {
11543        ident,
11544        column,
11545        primary_key,
11546        auto,
11547        value_ty: &field.ty,
11548        field_type_tokens,
11549        schema,
11550        from_row_init,
11551        from_aliased_row_init,
11552        from_row_init_my,
11553        from_aliased_row_init_my,
11554        uuid_column,
11555        fk_inner: fk_inner.cloned(),
11556        fk_pk_kind: kind,
11557        nullable,
11558        auto_now: attrs.auto_now,
11559        auto_now_add: attrs.auto_now_add,
11560        soft_delete: attrs.soft_delete,
11561        generated_as: attrs.generated_as.clone(),
11562        default_uuid_v7: attrs.default_uuid_v7,
11563        related_name: attrs.related_name.clone(),
11564    })
11565}
11566
11567fn check_bound_compatibility(
11568    field: &syn::Field,
11569    attrs: &FieldAttrs,
11570    kind: DetectedKind,
11571) -> syn::Result<()> {
11572    if attrs.max_length.is_some() && kind != DetectedKind::String {
11573        return Err(syn::Error::new_spanned(
11574            field,
11575            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11576        ));
11577    }
11578    // These were silently dropped on the wrong type (#1937).
11579    if attrs.case_insensitive && kind != DetectedKind::String {
11580        return Err(syn::Error::new_spanned(
11581            field,
11582            "`citext` is only valid on `String` fields (or `Option<String>`)",
11583        ));
11584    }
11585    if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11586        return Err(syn::Error::new_spanned(
11587            field,
11588            "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11589        ));
11590    }
11591    if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11592        return Err(syn::Error::new_spanned(
11593            field,
11594            "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11595        ));
11596    }
11597    if attrs.choices.is_some() && kind != DetectedKind::String {
11598        return Err(syn::Error::new_spanned(
11599            field,
11600            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11601             integer-valued enumerations should be modeled with a Rust enum and \
11602             custom (de)serializer for now",
11603        ));
11604    }
11605    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11606        return Err(syn::Error::new_spanned(
11607            field,
11608            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11609        ));
11610    }
11611    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11612        if min > max {
11613            return Err(syn::Error::new_spanned(
11614                field,
11615                format!("`min` ({min}) is greater than `max` ({max})"),
11616            ));
11617        }
11618    }
11619    Ok(())
11620}
11621
11622/// A `&'static [ty]` of `const fn` results. The slice needs its own
11623/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11624fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11625    quote!({
11626        const ITEMS: &[#ty] = &[ #(#items),* ];
11627        ITEMS
11628    })
11629}
11630
11631fn optional_u32(value: Option<u32>) -> TokenStream2 {
11632    if let Some(v) = value {
11633        quote!(::core::option::Option::Some(#v))
11634    } else {
11635        quote!(::core::option::Option::None)
11636    }
11637}
11638
11639fn optional_i64(value: Option<i64>) -> TokenStream2 {
11640    if let Some(v) = value {
11641        quote!(::core::option::Option::Some(#v))
11642    } else {
11643        quote!(::core::option::Option::None)
11644    }
11645}
11646
11647fn optional_str(value: Option<&str>) -> TokenStream2 {
11648    if let Some(v) = value {
11649        quote!(::core::option::Option::Some(#v))
11650    } else {
11651        quote!(::core::option::Option::None)
11652    }
11653}
11654
11655fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11656    let Some(pairs) = pairs else {
11657        return quote!(::core::option::Option::None);
11658    };
11659    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11660    quote!(::core::option::Option::Some(&[#(#entries),*]))
11661}
11662
11663fn relation_tokens(
11664    field: &syn::Field,
11665    attrs: &FieldAttrs,
11666    fk_inner: Option<&syn::Type>,
11667    table: &str,
11668) -> syn::Result<TokenStream2> {
11669    let root = rustango_root();
11670    if let Some(inner) = fk_inner {
11671        if attrs.fk.is_some() || attrs.o2o.is_some() {
11672            return Err(syn::Error::new_spanned(
11673                field,
11674                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11675                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11676            ));
11677        }
11678        // No `on`: the target's PK column, which may be renamed.
11679        let on = match attrs.on.as_deref() {
11680            Some(on) => quote!(#on),
11681            None => quote! {
11682                match <#inner as #root::core::Model>::SCHEMA.pk_column() {
11683                    ::core::option::Option::Some(c) => c,
11684                    ::core::option::Option::None => "id",
11685                }
11686            },
11687        };
11688        return Ok(quote! {
11689            ::core::option::Option::Some(#root::core::Relation::fk(
11690                <#inner as #root::core::Model>::SCHEMA.table,
11691                #on,
11692            ))
11693        });
11694    }
11695    match (&attrs.fk, &attrs.o2o) {
11696        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11697            field,
11698            "`fk` and `o2o` are mutually exclusive",
11699        )),
11700        (Some(to), None) => {
11701            let on = attrs.on.as_deref().unwrap_or("id");
11702            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11703            // the model's own table. Threaded as a literal string at
11704            // macro-expansion time to sidestep the const-eval cycle
11705            // that `Self::SCHEMA.table` would create when referenced
11706            // inside Self::SCHEMA's own initializer.
11707            let resolved = if to == "self" { table } else { to };
11708            Ok(quote! {
11709                ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11710            })
11711        }
11712        (None, Some(to)) => {
11713            let on = attrs.on.as_deref().unwrap_or("id");
11714            let resolved = if to == "self" { table } else { to };
11715            Ok(quote! {
11716                ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11717            })
11718        }
11719        (None, None) => {
11720            if attrs.on.is_some() {
11721                return Err(syn::Error::new_spanned(
11722                    field,
11723                    "`on` requires `fk` or `o2o`",
11724                ));
11725            }
11726            Ok(quote!(::core::option::Option::None))
11727        }
11728    }
11729}
11730
11731/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11732/// about kinds without depending on `rustango-core` (which would require a
11733/// proc-macro/normal split it doesn't have today).
11734#[derive(Clone, Copy, PartialEq, Eq)]
11735enum DetectedKind {
11736    I16,
11737    I32,
11738    I64,
11739    F32,
11740    F64,
11741    Bool,
11742    String,
11743    DateTime,
11744    Date,
11745    Time,
11746    Uuid,
11747    Json,
11748    Decimal,
11749    Binary,
11750    /// `Array<String>` → PG `text[]` (#341).
11751    ArrayText,
11752    /// `Array<i32>` → PG `integer[]` (#341).
11753    ArrayInt,
11754    /// `Array<i64>` → PG `bigint[]` (#341).
11755    ArrayBigInt,
11756    /// `Range<i32>` → PG `int4range` (#343).
11757    RangeInt,
11758    /// `Range<i64>` → PG `int8range` (#343).
11759    RangeBigInt,
11760    /// `Range<Decimal>` → PG `numrange` (#343).
11761    RangeNumeric,
11762    /// `Range<NaiveDate>` → PG `daterange` (#343).
11763    RangeDate,
11764    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11765    RangeDateTime,
11766    /// `HStore` → PG `hstore` (#342).
11767    HStore,
11768    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11769    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11770    /// in at the `FieldType` emission site (not carried on this enum).
11771    Vector,
11772    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11773    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11774    /// threaded in at the `FieldType` emission site.
11775    Geometry,
11776}
11777
11778impl DetectedKind {
11779    fn variant_tokens(self) -> TokenStream2 {
11780        let root = rustango_root();
11781        match self {
11782            Self::I16 => quote!(#root::core::FieldType::I16),
11783            Self::I32 => quote!(#root::core::FieldType::I32),
11784            Self::I64 => quote!(#root::core::FieldType::I64),
11785            Self::F32 => quote!(#root::core::FieldType::F32),
11786            Self::F64 => quote!(#root::core::FieldType::F64),
11787            Self::Bool => quote!(#root::core::FieldType::Bool),
11788            Self::String => quote!(#root::core::FieldType::String),
11789            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11790            Self::Date => quote!(#root::core::FieldType::Date),
11791            Self::Time => quote!(#root::core::FieldType::Time),
11792            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11793            Self::Json => quote!(#root::core::FieldType::Json),
11794            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11795            Self::Binary => quote!(#root::core::FieldType::Binary),
11796            Self::ArrayText => {
11797                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11798            }
11799            Self::ArrayInt => {
11800                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11801            }
11802            Self::ArrayBigInt => {
11803                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11804            }
11805            Self::RangeInt => {
11806                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11807            }
11808            Self::RangeBigInt => {
11809                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11810            }
11811            Self::RangeNumeric => {
11812                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11813            }
11814            Self::RangeDate => {
11815                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11816            }
11817            Self::RangeDateTime => {
11818                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11819            }
11820            Self::HStore => quote!(#root::core::FieldType::HStore),
11821            // Dimension comes from the `vector(dims = N)` attribute,
11822            // applied at the emission site; `0` here is just a fallback.
11823            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11824            // SRID comes from the `geometry(srid = N)` attribute, applied
11825            // at the emission site; `0` here is just a fallback.
11826            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11827        }
11828    }
11829
11830    fn is_integer(self) -> bool {
11831        matches!(self, Self::I16 | Self::I32 | Self::I64)
11832    }
11833}
11834
11835/// Result of walking a field's Rust type. `kind` is the underlying
11836/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11837/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11838/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11839/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11840#[derive(Clone, Copy)]
11841struct DetectedType<'a> {
11842    kind: DetectedKind,
11843    nullable: bool,
11844    auto: bool,
11845    fk_inner: Option<&'a syn::Type>,
11846}
11847
11848/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11849/// non-`Auto` types — the caller should already have routed Auto-only
11850/// codegen through this helper, so a `None` indicates a macro-internal
11851/// invariant break.
11852fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11853    let Type::Path(TypePath { path, qself: None }) = ty else {
11854        return None;
11855    };
11856    let last = path.segments.last()?;
11857    if last.ident != "Auto" {
11858        return None;
11859    }
11860    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11861        return None;
11862    };
11863    args.args.iter().find_map(|a| match a {
11864        syn::GenericArgument::Type(t) => Some(t),
11865        _ => None,
11866    })
11867}
11868
11869fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11870    let Type::Path(TypePath { path, qself: None }) = ty else {
11871        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11872    };
11873    let last = path
11874        .segments
11875        .last()
11876        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11877
11878    if last.ident == "Option" {
11879        let inner = generic_inner(ty, &last.arguments, "Option")?;
11880        let inner_det = detect_type(inner)?;
11881        if inner_det.nullable {
11882            return Err(syn::Error::new_spanned(
11883                ty,
11884                "nested Option is not supported",
11885            ));
11886        }
11887        if inner_det.auto {
11888            return Err(syn::Error::new_spanned(
11889                ty,
11890                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11891            ));
11892        }
11893        return Ok(DetectedType {
11894            nullable: true,
11895            ..inner_det
11896        });
11897    }
11898
11899    if last.ident == "Auto" {
11900        let inner = generic_inner(ty, &last.arguments, "Auto")?;
11901        let inner_det = detect_type(inner)?;
11902        if inner_det.auto {
11903            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11904        }
11905        if inner_det.nullable {
11906            return Err(syn::Error::new_spanned(
11907                ty,
11908                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11909            ));
11910        }
11911        if inner_det.fk_inner.is_some() {
11912            return Err(syn::Error::new_spanned(
11913                ty,
11914                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11915            ));
11916        }
11917        if !matches!(
11918            inner_det.kind,
11919            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11920        ) {
11921            return Err(syn::Error::new_spanned(
11922                ty,
11923                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11924                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11925                 (DEFAULT now())",
11926            ));
11927        }
11928        return Ok(DetectedType {
11929            auto: true,
11930            ..inner_det
11931        });
11932    }
11933
11934    if last.ident == "ForeignKey" {
11935        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11936        // Resolve the FK column's underlying SQL type from `K`. When the
11937        // user wrote `ForeignKey<T>` without a key parameter, the type
11938        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11939        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11940        // recurse into K so the column DDL emits the right SQL type
11941        // (VARCHAR for String, UUID for Uuid, …) and the load_related
11942        // emitter knows which `SqlValue` variant to match.
11943        let kind = match key_ty {
11944            Some(k) => detect_type(k)?.kind,
11945            None => DetectedKind::I64,
11946        };
11947        return Ok(DetectedType {
11948            kind,
11949            nullable: false,
11950            auto: false,
11951            fk_inner: Some(inner),
11952        });
11953    }
11954
11955    let kind = match last.ident.to_string().as_str() {
11956        "i16" => DetectedKind::I16,
11957        "i32" => DetectedKind::I32,
11958        "i64" => DetectedKind::I64,
11959        "f32" => DetectedKind::F32,
11960        "f64" => DetectedKind::F64,
11961        "bool" => DetectedKind::Bool,
11962        "String" => DetectedKind::String,
11963        "DateTime" => DetectedKind::DateTime,
11964        "NaiveDate" => DetectedKind::Date,
11965        "NaiveTime" => DetectedKind::Time,
11966        "Uuid" => DetectedKind::Uuid,
11967        "Value" => DetectedKind::Json,
11968        "Decimal" => DetectedKind::Decimal,
11969        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11970        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11971        // — that would emit Binary DDL and decode-fail at runtime.
11972        "Vec" => {
11973            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11974            if let Type::Path(TypePath { path, qself: None }) = inner {
11975                if let Some(seg) = path.segments.last() {
11976                    if seg.ident == "u8" && seg.arguments.is_empty() {
11977                        return Ok(DetectedType {
11978                            kind: DetectedKind::Binary,
11979                            nullable: false,
11980                            auto: false,
11981                            fk_inner: None,
11982                        });
11983                    }
11984                }
11985            }
11986            return Err(syn::Error::new_spanned(
11987                ty,
11988                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11989                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11990            ));
11991        }
11992        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11993        // `integer[]` / `bigint[]` (#341).
11994        "Array" => {
11995            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11996            let elem = match inner {
11997                Type::Path(TypePath { path, qself: None }) => {
11998                    path.segments.last().map(|s| s.ident.to_string())
11999                }
12000                _ => None,
12001            };
12002            let kind = match elem.as_deref() {
12003                Some("String") => DetectedKind::ArrayText,
12004                Some("i32") => DetectedKind::ArrayInt,
12005                Some("i64") => DetectedKind::ArrayBigInt,
12006                _ => {
12007                    return Err(syn::Error::new_spanned(
12008                        ty,
12009                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12010                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12011                    ));
12012                }
12013            };
12014            return Ok(DetectedType {
12015                kind,
12016                nullable: false,
12017                auto: false,
12018                fk_inner: None,
12019            });
12020        }
12021        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12022        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12023        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12024        "Range" => {
12025            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12026            let elem = match inner {
12027                Type::Path(TypePath { path, qself: None }) => {
12028                    path.segments.last().map(|s| s.ident.to_string())
12029                }
12030                _ => None,
12031            };
12032            let kind = match elem.as_deref() {
12033                Some("i32") => DetectedKind::RangeInt,
12034                Some("i64") => DetectedKind::RangeBigInt,
12035                Some("Decimal") => DetectedKind::RangeNumeric,
12036                Some("NaiveDate") => DetectedKind::RangeDate,
12037                Some("DateTime") => DetectedKind::RangeDateTime,
12038                _ => {
12039                    return Err(syn::Error::new_spanned(
12040                        ty,
12041                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12042                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12043                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12044                         (→ tstzrange) are supported (#343)",
12045                    ));
12046                }
12047            };
12048            return Ok(DetectedType {
12049                kind,
12050                nullable: false,
12051                auto: false,
12052                fk_inner: None,
12053            });
12054        }
12055        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12056        // (the `CastValue` impl bridges logical↔stored); the field's own
12057        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12058        // schema just needs `FieldType::String`.
12059        "Cast" => {
12060            return Ok(DetectedType {
12061                kind: DetectedKind::String,
12062                nullable: false,
12063                auto: false,
12064                fk_inner: None,
12065            });
12066        }
12067        // `HStore` → PG `hstore` (#342). No generic
12068        // parameter — always a string→string map.
12069        "HStore" => {
12070            return Ok(DetectedType {
12071                kind: DetectedKind::HStore,
12072                nullable: false,
12073                auto: false,
12074                fk_inner: None,
12075            });
12076        }
12077        // `Vector` → pgvector `vector(N)` (#824). The dimension is
12078        // supplied by `#[rustango(vector(dims = N))]`, not the type.
12079        "Vector" => {
12080            return Ok(DetectedType {
12081                kind: DetectedKind::Vector,
12082                nullable: false,
12083                auto: false,
12084                fk_inner: None,
12085            });
12086        }
12087        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12088        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12089        "Point" => {
12090            return Ok(DetectedType {
12091                kind: DetectedKind::Geometry,
12092                nullable: false,
12093                auto: false,
12094                fk_inner: None,
12095            });
12096        }
12097        other => {
12098            return Err(syn::Error::new_spanned(
12099                ty,
12100                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)"),
12101            ));
12102        }
12103    };
12104    Ok(DetectedType {
12105        kind,
12106        nullable: false,
12107        auto: false,
12108        fk_inner: None,
12109    })
12110}
12111
12112fn generic_inner<'a>(
12113    ty: &'a Type,
12114    arguments: &'a PathArguments,
12115    wrapper: &str,
12116) -> syn::Result<&'a Type> {
12117    let PathArguments::AngleBracketed(args) = arguments else {
12118        return Err(syn::Error::new_spanned(
12119            ty,
12120            format!("{wrapper} requires a generic argument"),
12121        ));
12122    };
12123    args.args
12124        .iter()
12125        .find_map(|a| match a {
12126            GenericArgument::Type(t) => Some(t),
12127            _ => None,
12128        })
12129        .ok_or_else(|| {
12130            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12131        })
12132}
12133
12134/// Like [`generic_inner`] but pulls *two* type args — the first is
12135/// required, the second is optional. Used by the `ForeignKey<T, K>`
12136/// detection where K defaults to `i64` when omitted.
12137fn generic_pair<'a>(
12138    ty: &'a Type,
12139    arguments: &'a PathArguments,
12140    wrapper: &str,
12141) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12142    let PathArguments::AngleBracketed(args) = arguments else {
12143        return Err(syn::Error::new_spanned(
12144            ty,
12145            format!("{wrapper} requires a generic argument"),
12146        ));
12147    };
12148    let mut types = args.args.iter().filter_map(|a| match a {
12149        GenericArgument::Type(t) => Some(t),
12150        _ => None,
12151    });
12152    let first = types.next().ok_or_else(|| {
12153        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12154    })?;
12155    let second = types.next();
12156    Ok((first, second))
12157}
12158
12159fn to_snake_case(s: &str) -> String {
12160    let mut out = String::with_capacity(s.len() + 4);
12161    for (i, ch) in s.chars().enumerate() {
12162        if ch.is_ascii_uppercase() {
12163            if i > 0 {
12164                out.push('_');
12165            }
12166            out.push(ch.to_ascii_lowercase());
12167        } else {
12168            out.push(ch);
12169        }
12170    }
12171    out
12172}
12173
12174// ============================================================
12175//  #[derive(Form)]  —  slice 8.4B
12176// ============================================================
12177
12178/// Per-field `#[form(...)]` attributes recognised by the derive.
12179#[derive(Default)]
12180struct FormFieldAttrs {
12181    min: Option<i64>,
12182    max: Option<i64>,
12183    min_length: Option<u32>,
12184    max_length: Option<u32>,
12185    /// `#[form(clean = "fn_name")]` — per-field clean hook.
12186    /// The named static method on the form struct is called after the
12187    /// field's typed parse + length/range checks; it gets the parsed
12188    /// value by reference and returns `Result<<FieldType>, String>`.
12189    /// On Ok, the returned value replaces the parsed one; on Err, the
12190    /// message is attached to the field error list. Issue #372.
12191    clean: Option<syn::Ident>,
12192}
12193
12194/// Container-level `#[form(...)]` attributes. Currently only the
12195/// cross-field `validate` hook (issue #373).
12196#[derive(Default)]
12197struct FormContainerAttrs {
12198    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12199    /// After every per-field parse succeeds, the named method on the
12200    /// form struct is called with `&self` and may return
12201    /// `Result<(), FormErrors>`. Errors merge into the field error
12202    /// list. Issue #373.
12203    validate: Option<syn::Ident>,
12204}
12205
12206/// Detected shape of a form field's Rust type.
12207#[derive(Clone, Copy)]
12208enum FormFieldKind {
12209    String,
12210    I16,
12211    I32,
12212    I64,
12213    F32,
12214    F64,
12215    Bool,
12216}
12217
12218impl FormFieldKind {
12219    fn parse_method(self) -> &'static str {
12220        match self {
12221            Self::I16 => "i16",
12222            Self::I32 => "i32",
12223            Self::I64 => "i64",
12224            Self::F32 => "f32",
12225            Self::F64 => "f64",
12226            // String + Bool don't go through `str::parse`; the codegen
12227            // handles them inline.
12228            Self::String | Self::Bool => "",
12229        }
12230    }
12231}
12232
12233fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12234    let root = rustango_root();
12235    let struct_name = &input.ident;
12236
12237    let Data::Struct(data) = &input.data else {
12238        return Err(syn::Error::new_spanned(
12239            struct_name,
12240            "Form can only be derived on structs",
12241        ));
12242    };
12243    let Fields::Named(named) = &data.fields else {
12244        return Err(syn::Error::new_spanned(
12245            struct_name,
12246            "Form requires a struct with named fields",
12247        ));
12248    };
12249
12250    // #373 — container-level `#[form(validate = "fn")]` hook.
12251    let container = parse_form_container_attrs(input)?;
12252    let post_field_clean: Vec<TokenStream2> = Vec::new();
12253    let _ = post_field_clean;
12254
12255    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12256    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12257
12258    for field in &named.named {
12259        let ident = field
12260            .ident
12261            .as_ref()
12262            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12263        let attrs = parse_form_field_attrs(field)?;
12264        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12265        let is_string = matches!(kind, FormFieldKind::String);
12266        let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12267        if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12268            return Err(syn::Error::new(
12269                field.span(),
12270                "`min_length` / `max_length` need a String field",
12271            ));
12272        }
12273        if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12274            return Err(syn::Error::new(
12275                field.span(),
12276                "`min` / `max` need a numeric field",
12277            ));
12278        }
12279
12280        let name_lit = ident_name(ident);
12281        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12282        // #372 — append the per-field `clean_<field>` call right after
12283        // the parse block when the attribute is set. The clean fn
12284        // takes &T and returns Result<T, String>; on Err we attach
12285        // the message to the field error list without aborting, so
12286        // the caller sees every field error at once.
12287        let clean_block = if let Some(clean_fn) = &attrs.clean {
12288            quote! {
12289                if __errors.fields().get(#name_lit).is_none() {
12290                    match Self::#clean_fn(&#ident) {
12291                        ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12292                        ::core::result::Result::Err(__msg) => {
12293                            __errors.add(#name_lit, __msg);
12294                        }
12295                    }
12296                }
12297            }
12298        } else {
12299            quote! {}
12300        };
12301        field_blocks.push(quote! {
12302            #parse_block
12303            #clean_block
12304        });
12305        field_idents.push(ident);
12306    }
12307
12308    // #373 — after every per-field parse + clean succeeds, call the
12309    // cross-field validator if declared. Errors merge into the
12310    // outgoing FormErrors via the existing `FormErrors::merge` helper
12311    // (same primitive the serializer cross-field hook uses).
12312    let cross_field_call = if let Some(validate_fn) = &container.validate {
12313        quote! {
12314            if __errors.is_empty() {
12315                let __candidate = Self { #( #field_idents ),* };
12316                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12317                    __errors.merge(__other);
12318                }
12319                if !__errors.is_empty() {
12320                    return ::core::result::Result::Err(__errors);
12321                }
12322                return ::core::result::Result::Ok(__candidate);
12323            }
12324        }
12325    } else {
12326        quote! {}
12327    };
12328
12329    Ok(quote! {
12330        impl #root::forms::Form for #struct_name {
12331            fn parse(
12332                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12333            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12334                let mut __errors = #root::forms::FormErrors::default();
12335                #( #field_blocks )*
12336                #cross_field_call
12337                if !__errors.is_empty() {
12338                    return ::core::result::Result::Err(__errors);
12339                }
12340                ::core::result::Result::Ok(Self {
12341                    #( #field_idents ),*
12342                })
12343            }
12344        }
12345    })
12346}
12347
12348fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12349    let mut out = FormContainerAttrs::default();
12350    for attr in &input.attrs {
12351        if !attr.path().is_ident("form") {
12352            continue;
12353        }
12354        attr.parse_nested_meta(|meta| {
12355            if meta.path.is_ident("validate") {
12356                let s: LitStr = meta.value()?.parse()?;
12357                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12358                return Ok(());
12359            }
12360            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12361        })?;
12362    }
12363    Ok(out)
12364}
12365
12366fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12367    let mut out = FormFieldAttrs::default();
12368    for attr in &field.attrs {
12369        if !attr.path().is_ident("form") {
12370            continue;
12371        }
12372        attr.parse_nested_meta(|meta| {
12373            if meta.path.is_ident("min") {
12374                let lit: syn::LitInt = meta.value()?.parse()?;
12375                out.min = Some(lit.base10_parse::<i64>()?);
12376                return Ok(());
12377            }
12378            if meta.path.is_ident("max") {
12379                let lit: syn::LitInt = meta.value()?.parse()?;
12380                out.max = Some(lit.base10_parse::<i64>()?);
12381                return Ok(());
12382            }
12383            if meta.path.is_ident("min_length") {
12384                let lit: syn::LitInt = meta.value()?.parse()?;
12385                out.min_length = Some(lit.base10_parse::<u32>()?);
12386                return Ok(());
12387            }
12388            if meta.path.is_ident("max_length") {
12389                let lit: syn::LitInt = meta.value()?.parse()?;
12390                out.max_length = Some(lit.base10_parse::<u32>()?);
12391                return Ok(());
12392            }
12393            if meta.path.is_ident("clean") {
12394                let s: LitStr = meta.value()?.parse()?;
12395                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12396                return Ok(());
12397            }
12398            Err(meta.error(
12399                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12400            ))
12401        })?;
12402    }
12403    Ok(out)
12404}
12405
12406fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12407    let Type::Path(TypePath { path, qself: None }) = ty else {
12408        return Err(syn::Error::new(
12409            span,
12410            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12411        ));
12412    };
12413    let last = path
12414        .segments
12415        .last()
12416        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12417
12418    if last.ident == "Option" {
12419        let inner = generic_inner(ty, &last.arguments, "Option")?;
12420        let (kind, nested) = detect_form_field(inner, span)?;
12421        if nested {
12422            return Err(syn::Error::new(
12423                span,
12424                "nested Option in Form fields is not supported",
12425            ));
12426        }
12427        return Ok((kind, true));
12428    }
12429
12430    let kind = match last.ident.to_string().as_str() {
12431        "String" => FormFieldKind::String,
12432        "i16" => FormFieldKind::I16,
12433        "i32" => FormFieldKind::I32,
12434        "i64" => FormFieldKind::I64,
12435        "f32" => FormFieldKind::F32,
12436        "f64" => FormFieldKind::F64,
12437        "bool" => FormFieldKind::Bool,
12438        other => {
12439            return Err(syn::Error::new(
12440                span,
12441                format!(
12442                    "Form field type `{other}` is not supported in v0.8 — use String / \
12443                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12444                ),
12445            ));
12446        }
12447    };
12448    Ok((kind, false))
12449}
12450
12451#[allow(clippy::too_many_lines)]
12452fn render_form_field_parse(
12453    ident: &syn::Ident,
12454    name_lit: &str,
12455    kind: FormFieldKind,
12456    nullable: bool,
12457    attrs: &FormFieldAttrs,
12458) -> TokenStream2 {
12459    // Pull the raw &str from the payload. Uses variable name `data` to
12460    // match the new `Form::parse(data: &HashMap<…>)` signature.
12461    let lookup = quote! {
12462        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12463    };
12464
12465    let parsed_value = match kind {
12466        // `Option<bool>`: absent or blank is `None`, as for the other types.
12467        FormFieldKind::Bool if nullable => quote! {
12468            let __v: ::core::option::Option<bool> = match __raw {
12469                ::core::option::Option::None => ::core::option::Option::None,
12470                ::core::option::Option::Some(__s) if __s.is_empty() => {
12471                    ::core::option::Option::None
12472                }
12473                ::core::option::Option::Some(__s) => ::core::option::Option::Some(!matches!(
12474                    __s.to_ascii_lowercase().as_str(),
12475                    "false" | "0" | "off" | "no"
12476                )),
12477            };
12478        },
12479        FormFieldKind::Bool => quote! {
12480            let __v: bool = match __raw {
12481                ::core::option::Option::None => false,
12482                ::core::option::Option::Some(__s) => !matches!(
12483                    __s.to_ascii_lowercase().as_str(),
12484                    "" | "false" | "0" | "off" | "no"
12485                ),
12486            };
12487        },
12488        FormFieldKind::String => {
12489            if nullable {
12490                quote! {
12491                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12492                        ::core::option::Option::None => ::core::option::Option::None,
12493                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12494                            ::core::option::Option::None
12495                        }
12496                        ::core::option::Option::Some(__s) => {
12497                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12498                        }
12499                    };
12500                }
12501            } else {
12502                quote! {
12503                    let __v: ::std::string::String = match __raw {
12504                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12505                            ::core::clone::Clone::clone(__s)
12506                        }
12507                        _ => {
12508                            __errors.add(#name_lit, "This field is required.");
12509                            ::std::string::String::new()
12510                        }
12511                    };
12512                }
12513            }
12514        }
12515        FormFieldKind::I16
12516        | FormFieldKind::I32
12517        | FormFieldKind::I64
12518        | FormFieldKind::F32
12519        | FormFieldKind::F64 => {
12520            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12521            let ty_lit = kind.parse_method();
12522            let default_val = match kind {
12523                FormFieldKind::I16 => quote! { 0i16 },
12524                FormFieldKind::I32 => quote! { 0i32 },
12525                FormFieldKind::I64 => quote! { 0i64 },
12526                FormFieldKind::F32 => quote! { 0f32 },
12527                FormFieldKind::F64 => quote! { 0f64 },
12528                _ => quote! { Default::default() },
12529            };
12530            if nullable {
12531                quote! {
12532                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12533                        ::core::option::Option::None => ::core::option::Option::None,
12534                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12535                            ::core::option::Option::None
12536                        }
12537                        ::core::option::Option::Some(__s) => {
12538                            match __s.parse::<#parse_ty>() {
12539                                ::core::result::Result::Ok(__n) => {
12540                                    ::core::option::Option::Some(__n)
12541                                }
12542                                ::core::result::Result::Err(__e) => {
12543                                    __errors.add(
12544                                        #name_lit,
12545                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12546                                    );
12547                                    ::core::option::Option::None
12548                                }
12549                            }
12550                        }
12551                    };
12552                }
12553            } else {
12554                quote! {
12555                    let __v: #parse_ty = match __raw {
12556                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12557                            match __s.parse::<#parse_ty>() {
12558                                ::core::result::Result::Ok(__n) => __n,
12559                                ::core::result::Result::Err(__e) => {
12560                                    __errors.add(
12561                                        #name_lit,
12562                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12563                                    );
12564                                    #default_val
12565                                }
12566                            }
12567                        }
12568                        _ => {
12569                            __errors.add(#name_lit, "This field is required.");
12570                            #default_val
12571                        }
12572                    };
12573                }
12574            }
12575        }
12576    };
12577
12578    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12579
12580    quote! {
12581        // `mut` so the per-field `clean` hook (#372) can rewrite the
12582        // parsed value in-place when it returns Ok with a normalized
12583        // form (e.g. trim / lowercase).
12584        let mut #ident = {
12585            #lookup
12586            #parsed_value
12587            #validators
12588            __v
12589        };
12590    }
12591}
12592
12593fn render_form_validators(
12594    name_lit: &str,
12595    kind: FormFieldKind,
12596    nullable: bool,
12597    attrs: &FormFieldAttrs,
12598) -> TokenStream2 {
12599    let mut checks: Vec<TokenStream2> = Vec::new();
12600
12601    let val_ref = if nullable {
12602        quote! { __v.as_ref() }
12603    } else {
12604        quote! { ::core::option::Option::Some(&__v) }
12605    };
12606
12607    let is_string = matches!(kind, FormFieldKind::String);
12608    let is_numeric = matches!(
12609        kind,
12610        FormFieldKind::I16
12611            | FormFieldKind::I32
12612            | FormFieldKind::I64
12613            | FormFieldKind::F32
12614            | FormFieldKind::F64
12615    );
12616
12617    if is_string {
12618        if let Some(min_len) = attrs.min_length {
12619            let min_len_usize = min_len as usize;
12620            checks.push(quote! {
12621                if let ::core::option::Option::Some(__s) = #val_ref {
12622                    if __s.chars().count() < #min_len_usize {
12623                        __errors.add(
12624                            #name_lit,
12625                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12626                        );
12627                    }
12628                }
12629            });
12630        }
12631        if let Some(max_len) = attrs.max_length {
12632            let max_len_usize = max_len as usize;
12633            checks.push(quote! {
12634                if let ::core::option::Option::Some(__s) = #val_ref {
12635                    if __s.chars().count() > #max_len_usize {
12636                        __errors.add(
12637                            #name_lit,
12638                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12639                        );
12640                    }
12641                }
12642            });
12643        }
12644    }
12645
12646    if is_numeric {
12647        if let Some(min) = attrs.min {
12648            checks.push(quote! {
12649                if let ::core::option::Option::Some(__n) = #val_ref {
12650                    if (*__n as f64) < (#min as f64) {
12651                        __errors.add(
12652                            #name_lit,
12653                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12654                        );
12655                    }
12656                }
12657            });
12658        }
12659        if let Some(max) = attrs.max {
12660            checks.push(quote! {
12661                if let ::core::option::Option::Some(__n) = #val_ref {
12662                    if (*__n as f64) > (#max as f64) {
12663                        __errors.add(
12664                            #name_lit,
12665                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12666                        );
12667                    }
12668                }
12669            });
12670        }
12671    }
12672
12673    quote! { #( #checks )* }
12674}
12675
12676// ============================================================
12677//  #[derive(ViewSet)]
12678// ============================================================
12679
12680struct ViewSetAttrs {
12681    model: syn::Path,
12682    fields: Option<Vec<String>>,
12683    filter_fields: Vec<String>,
12684    search_fields: Vec<String>,
12685    /// (field_name, desc)
12686    ordering: Vec<(String, bool)>,
12687    page_size: Option<usize>,
12688    read_only: bool,
12689    allow_anonymous: bool,
12690    perms: ViewSetPermsAttrs,
12691    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12692    /// retrieve / create responses through this `#[derive(Serializer)]`
12693    /// type instead of the default field-level projection (requires the
12694    /// `serializer` feature). Tri-dialect.
12695    serializer: Option<syn::Path>,
12696}
12697
12698#[derive(Default)]
12699struct ViewSetPermsAttrs {
12700    list: Vec<String>,
12701    retrieve: Vec<String>,
12702    create: Vec<String>,
12703    update: Vec<String>,
12704    destroy: Vec<String>,
12705}
12706
12707fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12708    let root = rustango_root();
12709    let struct_name = &input.ident;
12710
12711    // Must be a unit struct or an empty named struct.
12712    match &input.data {
12713        Data::Struct(s) => match &s.fields {
12714            Fields::Unit | Fields::Named(_) => {}
12715            Fields::Unnamed(_) => {
12716                return Err(syn::Error::new_spanned(
12717                    struct_name,
12718                    "ViewSet can only be derived on a unit struct or an empty named struct",
12719                ));
12720            }
12721        },
12722        _ => {
12723            return Err(syn::Error::new_spanned(
12724                struct_name,
12725                "ViewSet can only be derived on a struct",
12726            ));
12727        }
12728    }
12729
12730    let attrs = parse_viewset_attrs(input)?;
12731    let model_path = &attrs.model;
12732
12733    // `.fields(&[...])` call — None means skip (use all scalar fields).
12734    let fields_call = if let Some(ref fields) = attrs.fields {
12735        let lits = fields.iter().map(|f| f.as_str());
12736        quote!(.fields(&[ #(#lits),* ]))
12737    } else {
12738        quote!()
12739    };
12740
12741    let filter_fields_call = if attrs.filter_fields.is_empty() {
12742        quote!()
12743    } else {
12744        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12745        quote!(.filter_fields(&[ #(#lits),* ]))
12746    };
12747
12748    let search_fields_call = if attrs.search_fields.is_empty() {
12749        quote!()
12750    } else {
12751        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12752        quote!(.search_fields(&[ #(#lits),* ]))
12753    };
12754
12755    let ordering_call = if attrs.ordering.is_empty() {
12756        quote!()
12757    } else {
12758        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12759            let f = f.as_str();
12760            quote!((#f, #desc))
12761        });
12762        quote!(.ordering(&[ #(#pairs),* ]))
12763    };
12764
12765    let page_size_call = if let Some(n) = attrs.page_size {
12766        quote!(.page_size(#n))
12767    } else {
12768        quote!()
12769    };
12770
12771    let read_only_call = if attrs.read_only {
12772        quote!(.read_only())
12773    } else {
12774        quote!()
12775    };
12776    let allow_anonymous_call = if attrs.allow_anonymous {
12777        quote!(.allow_anonymous())
12778    } else {
12779        quote!()
12780    };
12781
12782    // `.serializer::<S>()` — reshape responses through a derived
12783    // serializer. Requires the downstream crate to enable the
12784    // `serializer` feature (the method is gated on it).
12785    let serializer_call = if let Some(ref ser) = attrs.serializer {
12786        quote!(.serializer::<#ser>())
12787    } else {
12788        quote!()
12789    };
12790
12791    let perms = &attrs.perms;
12792    let perms_call = if perms.list.is_empty()
12793        && perms.retrieve.is_empty()
12794        && perms.create.is_empty()
12795        && perms.update.is_empty()
12796        && perms.destroy.is_empty()
12797    {
12798        quote!()
12799    } else {
12800        let list_lits = perms.list.iter().map(|s| s.as_str());
12801        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12802        let create_lits = perms.create.iter().map(|s| s.as_str());
12803        let update_lits = perms.update.iter().map(|s| s.as_str());
12804        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12805        quote! {
12806            .permissions(#root::viewset::ViewSetPerms {
12807                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
12808                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12809                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
12810                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
12811                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
12812            })
12813        }
12814    };
12815
12816    Ok(quote! {
12817        impl #struct_name {
12818            /// Build an `axum::Router` with the six standard REST endpoints
12819            /// for this ViewSet, mounted at `prefix`.
12820            ///
12821            /// Accepts any backend's pool — a `sqlx::PgPool`,
12822            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12823            /// enum — because they all `Into<Pool>` (#1273). This is why
12824            /// the derive no longer names `PgPool`, which would not
12825            /// exist on a project built without the `postgres` feature.
12826            pub fn router(
12827                prefix: &str,
12828                pool: impl ::core::convert::Into<#root::sql::Pool>,
12829            ) -> #root::__axum::Router {
12830                #root::viewset::ViewSet::for_model(
12831                    <#model_path as #root::core::Model>::SCHEMA
12832                )
12833                    #fields_call
12834                    #filter_fields_call
12835                    #search_fields_call
12836                    #ordering_call
12837                    #page_size_call
12838                    #perms_call
12839                    #read_only_call
12840                    #allow_anonymous_call
12841                    #serializer_call
12842                    .router_pool(prefix, pool.into())
12843            }
12844        }
12845    })
12846}
12847
12848fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12849    let mut model: Option<syn::Path> = None;
12850    let mut fields: Option<Vec<String>> = None;
12851    let mut filter_fields: Vec<String> = Vec::new();
12852    let mut search_fields: Vec<String> = Vec::new();
12853    let mut ordering: Vec<(String, bool)> = Vec::new();
12854    let mut page_size: Option<usize> = None;
12855    let mut read_only = false;
12856    let mut allow_anonymous = false;
12857    let mut perms = ViewSetPermsAttrs::default();
12858    let mut serializer: Option<syn::Path> = None;
12859
12860    for attr in &input.attrs {
12861        if !attr.path().is_ident("viewset") {
12862            continue;
12863        }
12864        attr.parse_nested_meta(|meta| {
12865            if meta.path.is_ident("model") {
12866                let path: syn::Path = meta.value()?.parse()?;
12867                model = Some(path);
12868                return Ok(());
12869            }
12870            if meta.path.is_ident("serializer") {
12871                let path: syn::Path = meta.value()?.parse()?;
12872                serializer = Some(path);
12873                return Ok(());
12874            }
12875            if meta.path.is_ident("fields") {
12876                let s: LitStr = meta.value()?.parse()?;
12877                fields = Some(split_field_list(&s.value()));
12878                return Ok(());
12879            }
12880            if meta.path.is_ident("filter_fields") {
12881                let s: LitStr = meta.value()?.parse()?;
12882                filter_fields = split_field_list(&s.value());
12883                return Ok(());
12884            }
12885            if meta.path.is_ident("search_fields") {
12886                let s: LitStr = meta.value()?.parse()?;
12887                search_fields = split_field_list(&s.value());
12888                return Ok(());
12889            }
12890            if meta.path.is_ident("ordering") {
12891                let s: LitStr = meta.value()?.parse()?;
12892                ordering = parse_ordering_list(&s.value());
12893                return Ok(());
12894            }
12895            if meta.path.is_ident("page_size") {
12896                let lit: syn::LitInt = meta.value()?.parse()?;
12897                page_size = Some(lit.base10_parse::<usize>()?);
12898                return Ok(());
12899            }
12900            if meta.path.is_ident("read_only") {
12901                read_only = true;
12902                return Ok(());
12903            }
12904            if meta.path.is_ident("allow_anonymous") {
12905                allow_anonymous = true;
12906                return Ok(());
12907            }
12908            if meta.path.is_ident("permissions") {
12909                meta.parse_nested_meta(|inner| {
12910                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12911                        let s: LitStr = inner.value()?.parse()?;
12912                        Ok(split_field_list(&s.value()))
12913                    };
12914                    if inner.path.is_ident("list") {
12915                        perms.list = parse_codenames(&inner)?;
12916                    } else if inner.path.is_ident("retrieve") {
12917                        perms.retrieve = parse_codenames(&inner)?;
12918                    } else if inner.path.is_ident("create") {
12919                        perms.create = parse_codenames(&inner)?;
12920                    } else if inner.path.is_ident("update") {
12921                        perms.update = parse_codenames(&inner)?;
12922                    } else if inner.path.is_ident("destroy") {
12923                        perms.destroy = parse_codenames(&inner)?;
12924                    } else {
12925                        return Err(inner.error(
12926                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12927                        ));
12928                    }
12929                    Ok(())
12930                })?;
12931                return Ok(());
12932            }
12933            Err(meta.error(
12934                "unknown viewset attribute (supported: model, fields, filter_fields, \
12935                 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
12936                 permissions(...))",
12937            ))
12938        })?;
12939    }
12940
12941    let model = model.ok_or_else(|| {
12942        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12943    })?;
12944
12945    Ok(ViewSetAttrs {
12946        model,
12947        fields,
12948        filter_fields,
12949        search_fields,
12950        ordering,
12951        page_size,
12952        read_only,
12953        allow_anonymous,
12954        perms,
12955        serializer,
12956    })
12957}
12958
12959// ============================================================ #[derive(Serializer)]
12960
12961struct SerializerContainerAttrs {
12962    model: syn::Path,
12963    /// `#[serializer(validate = "fn_name")]` on the struct — the
12964    /// cross-field validation hook (#436). The named inherent method
12965    /// must take `&self` and return
12966    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12967    /// `validate()` runs every per-field validator first; then calls
12968    /// the cross-field method if declared; aggregates all errors into
12969    /// one `FormErrors`.
12970    cross_validate: Option<syn::Ident>,
12971}
12972
12973#[derive(Default)]
12974struct SerializerFieldAttrs {
12975    read_only: bool,
12976    write_only: bool,
12977    source: Option<String>,
12978    skip: bool,
12979    /// `#[serializer(method = "fn_name")]` — computed field. The macro
12980    /// emits a `from_model` initializer that calls
12981    /// `Self::fn_name(&model)` and stores the return value.
12982    method: Option<String>,
12983    /// `#[serializer(validate = "fn_name")]` — per-field validator
12984    /// callable run by `Self::validate(&self)`. Must return
12985    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12986    /// the field name.
12987    validate: Option<String>,
12988    /// `#[serializer(nested)]` on a field whose type is another
12989    /// `Serializer` — the macro emits `from_model` initializer that
12990    /// reads the parent via `model.<source>.value()` then calls the
12991    /// child serializer's `from_model(parent)`. When the FK is
12992    /// unloaded the field falls back to `Default::default()` (does
12993    /// NOT panic) so a missing prefetch in prod degrades gracefully.
12994    /// Source field on the model defaults to the field name; override
12995    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12996    /// panic-on-unloaded behavior for tests.
12997    nested: bool,
12998    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12999    /// strict behavior: panic when the FK isn't loaded. Useful in
13000    /// test code where forgetting select_related must trip a hard
13001    /// failure rather than render a blank nested object.
13002    nested_strict: bool,
13003    /// `#[serializer(many = TagSerializer)]` — declare the field as
13004    /// a list of nested serializers. Field type must be `Vec<S>`
13005    /// where `S` is the inner serializer. The macro initializes the
13006    /// field to `Vec::new()` in `from_model` and emits a typed
13007    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13008    /// maps each model row through `S::from_model`. Auto-load isn't
13009    /// possible (the M2M / one-to-many accessor is async); callers
13010    /// fetch the children + call the setter post-from_model.
13011    many: Option<syn::Type>,
13012    /// `#[serializer(slug = "name")]` — render a relation as one of
13013    /// its fields.
13014    /// Source field on the model must be a `ForeignKey<T>`; the
13015    /// macro emits `from_model` glue that walks
13016    /// `model.<source>.value()?.<slug>` and clones it. Field type on
13017    /// the serializer is typically `String` (whatever type the slug
13018    /// column has). When the FK is unloaded the field falls back to
13019    /// `Default::default()`, same graceful-degrade contract as
13020    /// `nested`. Source defaults to the field name; override with
13021    /// `source = "..."`. v0.44.
13022    slug: Option<String>,
13023    /// `#[serializer(max_length = N)]` — caps
13024    /// the character count of a string field on write. Overrides the
13025    /// model's `max_length`; when absent the model value is inherited.
13026    max_length: Option<u64>,
13027    /// `#[serializer(min_length = N)]` — minimum character count.
13028    /// Serializer-only (the model has no `min_length` column).
13029    min_length: Option<u64>,
13030    /// `#[serializer(min = N)]` — inclusive
13031    /// integer lower bound; overrides the model's `min` when given.
13032    min: Option<i64>,
13033    /// `#[serializer(max = N)]` — inclusive
13034    /// integer upper bound; overrides the model's `max` when given.
13035    max: Option<i64>,
13036}
13037
13038fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13039    let mut model: Option<syn::Path> = None;
13040    let mut cross_validate: Option<syn::Ident> = None;
13041    for attr in &input.attrs {
13042        if !attr.path().is_ident("serializer") {
13043            continue;
13044        }
13045        attr.parse_nested_meta(|meta| {
13046            if meta.path.is_ident("model") {
13047                let _eq: syn::Token![=] = meta.input.parse()?;
13048                model = Some(meta.input.parse()?);
13049                return Ok(());
13050            }
13051            if meta.path.is_ident("validate") {
13052                // #436 — container-level `validate = "fn_name"` for
13053                // cross-field validation. Field-level
13054                // `#[serializer(validate = "...")]` on a field is
13055                // parsed separately in `parse_serializer_field_attrs`.
13056                let s: LitStr = meta.value()?.parse()?;
13057                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13058                return Ok(());
13059            }
13060            Err(meta.error(
13061                "unknown serializer container attribute \
13062                 (supported: `model`, `validate`)",
13063            ))
13064        })?;
13065    }
13066    let model = model.ok_or_else(|| {
13067        syn::Error::new_spanned(
13068            &input.ident,
13069            "`#[serializer(model = SomeModel)]` is required",
13070        )
13071    })?;
13072    Ok(SerializerContainerAttrs {
13073        model,
13074        cross_validate,
13075    })
13076}
13077
13078fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13079    let mut out = SerializerFieldAttrs::default();
13080    for attr in &field.attrs {
13081        if !attr.path().is_ident("serializer") {
13082            continue;
13083        }
13084        attr.parse_nested_meta(|meta| {
13085            if meta.path.is_ident("read_only") {
13086                out.read_only = true;
13087                return Ok(());
13088            }
13089            if meta.path.is_ident("write_only") {
13090                out.write_only = true;
13091                return Ok(());
13092            }
13093            if meta.path.is_ident("skip") {
13094                out.skip = true;
13095                return Ok(());
13096            }
13097            if meta.path.is_ident("source") {
13098                let s: LitStr = meta.value()?.parse()?;
13099                out.source = Some(s.value());
13100                return Ok(());
13101            }
13102            if meta.path.is_ident("method") {
13103                let s: LitStr = meta.value()?.parse()?;
13104                out.method = Some(s.value());
13105                return Ok(());
13106            }
13107            if meta.path.is_ident("validate") {
13108                let s: LitStr = meta.value()?.parse()?;
13109                out.validate = Some(s.value());
13110                return Ok(());
13111            }
13112            if meta.path.is_ident("many") {
13113                let _eq: syn::Token![=] = meta.input.parse()?;
13114                out.many = Some(meta.input.parse()?);
13115                return Ok(());
13116            }
13117            if meta.path.is_ident("nested") {
13118                out.nested = true;
13119                // Optional strict flag inside parentheses:
13120                //   #[serializer(nested(strict))]
13121                if meta.input.peek(syn::token::Paren) {
13122                    meta.parse_nested_meta(|inner| {
13123                        if inner.path.is_ident("strict") {
13124                            out.nested_strict = true;
13125                            return Ok(());
13126                        }
13127                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13128                    })?;
13129                }
13130                return Ok(());
13131            }
13132            if meta.path.is_ident("slug") {
13133                let s: LitStr = meta.value()?.parse()?;
13134                out.slug = Some(s.value());
13135                return Ok(());
13136            }
13137            if meta.path.is_ident("max_length") {
13138                let lit: syn::LitInt = meta.value()?.parse()?;
13139                out.max_length = Some(lit.base10_parse::<u64>()?);
13140                return Ok(());
13141            }
13142            if meta.path.is_ident("min_length") {
13143                let lit: syn::LitInt = meta.value()?.parse()?;
13144                out.min_length = Some(lit.base10_parse::<u64>()?);
13145                return Ok(());
13146            }
13147            if meta.path.is_ident("min") {
13148                let lit: syn::LitInt = meta.value()?.parse()?;
13149                out.min = Some(lit.base10_parse::<i64>()?);
13150                return Ok(());
13151            }
13152            if meta.path.is_ident("max") {
13153                let lit: syn::LitInt = meta.value()?.parse()?;
13154                out.max = Some(lit.base10_parse::<i64>()?);
13155                return Ok(());
13156            }
13157            Err(meta.error(
13158                "unknown serializer field attribute (supported: \
13159                 `read_only`, `write_only`, `source`, `skip`, `method`, \
13160                 `validate`, `nested`, `many`, `slug`, `max_length`, \
13161                 `min_length`, `min`, `max`)",
13162            ))
13163        })?;
13164    }
13165    // Validate: read_only + write_only is nonsensical
13166    if out.read_only && out.write_only {
13167        return Err(syn::Error::new_spanned(
13168            field,
13169            "a field cannot be both `read_only` and `write_only`",
13170        ));
13171    }
13172    if out.method.is_some() && out.source.is_some() {
13173        return Err(syn::Error::new_spanned(
13174            field,
13175            "`method` and `source` are mutually exclusive — `method` computes \
13176             the value from a method, `source` reads it from a different model field",
13177        ));
13178    }
13179    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13180        return Err(syn::Error::new_spanned(
13181            field,
13182            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13183             — pick one strategy for populating the field",
13184        ));
13185    }
13186    Ok(out)
13187}
13188
13189fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13190    let root = rustango_root();
13191    let struct_name = &input.ident;
13192    let struct_name_lit = struct_name.to_string();
13193
13194    let Data::Struct(data) = &input.data else {
13195        return Err(syn::Error::new_spanned(
13196            struct_name,
13197            "Serializer can only be derived on structs",
13198        ));
13199    };
13200    let Fields::Named(named) = &data.fields else {
13201        return Err(syn::Error::new_spanned(
13202            struct_name,
13203            "Serializer requires a struct with named fields",
13204        ));
13205    };
13206
13207    let container = parse_serializer_container_attrs(input)?;
13208    let model_path = &container.model;
13209
13210    // Classify each field. `ty` is only consumed by the
13211    // `#[cfg(feature = "openapi")]` block below, but we always
13212    // capture it to keep the field-info build a single pass.
13213    #[allow(dead_code)]
13214    struct FieldInfo {
13215        ident: syn::Ident,
13216        ty: syn::Type,
13217        attrs: SerializerFieldAttrs,
13218    }
13219    let mut fields_info: Vec<FieldInfo> = Vec::new();
13220    for field in &named.named {
13221        let ident = field.ident.clone().expect("named field has ident");
13222        let attrs = parse_serializer_field_attrs(field)?;
13223        fields_info.push(FieldInfo {
13224            ident,
13225            ty: field.ty.clone(),
13226            attrs,
13227        });
13228    }
13229
13230    // Generate from_model body: struct literal with each field assigned.
13231    let from_model_fields = fields_info.iter().map(|fi| {
13232        let ident = &fi.ident;
13233        if let Some(_inner) = &fi.attrs.many {
13234            // Many — collection field. Initialize empty; caller
13235            // populates via the macro-emitted set_<field> helper
13236            // after fetching the M2M children.
13237            quote! { #ident: ::std::vec::Vec::new() }
13238        } else if let Some(method) = &fi.attrs.method {
13239            // SerializerMethodField: call Self::<method>(&model) to
13240            // compute the value. Method signature must be
13241            // `fn <method>(model: &T) -> <field type>`.
13242            let method_ident = syn::Ident::new(method, ident.span());
13243            quote! { #ident: Self::#method_ident(model) }
13244        } else if let Some(slug_field) = &fi.attrs.slug {
13245            // v0.44 — SlugRelatedField. Source defaults to the field
13246            // name on this struct; override via `source = "..."`. The
13247            // source field on the model is expected to be a
13248            // `ForeignKey<T>`; the slug field on the parent is named
13249            // by the attribute value. When the FK is unloaded the
13250            // field falls back to `Default::default()` — same
13251            // graceful-degrade contract as `nested`; a NULL
13252            // `Option<ForeignKey>` renders `None`.
13253            let src_name = fi
13254                .attrs
13255                .source
13256                .as_deref()
13257                .unwrap_or(&ident_name(&fi.ident))
13258                .to_owned();
13259            let src_ident = ident_from_name(&src_name, ident.span());
13260            let slug_ident = syn::Ident::new(slug_field, ident.span());
13261            quote! {
13262                #ident: #root::serializer::LoadedRelation::render(
13263                    &model.#src_ident,
13264                    |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
13265                    ::core::default::Default::default,
13266                )
13267            }
13268        } else if fi.attrs.nested {
13269            // Nested serializer. Source defaults to the field name on
13270            // this struct; override via `source = "..."`. The source
13271            // field on the model is a `ForeignKey<T>` or
13272            // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
13273            //
13274            // Behavior matrix (tweakable per-field):
13275            //   * FK loaded   → nested object materializes via
13276            //                   ChildSerializer::from_model(parent).
13277            //   * FK unloaded → fall back to ChildSerializer::default()
13278            //                   (so prod doesn't crash on a missing
13279            //                   prefetch — just renders a blank nested
13280            //                   object). Add `#[serializer(nested(
13281            //                   strict))]` to keep the v0.18.1
13282            //                   panic-on-unloaded behavior for tests
13283            //                   that want hard guardrails.
13284            //   * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
13285            //                   field (JSON null), strict or not.
13286            let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13287            let src_ident = ident_from_name(&src_name, ident.span());
13288            let unloaded = if fi.attrs.nested_strict {
13289                let panic_msg = format!(
13290                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13291                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13292                );
13293                quote! { || ::core::panic!("{}", #panic_msg) }
13294            } else {
13295                quote! { ::core::default::Default::default }
13296            };
13297            quote! {
13298                #ident: #root::serializer::LoadedRelation::render(
13299                    &model.#src_ident,
13300                    |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
13301                    #unloaded,
13302                )
13303            }
13304        } else if fi.attrs.write_only || fi.attrs.skip {
13305            // Not read from model — use default
13306            quote! { #ident: ::core::default::Default::default() }
13307        } else if let Some(src) = &fi.attrs.source {
13308            let src_ident = ident_from_name(src, ident.span());
13309            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13310        } else {
13311            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13312        }
13313    });
13314
13315    // is_writable predicate (also used by writable_lits /
13316    // writable_source_lits below).
13317    let is_writable = |fi: &&FieldInfo| {
13318        !fi.attrs.read_only
13319            && !fi.attrs.skip
13320            && fi.attrs.method.is_none()
13321            && !fi.attrs.nested
13322            && fi.attrs.many.is_none()
13323            && fi.attrs.slug.is_none()
13324    };
13325
13326    // Declarative field constraints: one block
13327    // per writable field, run inside `validate()`. Each resolves its
13328    // bounds as the serializer attr when given (`#[serializer(max_length
13329    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13330    // `max` / `choices`), then dispatches on the field's JSON value via
13331    // `forms::validators::check_value` (string → length/choices, integer
13332    // → min/max).
13333    let opt_usize = |v: Option<u64>| match v {
13334        Some(n) => {
13335            let n = n as usize;
13336            quote!(::core::option::Option::Some(#n))
13337        }
13338        None => quote!(::core::option::Option::None),
13339    };
13340    let opt_i64 = |v: Option<i64>| match v {
13341        Some(n) => quote!(::core::option::Option::Some(#n)),
13342        None => quote!(::core::option::Option::None),
13343    };
13344    let constraint_blocks: Vec<_> = fields_info
13345        .iter()
13346        .filter(is_writable)
13347        .map(|fi| {
13348            let ident = &fi.ident;
13349            let fname = ident_name(ident);
13350            let mname = fi
13351                .attrs
13352                .source
13353                .clone()
13354                .unwrap_or_else(|| ident_name(&fi.ident));
13355            let attr_max_len = opt_usize(fi.attrs.max_length);
13356            let attr_min_len = opt_usize(fi.attrs.min_length);
13357            let attr_min = opt_i64(fi.attrs.min);
13358            let attr_max = opt_i64(fi.attrs.max);
13359            // A bound the field's type cannot take is a compile error, not a no-op.
13360            let ty = &fi.ty;
13361            let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
13362                .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
13363            let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
13364                .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
13365            quote! {
13366                #len_check
13367                #num_check
13368                if __sent(#fname) {
13369                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13370                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13371                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13372                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13373                    let __min: ::core::option::Option<i64> =
13374                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13375                    let __max: ::core::option::Option<i64> =
13376                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13377                    let __choices = __sf.and_then(|__f| __f.choices);
13378                    let __v = #root::__serde_json::to_value(&self.#ident)
13379                        .unwrap_or(#root::__serde_json::Value::Null);
13380                    #root::forms::validators::check_value(
13381                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13382                        &mut __errors,
13383                    );
13384                }
13385            }
13386        })
13387        .collect();
13388    let has_constraints = !constraint_blocks.is_empty();
13389
13390    // Per-field validators. Emit a
13391    // `validate(&self)` method that runs each user-defined validator
13392    // and aggregates errors into `FormErrors`.
13393    let validator_calls: Vec<_> = fields_info
13394        .iter()
13395        .filter_map(|fi| {
13396            let ident = &fi.ident;
13397            let name_lit = ident_name(ident);
13398            let method = fi.attrs.validate.as_ref()?;
13399            let method_ident = syn::Ident::new(method, ident.span());
13400            Some(quote! {
13401                if __sent(#name_lit) {
13402                    if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13403                        __errors.add(#name_lit.to_owned(), __e);
13404                    }
13405                }
13406            })
13407        })
13408        .collect();
13409    // #436 — cross-field `validate(&self)`. If the
13410    // container declared `#[serializer(validate = "fn_name")]`,
13411    // the macro-generated `validate(&self)` runs every per-field
13412    // validator first, then calls the user's cross-field method,
13413    // merging its `FormErrors` into the per-field errors. Either
13414    // alone is enough to emit the wrapper.
13415    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13416        quote! {
13417            // Merge cross-field errors into the per-field bucket so
13418            // a single .validate() call surfaces both layers.
13419            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13420                __errors.merge(__cross);
13421            }
13422        }
13423    });
13424    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13425    // Anything to run? Declarative constraints (for any writable field) +
13426    // per-field validators + cross-field hook.
13427    let has_run_validations = has_validators || has_constraints;
13428    // Shared body: declarative field constraints first (length / range /
13429    // choices, serializer-attr-or-model), then per-field validators, then
13430    // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13431    let checks = quote! {
13432        let mut __errors = #root::forms::FormErrors::default();
13433        #( #constraint_blocks )*
13434        #( #validator_calls )*
13435        #cross_validate_call
13436        if __errors.is_empty() {
13437            ::core::result::Result::Ok(())
13438        } else {
13439            ::core::result::Result::Err(__errors)
13440        }
13441    };
13442    let validate_body = quote! {
13443        let __sent = |_: &str| true;
13444        #checks
13445    };
13446    // PATCH: only the keys the body carries.
13447    let validate_patch_body = quote! {
13448        let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13449        #checks
13450    };
13451    // Inherent `validate(&self)` — kept for back-compat with direct
13452    // `serializer.validate()` calls that don't import `ModelSerializer`.
13453    // Emitted only when the serializer declares per-field/cross-field
13454    // validators (unchanged rule) so it never collides with a
13455    // hand-written inherent `validate`.
13456    let validate_method = if has_validators {
13457        quote! {
13458            impl #struct_name {
13459                /// Run the declarative field constraints, every
13460                /// `#[serializer(validate = "...")]` per-field validator,
13461                /// and (when declared) the container-level cross-field
13462                /// validator. Aggregates errors into `FormErrors` keyed by
13463                /// the field name. Returns `Ok(())` when all pass.
13464                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13465                    #validate_body
13466                }
13467            }
13468        }
13469    } else {
13470        quote! {}
13471    };
13472    // Trait-method override so the type-erased ViewSet write path can
13473    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13474    // there's anything to run (declarative constraints inherited from the
13475    // model count); otherwise the trait's default no-op is used.
13476    let trait_validate_override = if has_run_validations {
13477        quote! {
13478            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13479                #validate_body
13480            }
13481        }
13482    } else {
13483        quote! {}
13484    };
13485
13486    // For every `#[serializer(many = S)]` field, emit a
13487    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13488    // helper that maps the parents through `S::from_model`.
13489    let many_setters: Vec<_> = fields_info
13490        .iter()
13491        .filter_map(|fi| {
13492            let many_ty = fi.attrs.many.as_ref()?;
13493            let ident = &fi.ident;
13494            let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13495            Some(quote! {
13496                /// Populate this `many` field by mapping each parent model
13497                /// through the inner serializer's `from_model`. Call after
13498                /// fetching the M2M / one-to-many children since
13499                /// `from_model` itself can't await an SQL query.
13500                pub fn #setter(
13501                    &mut self,
13502                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13503                ) -> &mut Self {
13504                    self.#ident = models.iter()
13505                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13506                        .collect();
13507                    self
13508                }
13509            })
13510        })
13511        .collect();
13512    let many_setters_impl = if many_setters.is_empty() {
13513        quote! {}
13514    } else {
13515        quote! {
13516            impl #struct_name {
13517                #( #many_setters )*
13518            }
13519        }
13520    };
13521
13522    // Generate custom Serialize: skip write_only fields
13523    let output_fields: Vec<_> = fields_info
13524        .iter()
13525        .filter(|fi| !fi.attrs.write_only)
13526        .collect();
13527    let output_field_count = output_fields.len();
13528    let serialize_fields = output_fields.iter().map(|fi| {
13529        let ident = &fi.ident;
13530        let name_lit = ident_name(ident);
13531        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13532    });
13533
13534    // writable_fields: normal + write_only.
13535    // Exclude:
13536    //   - `read_only` — server-computed.
13537    //   - `skip` — caller sets manually post-from_model.
13538    //   - `method` — computed from a Self::fn(&model) call; accepting
13539    //     it on write is meaningless.
13540    //   - `nested` / `many` — populated from related-model data, not
13541    //     from a field on the wire body.
13542    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13543    // `many` in `writable_fields()`, which made the ViewSet write
13544    // path accept those fields from the JSON body and try to bind
13545    // them to the SQL UPDATE — a silent no-op at best, a type
13546    // mismatch at worst.
13547    // (`is_writable` is defined above, near the constraint codegen.)
13548    let writable_lits: Vec<_> = fields_info
13549        .iter()
13550        .filter(is_writable)
13551        .map(|fi| ident_name(&fi.ident))
13552        .collect();
13553
13554    // `writable_source_fields`: the MODEL field names of writable
13555    // serializer fields (source-resolved). The ViewSet write path skips
13556    // every model column NOT in this set, so `read_only` / `method` /
13557    // computed fields a client posts are ignored instead of written.
13558    let writable_source_lits: Vec<String> = fields_info
13559        .iter()
13560        .filter(is_writable)
13561        .map(|fi| {
13562            fi.attrs
13563                .source
13564                .clone()
13565                .unwrap_or_else(|| ident_name(&fi.ident))
13566        })
13567        .collect();
13568
13569    // `readable_source_fields`: model columns the JSON output copies
13570    // straight from the model, so `?ordering=` cannot sort on a column
13571    // the API never shows (#1845).
13572    let readable_source_lits: Vec<String> = fields_info
13573        .iter()
13574        .filter(|fi| {
13575            !fi.attrs.write_only
13576                && !fi.attrs.skip
13577                && fi.attrs.method.is_none()
13578                && !fi.attrs.nested
13579                && fi.attrs.many.is_none()
13580                && fi.attrs.slug.is_none()
13581        })
13582        .map(|fi| {
13583            fi.attrs
13584                .source
13585                .clone()
13586                .unwrap_or_else(|| ident_name(&fi.ident))
13587        })
13588        .collect();
13589
13590    // `from_writable_json`: build a partial instance for input
13591    // validation. Writable fields are parsed from the JSON body (keyed
13592    // by serializer field name); every other field defaults. Per-field
13593    // type errors collect into `FormErrors` keyed by the field name.
13594    // Construction is field-by-field (not `Self::default()`) so it works
13595    // for serializers regardless of a struct-level `Default`.
13596    let from_writable_json_inits: Vec<_> = fields_info
13597        .iter()
13598        .map(|fi| {
13599            let ident = &fi.ident;
13600            let fname = ident_name(ident);
13601            let ty = &fi.ty;
13602            if is_writable(&fi) {
13603                quote! {
13604                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13605                        ::core::option::Option::Some(__v) => {
13606                            match #root::__serde_json::from_value::<#ty>(
13607                                ::core::clone::Clone::clone(__v),
13608                            ) {
13609                                ::core::result::Result::Ok(__x) => __x,
13610                                ::core::result::Result::Err(__e) => {
13611                                    __errors.add(#fname.to_owned(), __e.to_string());
13612                                    ::core::default::Default::default()
13613                                }
13614                            }
13615                        }
13616                        ::core::option::Option::None => ::core::default::Default::default(),
13617                    }
13618                }
13619            } else {
13620                quote! { #ident: ::core::default::Default::default() }
13621            }
13622        })
13623        .collect();
13624    // `validate_patch`: overlay each sent writable field on the stored row.
13625    let patch_overlays: Vec<_> = fields_info
13626        .iter()
13627        .filter(is_writable)
13628        .map(|fi| {
13629            let ident = &fi.ident;
13630            let fname = ident_name(ident);
13631            let ty = &fi.ty;
13632            quote! {
13633                if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
13634                    match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
13635                        ::core::result::Result::Ok(__x) => __merged.#ident = __x,
13636                        ::core::result::Result::Err(__e) => {
13637                            __errors.add(#fname.to_owned(), __e.to_string());
13638                        }
13639                    }
13640                }
13641            }
13642        })
13643        .collect();
13644    let patch_validate_call = if has_run_validations {
13645        quote! { __merged.__rustango_validate_patch(__obj) }
13646    } else {
13647        quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
13648    };
13649    let patch_validate_helper = if has_run_validations {
13650        quote! {
13651            impl #struct_name {
13652                #[doc(hidden)]
13653                fn __rustango_validate_patch(
13654                    &self,
13655                    __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
13656                ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13657                    #validate_patch_body
13658                }
13659            }
13660        }
13661    } else {
13662        quote! {}
13663    };
13664
13665    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13666    // Only includes fields shown in JSON output (skips write_only). For each
13667    // `Option<T>` field, omit from `required` and add `.nullable()`.
13668    let openapi_impl = {
13669        #[cfg(feature = "openapi")]
13670        {
13671            let property_calls = output_fields.iter().map(|fi| {
13672                let ident = &fi.ident;
13673                let name_lit = ident_name(ident);
13674                let ty = &fi.ty;
13675                let nullable_call = if is_option(ty) {
13676                    quote! { .nullable() }
13677                } else {
13678                    quote! {}
13679                };
13680                quote! {
13681                    .property(
13682                        #name_lit,
13683                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13684                            #nullable_call,
13685                    )
13686                }
13687            });
13688            let required_lits: Vec<_> = output_fields
13689                .iter()
13690                .filter(|fi| !is_option(&fi.ty))
13691                .map(|fi| ident_name(&fi.ident))
13692                .collect();
13693            quote! {
13694                impl #root::openapi::OpenApiSchema for #struct_name {
13695                    fn openapi_schema() -> #root::openapi::Schema {
13696                        #root::openapi::Schema::object()
13697                            #( #property_calls )*
13698                            .required([ #( #required_lits ),* ])
13699                    }
13700                }
13701            }
13702        }
13703        #[cfg(not(feature = "openapi"))]
13704        {
13705            quote! {}
13706        }
13707    };
13708
13709    Ok(quote! {
13710        impl #root::serializer::ModelSerializer for #struct_name {
13711            type Model = #model_path;
13712
13713            fn from_model(model: &Self::Model) -> Self {
13714                Self {
13715                    #( #from_model_fields ),*
13716                }
13717            }
13718
13719            fn writable_fields() -> &'static [&'static str] {
13720                &[ #( #writable_lits ),* ]
13721            }
13722
13723            fn writable_source_fields() -> &'static [&'static str] {
13724                &[ #( #writable_source_lits ),* ]
13725            }
13726
13727            fn readable_source_fields() -> &'static [&'static str] {
13728                &[ #( #readable_source_lits ),* ]
13729            }
13730
13731            fn from_writable_json(
13732                __body: &#root::__serde_json::Value,
13733            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13734                let mut __errors = #root::forms::FormErrors::default();
13735                let __obj = __body.as_object();
13736                let __out = Self {
13737                    #( #from_writable_json_inits ),*
13738                };
13739                if __errors.is_empty() {
13740                    ::core::result::Result::Ok(__out)
13741                } else {
13742                    ::core::result::Result::Err(__errors)
13743                }
13744            }
13745
13746            fn validate_patch(
13747                __stored: &Self::Model,
13748                __body: &#root::__serde_json::Value,
13749            ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13750                let mut __errors = #root::forms::FormErrors::default();
13751                let __obj = __body.as_object();
13752                #[allow(unused_mut)]
13753                let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
13754                #( #patch_overlays )*
13755                if !__errors.is_empty() {
13756                    return ::core::result::Result::Err(__errors);
13757                }
13758                #patch_validate_call
13759            }
13760
13761            #trait_validate_override
13762        }
13763
13764        #patch_validate_helper
13765
13766        impl #root::__serde::Serialize for #struct_name {
13767            fn serialize<S>(&self, serializer: S)
13768                -> ::core::result::Result<S::Ok, S::Error>
13769            where
13770                S: #root::__serde::Serializer,
13771            {
13772                use #root::__serde::ser::SerializeStruct;
13773                let mut __state = serializer.serialize_struct(
13774                    #struct_name_lit,
13775                    #output_field_count,
13776                )?;
13777                #( #serialize_fields )*
13778                __state.end()
13779            }
13780        }
13781
13782        #openapi_impl
13783
13784        #validate_method
13785
13786        #many_setters_impl
13787    })
13788}
13789
13790/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13791/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13792/// when the feature is off.
13793#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13794fn is_option(ty: &syn::Type) -> bool {
13795    if let syn::Type::Path(p) = ty {
13796        if let Some(last) = p.path.segments.last() {
13797            return last.ident == "Option";
13798        }
13799    }
13800    false
13801}
13802
13803#[cfg(test)]
13804mod main_attr_tests {
13805    use super::*;
13806
13807    fn expand(args: &str) -> String {
13808        let body = quote! { async fn main() { run().await } };
13809        expand_main(args.parse().unwrap(), body)
13810            .expect("expansion failed")
13811            .to_string()
13812    }
13813
13814    /// Asserts on the expansion rather than on `parse_logging`,
13815    /// because the defect was a subscriber reaching the output, not
13816    /// an argument being misread (#1465).
13817    #[test]
13818    fn logging_false_emits_no_subscriber() {
13819        let out = expand("logging = false");
13820        assert!(
13821            !out.contains("logging :: Setup"),
13822            "`logging = false` must not install a subscriber:\n{out}",
13823        );
13824        assert!(
13825            out.contains("block_on"),
13826            "the runtime must still be built:\n{out}",
13827        );
13828    }
13829
13830    #[test]
13831    fn default_still_installs_a_subscriber() {
13832        // The control. Without it, an `expand_main` that dropped the
13833        // prologue unconditionally would pass the test above while
13834        // leaving every default project with no logging at all.
13835        let out = expand("");
13836        assert!(
13837            out.contains("logging :: Setup"),
13838            "the default entrypoint must still install one:\n{out}",
13839        );
13840    }
13841
13842    #[test]
13843    fn logging_false_composes_with_flavor() {
13844        let out = expand("flavor = \"current_thread\", logging = false");
13845        assert!(!out.contains("logging :: Setup"), "{out}");
13846        assert!(out.contains("new_current_thread"), "{out}");
13847    }
13848
13849    #[test]
13850    fn logging_true_is_the_default_shape() {
13851        // `logging = true` is accepted and means the default, so the
13852        // match must not fire on the substring `logging`.
13853        assert!(parse_logging(&"logging = true".parse().unwrap()));
13854        assert!(!parse_logging(&"logging=false".parse().unwrap()));
13855    }
13856}
13857
13858#[cfg(test)]
13859mod embed_migrations_tests {
13860    use super::*;
13861
13862    fn expand(path: &str) -> String {
13863        expand_embed_migrations(quote! { #path })
13864            .expect("expansion failed")
13865            .to_string()
13866    }
13867
13868    #[test]
13869    fn missing_dir_warns() {
13870        let out = expand("./no_such_migrations_dir");
13871        assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
13872        assert!(out.contains("no_such_migrations_dir"), "{out}");
13873    }
13874
13875    #[test]
13876    fn existing_dir_does_not_warn() {
13877        let out = expand("./src");
13878        assert!(!out.contains("deprecated"), "{out}");
13879    }
13880}
13881
13882#[cfg(test)]
13883mod field_attr_tests {
13884    use super::*;
13885
13886    fn error_for(field: syn::Field) -> Option<String> {
13887        let attrs = parse_field_attrs(&field).unwrap();
13888        let kind = detect_type(&field.ty).unwrap().kind;
13889        check_bound_compatibility(&field, &attrs, kind)
13890            .err()
13891            .map(|e| e.to_string())
13892    }
13893
13894    /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
13895    #[test]
13896    fn type_specific_attrs_reject_other_types() {
13897        let f: syn::FieldsNamed = syn::parse_quote!({
13898            #[rustango(citext)] a: i64,
13899            #[rustango(vector(dims = 3))] b: String,
13900            #[rustango(geometry(srid = 4326))] c: String,
13901            #[rustango(citext)] d: String,
13902        });
13903        let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
13904        assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
13905        assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
13906        assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
13907        assert_eq!(errs[3], None);
13908    }
13909
13910    /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
13911    #[test]
13912    fn manager_ext_is_refused() {
13913        let input: DeriveInput = syn::parse_quote! {
13914            #[rustango(table = "p", manager(ext = "PostManagerExt"))]
13915            struct Post { id: i64 }
13916        };
13917        let err = parse_container_attrs(&input).err().expect("refused");
13918        assert!(err.to_string().contains("#2132"), "{err}");
13919    }
13920
13921    /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
13922    #[test]
13923    fn set_null_needs_an_optional_foreign_key() {
13924        let input: DeriveInput = syn::parse_quote! {
13925            #[rustango(table = "c")]
13926            struct Child {
13927                #[rustango(primary_key)] id: i64,
13928                #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
13929            }
13930        };
13931        let err = expand(&input).err().expect("refused");
13932        assert!(err.to_string().contains("set_null"), "{err}");
13933        let input: DeriveInput = syn::parse_quote! {
13934            #[rustango(table = "c")]
13935            struct Child {
13936                #[rustango(primary_key)] id: i64,
13937                #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
13938            }
13939        };
13940        assert!(expand(&input).is_ok());
13941    }
13942
13943    /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
13944    #[test]
13945    fn set_null_needs_a_nullable_fk_column() {
13946        let input: DeriveInput = syn::parse_quote! {
13947            #[rustango(table = "c")]
13948            struct Child {
13949                #[rustango(primary_key)] id: i64,
13950                #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
13951                #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
13952            }
13953        };
13954        let err = expand(&input).err().expect("refused");
13955        assert!(err.to_string().contains("set_null"), "{err}");
13956        let input: DeriveInput = syn::parse_quote! {
13957            #[rustango(table = "c")]
13958            struct Child {
13959                #[rustango(primary_key)] id: i64,
13960                #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
13961                #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
13962                #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
13963            }
13964        };
13965        assert!(expand(&input).is_ok());
13966    }
13967
13968    /// Two FKs to one parent would emit the same reverse accessor twice.
13969    #[test]
13970    fn reverse_accessor_clash_names_related_name() {
13971        let input: DeriveInput = syn::parse_quote! {
13972            #[rustango(table = "m")]
13973            struct Message {
13974                #[rustango(primary_key)] id: i64,
13975                sender: ForeignKey<User>,
13976                recipient: ForeignKey<User>,
13977            }
13978        };
13979        let err = expand(&input).err().expect("refused");
13980        assert!(err.to_string().contains("related_name"), "{err}");
13981    }
13982
13983    /// `related_name = "+"` emits no reverse accessor, so a parent from
13984    /// another crate, which cannot take an inherent impl, still derives.
13985    #[test]
13986    fn related_name_plus_skips_the_reverse_accessor() {
13987        let input: DeriveInput = syn::parse_quote! {
13988            #[rustango(table = "m")]
13989            struct Message {
13990                #[rustango(primary_key)] id: i64,
13991                sender: ForeignKey<User>,
13992                #[rustango(related_name = "+")] recipient: ForeignKey<User>,
13993                #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
13994            }
13995        };
13996        let out = expand(&input).expect("derives").to_string();
13997        assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
13998        assert!(!out.contains("impl other :: Account {"), "{out}");
13999    }
14000
14001    /// A raw ident derives names without `r#`; this used to panic (#1937).
14002    #[test]
14003    fn raw_idents_derive_plain_names() {
14004        let ident: syn::Ident = syn::parse_quote!(r#type);
14005        assert_eq!(column_type_ident(&ident).to_string(), "type_col");
14006        assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
14007    }
14008}