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"` — *required*. The only scalar fields list/retrieve
82///   JSON renders and create/update accept; other body keys are ignored.
83/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
84/// * `search_fields = "a, b"` — fields searched by `?search=...`.
85/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
86/// * `page_size = N` — default page size (default: 20, 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 still
339    // compile, with a warning that they are ignored.
340    let root = rustango_root();
341    let arg_warnings = main_arg_warnings(&args);
342    let flavor = parse_flavor(&args);
343    let builder_call = match flavor {
344        Flavor::CurrentThread => quote! {
345            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
346        },
347        Flavor::MultiThread => quote! {
348            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
349        },
350    };
351
352    // Detach the user body and rewrite `main` as a sync fn that
353    // builds the runtime and blocks on the async body.
354    let user_body = input.block.clone();
355    input.sig.asyncness = None;
356
357    // `logging = false` drops the block entirely, so a later
358    // `Cli::with_logging()` is the first installer and its
359    // `[logging]` settings actually take effect (#1465).
360    let logging_prologue = if parse_logging(&args) {
361        quote! {
362            // `Setup`'s defaults, not a copy of them: filter, format and
363            // the `Color::Auto` rule live in one place (#1493).
364            // `logging` and this macro share the `runtime` gate.
365            // The real `RUST_LOG` still wins over `./.env`'s (#2204).
366            let _ = #root::logging::Setup::new()
367                .with_default_env_filter(#root::__private_runtime::main_default_filter())
368                .with_format(#root::__private_runtime::main_format())
369                .install();
370        }
371    } else {
372        quote! {}
373    };
374
375    input.block = syn::parse2(quote! {{
376        #arg_warnings
377        { #logging_prologue }
378        let __rt = #builder_call
379            .enable_all()
380            .build()
381            .expect("failed to build tokio runtime");
382        __rt.block_on(async move #user_body)
383    }})?;
384
385    Ok(quote! {
386        #input
387    })
388}
389
390enum Flavor {
391    MultiThread,
392    CurrentThread,
393}
394
395/// `#[tokio::main]` argument names, accepted without a warning.
396const TOKIO_MAIN_ARGS: &[&str] = &[
397    "flavor",
398    "worker_threads",
399    "start_paused",
400    "crate",
401    "unhandled_panic",
402];
403
404/// A warning per `#[rustango::main]` argument the parsers below ignore.
405fn main_arg_warnings(args: &TokenStream2) -> TokenStream2 {
406    use syn::parse::Parser as _;
407    let parsed = syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
408        .parse2(args.clone());
409    let metas = match parsed {
410        Ok(m) => m,
411        Err(e) => {
412            return deprecation_warning(
413                e.span(),
414                &format!("#[rustango::main]: cannot parse the arguments ({e}); they are ignored"),
415            )
416        }
417    };
418    let lit_bool = |m: &syn::MetaNameValue| match &m.value {
419        syn::Expr::Lit(syn::ExprLit {
420            lit: syn::Lit::Bool(_),
421            ..
422        }) => true,
423        _ => false,
424    };
425    metas
426        .iter()
427        .filter_map(|meta| {
428            // `#[tokio::main]`'s own names stay silent: a `deny(warnings)`
429            // crate passing one must keep compiling in a patch release.
430            let tokio_arg = TOKIO_MAIN_ARGS.iter().any(|n| meta.path().is_ident(n));
431            let ok = match meta {
432                syn::Meta::NameValue(m) if m.path.is_ident("logging") => lit_bool(m),
433                _ => tokio_arg,
434            };
435            (!ok).then(|| {
436                deprecation_warning(
437                    meta.span(),
438                    "#[rustango::main]: unknown argument, ignored (supported: \
439                     `flavor = \"current_thread\" | \"multi_thread\"`, `logging = false`)",
440                )
441            })
442        })
443        .collect()
444}
445
446/// A compile warning at `span`: a proc macro's only stable way to warn
447/// is a `#[deprecated]` item that its own output then uses.
448fn deprecation_warning(span: proc_macro2::Span, note: &str) -> TokenStream2 {
449    quote::quote_spanned! {span=>
450        const _: () = {
451            #[deprecated(note = #note)]
452            const RUSTANGO_WARNING: () = ();
453            RUSTANGO_WARNING
454        };
455    }
456}
457
458fn parse_flavor(args: &TokenStream2) -> Flavor {
459    // Cheap parser: look for the literal token sequence
460    // `flavor = "current_thread"`. Everything else (including
461    // bare `multi_thread` or no args) defaults to multi-thread.
462    let s = args.to_string();
463    if s.contains("current_thread") {
464        Flavor::CurrentThread
465    } else {
466        Flavor::MultiThread
467    }
468}
469
470/// `false` when the attribute carries `logging = false` — the caller
471/// installs their own subscriber, usually via `Cli::with_logging`
472/// (#1465). Same cheap token match as `parse_flavor`.
473fn parse_logging(args: &TokenStream2) -> bool {
474    !args.to_string().replace(' ', "").contains("logging=false")
475}
476
477/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
478struct QInput {
479    base_path: syn::Path,
480    field: syn::Ident,
481    value: syn::Expr,
482}
483
484impl syn::parse::Parse for QInput {
485    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
486        let base_path: syn::Path = input.parse()?;
487        input.parse::<syn::Token![.]>()?;
488        let field: syn::Ident = input.parse()?;
489        input.parse::<syn::Token![=]>()?;
490        let value: syn::Expr = input.parse()?;
491        Ok(QInput {
492            base_path,
493            field,
494            value,
495        })
496    }
497}
498
499fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
500    let q: QInput = syn::parse2(input)?;
501    let root = rustango_root();
502    let field_str = ident_name(&q.field);
503    let field_span = q.field.span();
504    let (base, suffix) = match field_str.find("__") {
505        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
506        None => (field_str.as_str(), ""),
507    };
508    if base.is_empty() {
509        return Err(syn::Error::new(
510            field_span,
511            "Q!(): field name is empty before `__` suffix",
512        ));
513    }
514    let base_ident = ident_from_name(base, field_span);
515    let value = &q.value;
516    let path = &q.base_path;
517
518    // Most suffixes map directly to a Column method with the value
519    // forwarded unchanged. Some need value-shape massaging (wildcards
520    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
521    // ISNULL). Unknown suffixes fail the build.
522    let expanded = match suffix {
523        "" | "exact" => quote! {
524            #root::core::Column::eq(#path::#base_ident, #value)
525        },
526        "ne" => quote! {
527            #root::core::Column::ne(#path::#base_ident, #value)
528        },
529        "gt" => quote! {
530            #root::core::Column::gt(#path::#base_ident, #value)
531        },
532        "gte" => quote! {
533            #root::core::Column::gte(#path::#base_ident, #value)
534        },
535        "lt" => quote! {
536            #root::core::Column::lt(#path::#base_ident, #value)
537        },
538        "lte" => quote! {
539            #root::core::Column::lte(#path::#base_ident, #value)
540        },
541        // The escaped-LIKE lookups delegate to the typed Column
542        // methods, which escape the value and pair it with the
543        // *Escaped ops so `%` / `_` match literally on every dialect
544        // (#1257) — the macro no longer owns any pattern wrapping.
545        "iexact" => quote! {
546            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
547        },
548        "contains" => quote! {
549            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
550        },
551        "icontains" => quote! {
552            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
553        },
554        "startswith" => quote! {
555            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
556        },
557        "istartswith" => quote! {
558            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
559        },
560        "endswith" => quote! {
561            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
562        },
563        "iendswith" => quote! {
564            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
565        },
566        "in" => quote! {
567            #root::core::Column::is_in(#path::#base_ident, #value)
568        },
569        "not_in" => quote! {
570            #root::core::Column::not_in(#path::#base_ident, #value)
571        },
572        "isnull" => {
573            // Must be a bool literal at macro time so we can route to
574            // is_null vs is_not_null without a runtime branch.
575            let b = match value {
576                syn::Expr::Lit(syn::ExprLit {
577                    lit: syn::Lit::Bool(b),
578                    ..
579                }) => b.value(),
580                _ => {
581                    return Err(syn::Error::new_spanned(
582                        value,
583                        "Q!(): `__isnull` requires a `true` or `false` literal",
584                    ));
585                }
586            };
587            if b {
588                quote! { #root::core::Column::is_null(#path::#base_ident) }
589            } else {
590                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
591            }
592        }
593        "between" => {
594            // Accept a tuple literal `(lo, hi)`.
595            let tuple = match value {
596                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
597                _ => {
598                    return Err(syn::Error::new_spanned(
599                        value,
600                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
601                    ));
602                }
603            };
604            let lo = &tuple.elems[0];
605            let hi = &tuple.elems[1];
606            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
607        }
608        "regex" => quote! {
609            #root::core::Column::regex(#path::#base_ident, #value)
610        },
611        "iregex" => quote! {
612            #root::core::Column::iregex(#path::#base_ident, #value)
613        },
614        _ => {
615            return Err(syn::Error::new(
616                field_span,
617                format!(
618                    "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",
619                    suffix
620                ),
621            ));
622        }
623    };
624    Ok(expanded)
625}
626
627fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
628    // Default to "./migrations" if invoked without args.
629    let path_str = if input.is_empty() {
630        "./migrations".to_string()
631    } else {
632        let lit: LitStr = syn::parse2(input)?;
633        lit.value()
634    };
635
636    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
637        syn::Error::new(
638            proc_macro2::Span::call_site(),
639            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
640        )
641    })?;
642    let abs = std::path::Path::new(&manifest).join(&path_str);
643
644    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
645    if abs.is_dir() {
646        let read = std::fs::read_dir(&abs).map_err(|e| {
647            syn::Error::new(
648                proc_macro2::Span::call_site(),
649                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
650            )
651        })?;
652        for entry in read.flatten() {
653            let path = entry.path();
654            if !path.is_file() {
655                continue;
656            }
657            if path.extension().and_then(|s| s.to_str()) != Some("json") {
658                continue;
659            }
660            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
661                continue;
662            };
663            entries.push((stem.to_owned(), path));
664        }
665    }
666    entries.sort_by(|a, b| a.0.cmp(&b.0));
667
668    // Compile-time chain validation: read each migration's JSON,
669    // pull `name` and `prev` (file-stem-keyed for the chain check),
670    // and verify every `prev` points to another migration in the
671    // slice. Mismatches between the file stem and the embedded
672    // `name` field — and broken `prev` chains — fail at MACRO
673    // EXPANSION time so a misshapen migration set never compiles.
674    //
675    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
676    // migrations + a Rust proc-macro that reads them is a combo no
677    // other Rust web framework matches (Cot's are imperative Rust
678    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
679    // a static chain to validate).
680    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
681    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
682    for (stem, path) in &entries {
683        let raw = std::fs::read_to_string(path).map_err(|e| {
684            syn::Error::new(
685                proc_macro2::Span::call_site(),
686                format!(
687                    "embed_migrations!: cannot read {} for chain validation: {e}",
688                    path.display()
689                ),
690            )
691        })?;
692        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
693            syn::Error::new(
694                proc_macro2::Span::call_site(),
695                format!(
696                    "embed_migrations!: {} is not valid JSON: {e}",
697                    path.display()
698                ),
699            )
700        })?;
701        let name = json
702            .get("name")
703            .and_then(|v| v.as_str())
704            .ok_or_else(|| {
705                syn::Error::new(
706                    proc_macro2::Span::call_site(),
707                    format!(
708                        "embed_migrations!: {} is missing the `name` field",
709                        path.display()
710                    ),
711                )
712            })?
713            .to_owned();
714        if name != *stem {
715            return Err(syn::Error::new(
716                proc_macro2::Span::call_site(),
717                format!(
718                    "embed_migrations!: file stem `{stem}` does not match the migration's \
719                     `name` field `{name}` — rename the file or fix the JSON",
720                ),
721            ));
722        }
723        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
724        chain_names.push(name.clone());
725        prev_refs.push((name, prev));
726    }
727
728    let name_set: std::collections::HashSet<&str> =
729        chain_names.iter().map(String::as_str).collect();
730    for (name, prev) in &prev_refs {
731        if let Some(p) = prev {
732            if !name_set.contains(p.as_str()) {
733                return Err(syn::Error::new(
734                    proc_macro2::Span::call_site(),
735                    format!(
736                        "embed_migrations!: broken migration chain — `{name}` declares \
737                         prev=`{p}` but no migration with that name exists in {}",
738                        abs.display()
739                    ),
740                ));
741            }
742        }
743    }
744
745    let pairs: Vec<TokenStream2> = entries
746        .iter()
747        .map(|(name, path)| {
748            let path_lit = path.display().to_string();
749            quote! { (#name, ::core::include_str!(#path_lit)) }
750        })
751        .collect();
752
753    // A wrong path used to embed nothing silently; a deprecation use is
754    // the only stable way for a proc macro to warn without breaking builds.
755    let missing_warning = if abs.is_dir() {
756        quote! {}
757    } else {
758        let note = format!(
759            "embed_migrations!: directory {} does not exist; no migrations are embedded",
760            abs.display()
761        );
762        quote! {
763            #[deprecated(note = #note)]
764            const __RUSTANGO_MIGRATIONS_DIR_MISSING: () = ();
765            #[allow(clippy::let_unit_value)]
766            let _ = __RUSTANGO_MIGRATIONS_DIR_MISSING;
767        }
768    };
769
770    Ok(quote! {
771        {
772            #missing_warning
773            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
774            __RUSTANGO_EMBEDDED
775        }
776    })
777}
778
779fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
780    let root = rustango_root();
781    let struct_name = &input.ident;
782
783    let Data::Struct(data) = &input.data else {
784        return Err(syn::Error::new_spanned(
785            struct_name,
786            "Model can only be derived on structs",
787        ));
788    };
789    let Fields::Named(named) = &data.fields else {
790        return Err(syn::Error::new_spanned(
791            struct_name,
792            "Model requires a struct with named fields",
793        ));
794    };
795
796    let mut container = parse_container_attrs(input)?;
797    let table = container
798        .table
799        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
800    let model_name = struct_name.to_string();
801
802    let collected = collect_fields(named, &table)?;
803    // `reverse_has` / `generic_has` join on the PK column unless they name another.
804    if let Some((_, pk_column)) = &collected.primary_key {
805        for rel in &mut container.reverse_has_relations {
806            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
807        }
808        for rel in &mut container.generic_has_relations {
809            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
810        }
811    }
812
813    // Validate that #[rustango(display = "…")] names a real field.
814    if let Some((ref display, span)) = container.display {
815        if !collected.field_names.iter().any(|n| n == display) {
816            return Err(syn::Error::new(
817                span,
818                format!("`display = \"{display}\"` does not match any field on this struct"),
819            ));
820        }
821    }
822    let display = container.display.map(|(name, _)| name);
823    let app_label = container.app.clone();
824
825    // Validate admin field-name lists against declared field names.
826    // Note: `list_display` is intentionally NOT validated here. As of
827    // v0.32 it may also reference inventory-registered computed
828    // fields (via `register_admin_computed!`) whose existence the
829    // macro can't see at compile time — they're submitted from any
830    // crate that depends on rustango. The runtime list-view resolves
831    // unknown names against the inventory + silently drops the
832    // truly-bogus ones, which is the cheaper trade-off versus
833    // forcing a per-Model attr to opt out.
834    if let Some(admin) = &container.admin {
835        for (label, list) in [
836            ("search_fields", &admin.search_fields),
837            ("readonly_fields", &admin.readonly_fields),
838            ("list_filter", &admin.list_filter),
839        ] {
840            if let Some((names, span)) = list {
841                for name in names {
842                    if !collected.field_names.iter().any(|n| n == name) {
843                        return Err(syn::Error::new(
844                            *span,
845                            format!(
846                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
847                            ),
848                        ));
849                    }
850                }
851            }
852        }
853        if let Some((pairs, span)) = &admin.ordering {
854            for (name, _) in pairs {
855                if !collected.field_names.iter().any(|n| n == name) {
856                    return Err(syn::Error::new(
857                        *span,
858                        format!(
859                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
860                        ),
861                    ));
862                }
863            }
864        }
865        if let Some((groups, span)) = &admin.fieldsets {
866            for (_, fields) in groups {
867                for name in fields {
868                    if !collected.field_names.iter().any(|n| n == name) {
869                        return Err(syn::Error::new(
870                            *span,
871                            format!(
872                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
873                            ),
874                        ));
875                    }
876                }
877            }
878        }
879    }
880    if let Some(audit) = &container.audit {
881        if let Some((names, span)) = &audit.track {
882            for name in names {
883                if !collected.field_names.iter().any(|n| n == name) {
884                    return Err(syn::Error::new(
885                        *span,
886                        format!(
887                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
888                        ),
889                    ));
890                }
891            }
892        }
893    }
894
895    // Issue #291 / T2.5 — validate each `default_order` column name
896    // against the model's collected fields. Typos fail at macro-expand
897    // time, not at the database.
898    for (col, _desc, span) in &container.default_order {
899        if !collected.field_names.iter().any(|n| n == col) {
900            return Err(syn::Error::new(
901                *span,
902                format!(
903                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
904                ),
905            ));
906        }
907    }
908
909    let column_warnings =
910        unknown_column_warnings(&container.indexes, &container.get_latest_by, &collected);
911
912    // Build the audit_track list for ModelSchema: None when no audit attr,
913    // Some(empty) when audit present without track, Some(names) when explicit.
914    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
915        audit
916            .track
917            .as_ref()
918            .map(|(names, _)| names.clone())
919            .unwrap_or_default()
920    });
921
922    // The upsert conflict target: a container-level composite unique,
923    // never a field `index(unique)` or a partial index PG can't target.
924    let upsert_unique: Option<Vec<String>> = container
925        .indexes
926        .iter()
927        .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
928        .map(|i| i.columns.clone());
929
930    // Merge field-level indexes into the container's index list.
931    let mut all_indexes: Vec<IndexAttr> = container.indexes;
932    for field in &named.named {
933        let ident = field.ident.as_ref().expect("named");
934        // The same default column as `process_field` (#1937).
935        let col = ident_name(ident);
936        // Re-parse field attrs to check for index flag
937        if let Ok(fa) = parse_field_attrs(field) {
938            if fa.index {
939                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
940                let auto_name = capped_index_name(if fa.index_unique {
941                    format!("{table}_{col_name}_uq_idx")
942                } else {
943                    format!("{table}_{col_name}_idx")
944                });
945                all_indexes.push(IndexAttr {
946                    span: field.span(),
947                    name: fa.index_name.or(Some(auto_name)),
948                    columns: vec![col_name],
949                    unique: fa.index_unique,
950                    method: fa.index_method,
951                    where_clause: None,
952                    include: Vec::new(),
953                });
954            }
955        }
956    }
957    // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
958    for field in &named.named {
959        let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
960            continue;
961        };
962        if det.kind != DetectedKind::String || fa.max_length.is_some() {
963            continue;
964        }
965        let col = fa
966            .column
967            .clone()
968            .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
969        // MySQL indexes every foreign-key column too.
970        let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
971        if fa.primary_key
972            || fa.unique
973            || relation
974            || all_indexes.iter().any(|i| i.columns.contains(&col))
975        {
976            return Err(syn::Error::new_spanned(
977                field,
978                "a primary-key, unique, indexed or foreign-key `String` needs \
979                 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
980            ));
981        }
982    }
983
984    let model_impl = model_impl_tokens(
985        struct_name,
986        &model_name,
987        &table,
988        display.as_deref(),
989        app_label.as_deref(),
990        container.admin.as_ref(),
991        &container.default_order,
992        &collected.field_schemas,
993        collected.soft_delete_column.as_deref(),
994        container.permissions,
995        audit_track_names.as_deref(),
996        &container.m2m,
997        &all_indexes,
998        &container.checks,
999        &container.excludes,
1000        &container.composite_fks,
1001        &container.generic_fks,
1002        container.scope.as_deref(),
1003        container.is_view,
1004        container.verbose_name.as_deref(),
1005        container.verbose_name_plural.as_deref(),
1006        container.managed,
1007        container.base_manager_name.as_deref(),
1008        container.order_with_respect_to.as_deref(),
1009        container.proxy,
1010        &container.required_db_features,
1011        container.required_db_vendor.as_deref(),
1012        container.default_related_name.as_deref(),
1013        container.db_table_comment.as_deref(),
1014        container
1015            .get_latest_by
1016            .as_ref()
1017            .map(|(c, d, _)| (c.as_str(), *d)),
1018        &container.extra_permissions,
1019        &container.default_permissions,
1020        &container.global_scopes,
1021        &container.reverse_has_relations,
1022        &container.generic_has_relations,
1023    );
1024    let module_ident = column_module_ident(struct_name);
1025    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
1026    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
1027        let track_set: Option<std::collections::HashSet<&str>> = audit
1028            .track
1029            .as_ref()
1030            .map(|(names, _)| names.iter().map(String::as_str).collect());
1031        collected
1032            .column_entries
1033            .iter()
1034            .filter(|c| {
1035                track_set
1036                    .as_ref()
1037                    .map_or(true, |s| s.contains(c.name.as_str()))
1038            })
1039            .collect()
1040    });
1041    let inherent_impl = inherent_impl_tokens(
1042        struct_name,
1043        &collected,
1044        collected.primary_key.as_ref(),
1045        &column_consts,
1046        audited_fields.as_deref(),
1047        upsert_unique.as_deref(),
1048        &container.manager_fns,
1049    );
1050    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
1051    let from_row_impl = from_row_impl_tokens(
1052        struct_name,
1053        &collected.from_row_inits,
1054        &collected.from_row_inits_my,
1055    );
1056    let reverse_helpers = reverse_helper_tokens(
1057        struct_name,
1058        &collected.fk_relations,
1059        container.default_related_name.as_deref(),
1060    )?;
1061    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
1062    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
1063    // Issue #817 — `#[rustango(through(...))]` accessors.
1064    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
1065    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
1066    let reverse_has_accessors = reverse_has_accessor_tokens(
1067        struct_name,
1068        &container.reverse_has_relations,
1069        &collected.column_entries,
1070    );
1071    let generic_fk_accessors = generic_fk_accessor_tokens(
1072        struct_name,
1073        &container.generic_fks,
1074        &collected.column_entries,
1075    );
1076
1077    Ok(quote! {
1078        #column_warnings
1079        #model_impl
1080        #inherent_impl
1081        #from_row_impl
1082        #column_module
1083        #reverse_helpers
1084        #m2m_accessors
1085        #generic_m2m_accessors
1086        #through_accessors
1087        #reverse_has_accessors
1088        #generic_fk_accessors
1089
1090        #root::core::inventory::submit! {
1091            // `module_path!()` evaluates at the registration site,
1092            // so a Model declared in `crate::blog::models` records
1093            // `"<crate>::blog::models"` and `resolved_app_label()`
1094            // can infer "blog" without an explicit attribute.
1095            #root::core::ModelEntry::new(
1096                <#struct_name as #root::core::Model>::SCHEMA,
1097                ::core::module_path!(),
1098            )
1099            .with_audited(
1100                <#struct_name as #root::core::Model>::__rustango_audited_update,
1101                <#struct_name as #root::core::Model>::__rustango_audited_delete,
1102            )
1103            .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1104            .with_audited_update_record(
1105                <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1106            )
1107            .with_audited_cascade(<#struct_name as #root::core::Model>::__rustango_audited_cascade)
1108        }
1109    })
1110}
1111
1112/// A deprecation-style warning per index or `get_latest_by` name that is
1113/// no column of the model: an error would break code that compiles today.
1114fn unknown_column_warnings(
1115    indexes: &[IndexAttr],
1116    get_latest_by: &Option<(String, bool, proc_macro2::Span)>,
1117    collected: &CollectedFields,
1118) -> TokenStream2 {
1119    let is_column = |c: &str| collected.column_entries.iter().any(|e| e.column == c);
1120    // Only plain names: an expression index such as `lower(email)` is legal.
1121    let plain = |c: &str| c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
1122    let mut found: Vec<(proc_macro2::Span, String)> = Vec::new();
1123    for idx in indexes {
1124        for col in idx.columns.iter().filter(|c| plain(c) && !is_column(c)) {
1125            found.push((
1126                idx.span,
1127                format!("index column `{col}` is not a column of this model; its DDL will fail"),
1128            ));
1129        }
1130    }
1131    if let Some((col, _, span)) = get_latest_by {
1132        if !is_column(col) && !collected.field_names.iter().any(|n| n == col) {
1133            found.push((
1134                *span,
1135                format!("`get_latest_by`: `{col}` is not a field of this model"),
1136            ));
1137        }
1138    }
1139    found
1140        .into_iter()
1141        .map(|(span, note)| deprecation_warning(span, &note))
1142        .collect()
1143}
1144
1145/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1146/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1147fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1148    let root = rustango_root();
1149    let parent_ty = &rel.parent_type;
1150    let field_ident = &rel.field_ident;
1151    let null_fk = if rel.nullable {
1152        quote! {
1153            if #receiver.#field_ident.is_none() {
1154                return ::core::result::Result::Ok(false);
1155            }
1156        }
1157    } else {
1158        quote! {}
1159    };
1160    quote! {
1161        #null_fk
1162        #root::sql::__rustango_require_join(
1163            row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1164        )?;
1165    }
1166}
1167
1168/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1169/// reused, and the deeper chain stitched into it; re-decoding it would
1170/// drop what that chain loaded.
1171fn loaded_reuse_tokens(
1172    rel: &FkRelation,
1173    receiver: &TokenStream2,
1174    load: &TokenStream2,
1175) -> TokenStream2 {
1176    let root = rustango_root();
1177    let field_ident = &rel.field_ident;
1178    let pattern = if rel.nullable {
1179        quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1180    } else {
1181        quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1182    };
1183    quote! {
1184        if let #pattern = &mut #receiver.#field_ident {
1185            if let ::core::option::Option::Some(__r) = __rest {
1186                let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1187            }
1188            return ::core::result::Result::Ok(true);
1189        }
1190    }
1191}
1192
1193/// Store the decoded `_parent` on the FK field, keeping the key the row
1194/// holds: with `on = <unique column>` it is not the parent's PK.
1195fn loaded_assign_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1196    let root = rustango_root();
1197    let field_ident = &rel.field_ident;
1198    if rel.nullable {
1199        quote! {
1200            if let ::core::option::Option::Some(__fk) = &mut #receiver.#field_ident {
1201                let _pk = ::core::clone::Clone::clone(__fk.pk_ref());
1202                *__fk = #root::sql::ForeignKey::loaded(_pk, _parent);
1203            }
1204        }
1205    } else {
1206        quote! {
1207            let _pk = ::core::clone::Clone::clone(#receiver.#field_ident.pk_ref());
1208            #receiver.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1209        }
1210    }
1211}
1212
1213/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1214/// matches `field_name` against the model's FK fields and, for a
1215/// match, decodes the FK target via the parent's macro-generated
1216/// `__rustango_from_aliased_row`, and stores `ForeignKey::Loaded` on
1217/// `self` under the key the row already holds.
1218///
1219/// Always emitted (with empty arms for FK-less models, which
1220/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1221/// trait bound on `fetch_on` is universally satisfied — users
1222/// never have to think about implementing it.
1223fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1224    let root = rustango_root();
1225    let arms = fk_relations.iter().map(|rel| {
1226        let parent_ty = &rel.parent_type;
1227        let field_ident = &rel.field_ident;
1228        let fk_name = ident_name(field_ident);
1229        let fk_col = fk_name.as_str();
1230        let guard = join_guard_tokens(rel, &quote!(self));
1231        let reuse = loaded_reuse_tokens(
1232            rel,
1233            &quote!(self),
1234            &quote!(#root::sql::LoadRelated::__rustango_load_related),
1235        );
1236        let assign = loaded_assign_tokens(rel, &quote!(self));
1237        quote! {
1238            #fk_col => {
1239                #guard
1240                #reuse
1241                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1242                // Audit #451 — multi-hop `select_related("a__b__c")`:
1243                // stitch the deeper relation onto this parent first,
1244                // decoding it at the accumulated `__next_alias`. The
1245                // parent type also impls `LoadRelated`, so this recurses
1246                // the FK chain to arbitrary depth.
1247                if let ::core::option::Option::Some(__r) = __rest {
1248                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1249                        &mut _parent, row, __r, &__next_alias,
1250                    )?;
1251                }
1252                #assign
1253                ::core::result::Result::Ok(true)
1254            }
1255        }
1256    });
1257    quote! {
1258        #[cfg(feature = "postgres")]
1259        impl #root::sql::LoadRelated for #struct_name {
1260            #[allow(unused_variables)]
1261            fn __rustango_load_related(
1262                &mut self,
1263                row: &#root::sql::sqlx::postgres::PgRow,
1264                field_name: &str,
1265                alias: &str,
1266            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1267                // Audit #451 — split the multi-hop path: `base` is the FK
1268                // on THIS model, `__rest` (if any) is the remaining chain
1269                // to stitch onto the loaded parent. `__next_alias` is the
1270                // accumulated join alias the parent's columns live under
1271                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1272                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1273                    match field_name.split_once("__") {
1274                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1275                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1276                    };
1277                let __next_alias: ::std::string::String = match __rest {
1278                    ::core::option::Option::Some(__r) => {
1279                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1280                        ::std::format!("{}__{}", alias, __rb)
1281                    }
1282                    ::core::option::Option::None => ::std::string::String::new(),
1283                };
1284                match __base {
1285                    #( #arms )*
1286                    _ => ::core::result::Result::Ok(false),
1287                }
1288            }
1289        }
1290    }
1291}
1292
1293/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1294/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1295/// which expands to a `LoadRelatedMy` impl when rustango is built with
1296/// the `mysql` feature, and to nothing otherwise. The decoded parent
1297/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1298/// decoder, also batch8) so the dual emission is symmetric across
1299/// backends.
1300fn load_related_impl_my_tokens(
1301    struct_name: &syn::Ident,
1302    fk_relations: &[FkRelation],
1303) -> TokenStream2 {
1304    let root = rustango_root();
1305    let arms = fk_relations.iter().map(|rel| {
1306        let parent_ty = &rel.parent_type;
1307        let field_ident = &rel.field_ident;
1308        let fk_name = ident_name(field_ident);
1309        let fk_col = fk_name.as_str();
1310        let guard = join_guard_tokens(rel, &quote!(__self));
1311        let reuse = loaded_reuse_tokens(
1312            rel,
1313            &quote!(__self),
1314            &quote!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1315        );
1316        let assign = loaded_assign_tokens(rel, &quote!(__self));
1317        // `self` IS hygiene-tracked through macro_rules — emitted from
1318        // a different context than the `&mut self` parameter inside
1319        // the macro_rules-expanded fn. Pass it through as `__self`
1320        // and let the macro_rules rebind it to the receiver.
1321        quote! {
1322            #fk_col => {
1323                #guard
1324                #reuse
1325                let mut _parent: #parent_ty =
1326                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1327                // Audit #451 — multi-hop: stitch the deeper relation onto
1328                // the parent at the accumulated alias (see PG twin).
1329                if let ::core::option::Option::Some(__r) = __rest {
1330                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1331                        &mut _parent, row, __r, &__next_alias,
1332                    )?;
1333                }
1334                #assign
1335                ::core::result::Result::Ok(true)
1336            }
1337        }
1338    });
1339    quote! {
1340        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1341            #( #arms )*
1342        });
1343    }
1344}
1345
1346/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1347/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1348/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1349fn load_related_impl_sqlite_tokens(
1350    struct_name: &syn::Ident,
1351    fk_relations: &[FkRelation],
1352) -> TokenStream2 {
1353    let root = rustango_root();
1354    let arms = fk_relations.iter().map(|rel| {
1355        let parent_ty = &rel.parent_type;
1356        let field_ident = &rel.field_ident;
1357        let fk_name = ident_name(field_ident);
1358        let fk_col = fk_name.as_str();
1359        let guard = join_guard_tokens(rel, &quote!(__self));
1360        let reuse = loaded_reuse_tokens(
1361            rel,
1362            &quote!(__self),
1363            &quote!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1364        );
1365        let assign = loaded_assign_tokens(rel, &quote!(__self));
1366        quote! {
1367            #fk_col => {
1368                #guard
1369                #reuse
1370                let mut _parent: #parent_ty =
1371                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1372                // Audit #451 — multi-hop: stitch the deeper relation onto
1373                // the parent at the accumulated alias (see PG twin).
1374                if let ::core::option::Option::Some(__r) = __rest {
1375                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1376                        &mut _parent, row, __r, &__next_alias,
1377                    )?;
1378                }
1379                #assign
1380                ::core::result::Result::Ok(true)
1381            }
1382        }
1383    });
1384    quote! {
1385        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1386            #( #arms )*
1387        });
1388    }
1389}
1390
1391/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1392/// matches `field_name` against the model's FK fields and returns
1393/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1394/// group children by parent PK.
1395///
1396/// Always emitted (with `_ => None` for FK-less models) so the
1397/// trait bound on `fetch_with_prefetch` is universally satisfied.
1398fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1399    let root = rustango_root();
1400    let arms = fk_relations.iter().map(|rel| {
1401        let field_ident = &rel.field_ident;
1402        let fk_name = ident_name(field_ident);
1403        let fk_col = fk_name.as_str();
1404        if rel.pk_kind == DetectedKind::I64 {
1405            // i64 FK — return the stored PK so prefetch_related can
1406            // group children by it. Nullable variant unwraps via
1407            // `as_ref().map(...)`: an unset (NULL) FK column yields
1408            // `None` and that child sits out of the grouping (correct
1409            // semantics — it has no parent to attach to).
1410            if rel.nullable {
1411                quote! {
1412                    #fk_col => self.#field_ident
1413                        .as_ref()
1414                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1415                }
1416            } else {
1417                quote! {
1418                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1419                }
1420            }
1421        } else {
1422            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1423            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1424            // i64-keyed grouping path — the trait signature is
1425            // `Option<i64>` and a non-i64 PK can't lower into it.
1426            // The FK still works for everything else (CRUD, lazy
1427            // load via `.get()`, select_related JOINs); only the
1428            // bulk prefetch grouper needs the integer key.
1429            quote! {
1430                #fk_col => ::core::option::Option::None,
1431            }
1432        }
1433    });
1434    // PK-type-agnostic version: every FK arm emits an
1435    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1436    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1437    // opt OUT of the legacy i64 method (it returns None) but opt IN
1438    // here.
1439    let value_arms = fk_relations.iter().map(|rel| {
1440        let field_ident = &rel.field_ident;
1441        let fk_name = ident_name(field_ident);
1442        let fk_col = fk_name.as_str();
1443        if rel.nullable {
1444            quote! {
1445                #fk_col => self.#field_ident
1446                    .as_ref()
1447                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1448                        #root::sql::ForeignKey::pk(fk)
1449                    )),
1450            }
1451        } else {
1452            quote! {
1453                #fk_col => ::core::option::Option::Some(
1454                    ::core::convert::Into::<#root::core::SqlValue>::into(
1455                        self.#field_ident.pk()
1456                    )
1457                ),
1458            }
1459        }
1460    });
1461    quote! {
1462        impl #root::sql::FkPkAccess for #struct_name {
1463            #[allow(unused_variables)]
1464            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1465                match field_name {
1466                    #( #arms )*
1467                    _ => ::core::option::Option::None,
1468                }
1469            }
1470            #[allow(unused_variables)]
1471            fn __rustango_fk_pk_value(
1472                &self,
1473                field_name: &str,
1474            ) -> ::core::option::Option<#root::core::SqlValue> {
1475                match field_name {
1476                    #( #value_arms )*
1477                    _ => ::core::option::Option::None,
1478                }
1479            }
1480        }
1481    }
1482}
1483
1484/// `related_name = "+"`: the FK gets no reverse accessor on its parent.
1485const NO_REVERSE_ACCESSOR: &str = "+";
1486
1487/// For every `ForeignKey<Parent>` field on `Child`, emit
1488/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1489/// Reads the column the FK points at (`on`, else the PK) and runs one
1490/// `SELECT … WHERE <fk_column> = $1`: one round trip, no N+1.
1491///
1492/// **PG-only emission**: the accessor is bounded on
1493/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1494/// both of which are gated behind the `postgres` cargo feature. The
1495/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1496/// model derive itself compiles on tri-dialect / sqlite-only
1497/// downstream builds — the accessor just isn't materialised. A tri-
1498/// dialect `_set_pool` variant is a separate follow-up.
1499fn reverse_helper_tokens(
1500    child_ident: &syn::Ident,
1501    fk_relations: &[FkRelation],
1502    default_related_name: Option<&str>,
1503) -> syn::Result<TokenStream2> {
1504    let root = rustango_root();
1505    // `related_name = "+"`: no accessor, so a parent from another crate works.
1506    let fk_relations: Vec<&FkRelation> = fk_relations
1507        .iter()
1508        .filter(|rel| rel.related_name.as_deref() != Some(NO_REVERSE_ACCESSOR))
1509        .collect();
1510    if fk_relations.is_empty() {
1511        return Ok(TokenStream2::new());
1512    }
1513    // Method-name resolution per FK (issue #816 + follow-up):
1514    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1515    //      itself — wins over everything else.
1516    //   2. Container-level `default_related_name = "..."` on the
1517    //      child. Applies to every FK on this model that didn't
1518    //      override.
1519    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1520    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1521    //      edge cases.
1522    //
1523    // The PG-on-executor variant keeps the resolved name; the
1524    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1525    // framework's convention for the `&Pool` flavor of every helper).
1526    let default_pg_suffix = default_related_name
1527        .map(str::to_owned)
1528        .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1529    let name_of = |rel: &FkRelation| {
1530        rel.related_name
1531            .clone()
1532            .unwrap_or_else(|| default_pg_suffix.clone())
1533    };
1534    // Two FKs to one parent would define the same method twice (E0592).
1535    for (i, rel) in fk_relations.iter().enumerate() {
1536        let parent_ty = &rel.parent_type;
1537        let parent = quote!(#parent_ty).to_string();
1538        let name = name_of(rel);
1539        if let Some(first) = fk_relations[..i].iter().find(|r| {
1540            let other = &r.parent_type;
1541            quote!(#other).to_string() == parent && name_of(r) == name
1542        }) {
1543            return Err(syn::Error::new_spanned(
1544                &rel.field_ident,
1545                format!(
1546                    "`{}` and `{}` both point at `{parent}` and would both add `{parent}::{name}`; \
1547                     give one `#[rustango(related_name = \"...\")]`, or `related_name = \"+\"` for none",
1548                    first.field_ident, rel.field_ident
1549                ),
1550            ));
1551        }
1552    }
1553    let impls = fk_relations.iter().map(|rel| {
1554        let pg_suffix = name_of(rel);
1555        let pool_suffix = format!("{}_pool", pg_suffix);
1556        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1557        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1558        let parent_ty = &rel.parent_type;
1559        let fk_name = ident_name(&rel.field_ident);
1560        let fk_col = fk_name.as_str();
1561        let doc = format!(
1562            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1563             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1564             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1565             further `{child_ident}::objects()` filters via direct queryset use."
1566        );
1567        let pool_doc = format!(
1568            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1569             [`#root::sql::Pool`] and dispatches per backend so the \
1570             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1571             Use this from framework code that holds a `&Pool` (admin, \
1572             tenancy resolver, viewset handlers); reach for the executor- \
1573             bound variant when you already have a typed `sqlx::Executor`."
1574        );
1575        quote! {
1576            #[cfg(feature = "postgres")]
1577            impl #parent_ty {
1578                #[doc = #doc]
1579                ///
1580                /// # Errors
1581                /// Returns [`#root::sql::ExecError`] for SQL-writing
1582                /// or driver failures.
1583                pub async fn #pg_method_ident<'_c, _E>(
1584                    &self,
1585                    _executor: _E,
1586                ) -> ::core::result::Result<
1587                    ::std::vec::Vec<#child_ident>,
1588                    #root::sql::ExecError,
1589                >
1590                where
1591                    _E: #root::sql::sqlx::Executor<
1592                        '_c,
1593                        Database = #root::sql::sqlx::Postgres,
1594                    >,
1595                {
1596                    let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1597                    #root::query::QuerySet::<#child_ident>::new()
1598                        .filter_op(#fk_col, #root::core::Op::Eq, _key)
1599                        .fetch_on(_executor)
1600                        .await
1601                }
1602            }
1603
1604            impl #parent_ty {
1605                #[doc = #pool_doc]
1606                ///
1607                /// # Errors
1608                /// Returns [`#root::sql::ExecError`] for SQL-writing
1609                /// or driver failures.
1610                pub async fn #pool_method_ident(
1611                    &self,
1612                    pool: &#root::sql::Pool,
1613                ) -> ::core::result::Result<
1614                    ::std::vec::Vec<#child_ident>,
1615                    #root::sql::ExecError,
1616                > {
1617                    use #root::sql::FetcherPool as _;
1618                    let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1619                    #root::query::QuerySet::<#child_ident>::new()
1620                        .filter_op(#fk_col, #root::core::Op::Eq, _key)
1621                        .fetch(pool)
1622                        .await
1623                }
1624            }
1625        }
1626    });
1627    Ok(quote! { #( #impls )* })
1628}
1629
1630/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1631/// relation declared on the model.
1632/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1633/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1634///
1635/// Closes #239 + #240 — `comment.content_object` /
1636/// `comment.content_object = post` ergonomics on top of the existing
1637/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1638///
1639/// `column_entries` is passed so we can resolve each `ct_column` /
1640/// `pk_column` SQL name back to its Rust field ident — the macro
1641/// only sees the column-side strings in the attribute, but the
1642/// emitted accessor needs to read the actual struct field.
1643fn generic_fk_accessor_tokens(
1644    struct_name: &syn::Ident,
1645    generic_fks: &[GenericFkAttr],
1646    column_entries: &[ColumnEntry],
1647) -> TokenStream2 {
1648    let root = rustango_root();
1649    if generic_fks.is_empty() {
1650        return TokenStream2::new();
1651    }
1652    let methods = generic_fks.iter().filter_map(|gfk| {
1653        // Resolve `ct_column` + `pk_column` to the struct's Rust
1654        // field idents. A typo (column name doesn't match any field)
1655        // emits no method for that registration — the user will see
1656        // the compiler reject the SCHEMA literal anyway, so there's
1657        // a clear error path without us double-reporting.
1658        let ct_ident = column_entries
1659            .iter()
1660            .find(|c| c.column == gfk.ct_column)
1661            .map(|c| c.ident.clone())?;
1662        let pk_ident = column_entries
1663            .iter()
1664            .find(|c| c.column == gfk.pk_column)
1665            .map(|c| c.ident.clone())?;
1666
1667        let accessor_ident =
1668            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1669        let setter_ident =
1670            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1671        let name_literal = gfk.name.as_str();
1672
1673        Some(quote! {
1674            #[doc = concat!(
1675                "Resolve the polymorphic `",
1676                #name_literal,
1677                "` relation. Reads `self.",
1678                stringify!(#ct_ident),
1679                "` + `self.",
1680                stringify!(#pk_ident),
1681                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1682                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1683                #name_literal,
1684                "\", ...))]`."
1685            )]
1686            pub async fn #accessor_ident(
1687                &self,
1688                pool: &#root::sql::Pool,
1689            ) -> ::core::result::Result<
1690                ::core::option::Option<#root::__serde_json::Value>,
1691                #root::sql::ExecError,
1692            > {
1693                let gfk = #root::contenttypes::GenericForeignKey::new(
1694                    self.#ct_ident as i64,
1695                    self.#pk_ident as i64,
1696                );
1697                gfk.get_object(pool).await
1698            }
1699
1700            #[doc = concat!(
1701                "Set the polymorphic `",
1702                #name_literal,
1703                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1704                stringify!(#ct_ident),
1705                "` and `self.",
1706                stringify!(#pk_ident),
1707                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1708                #name_literal,
1709                "\", ...))]`."
1710            )]
1711            pub async fn #setter_ident<T: #root::core::Model>(
1712                &mut self,
1713                pool: &#root::sql::Pool,
1714                target_pk: i64,
1715            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1716                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1717                    pool,
1718                    target_pk,
1719                ).await?;
1720                self.#ct_ident = gfk.content_type_id as _;
1721                self.#pk_ident = gfk.object_pk as _;
1722                ::core::result::Result::Ok(())
1723            }
1724        })
1725    });
1726    quote! {
1727        impl #struct_name {
1728            #( #methods )*
1729        }
1730    }
1731}
1732
1733fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1734    let root = rustango_root();
1735    if m2m_relations.is_empty() {
1736        return TokenStream2::new();
1737    }
1738    let methods = m2m_relations.iter().map(|rel| {
1739        let method_name = format!("{}_m2m", rel.name);
1740        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1741        let through = rel.through.as_str();
1742        let src_col = rel.src.as_str();
1743        let dst_col = rel.dst.as_str();
1744        quote! {
1745            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1746                #root::sql::M2MManager {
1747                    src_pk: self.__rustango_pk_value(),
1748                    through: #through,
1749                    src_col: #src_col,
1750                    dst_col: #dst_col,
1751                }
1752            }
1753        }
1754    });
1755    quote! {
1756        impl #struct_name {
1757            #( #methods )*
1758        }
1759    }
1760}
1761
1762/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1763/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1764fn generic_m2m_accessor_tokens(
1765    struct_name: &syn::Ident,
1766    relations: &[GenericM2MAttr],
1767) -> TokenStream2 {
1768    let root = rustango_root();
1769    if relations.is_empty() {
1770        return TokenStream2::new();
1771    }
1772    let methods = relations.iter().map(|rel| {
1773        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1774        let through = rel.through.as_str();
1775        let pk_col = rel.pk_column.as_str();
1776        let ct_col = rel.ct_column.as_str();
1777        let related_col = rel.related_column.as_str();
1778        quote! {
1779            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1780                #root::sql::GenericM2MManager {
1781                    src_pk: self.__rustango_pk_value(),
1782                    src_schema: <Self as #root::core::Model>::SCHEMA,
1783                    through: #through,
1784                    pk_col: #pk_col,
1785                    ct_col: #ct_col,
1786                    dst_col: #related_col,
1787                }
1788            }
1789        }
1790    });
1791    // Lets an owner delete find and clear its junction rows.
1792    let entries = relations.iter().map(|rel| {
1793        let through = rel.through.as_str();
1794        let pk_col = rel.pk_column.as_str();
1795        let ct_col = rel.ct_column.as_str();
1796        quote! {
1797            #root::core::inventory::submit! {
1798                #root::core::GenericM2MEntry::new(
1799                    <#struct_name as #root::core::Model>::SCHEMA,
1800                    #through,
1801                    #pk_col,
1802                    #ct_col,
1803                )
1804            }
1805        }
1806    });
1807    quote! {
1808        impl #struct_name {
1809            #( #methods )*
1810        }
1811        #( #entries )*
1812    }
1813}
1814
1815/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1816/// associated functions for each `#[rustango(reverse_has(...))]`
1817/// attribute. Issue #830.
1818///
1819/// The two emitted functions return ready-to-use `WhereExpr` nodes
1820/// that downstream callers drop into
1821/// `QuerySet::<Self>::where_raw(...)`:
1822///
1823/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1824///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1825///   `whereHas` parity (without the closure-style sub-predicate
1826///   refinement — that's a follow-up; users can layer additional
1827///   predicates by constructing the SelectQuery themselves and
1828///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1829/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1830///   `whereDoesntHave` parity.
1831///
1832/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1833/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1834/// machinery threads the outer-table reference through automatically
1835/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1836fn reverse_has_accessor_tokens(
1837    struct_name: &syn::Ident,
1838    reverse_has_relations: &[ReverseHasAttr],
1839    columns: &[ColumnEntry],
1840) -> TokenStream2 {
1841    let root = rustango_root();
1842    if reverse_has_relations.is_empty() {
1843        return TokenStream2::new();
1844    }
1845    let methods = reverse_has_relations.iter().map(|rel| {
1846        let exists_name = format!("{}_exists_expr", rel.name);
1847        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1848        let count_name = format!("{}_count", rel.name);
1849        let fetch_name = format!("{}_fetch", rel.name);
1850        let first_name = format!("{}_first", rel.name);
1851        let pluck_name = format!("{}_pluck", rel.name);
1852        let accessor_name = rel.name.as_str();
1853        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1854        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1855        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1856        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1857        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1858        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1859        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1860        let child = &rel.child;
1861        let child_fk_column = rel.child_fk_column.as_str();
1862        let self_pk_column = rel.self_pk_column();
1863        // The instance helpers filter on the value the EXISTS form joins on.
1864        let self_value = columns
1865            .iter()
1866            .find(|c| c.column == self_pk_column)
1867            .map_or_else(
1868                || quote! { self.__rustango_pk_value() },
1869                |c| {
1870                    let ident = &c.ident;
1871                    quote! {
1872                        ::core::convert::Into::<#root::core::SqlValue>::into(
1873                            ::core::clone::Clone::clone(&self.#ident)
1874                        )
1875                    }
1876                },
1877            );
1878        let exists_doc = format!(
1879            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1880             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1881             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1882             filter to {struct_name}s with at least one matching child.",
1883        );
1884        let not_exists_doc = format!(
1885            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1886             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1887             <outer>.{self_pk_column})`. Drop into \
1888             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1889             {struct_name}s with **no** matching child.",
1890        );
1891        let count_doc = format!(
1892            "Eloquent `$model->{name}->count()` analog — returns \
1893             the number of `{child}` rows whose `{child_fk_column}` \
1894             matches this `{struct_name}` instance's `{self_pk_column}`. \
1895             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1896             {child_fk_column} = <self.{self_pk_column}>`.",
1897            name = rel.name,
1898        );
1899        let accessor_doc = format!(
1900            "Eloquent `$model->{name}` accessor — returns a \
1901             `QuerySet<{child}>` filtered to rows whose \
1902             `{child_fk_column}` matches this `{struct_name}` \
1903             instance's `{self_pk_column}`. **Chainable**: compose `.filter()` \
1904             / `.order_by()` / `.limit()` etc. on top, then call \
1905             `.fetch(&pool)` (the QuerySet trait method) when \
1906             done. For the simple \"fetch all\" hot path with no \
1907             further composition, prefer the bare-name \
1908             `{name}_fetch(&pool)` companion.",
1909            name = rel.name,
1910        );
1911        let fetch_doc = format!(
1912            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1913             over `{name}(&self).fetch(&pool)`. Use this when \
1914             you don't need further `.filter()` / `.order_by()` \
1915             composition; falls back to the chainable accessor when \
1916             you do. Avoids the `_pool` suffix on the most common \
1917             call-site shape.",
1918            name = rel.name,
1919        );
1920        quote! {
1921            #[doc = #accessor_doc]
1922            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1923                #root::query::QuerySet::<#child>::new()
1924                    .filter(#child_fk_column, #self_value)
1925            }
1926
1927            #[doc = #fetch_doc]
1928            pub async fn #fetch_ident(
1929                &self,
1930                pool: &#root::sql::Pool,
1931            ) -> ::core::result::Result<
1932                ::std::vec::Vec<#child>,
1933                #root::sql::ExecError,
1934            > {
1935                use #root::sql::FetcherPool as _;
1936                self.#accessor_ident().fetch(pool).await
1937            }
1938
1939            /// Eloquent `$model->relation->first()` / `hasOne`
1940            /// semantics — bare-name shortcut over
1941            /// `self.<name>().first(&pool)`. Returns `None` when no
1942            /// child rows match. Useful when the relation is
1943            /// nominally many-to-one in shape but at most one row is
1944            /// expected (latest comment, primary tag, etc.).
1945            pub async fn #first_ident(
1946                &self,
1947                pool: &#root::sql::Pool,
1948            ) -> ::core::result::Result<
1949                ::core::option::Option<#child>,
1950                #root::sql::ExecError,
1951            > {
1952                self.#accessor_ident().first(pool).await
1953            }
1954
1955            /// Eloquent `$model->relation->pluck($col)` — project a
1956            /// single column from the child rows into a `Vec<U>`.
1957            /// Skips the typed `Child` decode, which is cheaper when
1958            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1959            pub async fn #pluck_ident<U>(
1960                &self,
1961                col: &'static str,
1962                pool: &#root::sql::Pool,
1963            ) -> ::core::result::Result<
1964                ::std::vec::Vec<U>,
1965                #root::sql::ExecError,
1966            >
1967            where
1968                U: #root::sql::FlatScalar,
1969            {
1970                self.#accessor_ident()
1971                    .values_list_flat(col)
1972                    .fetch::<U>(pool)
1973                    .await
1974            }
1975
1976            #[doc = #exists_doc]
1977            pub fn #exists_ident() -> #root::core::WhereExpr {
1978                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1979                let child_schema =
1980                    <#child as #root::core::Model>::SCHEMA;
1981                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1982                    lhs: Expr::Column(#child_fk_column),
1983                    op: Op::Eq,
1984                    rhs: Expr::OuterRef(#self_pk_column),
1985                });
1986                WhereExpr::Exists(::std::boxed::Box::new(inner))
1987            }
1988
1989            #[doc = #not_exists_doc]
1990            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1991                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1992                let child_schema =
1993                    <#child as #root::core::Model>::SCHEMA;
1994                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1995                    lhs: Expr::Column(#child_fk_column),
1996                    op: Op::Eq,
1997                    rhs: Expr::OuterRef(#self_pk_column),
1998                });
1999                WhereExpr::NotExists(::std::boxed::Box::new(inner))
2000            }
2001
2002            #[doc = #count_doc]
2003            pub async fn #count_ident(
2004                &self,
2005                pool: &#root::sql::Pool,
2006            ) -> ::core::result::Result<
2007                i64,
2008                #root::sql::ExecError,
2009            > {
2010                use #root::sql::CounterPool as _;
2011                self.#accessor_ident().count(pool).await
2012            }
2013        }
2014    });
2015    quote! {
2016        impl #struct_name {
2017            #( #methods )*
2018        }
2019    }
2020}
2021
2022/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
2023/// `#[rustango(through(...))]` attribute. Issue #817.
2024///
2025/// Each accessor builds a correlated subquery via
2026/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
2027/// intermediate table, filters on the FK column pointing at this
2028/// model, and projects the intermediate PK. The outer queryset
2029/// filters the far model by its FK-to-intermediate column being in
2030/// that set.
2031///
2032/// The returned `QuerySet<Far>` is **chainable** — the subquery
2033/// lives inside a `where_raw` clause so the user's later
2034/// `.filter()` / `.order_by()` / `.limit()` compositions don't
2035/// disturb it.
2036///
2037/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
2038/// SQLite — no LATERAL or backend-specific syntax involved.
2039fn through_accessor_tokens(
2040    struct_name: &syn::Ident,
2041    through_relations: &[ThroughAttr],
2042) -> TokenStream2 {
2043    let root = rustango_root();
2044    if through_relations.is_empty() {
2045        return TokenStream2::new();
2046    }
2047    let methods = through_relations.iter().map(|rel| {
2048        let method_name = format!("{}_through", rel.name);
2049        let count_name = format!("{}_through_count", rel.name);
2050        let fetch_name = format!("{}_through_fetch", rel.name);
2051        let first_name = format!("{}_through_first", rel.name);
2052        let pluck_name = format!("{}_through_pluck", rel.name);
2053        let method_ident = syn::Ident::new(&method_name, struct_name.span());
2054        let count_ident = syn::Ident::new(&count_name, struct_name.span());
2055        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
2056        let first_ident = syn::Ident::new(&first_name, struct_name.span());
2057        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
2058        let far = &rel.far;
2059        let intermediate = &rel.intermediate;
2060        let far_fk_column = rel.far_fk_column.as_str();
2061        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
2062        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
2063        let doc = format!(
2064            "Eloquent `hasManyThrough` accessor — returns a \
2065             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
2066             instance through the intermediate `{intermediate}` table. \
2067             Generated SQL shape: \
2068             `… WHERE {far_fk_column} IN (SELECT \
2069             {intermediate_pk_column} FROM <{intermediate}> WHERE \
2070             {intermediate_fk_column} = self.pk)`. Chainable like any \
2071             other QuerySet — compose `.filter()` / `.order_by()` / \
2072             `.limit()` etc. on top.",
2073        );
2074        let count_doc = format!(
2075            "Eloquent `$model->{name}->count()` analog for the \
2076             through-relation — returns the number of `{far}` rows \
2077             reachable through `{intermediate}`. Equivalent to \
2078             `self.{name}_through().count(pool)` but spelled \
2079             as a bare instance method for parity with the \
2080             `reverse_has` `<name>_count` shape.",
2081            name = rel.name,
2082        );
2083        quote! {
2084            #[doc = #doc]
2085            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
2086                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
2087                let intermediate_schema =
2088                    <#intermediate as #root::core::Model>::SCHEMA;
2089                let sub = SelectQuery::new(intermediate_schema)
2090                    .where_clause(WhereExpr::Predicate(Filter::new(
2091                        #intermediate_fk_column,
2092                        Op::Eq,
2093                        self.__rustango_pk_value(),
2094                    )))
2095                    .projection(::std::vec![#intermediate_pk_column]);
2096                #root::query::QuerySet::<#far>::new().where_raw(
2097                    WhereExpr::InSubquery {
2098                        column: #far_fk_column,
2099                        negated: false,
2100                        subquery: ::std::boxed::Box::new(sub),
2101                    },
2102                )
2103            }
2104
2105            #[doc = #count_doc]
2106            pub async fn #count_ident(
2107                &self,
2108                pool: &#root::sql::Pool,
2109            ) -> ::core::result::Result<
2110                i64,
2111                #root::sql::ExecError,
2112            > {
2113                use #root::sql::CounterPool as _;
2114                self.#method_ident().count(pool).await
2115            }
2116
2117            /// Eloquent `$model->relation->get()` for the
2118            /// through-relation — bare-name hot-path over
2119            /// `self.<name>_through().fetch(&pool)`. Use this
2120            /// when you don't need further `.filter()` /
2121            /// `.order_by()` composition; falls back to the
2122            /// chainable accessor when you do. Avoids the `_pool`
2123            /// suffix on the most common call-site shape.
2124            pub async fn #fetch_ident(
2125                &self,
2126                pool: &#root::sql::Pool,
2127            ) -> ::core::result::Result<
2128                ::std::vec::Vec<#far>,
2129                #root::sql::ExecError,
2130            > {
2131                use #root::sql::FetcherPool as _;
2132                self.#method_ident().fetch(pool).await
2133            }
2134
2135            /// Eloquent `hasOneThrough` analog — bare-name shortcut
2136            /// over `self.<name>_through().first(&pool)`. Returns
2137            /// `None` when no far rows are reachable through the
2138            /// intermediate. Useful when at most one row is
2139            /// expected (latest comment by country, primary tag,
2140            /// etc.).
2141            pub async fn #first_ident(
2142                &self,
2143                pool: &#root::sql::Pool,
2144            ) -> ::core::result::Result<
2145                ::core::option::Option<#far>,
2146                #root::sql::ExecError,
2147            > {
2148                self.#method_ident().first(pool).await
2149            }
2150
2151            /// Pluck a single column from the far rows into `Vec<U>`
2152            /// — cheaper than the typed `<Far>` decode when only one
2153            /// scalar column is needed.
2154            pub async fn #pluck_ident<U>(
2155                &self,
2156                col: &'static str,
2157                pool: &#root::sql::Pool,
2158            ) -> ::core::result::Result<
2159                ::std::vec::Vec<U>,
2160                #root::sql::ExecError,
2161            >
2162            where
2163                U: #root::sql::FlatScalar,
2164            {
2165                self.#method_ident()
2166                    .values_list_flat(col)
2167                    .fetch::<U>(pool)
2168                    .await
2169            }
2170        }
2171    });
2172    quote! {
2173        impl #struct_name {
2174            #( #methods )*
2175        }
2176    }
2177}
2178
2179struct ColumnEntry {
2180    /// The struct field ident, used both for the inherent const name on
2181    /// the model and for the inner column type's name.
2182    ident: syn::Ident,
2183    /// The struct's field type, used as `Column::Value`.
2184    value_ty: Type,
2185    /// Rust-side field name (e.g. `"id"`).
2186    name: String,
2187    /// SQL-side column name (e.g. `"user_id"`).
2188    column: String,
2189    /// `#root::core::FieldType::I64` etc.
2190    field_type_tokens: TokenStream2,
2191    /// A bare or optional `Uuid` field; MySQL decodes it from text.
2192    uuid_column: bool,
2193}
2194
2195struct CollectedFields {
2196    field_schemas: Vec<TokenStream2>,
2197    from_row_inits: Vec<TokenStream2>,
2198    /// Aliased counterparts of `from_row_inits` — read columns via
2199    /// `format!("{prefix}__{col}")` aliases so a Model can be
2200    /// decoded from a JOINed row's projected target columns.
2201    from_aliased_row_inits: Vec<TokenStream2>,
2202    /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2203    from_row_inits_my: Vec<TokenStream2>,
2204    from_aliased_row_inits_my: Vec<TokenStream2>,
2205    /// Static column-name list — used by the simple insert path
2206    /// (no `Auto<T>` fields). Aligned with `insert_values`.
2207    insert_columns: Vec<TokenStream2>,
2208    /// Static `Into<SqlValue>` expressions, one per field. Aligned
2209    /// with `insert_columns`. Used by the simple insert path only.
2210    insert_values: Vec<TokenStream2>,
2211    /// Per-field push expressions for the dynamic (Auto-aware)
2212    /// insert path. Each statement either unconditionally pushes
2213    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2214    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2215    insert_pushes: Vec<TokenStream2>,
2216    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2217    /// when `has_auto == false`.
2218    returning_cols: Vec<TokenStream2>,
2219    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2220    /// field. Run after `insert_returning` to populate the model.
2221    auto_assigns: Vec<TokenStream2>,
2222    /// `(ident, column_literal, is_uuid)` for every Auto field. Used by
2223    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2224    /// instead of `self`.
2225    auto_field_idents: Vec<(syn::Ident, String, bool)>,
2226    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2227    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2228    /// back into the struct after insert (MySQL has no RETURNING → the
2229    /// field stays at its placeholder until the next read).
2230    generated_field_idents: Vec<(syn::Ident, String)>,
2231    /// First `Auto<T>` field the database fills, with its inner `T`: the
2232    /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2233    first_db_auto: Option<(syn::Ident, Type)>,
2234    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2235    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2236    /// `columns`).
2237    bulk_pushes_no_auto: Vec<TokenStream2>,
2238    /// Bulk-insert per-row pushes for **all fields including Auto**.
2239    /// Used by the all-Auto-Set bulk path (Auto col included with the
2240    /// caller-supplied value).
2241    bulk_pushes_all: Vec<TokenStream2>,
2242    /// Column-name literals for non-Auto fields only (paired with
2243    /// `bulk_pushes_no_auto`).
2244    bulk_columns_no_auto: Vec<TokenStream2>,
2245    /// Column-name literals for every field including Auto (paired
2246    /// with `bulk_pushes_all`).
2247    bulk_columns_all: Vec<TokenStream2>,
2248    /// Fills each `default_uuid_v7` field of `_row` in place, so a
2249    /// mutable bulk insert hands the PKs back (#1934).
2250    bulk_fill_in_place: Vec<TokenStream2>,
2251    /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2252    rust_filled_idents: Vec<syn::Ident>,
2253    /// `auto_now` fields: `upsert` stamps them before binding.
2254    auto_now_idents: Vec<syn::Ident>,
2255    /// `default_uuid_v7` fields: an `Unset` one gets a fresh id, so the
2256    /// upsert marks it `generated` for the MySQL key guard.
2257    uuid_v7_fields: Vec<(syn::Ident, String)>,
2258    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2259    /// + the loop that asserts every row matches. One pair per Auto
2260    /// field. Empty when `has_auto == false`.
2261    bulk_auto_uniformity: Vec<TokenStream2>,
2262    /// Identifier of the first Auto field, used as the witness for
2263    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2264    first_auto_ident: Option<syn::Ident>,
2265    /// `true` if any field on the struct is `Auto<T>`.
2266    has_auto: bool,
2267    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2268    /// Gates `save()` codegen — only Auto PKs let us infer
2269    /// insert-vs-update from the in-memory value.
2270    pk_is_auto: bool,
2271    /// `Assignment` constructors for every non-PK column. Drives the
2272    /// UPDATE branch of `save()`.
2273    update_assignments: Vec<TokenStream2>,
2274    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2275    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2276    upsert_update_columns: Vec<TokenStream2>,
2277    primary_key: Option<(syn::Ident, String)>,
2278    column_entries: Vec<ColumnEntry>,
2279    /// Rust-side field names, in declaration order. Used to validate
2280    /// container attributes like `display = "…"`.
2281    field_names: Vec<String>,
2282    /// FK fields on this child model. Drives the reverse-relation
2283    /// helper emit — for each FK, the macro adds an inherent
2284    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2285    /// method on the parent type.
2286    fk_relations: Vec<FkRelation>,
2287    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2288    /// the model has one. Drives emission of the `soft_delete_on` /
2289    /// `restore_on` inherent methods. At most one such column per
2290    /// model is allowed; collect_fields rejects duplicates.
2291    soft_delete_column: Option<String>,
2292    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2293    /// companion to `soft_delete_column` for emitting predicates
2294    /// that need to read the field off `&self` (e.g. `trashed()`).
2295    soft_delete_field_ident: Option<syn::Ident>,
2296}
2297
2298#[derive(Clone)]
2299struct FkRelation {
2300    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2301    /// helper is emitted as `impl <ParentType> { … }`.
2302    parent_type: Type,
2303    /// Rust field ident of the FK. Its name is the key for select_related,
2304    /// prefetch and filters; the SQL column may differ (#1936).
2305    field_ident: syn::Ident,
2306    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2307    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2308    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2309    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2310    pk_kind: DetectedKind,
2311    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2312    /// FK column). Drives the `Some(...)` wrapping in load_related
2313    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2314    /// the codegen matches the field's declared shape.
2315    nullable: bool,
2316    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2317    /// override. When set, the reverse helper picks this name instead
2318    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2319    /// #816 (issue's "Related" note re: per-FK override).
2320    related_name: Option<String>,
2321}
2322
2323fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2324    let root = rustango_root();
2325    let cap = named.named.len();
2326    let mut out = CollectedFields {
2327        field_schemas: Vec::with_capacity(cap),
2328        from_row_inits: Vec::with_capacity(cap),
2329        from_aliased_row_inits: Vec::with_capacity(cap),
2330        from_row_inits_my: Vec::with_capacity(cap),
2331        from_aliased_row_inits_my: Vec::with_capacity(cap),
2332        insert_columns: Vec::with_capacity(cap),
2333        insert_values: Vec::with_capacity(cap),
2334        insert_pushes: Vec::with_capacity(cap),
2335        returning_cols: Vec::new(),
2336        auto_assigns: Vec::new(),
2337        auto_field_idents: Vec::new(),
2338        generated_field_idents: Vec::new(),
2339        first_db_auto: None,
2340        bulk_pushes_no_auto: Vec::with_capacity(cap),
2341        bulk_pushes_all: Vec::with_capacity(cap),
2342        bulk_columns_no_auto: Vec::with_capacity(cap),
2343        bulk_columns_all: Vec::with_capacity(cap),
2344        bulk_fill_in_place: Vec::new(),
2345        rust_filled_idents: Vec::new(),
2346        auto_now_idents: Vec::new(),
2347        uuid_v7_fields: Vec::new(),
2348        bulk_auto_uniformity: Vec::new(),
2349        first_auto_ident: None,
2350        has_auto: false,
2351        pk_is_auto: false,
2352        update_assignments: Vec::with_capacity(cap),
2353        upsert_update_columns: Vec::with_capacity(cap),
2354        primary_key: None,
2355        column_entries: Vec::with_capacity(cap),
2356        field_names: Vec::with_capacity(cap),
2357        fk_relations: Vec::new(),
2358        soft_delete_column: None,
2359        soft_delete_field_ident: None,
2360    };
2361
2362    for field in &named.named {
2363        let info = process_field(field, table)?;
2364        out.field_names.push(ident_name(info.ident));
2365        out.field_schemas.push(info.schema);
2366        out.from_row_inits.push(info.from_row_init);
2367        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2368        out.from_row_inits_my.push(info.from_row_init_my);
2369        out.from_aliased_row_inits_my
2370            .push(info.from_aliased_row_init_my);
2371        if let Some(parent_ty) = info.fk_inner.clone() {
2372            out.fk_relations.push(FkRelation {
2373                parent_type: parent_ty,
2374                field_ident: info.ident.clone(),
2375                pk_kind: info.fk_pk_kind,
2376                nullable: info.nullable,
2377                related_name: info.related_name.clone(),
2378            });
2379        }
2380        if info.soft_delete {
2381            if out.soft_delete_column.is_some() {
2382                return Err(syn::Error::new_spanned(
2383                    field,
2384                    "only one field may be marked `#[rustango(soft_delete)]`",
2385                ));
2386            }
2387            out.soft_delete_column = Some(info.column.clone());
2388            out.soft_delete_field_ident = Some(info.ident.clone());
2389        }
2390        let column = info.column.as_str();
2391        let ident = info.ident;
2392        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2393        // skip every write path — Postgres recomputes the value
2394        // from EXPR. Push only the column-entry record (so typed
2395        // column constants still exist for filtering / projection)
2396        // and the schema literal (already pushed above) and move
2397        // on. No insert_columns/values, no insert_pushes, no
2398        // bulk_*, no update_assignments, no upsert_update_columns,
2399        // no returning_cols.
2400        if info.generated_as.is_some() {
2401            out.column_entries.push(ColumnEntry {
2402                ident: ident.clone(),
2403                value_ty: info.value_ty.clone(),
2404                name: ident_name(ident),
2405                column: info.column.clone(),
2406                field_type_tokens: info.field_type_tokens,
2407                uuid_column: info.uuid_column,
2408            });
2409            // #1028 — refresh the DB-computed value after insert on
2410            // RETURNING-capable backends. The column joins `returning_cols`
2411            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2412            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2413            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2414            // it keeps the placeholder until the next read.
2415            out.returning_cols.push(quote!(#column));
2416            out.generated_field_idents
2417                .push((ident.clone(), info.column.clone()));
2418            continue;
2419        }
2420        out.insert_columns.push(quote!(#column));
2421        out.insert_values.push(quote! {
2422            ::core::convert::Into::<#root::core::SqlValue>::into(
2423                ::core::clone::Clone::clone(&self.#ident)
2424            )
2425        });
2426        if info.auto {
2427            out.has_auto = true;
2428            if out.first_auto_ident.is_none() {
2429                out.first_auto_ident = Some(ident.clone());
2430            }
2431            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2432            // before binding, so the value is already in
2433            // `self.#ident` after the insert_push — RETURNING is
2434            // redundant. Skip adding this column to returning_cols /
2435            // auto_assigns / auto_field_idents to avoid (a) an
2436            // unnecessary RETURNING column on every dialect, and (b)
2437            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2438            // integer PK.
2439            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2440            // for the same reason: they are filled Rust-side below and
2441            // bound, so the value is already in `self` and RETURNING
2442            // would be a redundant column on every dialect.
2443            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2444                if out.first_db_auto.is_none() {
2445                    out.first_db_auto = auto_inner_type(info.value_ty)
2446                        .cloned()
2447                        .map(|ty| (ident.clone(), ty));
2448                }
2449                out.returning_cols.push(quote!(#column));
2450                out.auto_field_idents.push((
2451                    ident.clone(),
2452                    info.column.clone(),
2453                    info.fk_pk_kind == DetectedKind::Uuid,
2454                ));
2455                out.auto_assigns.push(quote! {
2456                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2457                });
2458            }
2459            if info.default_uuid_v7 {
2460                // Rust-side UUIDv7 generation (issue #823, Eloquent
2461                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2462                // and bind; Auto::Set → bind the user's value. The
2463                // column is ALWAYS present in the INSERT statement —
2464                // no RETURNING / no DB DEFAULT needed.
2465                out.insert_pushes.push(quote! {
2466                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2467                        self.#ident = #root::sql::Auto::Set(
2468                            #root::__uuid::Uuid::now_v7(),
2469                        );
2470                    }
2471                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2472                        _columns.push(#column);
2473                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2474                            ::core::clone::Clone::clone(_v)
2475                        ));
2476                    }
2477                });
2478            } else if info.auto_now_add || info.auto_now {
2479                // Rust-side write timestamp (#1464), mirroring
2480                // `default_uuid_v7` above. `auto_now` has always bound
2481                // `Utc::now()` on UPDATE but not on INSERT, so the
2482                // first write of an `updated_at` took the DB default
2483                // while every later one took the clock — the same
2484                // column, filled two different ways.
2485                //
2486                // The default is the problem. `CREATE TABLE IF NOT
2487                // EXISTS` leaves an upgraded table's
2488                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2489                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2490                // statement that changes it — so every insert relying
2491                // on that default wrote the legacy shape forever, while
2492                // the migrate sweep converted the rows around it. The
2493                // column went permanently mixed and `ORDER BY` inverted
2494                // the admin audit log (#1616 rework review,
2495                // correctness-001 / dialects-005 / security-002).
2496                //
2497                // Binding it here makes the stale default unreachable
2498                // rather than trying to migrate it: the timestamp comes
2499                // from the application, not the database.
2500                out.insert_pushes.push(quote! {
2501                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2502                        self.#ident = #root::sql::Auto::Set(
2503                            #root::__chrono::Utc::now(),
2504                        );
2505                    }
2506                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2507                        _columns.push(#column);
2508                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2509                            ::core::clone::Clone::clone(_v)
2510                        ));
2511                    }
2512                });
2513            } else {
2514                out.insert_pushes.push(quote! {
2515                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2516                        _columns.push(#column);
2517                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2518                            ::core::clone::Clone::clone(_v)
2519                        ));
2520                    }
2521                });
2522            }
2523            // …except the timestamp columns, which must appear in BOTH
2524            // paths (#1464). The path is chosen by the *first* Auto
2525            // field — in practice the PK — so a bulk insert of rows
2526            // with an unset PK took the no-auto branch and dropped the
2527            // timestamp column, falling back to the DB default that
2528            // this change exists to stop depending on.
2529            //
2530            // `rows` is borrowed immutably here, so this fills per row
2531            // at push time rather than writing back into the struct. An
2532            // explicitly-Set value is still honoured; only `Unset`
2533            // takes the clock.
2534            // `default_uuid_v7` likewise: its column has no DB default (#1934).
2535            if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2536                out.rust_filled_idents.push(ident.clone());
2537            }
2538            if info.auto_now {
2539                out.auto_now_idents.push(ident.clone());
2540            }
2541            if info.default_uuid_v7 {
2542                out.uuid_v7_fields
2543                    .push((ident.clone(), info.column.clone()));
2544            }
2545            let rust_fill = if info.default_uuid_v7 {
2546                Some(quote!(#root::__uuid::Uuid::now_v7()))
2547            } else if info.auto_now_add || info.auto_now {
2548                Some(quote!(#root::__chrono::Utc::now()))
2549            } else {
2550                None
2551            };
2552            // Bulk: Auto fields appear in the all-Set path. An unset filled
2553            // field takes its fill there too, not NULL (#1950).
2554            out.bulk_columns_all.push(quote!(#column));
2555            if let Some(fill) = &rust_fill {
2556                let push = quote! {
2557                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2558                        match &_row.#ident {
2559                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2560                            #root::sql::Auto::Unset => #fill,
2561                        }
2562                    ));
2563                };
2564                out.bulk_columns_no_auto.push(quote!(#column));
2565                out.bulk_pushes_no_auto.push(push.clone());
2566                out.bulk_pushes_all.push(push);
2567            } else {
2568                out.bulk_pushes_all.push(quote! {
2569                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2570                        ::core::clone::Clone::clone(&_row.#ident)
2571                    ));
2572                });
2573            }
2574            if info.default_uuid_v7 {
2575                out.bulk_fill_in_place.push(quote! {
2576                    if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2577                        _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2578                    }
2579                });
2580            }
2581            // Uniformity check: every row's Auto state must match the
2582            // first row's. Mixed Set/Unset within one bulk_insert is
2583            // rejected here so the column list stays consistent.
2584            // A filled field may be unset on any row, so it is exempt.
2585            let ident_clone = ident.clone();
2586            if rust_fill.is_none() {
2587                out.bulk_auto_uniformity.push(quote! {
2588                    for _r in rows.iter().skip(1) {
2589                        if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2590                            return ::core::result::Result::Err(
2591                                #root::sql::ExecError::Sql(
2592                                    #root::sql::SqlError::BulkAutoMixed
2593                                )
2594                            );
2595                        }
2596                    }
2597                });
2598            }
2599        } else {
2600            out.insert_pushes.push(quote! {
2601                _columns.push(#column);
2602                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2603                    ::core::clone::Clone::clone(&self.#ident)
2604                ));
2605            });
2606            // Bulk: non-Auto fields appear in BOTH paths.
2607            out.bulk_columns_no_auto.push(quote!(#column));
2608            out.bulk_columns_all.push(quote!(#column));
2609            let push_expr = quote! {
2610                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2611                    ::core::clone::Clone::clone(&_row.#ident)
2612                ));
2613            };
2614            out.bulk_pushes_no_auto.push(push_expr.clone());
2615            out.bulk_pushes_all.push(push_expr);
2616        }
2617        if info.primary_key {
2618            if out.primary_key.is_some() {
2619                return Err(syn::Error::new_spanned(
2620                    field,
2621                    "only one field may be marked `#[rustango(primary_key)]`",
2622                ));
2623            }
2624            out.primary_key = Some((ident.clone(), info.column.clone()));
2625            if info.auto {
2626                out.pk_is_auto = true;
2627            }
2628        } else if info.auto_now_add {
2629            // Immutable post-insert: skip from UPDATE entirely.
2630        } else if info.auto_now {
2631            // `auto_now` columns: bind `chrono::Utc::now()` on every
2632            // UPDATE, and write it back so `self` matches the row.
2633            out.update_assignments.push(quote! {
2634                #root::core::Assignment::new(
2635                    #column,
2636                    {
2637                        let _now = #root::__chrono::Utc::now();
2638                        self.#ident = #root::sql::Auto::Set(_now);
2639                        ::core::convert::Into::<#root::core::SqlValue>::into(_now)
2640                    },
2641                )
2642            });
2643            out.upsert_update_columns.push(quote!(#column));
2644        } else {
2645            out.update_assignments.push(quote! {
2646                #root::core::Assignment::new(
2647                    #column,
2648                    ::core::convert::Into::<#root::core::SqlValue>::into(
2649                        ::core::clone::Clone::clone(&self.#ident)
2650                    ),
2651                )
2652            });
2653            out.upsert_update_columns.push(quote!(#column));
2654        }
2655        out.column_entries.push(ColumnEntry {
2656            ident: ident.clone(),
2657            value_ty: info.value_ty.clone(),
2658            name: ident_name(ident),
2659            column: info.column.clone(),
2660            field_type_tokens: info.field_type_tokens,
2661            uuid_column: info.uuid_column,
2662        });
2663    }
2664    Ok(out)
2665}
2666
2667/// A derived index name within PG's 63 bytes (MySQL takes 64); a longer one
2668/// keeps a prefix plus a hash of the whole, so two long names stay distinct.
2669fn capped_index_name(name: String) -> String {
2670    const MAX: usize = 63;
2671    if name.len() <= MAX {
2672        return name;
2673    }
2674    // FNV-1a: stable across builds, unlike `DefaultHasher`.
2675    let hash = name.bytes().fold(0x811c_9dc5_u32, |h, b| {
2676        (h ^ u32::from(b)).wrapping_mul(0x0100_0193)
2677    });
2678    let mut cut = MAX - 9;
2679    while !name.is_char_boundary(cut) {
2680        cut -= 1;
2681    }
2682    format!("{}_{hash:08x}", &name[..cut])
2683}
2684
2685fn model_impl_tokens(
2686    struct_name: &syn::Ident,
2687    model_name: &str,
2688    table: &str,
2689    display: Option<&str>,
2690    app_label: Option<&str>,
2691    admin: Option<&AdminAttrs>,
2692    default_order: &[(String, bool, proc_macro2::Span)],
2693    field_schemas: &[TokenStream2],
2694    soft_delete_column: Option<&str>,
2695    permissions: bool,
2696    audit_track: Option<&[String]>,
2697    m2m_relations: &[M2MAttr],
2698    indexes: &[IndexAttr],
2699    checks: &[CheckAttr],
2700    excludes: &[ExcludeAttr],
2701    composite_fks: &[CompositeFkAttr],
2702    generic_fks: &[GenericFkAttr],
2703    scope: Option<&str>,
2704    is_view: bool,
2705    verbose_name: Option<&str>,
2706    verbose_name_plural: Option<&str>,
2707    managed: bool,
2708    base_manager_name: Option<&str>,
2709    order_with_respect_to: Option<&str>,
2710    proxy: bool,
2711    required_db_features: &[String],
2712    required_db_vendor: Option<&str>,
2713    default_related_name: Option<&str>,
2714    db_table_comment: Option<&str>,
2715    get_latest_by: Option<(&str, bool)>,
2716    extra_permissions: &[(String, String)],
2717    default_permissions: &[String],
2718    global_scopes: &[GlobalScopeAttr],
2719    reverse_has_relations: &[ReverseHasAttr],
2720    generic_has_relations: &[GenericHasAttr],
2721) -> TokenStream2 {
2722    let root = rustango_root();
2723    let display_tokens = if let Some(name) = display {
2724        quote!(::core::option::Option::Some(#name))
2725    } else {
2726        quote!(::core::option::Option::None)
2727    };
2728    let app_label_tokens = if let Some(name) = app_label {
2729        quote!(::core::option::Option::Some(#name))
2730    } else {
2731        quote!(::core::option::Option::None)
2732    };
2733    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2734        quote!(::core::option::Option::Some(#col))
2735    } else {
2736        quote!(::core::option::Option::None)
2737    };
2738    let audit_track_tokens = match audit_track {
2739        None => quote!(::core::option::Option::None),
2740        Some(names) => {
2741            let lits = names.iter().map(|n| n.as_str());
2742            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2743        }
2744    };
2745    let admin_tokens = admin_config_tokens(admin);
2746    // Default `tenant` so single-tenant projects (no `scope` attr
2747    // anywhere) keep the v0.24.x behavior. Container-attr parser
2748    // already validated the value is "registry" or "tenant".
2749    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2750        Some("registry") => quote!(#root::core::ModelScope::Registry),
2751        _ => quote!(#root::core::ModelScope::Tenant),
2752    };
2753    let verbose_name_tokens = optional_str(verbose_name);
2754    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2755    let base_manager_name_tokens = optional_str(base_manager_name);
2756    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2757    let required_db_features_lits: Vec<&str> =
2758        required_db_features.iter().map(String::as_str).collect();
2759    let required_db_vendor_tokens = optional_str(required_db_vendor);
2760    let default_related_name_tokens = optional_str(default_related_name);
2761    let db_table_comment_tokens = optional_str(db_table_comment);
2762    let get_latest_by_tokens = match get_latest_by {
2763        Some((col, desc)) => {
2764            quote!(::core::option::Option::Some((#col, #desc)))
2765        }
2766        None => quote!(::core::option::Option::None),
2767    };
2768    let extra_permission_tokens: Vec<_> = extra_permissions
2769        .iter()
2770        .map(|(c, l)| quote!((#c, #l)))
2771        .collect();
2772    let default_permission_tokens: Vec<_> = default_permissions
2773        .iter()
2774        .map(|action| quote!(#action))
2775        .collect();
2776    let indexes_tokens = indexes.iter().map(|idx| {
2777        // When no explicit `name = "..."` was given, derive a stable,
2778        // collision-free name from the table + columns
2779        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2780        // would clash the moment a model declares two unnamed indexes.
2781        let derived_name = idx
2782            .name
2783            .clone()
2784            .unwrap_or_else(|| capped_index_name(format!("{table}_{}_idx", idx.columns.join("_"))));
2785        let name = derived_name.as_str();
2786        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2787        let unique = idx.unique;
2788        // Map the parsed method string onto the IndexMethod enum
2789        // variant — kept at the codegen layer so the IR doesn't
2790        // carry the string form.
2791        let method_variant = match idx.method.as_str() {
2792            "gin" => quote!(#root::core::IndexMethod::Gin),
2793            "gist" => quote!(#root::core::IndexMethod::Gist),
2794            "brin" => quote!(#root::core::IndexMethod::Brin),
2795            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2796            "hash" => quote!(#root::core::IndexMethod::Hash),
2797            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2798            _ => quote!(#root::core::IndexMethod::BTree),
2799        };
2800        let where_clause = match &idx.where_clause {
2801            Some(s) => quote!(::core::option::Option::Some(#s)),
2802            None => quote!(::core::option::Option::None),
2803        };
2804        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2805        quote! {{
2806            let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2807            i.unique = #unique;
2808            i.method = #method_variant;
2809            i.where_clause = #where_clause;
2810            i.include = &[ #(#include_lits),* ];
2811            i
2812        }}
2813    });
2814    let checks_tokens = checks.iter().map(|c| {
2815        let name = c.name.as_str();
2816        let expr = c.expr.as_str();
2817        quote!(#root::core::CheckConstraint::new(#name, #expr))
2818    });
2819    let excludes_tokens = excludes.iter().map(|e| {
2820        let name = e.name.as_str();
2821        let using = e.using.as_str();
2822        let element_tokens = e.elements.iter().map(|(col, op)| {
2823            let col_s = col.as_str();
2824            let op_s = op.as_str();
2825            quote!((#col_s, #op_s))
2826        });
2827        let where_tokens = match e.where_clause.as_deref() {
2828            Some(w) => quote!(::core::option::Option::Some(#w)),
2829            None => quote!(::core::option::Option::None),
2830        };
2831        quote! {{
2832            let mut e = #root::core::ExclusionConstraint::new(
2833                #name,
2834                #using,
2835                &[ #(#element_tokens),* ],
2836            );
2837            e.where_clause = #where_tokens;
2838            e
2839        }}
2840    });
2841    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2842        let name = rel.name.as_str();
2843        let to = rel.to.as_str();
2844        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2845        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2846        quote! {
2847            #root::core::CompositeFkRelation::new(
2848                #name,
2849                #to,
2850                &[ #(#from_cols),* ],
2851                &[ #(#on_cols),* ],
2852            )
2853        }
2854    });
2855    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2856        let name = rel.name.as_str();
2857        let ct_col = rel.ct_column.as_str();
2858        let pk_col = rel.pk_column.as_str();
2859        quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2860    });
2861    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2862    // no `#[rustango(default_order = "...")]` attribute was supplied.
2863    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2864        let col_lit = col.as_str();
2865        quote! { (#col_lit, #desc) }
2866    });
2867
2868    // Issue #820 — `global_scopes` slice literal. Empty when no
2869    // `#[rustango(global_scope(...))]` attribute was supplied. The
2870    // `apply` path is re-emitted verbatim so it resolves in the
2871    // consumer's scope; the name is stored as a string literal.
2872    let global_scope_tokens = global_scopes.iter().map(|s| {
2873        let name = s.name.as_str();
2874        // The slice sits in a nested `const`, where `Self` does not resolve.
2875        let mut apply = s.apply.clone();
2876        if let Some(first) = apply.segments.first_mut() {
2877            if first.ident == "Self" {
2878                first.ident = struct_name.clone();
2879            }
2880        }
2881        quote!(#root::core::GlobalScope::new(#name, #apply))
2882    });
2883
2884    let m2m_tokens = m2m_relations.iter().map(|rel| {
2885        let name = rel.name.as_str();
2886        let to = rel.to.as_str();
2887        let through = rel.through.as_str();
2888        let src = rel.src.as_str();
2889        let dst = rel.dst.as_str();
2890        let auto_create = rel.auto_create;
2891        quote! {{
2892            let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2893            m.auto_create = #auto_create;
2894            m
2895        }}
2896    });
2897    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2898    // when the model declares `#[rustango(reverse_has(...))]`. Each
2899    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2900    // a const expression. Models without reverse_has fall through to
2901    // the trait's empty default — no override emitted.
2902    let reverse_relations_override = if reverse_has_relations.is_empty() {
2903        quote!()
2904    } else {
2905        let entries = reverse_has_relations.iter().map(|rel| {
2906            let name = rel.name.as_str();
2907            let child = &rel.child;
2908            let child_fk_column = rel.child_fk_column.as_str();
2909            let self_pk_column = rel.self_pk_column();
2910            quote! {
2911                #root::core::ReverseRelation::new(
2912                    #name,
2913                    <#child as #root::core::Model>::SCHEMA,
2914                    #child_fk_column,
2915                    #self_pk_column,
2916                )
2917            }
2918        });
2919        quote! {
2920            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2921                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2922                RELS
2923            }
2924        }
2925    };
2926    // Issue #830 — `Model::generic_reverse_relations()` override for
2927    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2928    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2929        quote!()
2930    } else {
2931        let entries = generic_has_relations.iter().map(|rel| {
2932            let name = rel.name.as_str();
2933            let child = &rel.child;
2934            let ct_column = rel.ct_column.as_str();
2935            let pk_column = rel.pk_column.as_str();
2936            let self_pk_column = rel.self_pk_column();
2937            quote! {
2938                #root::core::GenericReverseRelation::new(
2939                    #name,
2940                    <#child as #root::core::Model>::SCHEMA,
2941                    #ct_column,
2942                    #pk_column,
2943                    #self_pk_column,
2944                )
2945            }
2946        });
2947        quote! {
2948            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2949                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2950                RELS
2951            }
2952        }
2953    };
2954    let fields_slice = const_slice(
2955        &quote!(#root::core::FieldSchema),
2956        field_schemas.iter().cloned(),
2957    );
2958    let m2m_slice = const_slice(&quote!(#root::core::M2MRelation), m2m_tokens);
2959    let indexes_slice = const_slice(&quote!(#root::core::IndexSchema), indexes_tokens);
2960    let checks_slice = const_slice(&quote!(#root::core::CheckConstraint), checks_tokens);
2961    let excludes_slice = const_slice(&quote!(#root::core::ExclusionConstraint), excludes_tokens);
2962    let composite_fk_slice = const_slice(
2963        &quote!(#root::core::CompositeFkRelation),
2964        composite_fk_tokens,
2965    );
2966    let generic_fk_slice = const_slice(&quote!(#root::core::GenericRelation), generic_fk_tokens);
2967    let audited_update_override = if audit_track.is_some() {
2968        quote! {
2969            fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2970                ::core::option::Option::Some(Self::__rustango_update_audited)
2971            }
2972            fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2973                ::core::option::Option::Some(Self::__rustango_delete_audited)
2974            }
2975            fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2976                ::core::option::Option::Some(Self::__rustango_create_audited)
2977            }
2978            fn __rustango_audited_update_record(
2979            ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
2980                ::core::option::Option::Some(Self::__rustango_update_record_audited)
2981            }
2982            fn __rustango_audited_cascade() -> ::core::option::Option<#root::audit::AuditedCascade> {
2983                ::core::option::Option::Some(Self::__rustango_cascade_audited)
2984            }
2985        }
2986    } else {
2987        quote!()
2988    };
2989    let global_scope_slice = const_slice(&quote!(#root::core::GlobalScope), global_scope_tokens);
2990    quote! {
2991        impl #root::core::Model for #struct_name {
2992            const SCHEMA: &'static #root::core::ModelSchema = &{
2993                #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2994                    name: #model_name,
2995                    table: #table,
2996                    fields: #fields_slice,
2997                    display: #display_tokens,
2998                    app_label: #app_label_tokens,
2999                    admin: #admin_tokens,
3000                    soft_delete_column: #soft_delete_tokens,
3001                    permissions: #permissions,
3002                    audit_track: #audit_track_tokens,
3003                    m2m: #m2m_slice,
3004                    indexes: #indexes_slice,
3005                    check_constraints: #checks_slice,
3006                    exclusion_constraints: #excludes_slice,
3007                    composite_relations: #composite_fk_slice,
3008                    generic_relations: #generic_fk_slice,
3009                    scope: #scope_tokens,
3010                    default_order: &[ #(#default_order_tokens),* ],
3011                    is_view: #is_view,
3012                    verbose_name: #verbose_name_tokens,
3013                    verbose_name_plural: #verbose_name_plural_tokens,
3014                    managed: #managed,
3015                    base_manager_name: #base_manager_name_tokens,
3016                    order_with_respect_to: #order_with_respect_to_tokens,
3017                    proxy: #proxy,
3018                    required_db_features: &[ #(#required_db_features_lits),* ],
3019                    required_db_vendor: #required_db_vendor_tokens,
3020                    default_related_name: #default_related_name_tokens,
3021                    db_table_comment: #db_table_comment_tokens,
3022                    get_latest_by: #get_latest_by_tokens,
3023                    extra_permissions: &[ #(#extra_permission_tokens),* ],
3024                    default_permissions: &[ #(#default_permission_tokens),* ],
3025                    global_scopes: #global_scope_slice,
3026                })
3027            };
3028
3029            #reverse_relations_override
3030            #generic_reverse_relations_override
3031            #audited_update_override
3032        }
3033    }
3034}
3035
3036/// Emit the `admin: Option<&'static AdminConfig>` field for the model
3037/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
3038/// otherwise a static reference to a populated `AdminConfig`.
3039fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
3040    let root = rustango_root();
3041    let Some(admin) = admin else {
3042        return quote!(::core::option::Option::None);
3043    };
3044
3045    let list_display = admin
3046        .list_display
3047        .as_ref()
3048        .map(|(v, _)| v.as_slice())
3049        .unwrap_or(&[]);
3050    let list_display_lits = list_display.iter().map(|s| s.as_str());
3051
3052    let search_fields = admin
3053        .search_fields
3054        .as_ref()
3055        .map(|(v, _)| v.as_slice())
3056        .unwrap_or(&[]);
3057    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
3058
3059    let readonly_fields = admin
3060        .readonly_fields
3061        .as_ref()
3062        .map(|(v, _)| v.as_slice())
3063        .unwrap_or(&[]);
3064    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
3065
3066    let list_filter = admin
3067        .list_filter
3068        .as_ref()
3069        .map(|(v, _)| v.as_slice())
3070        .unwrap_or(&[]);
3071    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
3072
3073    let actions = admin
3074        .actions
3075        .as_ref()
3076        .map(|(v, _)| v.as_slice())
3077        .unwrap_or(&[]);
3078    let actions_lits = actions.iter().map(|s| s.as_str());
3079
3080    let fieldsets = admin
3081        .fieldsets
3082        .as_ref()
3083        .map(|(v, _)| v.as_slice())
3084        .unwrap_or(&[]);
3085    let fieldset_tokens = const_slice(
3086        &quote!(#root::core::Fieldset),
3087        fieldsets.iter().map(|(title, fields)| {
3088            let title = title.as_str();
3089            let field_lits = fields.iter().map(String::as_str);
3090            quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
3091        }),
3092    );
3093
3094    let list_display_links = admin
3095        .list_display_links
3096        .as_ref()
3097        .map(|(v, _)| v.as_slice())
3098        .unwrap_or(&[]);
3099    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
3100
3101    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
3102    let actions_on_top = admin.actions_on_top.unwrap_or(true);
3103    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
3104    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
3105
3106    let prepopulated = admin
3107        .prepopulated_fields
3108        .as_ref()
3109        .map(|(v, _)| v.as_slice())
3110        .unwrap_or(&[]);
3111    let prepopulated_tokens = const_slice(
3112        &quote!(#root::core::PrepopulatedField),
3113        prepopulated.iter().map(|(target, sources)| {
3114            let target = target.as_str();
3115            let source_lits = sources.iter().map(String::as_str);
3116            quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
3117        }),
3118    );
3119
3120    let raw_id_fields = admin
3121        .raw_id_fields
3122        .as_ref()
3123        .map(|(v, _)| v.as_slice())
3124        .unwrap_or(&[]);
3125    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
3126
3127    let autocomplete_fields = admin
3128        .autocomplete_fields
3129        .as_ref()
3130        .map(|(v, _)| v.as_slice())
3131        .unwrap_or(&[]);
3132    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
3133
3134    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
3135    let list_select_related_tokens = match admin.list_select_related.as_deref() {
3136        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
3137        Some("none") => quote!(#root::core::ListSelectRelated::None),
3138        Some(raw) => {
3139            let names: Vec<&str> = raw
3140                .split(',')
3141                .map(str::trim)
3142                .filter(|s| !s.is_empty())
3143                .collect();
3144            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
3145        }
3146    };
3147
3148    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
3149    // a Vec<(String, String)>. Empty / unset → no overrides.
3150    let formfield_pairs: Vec<(&str, &str)> = admin
3151        .formfield_overrides
3152        .as_ref()
3153        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
3154        .unwrap_or_default();
3155    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
3156        let field = *field;
3157        let widget = *widget;
3158        quote!((#field, #widget))
3159    });
3160
3161    let list_per_page = admin.list_per_page.unwrap_or(0);
3162
3163    let ordering_pairs = admin
3164        .ordering
3165        .as_ref()
3166        .map(|(v, _)| v.as_slice())
3167        .unwrap_or(&[]);
3168    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3169        let name = name.as_str();
3170        let desc = *desc;
3171        quote!((#name, #desc))
3172    });
3173
3174    quote! {
3175        ::core::option::Option::Some({
3176            const ADMIN: &#root::core::AdminConfig = &{
3177                #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3178                    list_display: &[ #( #list_display_lits ),* ],
3179                    search_fields: &[ #( #search_fields_lits ),* ],
3180                    list_per_page: #list_per_page,
3181                    ordering: &[ #( #ordering_tokens ),* ],
3182                    readonly_fields: &[ #( #readonly_fields_lits ),* ],
3183                    list_filter: &[ #( #list_filter_lits ),* ],
3184                    actions: &[ #( #actions_lits ),* ],
3185                    fieldsets: #fieldset_tokens,
3186                    list_display_links: &[ #( #list_display_links_lits ),* ],
3187                    search_help_text: #search_help_text,
3188                    actions_on_top: #actions_on_top,
3189                    actions_on_bottom: #actions_on_bottom,
3190                    date_hierarchy: #date_hierarchy,
3191                    prepopulated_fields: #prepopulated_tokens,
3192                    raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3193                    autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3194                    list_select_related: #list_select_related_tokens,
3195                    formfield_overrides: &[ #( #formfield_tokens ),* ],
3196                })
3197            };
3198            ADMIN
3199        })
3200    }
3201}
3202
3203fn inherent_impl_tokens(
3204    struct_name: &syn::Ident,
3205    fields: &CollectedFields,
3206    primary_key: Option<&(syn::Ident, String)>,
3207    column_consts: &TokenStream2,
3208    audited_fields: Option<&[&ColumnEntry]>,
3209    upsert_unique: Option<&[String]>,
3210    manager_fns: &[syn::Ident],
3211) -> TokenStream2 {
3212    let root = rustango_root();
3213    // Audit-emit fragments threaded into write paths. Non-empty only
3214    // when the model carries `#[rustango(audit(...))]`. They reborrow
3215    // `_executor` (a `&mut PgConnection` for audited models — the
3216    // macro switches the signature below) so the data write and the
3217    // audit INSERT both run on the same caller-supplied connection.
3218    let executor_passes_to_data_write = if audited_fields.is_some() {
3219        quote!(&mut *_executor)
3220    } else {
3221        quote!(_executor)
3222    };
3223    let executor_param = if audited_fields.is_some() {
3224        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
3225    } else {
3226        quote!(_executor: _E)
3227    };
3228    let executor_generics = if audited_fields.is_some() {
3229        quote!()
3230    } else {
3231        quote!(<'_c, _E>)
3232    };
3233    let executor_where = if audited_fields.is_some() {
3234        quote!()
3235    } else {
3236        quote! {
3237            where
3238                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
3239        }
3240    };
3241    // Audited `_on` methods take `&mut PgConnection`; `&PgPool` is an Executor for the rest.
3242    // One transaction: the before-image's FOR UPDATE must hold through
3243    // the UPDATE and its audit row.
3244    let pool_to_save_on = if audited_fields.is_some() {
3245        // Any failure, COMMIT included, rolls the INSERT back: put the pk and
3246        // stamps back too, or a retry takes the UPDATE path and writes nothing.
3247        let restored: Vec<&syn::Ident> = primary_key
3248            .map(|(pk, _)| pk)
3249            .into_iter()
3250            .chain(&fields.auto_now_idents)
3251            .collect();
3252        let prev: Vec<syn::Ident> = (0..restored.len())
3253            .map(|i| quote::format_ident!("__rustango_save_prev_{}", i))
3254            .collect();
3255        quote! {
3256            #( let #prev = ::core::clone::Clone::clone(&self.#restored); )*
3257            let _saved: ::core::result::Result<u64, #root::sql::ExecError> = async {
3258                let mut _tx = pool.begin().await?;
3259                let _affected = self.save_on(&mut *_tx).await?;
3260                _tx.commit().await?;
3261                ::core::result::Result::Ok(_affected)
3262            }
3263            .await;
3264            if _saved.is_err() {
3265                #( self.#restored = #prev; )*
3266            }
3267            _saved
3268        }
3269    } else {
3270        quote!(self.save_on(pool).await)
3271    };
3272    // One transaction so the write and its audit row commit together.
3273    let pool_to_insert_on = if audited_fields.is_some() {
3274        // A failed COMMIT leaves the RETURNING values on `self`; put them back.
3275        let assigned: Vec<syn::Ident> = fields
3276            .auto_field_idents
3277            .iter()
3278            .map(|(ident, _, _)| ident.clone())
3279            .collect();
3280        restore_auto_now_unless(
3281            &assigned,
3282            &quote!(::core::result::Result<(), #root::sql::ExecError>),
3283            &quote!(_saved.is_ok()),
3284            &quote! {
3285                let mut _tx = pool.begin().await?;
3286                self.insert_on(&mut *_tx).await?;
3287                _tx.commit().await?;
3288                ::core::result::Result::Ok(())
3289            },
3290        )
3291    } else {
3292        quote!(self.insert_on(pool).await)
3293    };
3294    let pool_to_delete_on = if audited_fields.is_some() {
3295        quote! {
3296            let mut _tx = pool.begin().await?;
3297            let _affected = self.delete_on(&mut *_tx).await?;
3298            _tx.commit().await?;
3299            ::core::result::Result::Ok(_affected)
3300        }
3301    } else {
3302        quote!(self.delete_on(pool).await)
3303    };
3304    let pool_to_bulk_insert_on = if audited_fields.is_some() {
3305        quote! {
3306            let mut _tx = pool.begin().await?;
3307            Self::bulk_insert_on(rows, &mut *_tx).await?;
3308            _tx.commit().await?;
3309            ::core::result::Result::Ok(())
3310        }
3311    } else {
3312        quote!(Self::bulk_insert_on(rows, pool).await)
3313    };
3314    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
3315    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
3316    // directly, but `upsert_on` for audited models takes
3317    // `&mut PgConnection` (the audit emit needs a real connection).
3318    // Add the missing acquire shim to keep audited Auto-PK upsert
3319    // compiling.
3320    let pool_to_upsert_on = if audited_fields.is_some() {
3321        quote! {
3322            let mut _tx = pool.begin().await?;
3323            self.upsert_on(&mut *_tx).await?;
3324            _tx.commit().await?;
3325            ::core::result::Result::Ok(())
3326        }
3327    } else {
3328        quote!(self.upsert_on(pool).await)
3329    };
3330
3331    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3332    // (audit-on-connection over &Pool needs a bi-dialect transaction
3333    // helper, deferred). Two body shapes:
3334    // - has_auto: build InsertQuery skipping Auto::Unset columns,
3335    //   request Auto cols in `returning`, dispatch via
3336    //   `insert_returning_pool`, then on the returned `PgRow` /
3337    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
3338    //   row OR drop the single i64 into the first Auto field on MySQL
3339    //   (multi-Auto models on MySQL error at runtime since
3340    //   `LAST_INSERT_ID()` only reports one)
3341    // - non-Auto: build InsertQuery with explicit columns/values and
3342    //   call `insert_pool` (no returning needed)
3343    // pool_insert_method body for the audited Auto-PK case is moved
3344    // to after audit_pair_tokens / audit_pk_to_string (they live
3345    // ~150 lines below). This block keeps the non-audited and
3346    // non-Auto branches in place — the audited Auto-PK arm is
3347    // computed below and merged via the dispatch helper variable.
3348    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3349        // Audited models with explicit (non-Auto) PKs go through
3350        // the non-Auto insert path below — the audit emit is one
3351        // round-trip after the INSERT inside the same tx via
3352        // audit::save_one_with_audit? No, INSERT semantics
3353        // differ. For non-Auto PK + audited, route through a
3354        // dedicated insert + audit emit on the same tx, but defer
3355        // the macro emission to the audit-bundle-aware block below
3356        // — this `quote!()` placeholder gets overwritten there.
3357        quote!()
3358    } else if audited_fields.is_some() && fields.has_auto {
3359        // Audited Auto-PK insert_pool — assembled after the audit
3360        // bundles. Placeholder; real emission below.
3361        quote!()
3362    } else if fields.has_auto {
3363        let pushes = &fields.insert_pushes;
3364        let returning_cols = &fields.returning_cols;
3365        // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3366        let filled = &fields.rust_filled_idents;
3367        let was_unset: Vec<syn::Ident> = filled
3368            .iter()
3369            .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3370            .collect();
3371        let insert_or_ignore_call = quote! {
3372            #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3373            let mut _columns: ::std::vec::Vec<&'static str> =
3374                ::std::vec::Vec::new();
3375            let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3376                ::std::vec::Vec::new();
3377            #( #pushes )*
3378            let _query = #root::core::InsertQuery::new(
3379                <Self as #root::core::Model>::SCHEMA,
3380                _columns,
3381                _values,
3382            );
3383            let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3384            if !matches!(_result, ::core::result::Result::Ok(true)) {
3385                #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3386            }
3387            _result
3388        };
3389        // When every `Auto<T>` field is filled Rust-side
3390        // (`default_uuid_v7`, issue #823), there is no column to read
3391        // back from the database — `returning_cols` is empty. Route
3392        // through plain `insert_pool` instead of
3393        // `insert_returning_pool` to skip the redundant RETURNING /
3394        // LAST_INSERT_ID round-trip.
3395        if fields.returning_cols.is_empty() {
3396            quote! {
3397                /// Insert this row against either backend. Every
3398                /// `Auto<T>` PK on this model is filled Rust-side
3399                /// (e.g. `default_uuid_v7`) before binding, so no
3400                /// RETURNING round-trip is needed.
3401                ///
3402                /// # Errors
3403                /// As [`Self::insert`].
3404                pub async fn insert_pool(
3405                    &mut self,
3406                    pool: &#root::sql::Pool,
3407                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3408                    let mut _columns: ::std::vec::Vec<&'static str> =
3409                        ::std::vec::Vec::new();
3410                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3411                        ::std::vec::Vec::new();
3412                    #( #pushes )*
3413                    let _query = #root::core::InsertQuery::new(
3414                        <Self as #root::core::Model>::SCHEMA,
3415                        _columns,
3416                        _values,
3417                    );
3418                    #root::sql::insert_pool(pool, &_query).await
3419                }
3420
3421                /// Eloquent `Model::insertOrIgnore()` — same shape
3422                /// as the auto-PK branch above. Returns `Ok(true)`
3423                /// when inserted, `Ok(false)` when a conflict caused
3424                /// the INSERT to silently skip.
3425                ///
3426                /// # Errors
3427                /// As [`Self::insert`].
3428                pub async fn insert_or_ignore(
3429                    &mut self,
3430                    pool: &#root::sql::Pool,
3431                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3432                    #insert_or_ignore_call
3433                }
3434            }
3435        } else {
3436            quote! {
3437                /// Insert this row against either backend, populating any
3438                /// `Auto<T>` PK from the auto-assigned value.
3439                ///
3440                /// # Errors
3441                /// As [`Self::insert`].
3442                pub async fn insert_pool(
3443                    &mut self,
3444                    pool: &#root::sql::Pool,
3445                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3446                    let mut _columns: ::std::vec::Vec<&'static str> =
3447                        ::std::vec::Vec::new();
3448                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3449                        ::std::vec::Vec::new();
3450                    #( #pushes )*
3451                    let _query = #root::core::InsertQuery::new(
3452                        <Self as #root::core::Model>::SCHEMA,
3453                        _columns,
3454                        _values,
3455                    )
3456                    .returning(::std::vec![ #( #returning_cols ),* ]);
3457                    let _result = #root::sql::insert_returning_pool(
3458                        pool, &_query,
3459                    ).await?;
3460                    #root::sql::apply_auto_pk(_result, self)
3461                }
3462
3463                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3464                /// row or silently skip on unique-constraint
3465                /// violation. Returns `Ok(true)` when a row was
3466                /// inserted, `Ok(false)` when a conflict caused the
3467                /// INSERT to silently skip.
3468                ///
3469                /// **Caveat on auto-PK models**: when the row is
3470                /// skipped (conflict), this instance's `Auto<T>`
3471                /// fields stay `Unset` — no PK is back-populated
3472                /// because the server didn't auto-assign one. For
3473                /// "insert then read back the PK or the existing
3474                /// row's PK", use the `upsert` family or
3475                /// `get_or_create`.
3476                ///
3477                /// # Errors
3478                /// As [`Self::insert`].
3479                pub async fn insert_or_ignore(
3480                    &mut self,
3481                    pool: &#root::sql::Pool,
3482                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3483                    #insert_or_ignore_call
3484                }
3485            }
3486        }
3487    } else {
3488        let insert_columns = &fields.insert_columns;
3489        let insert_values = &fields.insert_values;
3490        quote! {
3491            /// Insert this row into its table against either backend.
3492            /// Equivalent to [`Self::insert`] but takes
3493            /// [`#root::sql::Pool`].
3494            ///
3495            /// # Errors
3496            /// As [`Self::insert`].
3497            pub async fn insert_pool(
3498                &self,
3499                pool: &#root::sql::Pool,
3500            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3501                let _query = #root::core::InsertQuery::new(
3502                    <Self as #root::core::Model>::SCHEMA,
3503                    ::std::vec![ #( #insert_columns ),* ],
3504                    ::std::vec![ #( #insert_values ),* ],
3505                );
3506                #root::sql::insert_pool(pool, &_query).await
3507            }
3508
3509            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3510            /// or silently skip on unique-constraint violation. Maps
3511            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3512            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3513            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3514            /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3515            ///
3516            /// Returns `Ok(true)` when a row was inserted,
3517            /// `Ok(false)` when a conflict caused the INSERT to
3518            /// silently skip.
3519            ///
3520            /// Use for "create-if-absent" patterns where you don't
3521            /// need the row back. For "find-or-create with the row
3522            /// returned", use the queryset-level
3523            /// `crate::sql::get_or_create` free function.
3524            ///
3525            /// # Errors
3526            /// As [`Self::insert`], plus any dialect-specific
3527            /// translation error from the ConflictClause writer.
3528            pub async fn insert_or_ignore(
3529                &self,
3530                pool: &#root::sql::Pool,
3531            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3532                let _query = #root::core::InsertQuery::new(
3533                    <Self as #root::core::Model>::SCHEMA,
3534                    ::std::vec![ #( #insert_columns ),* ],
3535                    ::std::vec![ #( #insert_values ),* ],
3536                );
3537                #root::sql::insert_or_ignore(pool, &_query).await
3538            }
3539        }
3540    };
3541
3542    // The tracked (column, JSON value) pairs, spliced only into
3543    // `__rustango_audit_pairs`. Empty when the model isn't audited.
3544    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3545        .map(|tracked| {
3546            tracked
3547                .iter()
3548                .map(|c| {
3549                    let column_lit = c.column.as_str();
3550                    let ident = &c.ident;
3551                    quote! {
3552                        (
3553                            #column_lit,
3554                            #root::core::__json_value(&self.#ident),
3555                        )
3556                    }
3557                })
3558                .collect()
3559        })
3560        .unwrap_or_default();
3561    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3562        if fields.pk_is_auto {
3563            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3564        } else {
3565            quote!(::std::format!("{}", &self.#pk_ident))
3566        }
3567    } else {
3568        quote!(::std::string::String::new())
3569    };
3570    // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3571    // is built, so every audited write path records the same shape.
3572    let delete_audited_body = if primary_key.is_some() {
3573        quote! {
3574            ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3575                pool,
3576                query,
3577                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3578            ))
3579        }
3580    } else {
3581        quote! {
3582            let _ = (pool, query);
3583            ::std::boxed::Box::pin(async {
3584                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3585                    table: <Self as #root::core::Model>::SCHEMA.table,
3586                })
3587            })
3588        }
3589    };
3590    let cascade_audited_body = if primary_key.is_some() {
3591        quote! {
3592            ::std::boxed::Box::pin(#root::audit::cascade_rows::<Self>(
3593                tx,
3594                <Self as #root::core::Model>::SCHEMA,
3595                rows,
3596                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3597            ))
3598        }
3599    } else {
3600        quote! {
3601            let _ = (tx, rows);
3602            ::std::boxed::Box::pin(async {
3603                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3604                    table: <Self as #root::core::Model>::SCHEMA.table,
3605                })
3606            })
3607        }
3608    };
3609    let update_audited_body = if primary_key.is_some() {
3610        quote! {
3611            ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3612                pool,
3613                query,
3614                move |_r: &Self| _r.__rustango_audit_entry(op),
3615            ))
3616        }
3617    } else {
3618        quote! {
3619            let _ = (pool, query, op);
3620            ::std::boxed::Box::pin(async {
3621                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3622                    table: <Self as #root::core::Model>::SCHEMA.table,
3623                })
3624            })
3625        }
3626    };
3627    let audit_entry_method = if audited_fields.is_some() {
3628        let pairs = audit_pair_tokens.iter();
3629        let pk_str = audit_pk_to_string.clone();
3630        quote! {
3631            /// This row's tracked `(column, value)` pairs.
3632            #[doc(hidden)]
3633            pub fn __rustango_audit_pairs(
3634                &self,
3635            ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3636                ::std::vec![ #( #pairs ),* ]
3637            }
3638
3639            /// This row's audit PK.
3640            #[doc(hidden)]
3641            pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3642                #pk_str
3643            }
3644
3645            /// Snapshot audit entry for `operation` on this row.
3646            #[doc(hidden)]
3647            pub fn __rustango_audit_entry(
3648                &self,
3649                operation: #root::audit::AuditOp,
3650            ) -> #root::audit::PendingEntry {
3651                #root::audit::PendingEntry {
3652                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3653                    entity_pk: self.__rustango_audit_pk(),
3654                    operation,
3655                    source: #root::audit::current_source(),
3656                    changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3657                }
3658            }
3659
3660            /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3661            #[doc(hidden)]
3662            pub fn __rustango_update_audited<'a>(
3663                pool: &'a #root::sql::Pool,
3664                query: &'a #root::core::UpdateQuery,
3665                op: #root::audit::AuditOp,
3666            ) -> ::std::pin::Pin<::std::boxed::Box<
3667                dyn ::core::future::Future<
3668                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3669                > + ::core::marker::Send + 'a,
3670            >> {
3671                #update_audited_body
3672            }
3673
3674            /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3675            #[doc(hidden)]
3676            pub fn __rustango_create_audited<'a, 't>(
3677                tx: &'a mut #root::sql::PoolTx<'t>,
3678                pk: #root::core::SqlValue,
3679            ) -> ::std::pin::Pin<::std::boxed::Box<
3680                dyn ::core::future::Future<
3681                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3682                > + ::core::marker::Send + 'a,
3683            >> {
3684                ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3685                    tx,
3686                    <Self as #root::core::Model>::SCHEMA,
3687                    pk,
3688                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3689                ))
3690            }
3691
3692            /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3693            #[doc(hidden)]
3694            pub fn __rustango_update_record_audited<'a, 't>(
3695                tx: &'a mut #root::sql::PoolTx<'t>,
3696                pk: #root::core::SqlValue,
3697            ) -> ::std::pin::Pin<::std::boxed::Box<
3698                dyn ::core::future::Future<
3699                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3700                > + ::core::marker::Send + 'a,
3701            >> {
3702                ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3703                    tx,
3704                    <Self as #root::core::Model>::SCHEMA,
3705                    pk,
3706                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3707                ))
3708            }
3709
3710            /// Rows an FK cascade will delete, behind `Model::__rustango_audited_cascade`.
3711            #[doc(hidden)]
3712            pub fn __rustango_cascade_audited<'a, 't>(
3713                tx: &'a mut #root::sql::PoolTx<'t>,
3714                rows: #root::core::WhereExpr,
3715            ) -> ::std::pin::Pin<::std::boxed::Box<
3716                dyn ::core::future::Future<
3717                    Output = ::core::result::Result<
3718                        ::std::vec::Vec<(#root::core::SqlValue, #root::audit::PendingEntry)>,
3719                        #root::sql::ExecError,
3720                    >,
3721                > + ::core::marker::Send + 'a,
3722            >> {
3723                #cascade_audited_body
3724            }
3725
3726            /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3727            #[doc(hidden)]
3728            pub fn __rustango_delete_audited<'a>(
3729                pool: &'a #root::sql::Pool,
3730                query: &'a #root::core::DeleteQuery,
3731            ) -> ::std::pin::Pin<::std::boxed::Box<
3732                dyn ::core::future::Future<
3733                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3734                > + ::core::marker::Send + 'a,
3735            >> {
3736                #delete_audited_body
3737            }
3738        }
3739    } else {
3740        quote!()
3741    };
3742    // Builds `_audit_entry`; `written` is the soft-delete column's new
3743    // value, recorded in place of the stale `self` field.
3744    let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3745        if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3746            quote! {
3747                let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3748                _audit_entry.set_tracked(#col, #v);
3749            }
3750        } else {
3751            quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3752        }
3753    };
3754    let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3755        .unwrap_or(#root::__serde_json::Value::Null));
3756    let restored_json = quote!(#root::__serde_json::Value::Null);
3757    // `_on` emit; `affected` paths skip the row when nothing changed.
3758    let make_op_emit =
3759        |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3760            if audited_fields.is_none() {
3761                return quote!();
3762            }
3763            let entry = make_entry(op_path, written);
3764            let emit = quote! {
3765                #entry
3766                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3767            };
3768            if affected {
3769                quote!(if _affected > 0 { #emit })
3770            } else {
3771                emit
3772            }
3773        };
3774    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3775    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3776    let audit_softdelete_emit = make_op_emit(
3777        quote!(#root::audit::AuditOp::SoftDelete),
3778        Some(soft_deleted_json.clone()),
3779        true,
3780    );
3781    let audit_restore_emit = make_op_emit(
3782        quote!(#root::audit::AuditOp::Restore),
3783        Some(restored_json.clone()),
3784        true,
3785    );
3786    // `&Pool` update runner for soft_delete / restore: one tx with the
3787    // audit row when audited, a plain UPDATE otherwise.
3788    let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3789        if audited_fields.is_some() {
3790            let entry = make_entry(op_path, Some(written));
3791            quote! {
3792                #entry
3793                #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3794            }
3795        } else {
3796            quote!(#root::sql::update_pool(pool, &_query).await)
3797        }
3798    };
3799    let pool_softdelete_run =
3800        make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3801    let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3802
3803    // `save_pool(&Pool)` — emitted for every model with a PK. The
3804    // audited `save_pool` is built below on the diff path; both share
3805    // `save_partial_methods`, which differ only in how they run `_query`.
3806    let save_ret = quote!(::core::result::Result<u64, #root::sql::ExecError>);
3807    let save_keep = quote!(matches!(_saved, ::core::result::Result::Ok(n) if n > 0));
3808    let save_partial_methods = |pk_ident: &syn::Ident,
3809                                pk_column_lit: &str,
3810                                run: TokenStream2|
3811     -> TokenStream2 {
3812        let assignments = &fields.update_assignments;
3813        // Pushed in step with `update_assignments`: one column each.
3814        let assignment_cols = &fields.upsert_update_columns;
3815        let write = restore_auto_now_unless(
3816            &fields.auto_now_idents,
3817            &save_ret,
3818            &save_keep,
3819            &quote! {
3820                // Build only the wanted assignments, so an unnamed
3821                // `auto_now` field is not stamped.
3822                let mut _filtered: ::std::vec::Vec<#root::core::Assignment> =
3823                    ::std::vec::Vec::with_capacity(_wanted_cols.len());
3824                #(
3825                    if _wanted_cols.contains(#assignment_cols) {
3826                        _filtered.push(#assignments);
3827                    }
3828                )*
3829                if _filtered.is_empty() {
3830                    // All field names valid, but they all map to
3831                    // non-assignable slots (PK column, computed/
3832                    // virtual fields, relations without an
3833                    // assignment). Same no-op as an empty list.
3834                    #root::__tracing::warn!(
3835                        target: "rustango::save_partial",
3836                        model = _schema.name,
3837                        "save_partial: every named field maps to a non-assignable column — no-op"
3838                    );
3839                    return ::core::result::Result::Ok(0);
3840                }
3841                let _query = #root::core::UpdateQuery::new(
3842                    _schema,
3843                    _filtered,
3844                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3845                        #pk_column_lit,
3846                        #root::core::Op::Eq,
3847                        ::core::convert::Into::<#root::core::SqlValue>::into(
3848                            ::core::clone::Clone::clone(&self.#pk_ident)
3849                        ),
3850                    )),
3851                );
3852                #run
3853            },
3854        );
3855        quote! {
3856            /// Save (UPDATE) only the listed Rust-side fields,
3857            /// leaving every other column untouched. Issue #66.
3858            ///
3859            /// `fields` are Rust-side struct field names; the macro
3860            /// resolves each to its SQL column. Unknown field
3861            /// names return [`#root::core::QueryError::UnknownField`]
3862            /// wrapped in `ExecError::Query`. An empty list is a
3863            /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3864            /// On an audited model the audit diff covers only these fields.
3865            ///
3866            /// Use this when:
3867            /// * you only mutated a couple of fields on a wide row
3868            ///   (avoid re-writing every column on every save), or
3869            /// * two writers diverged after their initial read and
3870            ///   you want to preserve the other writer's changes to
3871            ///   columns you didn't touch.
3872            ///
3873            /// Auto-PK models with an unset PK return
3874            /// [`#root::core::QueryError::UnknownField`] with
3875            /// field name `<pk>` — `save_partial` is an
3876            /// UPDATE-only path. Call [`Self::insert_pool`]
3877            /// (or [`Self::save_pool`] which dispatches based on
3878            /// PK state) for the INSERT case.
3879            ///
3880            /// # Errors
3881            /// As [`Self::save_pool`], plus `UnknownField` for
3882            /// unknown / empty / Auto-Unset cases.
3883            pub async fn save_partial(
3884                &mut self,
3885                fields: &[&str],
3886                pool: &#root::sql::Pool,
3887            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3888                if fields.is_empty() {
3889                    #root::__tracing::warn!(
3890                        target: "rustango::save_partial",
3891                        model = <Self as #root::core::Model>::SCHEMA.name,
3892                        "save_partial called with empty field list — no-op"
3893                    );
3894                    return ::core::result::Result::Ok(0);
3895                }
3896                let _schema = <Self as #root::core::Model>::SCHEMA;
3897                // Validate field names against the schema.
3898                let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3899                    ::std::collections::HashSet::with_capacity(fields.len());
3900                for f in fields {
3901                    match _schema.field(f) {
3902                        ::core::option::Option::Some(fs) => {
3903                            _wanted_cols.insert(fs.column);
3904                        }
3905                        ::core::option::Option::None => {
3906                            return ::core::result::Result::Err(
3907                                #root::sql::ExecError::Query(
3908                                    #root::core::QueryError::UnknownField {
3909                                        model: _schema.name,
3910                                        field: (*f).to_owned(),
3911                                    }
3912                                )
3913                            );
3914                        }
3915                    }
3916                }
3917                #write
3918            }
3919
3920            /// Typed-column counterpart of [`Self::save_partial`] —
3921            /// issue #67. `fields` is a tuple of [`Column`]
3922            /// constants whose `Model` matches `Self`; typos and
3923            /// model mismatches surface at *compile time*
3924            /// (`Author::name` inside a `Post::save_partial_typed`
3925            /// call is a type error, no runtime check).
3926            ///
3927            /// ```ignore
3928            /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3929            /// ```
3930            ///
3931            /// Lowers to [`Self::save_partial`] under the hood — the
3932            /// tuple is reduced to a `&[&str]` slice of Rust-side
3933            /// field names and forwarded.
3934            ///
3935            /// [`Column`]: #root::core::Column
3936            ///
3937            /// # Errors
3938            /// As [`Self::save_partial`].
3939            pub async fn save_partial_typed<
3940                L: #root::core::TypedFieldList<Self>,
3941            >(
3942                &mut self,
3943                fields: L,
3944                pool: &#root::sql::Pool,
3945            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3946                let _names = fields.rust_field_names();
3947                let _refs: ::std::vec::Vec<&str> =
3948                    _names.iter().copied().collect();
3949                self.save_partial(&_refs, pool).await
3950            }
3951        }
3952    };
3953    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3954        let pk_column_lit = pk_col.as_str();
3955        let assignments = &fields.update_assignments;
3956        if audited_fields.is_some() {
3957            quote!()
3958        } else {
3959            let partial = save_partial_methods(
3960                pk_ident,
3961                pk_column_lit,
3962                quote!(#root::sql::update_pool(pool, &_query).await),
3963            );
3964            let dispatch_unset = if fields.pk_is_auto {
3965                quote! {
3966                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3967                        return self.insert_pool(pool).await.map(|()| 1u64);
3968                    }
3969                }
3970            } else {
3971                quote!()
3972            };
3973            let write = restore_auto_now_unless(
3974                &fields.auto_now_idents,
3975                &save_ret,
3976                &save_keep,
3977                &quote! {
3978                    let _query = #root::core::UpdateQuery::new(
3979                        <Self as #root::core::Model>::SCHEMA,
3980                        ::std::vec![ #( #assignments ),* ],
3981                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3982                            #pk_column_lit,
3983                            #root::core::Op::Eq,
3984                            ::core::convert::Into::<#root::core::SqlValue>::into(
3985                                ::core::clone::Clone::clone(&self.#pk_ident)
3986                            ),
3987                        )),
3988                    );
3989                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3990                    ::core::result::Result::Ok(_affected)
3991                },
3992            );
3993            quote! {
3994                /// Save this row to its table against either backend.
3995                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3996                /// `UPDATE` keyed on the PK; global scopes do not apply.
3997                ///
3998                /// # Errors
3999                /// As [`Self::save`].
4000                pub async fn save_pool(
4001                    &mut self,
4002                    pool: &#root::sql::Pool,
4003                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4004                    #dispatch_unset
4005                    #write
4006                }
4007
4008                #partial
4009            }
4010        }
4011    } else {
4012        quote!()
4013    };
4014
4015    // Audited `insert_pool` (overrides the placeholder set higher up
4016    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
4017    // audited models get insert_pool routing through
4018    // audit::insert_one_with_audit (per-backend tx wraps INSERT
4019    // + auto-PK readback + audit emit). Snapshot-style audit (the
4020    // PendingEntry's `changes` carries post-write field values).
4021    let pool_insert_method = if audited_fields.is_some() {
4022        if let Some(_) = primary_key {
4023            let pushes = if fields.has_auto {
4024                fields.insert_pushes.clone()
4025            } else {
4026                // For non-Auto-PK models, the macro normally builds
4027                // {columns, values} from fields.insert_columns +
4028                // fields.insert_values rather than insert_pushes.
4029                // Map those into the pushes shape.
4030                fields
4031                    .insert_columns
4032                    .iter()
4033                    .zip(&fields.insert_values)
4034                    .map(|(col, val)| {
4035                        quote! {
4036                            _columns.push(#col);
4037                            _values.push(#val);
4038                        }
4039                    })
4040                    .collect()
4041            };
4042            // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
4043            let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
4044            {
4045                fields.returning_cols.clone()
4046            } else {
4047                // Non-Auto-PK: still need RETURNING something for the
4048                // audit helper's contract (it errors on empty
4049                // returning). Return the PK column so the audit row
4050                // can carry the assigned PK back. Some non-Auto PKs
4051                // are server-side-default (e.g. UUIDv4 default), so
4052                // RETURNING is genuinely useful.
4053                primary_key
4054                    .map(|(_, col)| {
4055                        let lit = col.as_str();
4056                        vec![quote!(#lit)]
4057                    })
4058                    .unwrap_or_default()
4059            };
4060            quote! {
4061                /// Insert this row against either backend with audit
4062                /// emission inside the same transaction. Bi-dialect
4063                /// counterpart of [`Self::insert`] for audited models.
4064                ///
4065                /// Snapshot-style audit (post-write field values).
4066                ///
4067                /// # Errors
4068                /// As [`Self::insert`].
4069                pub async fn insert_pool(
4070                    &mut self,
4071                    pool: &#root::sql::Pool,
4072                ) -> ::core::result::Result<(), #root::sql::ExecError> {
4073                    let mut _columns: ::std::vec::Vec<&'static str> =
4074                        ::std::vec::Vec::new();
4075                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
4076                        ::std::vec::Vec::new();
4077                    #( #pushes )*
4078                    let _query = #root::core::InsertQuery::new(
4079                        <Self as #root::core::Model>::SCHEMA,
4080                        _columns,
4081                        _values,
4082                    )
4083                    .returning(::std::vec![ #( #returning_cols ),* ]);
4084                    #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
4085                        _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
4086                    })
4087                    .await
4088                }
4089            }
4090        } else {
4091            quote!()
4092        }
4093    } else {
4094        // Keep the non-audited pool_insert_method we built earlier.
4095        pool_insert_method
4096    };
4097
4098    // Update audited save_pool: now that insert_pool is wired for
4099    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
4100    // insert_pool. Non-audited save_pool already does this.
4101    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
4102    // Replaces the snapshot-only emission with a per-backend transaction
4103    // body that:
4104    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
4105    //     column), capturing BEFORE values
4106    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
4107    //  3. builds AFTER pairs from &self
4108    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
4109    //     AuditOp::Update + diff_changes(...) on the same tx connection
4110    //  5. commits
4111    //
4112    // Per-backend arms inline the SQL string + placeholder shape, then
4113    // share the `audit_before_pair_tokens` decoder block (Row::try_get
4114    // is polymorphic over Row type — the same tokens work against
4115    // PgRow and MySqlRow as long as the field's Rust type implements
4116    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
4117    // primitives + chrono/uuid/serde_json::Value all do).
4118    let pool_save_method = if let Some(tracked) = audited_fields {
4119        if let Some((pk_ident, pk_col)) = primary_key {
4120            let pk_column_lit = pk_col.as_str();
4121            let partial = save_partial_methods(
4122                pk_ident,
4123                pk_column_lit,
4124                quote! {
4125                    self.__rustango_save_with_diff(
4126                        pool,
4127                        &_query,
4128                        ::core::option::Option::Some(&_wanted_cols),
4129                    )
4130                    .await
4131                },
4132            );
4133            // Per-tracked-column BEFORE-pair token list. Each entry
4134            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
4135            // The Row alias resolves to PgRow / MySqlRow per call site,
4136            // so the same template generates both the PG and MySQL bodies.
4137            let mk_before_pairs =
4138                |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
4139                    tracked
4140                        .iter()
4141                        .map(|c| {
4142                            let getter = getter(c);
4143                            let column_lit = c.column.as_str();
4144                            let value_ty = &c.value_ty;
4145                            quote! {
4146                                (
4147                                    #column_lit,
4148                                    match #getter::<#value_ty>(
4149                                        _audit_before_row, #column_lit,
4150                                    ) {
4151                                        ::core::result::Result::Ok(v) => {
4152                                            #root::core::__json_value(&v)
4153                                        }
4154                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
4155                                    },
4156                                )
4157                            }
4158                        })
4159                        .collect()
4160                };
4161            let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
4162            let before_pairs_my = mk_before_pairs(&|c| {
4163                if c.uuid_column {
4164                    quote!(#root::sql::try_get_flat_my)
4165                } else {
4166                    quote!(#root::sql::try_get_returning_my)
4167                }
4168            });
4169            let before_pairs_sqlite =
4170                mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
4171            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
4172            let pk_value_for_bind = if fields.pk_is_auto {
4173                quote!(self.#pk_ident.get().copied().unwrap_or_default())
4174            } else {
4175                quote!(::core::clone::Clone::clone(&self.#pk_ident))
4176            };
4177            let assignments = &fields.update_assignments;
4178            let unset_dispatch = if fields.has_auto {
4179                quote! {
4180                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
4181                        return self.insert_pool(pool).await.map(|()| 1u64);
4182                    }
4183                }
4184            } else {
4185                quote!()
4186            };
4187            let write = restore_auto_now_unless(
4188                &fields.auto_now_idents,
4189                &save_ret,
4190                &save_keep,
4191                &quote! {
4192                    let _query = #root::core::UpdateQuery::new(
4193                        <Self as #root::core::Model>::SCHEMA,
4194                        ::std::vec![ #( #assignments ),* ],
4195                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4196                            #pk_column_lit,
4197                            #root::core::Op::Eq,
4198                            ::core::convert::Into::<#root::core::SqlValue>::into(
4199                                ::core::clone::Clone::clone(&self.#pk_ident)
4200                            ),
4201                        )),
4202                    );
4203                    self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
4204                        .await
4205                },
4206            );
4207            quote! {
4208                /// Save this row against either backend with audit
4209                /// emission (diff-style: BEFORE+AFTER) inside the
4210                /// same transaction. Auto::Unset PK routes to
4211                /// insert_pool. Bi-dialect counterpart of
4212                /// [`Self::save`] for audited models.
4213                ///
4214                /// The audit row's `changes` JSON contains one
4215                /// `{ "field": { "before": …, "after": … } }` entry
4216                /// per tracked column whose value actually changed
4217                /// — same shape as the existing &PgPool save() emits.
4218                /// Keyed on the PK; global scopes do not apply.
4219                ///
4220                /// # Errors
4221                /// As [`Self::save`].
4222                pub async fn save_pool(
4223                    &mut self,
4224                    pool: &#root::sql::Pool,
4225                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4226                    #unset_dispatch
4227                    #write
4228                }
4229
4230                /// Run `query` and write its diff entry, limited to `only` columns.
4231                #[doc(hidden)]
4232                async fn __rustango_save_with_diff(
4233                    &self,
4234                    pool: &#root::sql::Pool,
4235                    query: &#root::core::UpdateQuery,
4236                    only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
4237                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4238                    let mut _after_pairs = self.__rustango_audit_pairs();
4239                    if let ::core::option::Option::Some(only) = only {
4240                        _after_pairs.retain(|(col, _)| only.contains(col));
4241                    }
4242                    let _before_query = #root::audit::before_image_query(
4243                        <Self as #root::core::Model>::SCHEMA,
4244                        #pk_column_lit,
4245                        ::core::convert::Into::<#root::core::SqlValue>::into(
4246                            #pk_value_for_bind,
4247                        ),
4248                        &[ #( #tracked_cols ),* ],
4249                    );
4250                    #root::audit::save_one_with_diff(
4251                        pool,
4252                        query,
4253                        &_before_query,
4254                        <Self as #root::core::Model>::SCHEMA.table,
4255                        self.__rustango_audit_pk(),
4256                        _after_pairs,
4257                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4258                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4259                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4260                    ).await
4261                }
4262
4263                #partial
4264            }
4265        } else {
4266            quote!()
4267        }
4268    } else {
4269        pool_save_method
4270    };
4271
4272    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4273    // body shapes:
4274    // - non-audited: simple dispatch through `sql::delete_pool`
4275    // - audited: routes through `audit::delete_one_with_audit`,
4276    //   which opens a per-backend transaction wrapping DELETE +
4277    //   audit emit so the data write and audit row commit atomically.
4278    let pool_delete_method = {
4279        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4280        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4281        if let Some(pk_ident) = pk_ident_for_pool {
4282            if audited_fields.is_some() {
4283                quote! {
4284                    /// Delete this row against either backend with audit
4285                    /// emission inside the same transaction. Bi-dialect
4286                    /// counterpart of [`Self::delete`] for audited models.
4287                    /// Keyed on the PK; global scopes do not apply.
4288                    ///
4289                    /// # Errors
4290                    /// As [`Self::delete`].
4291                    pub async fn delete_pool(
4292                        &self,
4293                        pool: &#root::sql::Pool,
4294                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4295                        let _query = #root::core::DeleteQuery::by_pk(
4296                            <Self as #root::core::Model>::SCHEMA,
4297                            #pk_column_lit,
4298                            ::core::convert::Into::<#root::core::SqlValue>::into(
4299                                ::core::clone::Clone::clone(&self.#pk_ident)
4300                            ),
4301                        );
4302                        let _audit_entry =
4303                            self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4304                        #root::audit::delete_one_with_audit(
4305                            pool, &_query, &_audit_entry,
4306                        ).await
4307                    }
4308                }
4309            } else {
4310                quote! {
4311                    /// Delete the row identified by this instance's primary key
4312                    /// against either backend. Equivalent to [`Self::delete`] but
4313                    /// takes [`#root::sql::Pool`] and dispatches per backend.
4314                    /// Global scopes do not apply.
4315                    ///
4316                    /// # Errors
4317                    /// As [`Self::delete`].
4318                    pub async fn delete_pool(
4319                        &self,
4320                        pool: &#root::sql::Pool,
4321                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4322                        let _query = #root::core::DeleteQuery::by_pk(
4323                            <Self as #root::core::Model>::SCHEMA,
4324                            #pk_column_lit,
4325                            ::core::convert::Into::<#root::core::SqlValue>::into(
4326                                ::core::clone::Clone::clone(&self.#pk_ident)
4327                            ),
4328                        );
4329                        #root::sql::delete_pool(pool, &_query).await
4330                    }
4331                }
4332            }
4333        } else {
4334            quote!()
4335        }
4336    };
4337
4338    // Bulk writers on audited models write one audit row per affected
4339    // row, in the write's transaction (#1747).
4340    let (bulk_delete_run, bulk_update_run, truncate_run) = if audited_fields.is_some() {
4341        (
4342            quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4343                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4344            })
4345            .await),
4346            quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4347                _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4348            })
4349            .await),
4350            quote! {
4351                let _table = <Self as #root::core::Model>::SCHEMA.table;
4352                let _sql = #root::sql::truncate_sql(pool.dialect(), _table);
4353                #root::audit::truncate_with_audit(pool, _table, &_sql).await
4354            },
4355        )
4356    } else {
4357        (
4358            quote!(#root::sql::delete_pool(pool, &_query).await),
4359            quote!(#root::sql::update_pool(pool, &_query).await),
4360            quote!(#root::sql::truncate(pool, <Self as #root::core::Model>::SCHEMA).await),
4361        )
4362    };
4363
4364    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4365    // matching this instance's PK and overwrite the in-memory state
4366    // with the freshly-fetched columns.
4367    // Issue #825. Only emitted when the model declares a PK; non-PK
4368    // models can't address a specific row.
4369    //
4370    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4371    // every field from `self`; resets the PK to `Auto::Unset` when
4372    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4373    // a fresh autoincrement value. Non-Auto PKs preserve the source
4374    // PK — the caller must overwrite before insert. Pure-Rust, no
4375    // I/O, no dialect surface.
4376    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4377        let other_field_clones: Vec<TokenStream2> = fields
4378            .column_entries
4379            .iter()
4380            .filter(|c| &c.ident != pk_ident)
4381            .map(|c| {
4382                let ident = &c.ident;
4383                quote! {
4384                    #ident: ::core::clone::Clone::clone(&self.#ident)
4385                }
4386            })
4387            .collect();
4388        let pk_clone_token = if fields.pk_is_auto {
4389            quote! { #pk_ident: #root::sql::Auto::Unset }
4390        } else {
4391            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4392        };
4393        let replicate_doc = if fields.pk_is_auto {
4394            quote! {
4395                /// Eloquent-style `replicate()` — return a clone of this
4396                /// row with the primary key reset to [`Auto::Unset`] so
4397                /// the copy is ready for `insert_pool` / `save_pool` to
4398                /// allocate a fresh autoincrement value. Every other
4399                /// field is `Clone`d verbatim. Issue #825.
4400                ///
4401                /// `auto_now_add` / `auto_now` timestamp fields are
4402                /// **not** reset (Eloquent's `replicate` doesn't reset
4403                /// them either) — pass them through the normal insert
4404                /// path if you want fresh values, or assign them
4405                /// explicitly after the call.
4406            }
4407        } else {
4408            quote! {
4409                /// Eloquent-style `replicate()` — clone this row
4410                /// verbatim. Because the primary key is **not** an
4411                /// `Auto<T>`, the clone keeps the source PK; the
4412                /// caller must overwrite `copy.<pk>` before inserting
4413                /// to avoid a unique-key violation. Issue #825.
4414            }
4415        };
4416        // 2026-06-07 — field-name / shortcut collision guard.
4417        //
4418        // The macro emits both `pub const <field>: <field>_col = ...`
4419        // (per-field typed-column const, used by the typed-builder
4420        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4421        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4422        // model has a field named e.g. `count`, both items would land
4423        // in the same inherent impl with the same name, and the
4424        // compiler rejects the derive with "duplicate definitions".
4425        //
4426        // Drop the conflicting shortcut for that model. Callers can
4427        // still reach the same behavior via
4428        // `QuerySet::<Model>::default().count(&pool)`.
4429        let column_names: ::std::collections::HashSet<String> = fields
4430            .column_entries
4431            .iter()
4432            .map(|c| ident_name(&c.ident))
4433            .collect();
4434        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4435            if column_names.contains(name) {
4436                quote! {}
4437            } else {
4438                tokens
4439            }
4440        };
4441        let count_method = emit_if_no_field_collision(
4442            "count",
4443            quote! {
4444                /// Count rows of this model — `SELECT COUNT(*) FROM
4445                /// <table>`. Eloquent `Model::count()` parity.
4446                ///
4447                /// Skipped on models that already declare a field named
4448                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4449                /// in that case.
4450                ///
4451                /// # Errors
4452                /// As [`CounterPool::count`].
4453                ///
4454                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4455                pub async fn count(
4456                    pool: &#root::sql::Pool,
4457                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4458                    use #root::sql::CounterPool as _;
4459                    #root::query::QuerySet::<Self>::default()
4460                        .count(pool)
4461                        .await
4462                }
4463            },
4464        );
4465        let value_method = emit_if_no_field_collision(
4466            "value",
4467            quote! {
4468                /// Pluck a single scalar from the first row.
4469                /// Eloquent `Model::query()->value($col)` parity.
4470                ///
4471                /// Skipped on models that already declare a field named
4472                /// `value`. Drop into
4473                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4474                ///
4475                /// # Errors
4476                /// As `ValuesFlatQuerySet::first`.
4477                pub async fn value<U>(
4478                    col: &str,
4479                    pool: &#root::sql::Pool,
4480                ) -> ::core::result::Result<
4481                    ::core::option::Option<U>,
4482                    #root::sql::ExecError,
4483                >
4484                where
4485                    U: #root::sql::FlatScalar,
4486                {
4487                    let _col_static: &'static str = Self::__resolve_col(col)?;
4488                    #root::query::QuerySet::<Self>::default()
4489                        .values_list_flat(_col_static)
4490                        .first::<U>(pool)
4491                        .await
4492                }
4493            },
4494        );
4495        let sum_method = emit_if_no_field_collision(
4496            "sum",
4497            quote! {
4498                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4499                /// Skipped on models that already declare a field named
4500                /// `sum`.
4501                ///
4502                /// # Errors
4503                /// As [`#root::sql::fetch_aggregate_pool`].
4504                pub async fn sum<U>(
4505                    col: &str,
4506                    pool: &#root::sql::Pool,
4507                ) -> ::core::result::Result<
4508                    ::core::option::Option<U>,
4509                    #root::sql::ExecError,
4510                >
4511                where
4512                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4513                        + #root::sql::MaybeMyFromRow
4514                        + #root::sql::MaybeSqliteFromRow
4515                        + ::core::marker::Send
4516                        + ::core::marker::Unpin,
4517                {
4518                    #root::query::QuerySet::<Self>::default()
4519                        .sum::<U>(col, pool)
4520                        .await
4521                }
4522            },
4523        );
4524        let avg_method = emit_if_no_field_collision(
4525            "avg",
4526            quote! {
4527                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4528                /// models that already declare a field named `avg`.
4529                ///
4530                /// # Errors
4531                /// As [`#root::sql::fetch_aggregate_pool`].
4532                pub async fn avg<U>(
4533                    col: &str,
4534                    pool: &#root::sql::Pool,
4535                ) -> ::core::result::Result<
4536                    ::core::option::Option<U>,
4537                    #root::sql::ExecError,
4538                >
4539                where
4540                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4541                        + #root::sql::MaybeMyFromRow
4542                        + #root::sql::MaybeSqliteFromRow
4543                        + ::core::marker::Send
4544                        + ::core::marker::Unpin,
4545                {
4546                    #root::query::QuerySet::<Self>::default()
4547                        .avg::<U>(col, pool)
4548                        .await
4549                }
4550            },
4551        );
4552        let min_method = emit_if_no_field_collision(
4553            "min",
4554            quote! {
4555                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4556                /// models that already declare a field named `min`.
4557                ///
4558                /// # Errors
4559                /// As [`#root::sql::fetch_aggregate_pool`].
4560                pub async fn min<U>(
4561                    col: &str,
4562                    pool: &#root::sql::Pool,
4563                ) -> ::core::result::Result<
4564                    ::core::option::Option<U>,
4565                    #root::sql::ExecError,
4566                >
4567                where
4568                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4569                        + #root::sql::MaybeMyFromRow
4570                        + #root::sql::MaybeSqliteFromRow
4571                        + ::core::marker::Send
4572                        + ::core::marker::Unpin,
4573                {
4574                    #root::query::QuerySet::<Self>::default()
4575                        .min::<U>(col, pool)
4576                        .await
4577                }
4578            },
4579        );
4580        let max_method = emit_if_no_field_collision(
4581            "max",
4582            quote! {
4583                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4584                /// models that already declare a field named `max`.
4585                ///
4586                /// # Errors
4587                /// As [`#root::sql::fetch_aggregate_pool`].
4588                pub async fn max<U>(
4589                    col: &str,
4590                    pool: &#root::sql::Pool,
4591                ) -> ::core::result::Result<
4592                    ::core::option::Option<U>,
4593                    #root::sql::ExecError,
4594                >
4595                where
4596                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4597                        + #root::sql::MaybeMyFromRow
4598                        + #root::sql::MaybeSqliteFromRow
4599                        + ::core::marker::Send
4600                        + ::core::marker::Unpin,
4601                {
4602                    #root::query::QuerySet::<Self>::default()
4603                        .max::<U>(col, pool)
4604                        .await
4605                }
4606            },
4607        );
4608        let first_method = emit_if_no_field_collision(
4609            "first",
4610            quote! {
4611                /// First row of this model. Eloquent `Model::first()`.
4612                /// Skipped on models that already declare a field named
4613                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4614                ///
4615                /// # Errors
4616                /// As `QuerySet::first`.
4617                pub async fn first(
4618                    pool: &#root::sql::Pool,
4619                ) -> ::core::result::Result<
4620                    ::core::option::Option<Self>,
4621                    #root::sql::ExecError,
4622                > {
4623                    #root::query::QuerySet::<Self>::default()
4624                        .first(pool)
4625                        .await
4626                }
4627            },
4628        );
4629        let last_method = emit_if_no_field_collision(
4630            "last",
4631            quote! {
4632                /// Last row of this model by primary-key DESC.
4633                /// Eloquent `Model::query()->latest('id')->first()`
4634                /// parity — fetches the highest-PK row without
4635                /// requiring the caller to spell the PK column.
4636                /// Returns `None` on an empty table.
4637                ///
4638                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4639                /// Skipped on models that already declare a field
4640                /// named `last`.
4641                ///
4642                /// # Errors
4643                /// As `QuerySet::last`.
4644                pub async fn last(
4645                    pool: &#root::sql::Pool,
4646                ) -> ::core::result::Result<
4647                    ::core::option::Option<Self>,
4648                    #root::sql::ExecError,
4649                > {
4650                    #root::query::QuerySet::<Self>::default()
4651                        .last(pool)
4652                        .await
4653                }
4654            },
4655        );
4656        quote! {
4657            /// Re-SELECT this row by its primary key and overwrite
4658            /// every in-memory field with the freshly-fetched value.
4659            /// Issue #825.
4660            ///
4661            /// Use this when the row may have been modified by another
4662            /// process / connection / job since you read it — e.g. after
4663            /// a queued task callback, or to re-sync stale UI state
4664            /// before re-saving.
4665            ///
4666            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4667            /// when the primary key no longer matches any row (e.g.
4668            /// the row was deleted concurrently).
4669            ///
4670            /// # Errors
4671            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4672            /// the PK no longer exists.
4673            ///
4674            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4675            pub async fn refresh_from_db(
4676                &mut self,
4677                pool: &#root::sql::Pool,
4678            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4679                use #root::sql::FetcherPool as _;
4680                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4681                    ::core::clone::Clone::clone(&self.#pk_ident),
4682                );
4683                let mut _rows: ::std::vec::Vec<Self> =
4684                    #root::query::QuerySet::<Self>::default()
4685                        .filter(::core::stringify!(#pk_ident), _pk_val)
4686                        .limit(1)
4687                        .fetch(pool)
4688                        .await?;
4689                match _rows.into_iter().next() {
4690                    ::core::option::Option::Some(_fresh) => {
4691                        *self = _fresh;
4692                        ::core::result::Result::Ok(())
4693                    }
4694                    ::core::option::Option::None => ::core::result::Result::Err(
4695                        #root::sql::ExecError::Driver(
4696                            #root::sql::sqlx::Error::RowNotFound,
4697                        ),
4698                    ),
4699                }
4700            }
4701
4702            /// Atomically increment the integer column `col` by
4703            /// `by` for this row. Equivalent to
4704            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4705            ///
4706            /// **Doesn't mutate `self`** — the in-memory copy is now
4707            /// stale; call [`Self::refresh_from_db`] /
4708            /// [`Self::fresh`] to re-sync. Returns the rows-
4709            /// affected count (0 when the PK doesn't match any row,
4710            /// 1 on success).
4711            ///
4712            /// `col` is the Rust field name as a string; unknown
4713            /// fields surface as `UnknownField` at runtime. Negative
4714            /// `by` values atomically decrement (see also
4715            /// [`Self::decrement`]).
4716            ///
4717            /// # Errors
4718            /// As [`UpdaterPool::execute_pool`].
4719            ///
4720            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4721            pub async fn increment(
4722                &self,
4723                col: &str,
4724                by: i64,
4725                pool: &#root::sql::Pool,
4726            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4727                Self::__increment_one(self, col, by, pool).await
4728            }
4729
4730            /// Sibling of [`Self::increment`] — atomically
4731            /// decrement this row's `col` by `by`. Eloquent
4732            /// `$model->decrement($col, $by)` parity. Equivalent to
4733            /// `self.increment(col, -by, &pool)`; the separate name
4734            /// keeps call sites readable.
4735            ///
4736            /// # Errors
4737            /// As [`Self::increment`].
4738            pub async fn decrement(
4739                &self,
4740                col: &str,
4741                by: i64,
4742                pool: &#root::sql::Pool,
4743            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4744                Self::__increment_one(self, col, -by, pool).await
4745            }
4746
4747            /// Bulk-increment: add `by` to `col` on every row of the
4748            /// table. Eloquent `Model::query()->increment($col, $by)`
4749            /// parity. Use for counters, score adjustments, view
4750            /// rollups.
4751            ///
4752            /// # Errors
4753            /// As [`UpdaterPool::execute_pool`].
4754            ///
4755            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4756            pub async fn increment_each(
4757                col: &str,
4758                by: i64,
4759                pool: &#root::sql::Pool,
4760            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4761                Self::__increment_all(col, by, pool).await
4762            }
4763
4764            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4765            ///
4766            /// # Errors
4767            /// As [`Self::increment_each`].
4768            pub async fn decrement_each(
4769                col: &str,
4770                by: i64,
4771                pool: &#root::sql::Pool,
4772            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4773                Self::__increment_all(col, -by, pool).await
4774            }
4775
4776            /// Internal: forward to
4777            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4778            /// One-line wrapper kept as a per-Model method so the
4779            /// macro's emitted `increment` / `decrement` instance
4780            /// calls don't have to thread `Self` through manually.
4781            #[doc(hidden)]
4782            pub async fn __increment_one(
4783                this: &Self,
4784                col: &str,
4785                by: i64,
4786                pool: &#root::sql::Pool,
4787            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4788                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4789                    ::core::clone::Clone::clone(&this.#pk_ident),
4790                );
4791                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4792                    ::core::stringify!(#pk_ident),
4793                    _pk_val,
4794                    col,
4795                    by,
4796                    pool,
4797                )
4798                .await
4799            }
4800
4801            /// Internal: run
4802            /// [`#root::sql::model_shortcuts::increment_all_query`].
4803            #[doc(hidden)]
4804            pub async fn __increment_all(
4805                col: &str,
4806                by: i64,
4807                pool: &#root::sql::Pool,
4808            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4809                let _query =
4810                    #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4811                #bulk_update_run
4812            }
4813
4814            /// Internal: forward to
4815            /// [`#root::sql::model_shortcuts::resolve_col`].
4816            #[doc(hidden)]
4817            pub fn __resolve_col(
4818                col: &str,
4819            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4820                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4821            }
4822
4823            /// Internal: forward to
4824            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4825            #[doc(hidden)]
4826            #[must_use]
4827            pub fn __add_signed_expr(
4828                col_static: &'static str,
4829                signed_by: i64,
4830            ) -> #root::core::Expr {
4831                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4832            }
4833
4834            /// Re-SELECT this row by its primary key and return a
4835            /// **new** instance with the freshly-fetched fields.
4836            /// Eloquent `Model::fresh()` parity — non-mutating
4837            /// counterpart of [`Self::refresh_from_db`].
4838            ///
4839            /// Returns `Ok(None)` when the row was deleted
4840            /// concurrently — vs [`Self::refresh_from_db`]
4841            /// which surfaces that as `RowNotFound` because
4842            /// in-place mutation has nothing to write to.
4843            ///
4844            /// Useful when you want to compare the in-memory
4845            /// instance against the persisted state (audit-style
4846            /// diffs, conflict detection) without mutating the
4847            /// reference you already hold.
4848            ///
4849            /// # Errors
4850            /// As [`FetcherPool::fetch`].
4851            ///
4852            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4853            pub async fn fresh(
4854                &self,
4855                pool: &#root::sql::Pool,
4856            ) -> ::core::result::Result<
4857                ::core::option::Option<Self>,
4858                #root::sql::ExecError,
4859            > {
4860                use #root::sql::FetcherPool as _;
4861                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4862                    ::core::clone::Clone::clone(&self.#pk_ident),
4863                );
4864                let _rows: ::std::vec::Vec<Self> =
4865                    #root::query::QuerySet::<Self>::default()
4866                        .filter(::core::stringify!(#pk_ident), _pk_val)
4867                        .limit(1)
4868                        .fetch(pool)
4869                        .await?;
4870                ::core::result::Result::Ok(_rows.into_iter().next())
4871            }
4872
4873            #replicate_doc
4874            #[must_use]
4875            pub fn replicate(&self) -> Self {
4876                Self {
4877                    #pk_clone_token,
4878                    #( #other_field_clones, )*
4879                }
4880            }
4881
4882            #first_method
4883
4884            #last_method
4885
4886            /// Throwing counterpart of [`Self::first`] —
4887            /// errors with `RowNotFound` when the table is empty.
4888            /// Eloquent `Model::firstOrFail()` parity.
4889            ///
4890            /// # Errors
4891            /// As [`Self::first`]; additionally
4892            /// [`sqlx::Error::RowNotFound`] on empty tables.
4893            ///
4894            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4895            pub async fn first_or_fail(
4896                pool: &#root::sql::Pool,
4897            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4898                // Route through the queryset rather than `Self::first`,
4899                // which is suppressed on models with a field named
4900                // `first` (field/shortcut collision guard).
4901                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4902                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4903                    ::core::option::Option::None => ::core::result::Result::Err(
4904                        #root::sql::ExecError::Driver(
4905                            #root::sql::sqlx::Error::RowNotFound,
4906                        ),
4907                    ),
4908                }
4909            }
4910
4911            /// Single-column projection — `SELECT <col> FROM
4912            /// <table>`. Returns `Vec<U>` where each element is the
4913            /// decoded value of the column. Eloquent
4914            /// `Model::pluck($column)` parity.
4915            ///
4916            /// Thin wrapper over `QuerySet::<Self>::default()
4917            /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4918            /// `FlatScalar`; use `Option<_>` for a nullable column.
4919            ///
4920            /// # Errors
4921            /// As `ValuesFlatQuerySet::fetch`.
4922            pub async fn pluck<U>(
4923                col: &'static str,
4924                pool: &#root::sql::Pool,
4925            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4926            where
4927                U: #root::sql::FlatScalar,
4928            {
4929                #root::query::QuerySet::<Self>::default()
4930                    .values_list_flat(col)
4931                    .fetch::<U>(pool)
4932                    .await
4933            }
4934
4935            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4936            /// stream every row of this model in batches of `n`,
4937            /// invoking the callback once per batch. Stable PK-ASC
4938            /// ordering so the LIMIT/OFFSET pagination is
4939            /// deterministic across drivers.
4940            ///
4941            /// The callback is async — it can do further DB work
4942            /// (writes, related-row lookups, queue dispatch) per
4943            /// batch. Return `Err(...)` from the callback to abort
4944            /// the iteration early; the error bubbles up.
4945            ///
4946            /// **When to use**: bulk processing flows that can't
4947            /// fit the whole table in memory — sending newsletters,
4948            /// running data migrations, computing summary
4949            /// statistics. For small / known-bounded tables, plain
4950            /// `Self::all(&pool)` is simpler.
4951            ///
4952            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4953            /// large tables. For multi-million-row scans prefer
4954            /// keyset-by-PK (the standard "WHERE id > last_seen"
4955            /// shape) over `chunk(...)`.
4956            ///
4957            /// Skipped on models without a primary key — chunking
4958            /// needs a stable order to avoid skipping / repeating
4959            /// rows across batches.
4960            pub async fn chunk<F, Fut>(
4961                n: i64,
4962                pool: &#root::sql::Pool,
4963                mut cb: F,
4964            ) -> ::core::result::Result<(), #root::sql::ExecError>
4965            where
4966                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4967                Fut: ::core::future::Future<
4968                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4969                >,
4970            {
4971                use #root::sql::FetcherPool as _;
4972                let pk_col = match Self::primary_key_column() {
4973                    ::core::option::Option::Some(c) => c,
4974                    ::core::option::Option::None => {
4975                        return ::core::result::Result::Ok(());
4976                    }
4977                };
4978                let mut offset: i64 = 0;
4979                loop {
4980                    let rows: ::std::vec::Vec<Self> =
4981                        #root::query::QuerySet::<Self>::default()
4982                            .order_by(&[(pk_col, false)])
4983                            .limit(n)
4984                            .offset(offset)
4985                            .fetch(pool)
4986                            .await?;
4987                    if rows.is_empty() {
4988                        return ::core::result::Result::Ok(());
4989                    }
4990                    let len = rows.len() as i64;
4991                    cb(rows).await?;
4992                    if len < n {
4993                        return ::core::result::Result::Ok(());
4994                    }
4995                    offset += n;
4996                }
4997            }
4998
4999            /// Eloquent `Model::chunkById($n, fn (...))` — same
5000            /// per-batch callback shape as [`Self::chunk`], but uses
5001            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
5002            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
5003            /// — the right choice for multi-million-row sweeps.
5004            ///
5005            /// Requires the primary key to be a signed integer type
5006            /// (`i64` / `i32`); the keyset comparison rides on
5007            /// `__rustango_pk_value()` lowering through
5008            /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
5009            /// non-integer PKs (UUID, String) — those should use
5010            /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
5011            /// keyset loop.
5012            ///
5013            /// Callback errors abort iteration; the error bubbles up
5014            /// unchanged. Empty table → callback invoked zero times.
5015            pub async fn chunk_by_id<F, Fut>(
5016                n: i64,
5017                pool: &#root::sql::Pool,
5018                mut cb: F,
5019            ) -> ::core::result::Result<(), #root::sql::ExecError>
5020            where
5021                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
5022                Fut: ::core::future::Future<
5023                    Output = ::core::result::Result<(), #root::sql::ExecError>,
5024                >,
5025            {
5026                use #root::sql::FetcherPool as _;
5027                let pk_col = match Self::primary_key_column() {
5028                    ::core::option::Option::Some(c) => c,
5029                    ::core::option::Option::None => {
5030                        return ::core::result::Result::Ok(());
5031                    }
5032                };
5033                // Track the largest PK seen so the next batch picks
5034                // up from there. `i64::MIN` as the sentinel — the
5035                // very first iteration's `> MIN` matches every row,
5036                // so the loop entry is uniform with subsequent
5037                // iterations.
5038                let mut last_seen: i64 = i64::MIN;
5039                loop {
5040                    let key = ::std::format!("{}__gt", pk_col);
5041                    let rows: ::std::vec::Vec<Self> =
5042                        #root::query::QuerySet::<Self>::default()
5043                            .filter(key.as_str(), last_seen)
5044                            .order_by(&[(pk_col, false)])
5045                            .limit(n)
5046                            .fetch(pool)
5047                            .await?;
5048                    if rows.is_empty() {
5049                        return ::core::result::Result::Ok(());
5050                    }
5051                    let len = rows.len() as i64;
5052                    // Capture the last row's PK BEFORE moving rows
5053                    // into the callback.
5054                    let max_pk = match rows
5055                        .last()
5056                        .map(|r| r.__rustango_pk_value())
5057                    {
5058                        ::core::option::Option::Some(
5059                            #root::core::SqlValue::I64(v),
5060                        ) => v,
5061                        ::core::option::Option::Some(
5062                            #root::core::SqlValue::I32(v),
5063                        ) => i64::from(v),
5064                        _ => return ::core::result::Result::Ok(()),
5065                    };
5066                    cb(rows).await?;
5067                    if len < n {
5068                        return ::core::result::Result::Ok(());
5069                    }
5070                    last_seen = max_pk;
5071                }
5072            }
5073
5074            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
5075            /// per-row callback companion to [`Self::chunk`].
5076            /// Streams every row in keyset-paginated batches of
5077            /// `batch` size, calling `cb` once per row.
5078            ///
5079            /// Inherits the keyset-paginated scan of
5080            /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
5081            /// non-integer PKs are silently a no-op).
5082            ///
5083            /// ```ignore
5084            /// Post::each(500, &pool, |p| async move {
5085            ///     reindex(p).await?;
5086            ///     Ok(())
5087            /// }).await?;
5088            /// ```
5089            ///
5090            /// Return `Err(...)` from the callback to abort
5091            /// iteration; the error bubbles up unchanged.
5092            pub async fn each<F, Fut>(
5093                batch: i64,
5094                pool: &#root::sql::Pool,
5095                mut cb: F,
5096            ) -> ::core::result::Result<(), #root::sql::ExecError>
5097            where
5098                F: ::core::ops::FnMut(Self) -> Fut,
5099                Fut: ::core::future::Future<
5100                    Output = ::core::result::Result<(), #root::sql::ExecError>,
5101                >,
5102            {
5103                use #root::sql::FetcherPool as _;
5104                let pk_col = match Self::primary_key_column() {
5105                    ::core::option::Option::Some(c) => c,
5106                    ::core::option::Option::None => {
5107                        return ::core::result::Result::Ok(());
5108                    }
5109                };
5110                let mut last_seen: i64 = i64::MIN;
5111                loop {
5112                    let key = ::std::format!("{}__gt", pk_col);
5113                    let rows: ::std::vec::Vec<Self> =
5114                        #root::query::QuerySet::<Self>::default()
5115                            .filter(key.as_str(), last_seen)
5116                            .order_by(&[(pk_col, false)])
5117                            .limit(batch)
5118                            .fetch(pool)
5119                            .await?;
5120                    if rows.is_empty() {
5121                        return ::core::result::Result::Ok(());
5122                    }
5123                    let len = rows.len() as i64;
5124                    let max_pk = match rows
5125                        .last()
5126                        .map(|r| r.__rustango_pk_value())
5127                    {
5128                        ::core::option::Option::Some(
5129                            #root::core::SqlValue::I64(v),
5130                        ) => v,
5131                        ::core::option::Option::Some(
5132                            #root::core::SqlValue::I32(v),
5133                        ) => i64::from(v),
5134                        _ => return ::core::result::Result::Ok(()),
5135                    };
5136                    for row in rows {
5137                        cb(row).await?;
5138                    }
5139                    if len < batch {
5140                        return ::core::result::Result::Ok(());
5141                    }
5142                    last_seen = max_pk;
5143                }
5144            }
5145
5146            /// Delete every row of this model — `TRUNCATE TABLE
5147            /// <table> RESTART IDENTITY CASCADE` on Postgres,
5148            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
5149            /// support `TRUNCATE` inside foreign-key constraints
5150            /// or — for SQLite — at all). Eloquent `Model::truncate()`
5151            /// parity.
5152            ///
5153            /// **Use only in tests / fixture-reset flows.** Production
5154            /// writes through this would silently bypass the
5155            /// `pre_delete` / `post_delete` signals (no per-row hooks
5156            /// fire on a TRUNCATE / bulk DELETE FROM). An audited model
5157            /// gets one bulk audit entry, not one per row. Global scopes
5158            /// do not apply: every row goes, generic M2M links too.
5159            ///
5160            /// # Errors
5161            /// As [`truncate`].
5162            ///
5163            /// [`truncate`]: rustango::sql::truncate
5164            pub async fn truncate(
5165                pool: &#root::sql::Pool,
5166            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5167                #truncate_run
5168            }
5169
5170            /// Bulk-delete every row whose primary key is in
5171            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
5172            /// Returns the affected row count.
5173            ///
5174            /// Eloquent `Model::destroy([1, 2, 3])` parity.
5175            /// Empty `pks` is a no-op (returns 0).
5176            ///
5177            /// Accepts any iterable whose elements are
5178            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
5179            /// `[i64; N]`, etc.
5180            ///
5181            /// # Errors
5182            /// As `delete_pool`.
5183            pub async fn destroy<V>(
5184                pks: impl ::core::iter::IntoIterator<Item = V>,
5185                pool: &#root::sql::Pool,
5186            ) -> ::core::result::Result<u64, #root::sql::ExecError>
5187            where
5188                V: ::core::convert::Into<#root::core::SqlValue>,
5189            {
5190                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5191                    pks.into_iter().map(::core::convert::Into::into).collect();
5192                if _values.is_empty() {
5193                    return ::core::result::Result::Ok(0);
5194                }
5195                let _pk = <Self as #root::core::Model>::SCHEMA
5196                    .primary_key()
5197                    .ok_or_else(|| {
5198                        #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
5199                    })?;
5200                let _query = #root::query::QuerySet::<Self>::default()
5201                    .filter_op(
5202                        _pk.name,
5203                        #root::core::Op::In,
5204                        #root::core::SqlValue::List(_values),
5205                    )
5206                    .compile_delete()?;
5207                #bulk_delete_run
5208            }
5209
5210            /// Fetch every row where `<col> = <val>`. Eloquent
5211            /// `Model::where($col, $val)->get()` parity.
5212            ///
5213            /// Thin wrapper over `QuerySet::<Self>::default()
5214            /// .filter(col, val).fetch(pool)`. For one row,
5215            /// use [`Self::first_where`]; for a chain that
5216            /// needs further `.filter()` / `.order_by()` /
5217            /// `.limit()`, drop down to `Self::query().filter(...)`
5218            /// directly.
5219            ///
5220            /// `val` accepts any value `Into<SqlValue>` so plain
5221            /// strings, ints, UUIDs, etc. all work.
5222            ///
5223            /// # Errors
5224            /// As [`FetcherPool::fetch`].
5225            ///
5226            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5227            pub async fn where_(
5228                col: &str,
5229                val: impl ::core::convert::Into<#root::core::SqlValue>,
5230                pool: &#root::sql::Pool,
5231            ) -> ::core::result::Result<
5232                ::std::vec::Vec<Self>,
5233                #root::sql::ExecError,
5234            > {
5235                use #root::sql::FetcherPool as _;
5236                #root::query::QuerySet::<Self>::default()
5237                    .filter(col, val)
5238                    .fetch(pool)
5239                    .await
5240            }
5241
5242            /// Fetch every row where `<col> IN (vals)`. Eloquent
5243            /// `Model::whereIn($col, $vals)->get()` parity. Empty
5244            /// `vals` returns no rows (matches SQL's empty-IN
5245            /// semantics).
5246            ///
5247            /// `vals` accepts any iterable whose items are
5248            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5249            /// etc.
5250            ///
5251            /// # Errors
5252            /// As [`FetcherPool::fetch`].
5253            ///
5254            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5255            pub async fn where_in<V>(
5256                col: &str,
5257                vals: impl ::core::iter::IntoIterator<Item = V>,
5258                pool: &#root::sql::Pool,
5259            ) -> ::core::result::Result<
5260                ::std::vec::Vec<Self>,
5261                #root::sql::ExecError,
5262            >
5263            where
5264                V: ::core::convert::Into<#root::core::SqlValue>,
5265            {
5266                use #root::sql::FetcherPool as _;
5267                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5268                    vals.into_iter().map(::core::convert::Into::into).collect();
5269                if _values.is_empty() {
5270                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5271                }
5272                let _key = ::std::format!("{}__in", col);
5273                #root::query::QuerySet::<Self>::default()
5274                    .filter(&_key, #root::core::SqlValue::List(_values))
5275                    .fetch(pool)
5276                    .await
5277            }
5278
5279            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5280            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5281            /// `vals` returns every row (matches SQL's empty-NOT-IN
5282            /// semantics — vacuously true for every row).
5283            ///
5284            /// # Errors
5285            /// As [`FetcherPool::fetch`].
5286            ///
5287            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5288            pub async fn where_not_in<V>(
5289                col: &str,
5290                vals: impl ::core::iter::IntoIterator<Item = V>,
5291                pool: &#root::sql::Pool,
5292            ) -> ::core::result::Result<
5293                ::std::vec::Vec<Self>,
5294                #root::sql::ExecError,
5295            >
5296            where
5297                V: ::core::convert::Into<#root::core::SqlValue>,
5298            {
5299                use #root::sql::FetcherPool as _;
5300                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5301                    vals.into_iter().map(::core::convert::Into::into).collect();
5302                if _values.is_empty() {
5303                    return #root::query::QuerySet::<Self>::default()
5304                        .fetch(pool)
5305                        .await;
5306                }
5307                let _key = ::std::format!("{}__not_in", col);
5308                #root::query::QuerySet::<Self>::default()
5309                    .filter(&_key, #root::core::SqlValue::List(_values))
5310                    .fetch(pool)
5311                    .await
5312            }
5313
5314            /// Fetch every row where `<col> IS NULL`. Eloquent
5315            /// `Model::whereNull($col)->get()` parity.
5316            ///
5317            /// # Errors
5318            /// As [`FetcherPool::fetch`].
5319            ///
5320            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5321            pub async fn where_null(
5322                col: &str,
5323                pool: &#root::sql::Pool,
5324            ) -> ::core::result::Result<
5325                ::std::vec::Vec<Self>,
5326                #root::sql::ExecError,
5327            > {
5328                use #root::sql::FetcherPool as _;
5329                let _key = ::std::format!("{}__isnull", col);
5330                #root::query::QuerySet::<Self>::default()
5331                    .filter(&_key, #root::core::SqlValue::Bool(true))
5332                    .fetch(pool)
5333                    .await
5334            }
5335
5336            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5337            /// `Model::whereNotNull($col)->get()` parity.
5338            ///
5339            /// # Errors
5340            /// As [`FetcherPool::fetch`].
5341            ///
5342            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5343            pub async fn where_not_null(
5344                col: &str,
5345                pool: &#root::sql::Pool,
5346            ) -> ::core::result::Result<
5347                ::std::vec::Vec<Self>,
5348                #root::sql::ExecError,
5349            > {
5350                use #root::sql::FetcherPool as _;
5351                let _key = ::std::format!("{}__isnull", col);
5352                #root::query::QuerySet::<Self>::default()
5353                    .filter(&_key, #root::core::SqlValue::Bool(false))
5354                    .fetch(pool)
5355                    .await
5356            }
5357
5358            /// Fetch up to `n` rows in random order. Eloquent
5359            /// `Model::inRandomOrder()->limit($n)->get()` /
5360            /// `Model::query()->inRandomOrder()->get()->take($n)`
5361            /// parity. **Performance caveat**: random ordering
5362            /// forces a full table scan + per-row random key sort;
5363            /// the optimizer cannot use an index. Prefer a
5364            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5365            ///
5366            /// # Errors
5367            /// As [`FetcherPool::fetch`].
5368            ///
5369            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5370            pub async fn random_n(
5371                n: i64,
5372                pool: &#root::sql::Pool,
5373            ) -> ::core::result::Result<
5374                ::std::vec::Vec<Self>,
5375                #root::sql::ExecError,
5376            > {
5377                use #root::sql::FetcherPool as _;
5378                #root::query::QuerySet::<Self>::default()
5379                    .order_random()
5380                    .limit(n)
5381                    .fetch(pool)
5382                    .await
5383            }
5384
5385            /// Fetch one row in random order. Eloquent
5386            /// `Model::inRandomOrder()->first()` parity. Same
5387            /// performance caveat as [`Self::random_n`].
5388            ///
5389            /// # Errors
5390            /// As [`FetcherPool::fetch`].
5391            ///
5392            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5393            pub async fn random(
5394                pool: &#root::sql::Pool,
5395            ) -> ::core::result::Result<
5396                ::core::option::Option<Self>,
5397                #root::sql::ExecError,
5398            > {
5399                ::core::result::Result::Ok(
5400                    Self::random_n(1, pool).await?.into_iter().next(),
5401                )
5402            }
5403
5404            /// Fetch every row ordered ASC by `field`. Eloquent
5405            /// `Model::oldest($field)->get()` parity — the multi-row
5406            /// counterpart of [`Self::earliest`].
5407            ///
5408            /// # Errors
5409            /// As [`FetcherPool::fetch`].
5410            ///
5411            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5412            pub async fn oldest(
5413                field: &str,
5414                pool: &#root::sql::Pool,
5415            ) -> ::core::result::Result<
5416                ::std::vec::Vec<Self>,
5417                #root::sql::ExecError,
5418            > {
5419                use #root::sql::FetcherPool as _;
5420                #root::query::QuerySet::<Self>::default()
5421                    .order_by(&[(field, false)])
5422                    .fetch(pool)
5423                    .await
5424            }
5425
5426            /// Fetch every row ordered DESC by `field`. Eloquent
5427            /// `Model::latest($field)->get()` parity — the multi-row
5428            /// counterpart of [`Self::latest`].
5429            ///
5430            /// # Errors
5431            /// As [`FetcherPool::fetch`].
5432            ///
5433            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5434            pub async fn newest(
5435                field: &str,
5436                pool: &#root::sql::Pool,
5437            ) -> ::core::result::Result<
5438                ::std::vec::Vec<Self>,
5439                #root::sql::ExecError,
5440            > {
5441                use #root::sql::FetcherPool as _;
5442                #root::query::QuerySet::<Self>::default()
5443                    .order_by(&[(field, true)])
5444                    .fetch(pool)
5445                    .await
5446            }
5447
5448            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5449            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5450            /// Routes through the existing `__year` lookup suffix
5451            /// (issue #829).
5452            ///
5453            /// # Errors
5454            /// As [`FetcherPool::fetch`].
5455            ///
5456            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5457            pub async fn where_year(
5458                col: &str,
5459                year: i64,
5460                pool: &#root::sql::Pool,
5461            ) -> ::core::result::Result<
5462                ::std::vec::Vec<Self>,
5463                #root::sql::ExecError,
5464            > {
5465                use #root::sql::FetcherPool as _;
5466                let _key = ::std::format!("{}__year", col);
5467                #root::query::QuerySet::<Self>::default()
5468                    .filter(&_key, #root::core::SqlValue::I64(year))
5469                    .fetch(pool)
5470                    .await
5471            }
5472
5473            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5474            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5475            /// `month` is 1–12.
5476            ///
5477            /// # Errors
5478            /// As [`FetcherPool::fetch`].
5479            ///
5480            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5481            pub async fn where_month(
5482                col: &str,
5483                month: i64,
5484                pool: &#root::sql::Pool,
5485            ) -> ::core::result::Result<
5486                ::std::vec::Vec<Self>,
5487                #root::sql::ExecError,
5488            > {
5489                use #root::sql::FetcherPool as _;
5490                let _key = ::std::format!("{}__month", col);
5491                #root::query::QuerySet::<Self>::default()
5492                    .filter(&_key, #root::core::SqlValue::I64(month))
5493                    .fetch(pool)
5494                    .await
5495            }
5496
5497            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5498            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5499            /// `day` is 1–31.
5500            ///
5501            /// # Errors
5502            /// As [`FetcherPool::fetch`].
5503            ///
5504            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5505            pub async fn where_day(
5506                col: &str,
5507                day: i64,
5508                pool: &#root::sql::Pool,
5509            ) -> ::core::result::Result<
5510                ::std::vec::Vec<Self>,
5511                #root::sql::ExecError,
5512            > {
5513                use #root::sql::FetcherPool as _;
5514                let _key = ::std::format!("{}__day", col);
5515                #root::query::QuerySet::<Self>::default()
5516                    .filter(&_key, #root::core::SqlValue::I64(day))
5517                    .fetch(pool)
5518                    .await
5519            }
5520
5521            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5522            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5523            /// `hour` is 0–23.
5524            ///
5525            /// # Errors
5526            /// As [`FetcherPool::fetch`].
5527            ///
5528            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5529            pub async fn where_hour(
5530                col: &str,
5531                hour: i64,
5532                pool: &#root::sql::Pool,
5533            ) -> ::core::result::Result<
5534                ::std::vec::Vec<Self>,
5535                #root::sql::ExecError,
5536            > {
5537                use #root::sql::FetcherPool as _;
5538                let _key = ::std::format!("{}__hour", col);
5539                #root::query::QuerySet::<Self>::default()
5540                    .filter(&_key, #root::core::SqlValue::I64(hour))
5541                    .fetch(pool)
5542                    .await
5543            }
5544
5545            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5546            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5547            /// `minute` is 0–59.
5548            ///
5549            /// # Errors
5550            /// As [`FetcherPool::fetch`].
5551            ///
5552            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5553            pub async fn where_minute(
5554                col: &str,
5555                minute: i64,
5556                pool: &#root::sql::Pool,
5557            ) -> ::core::result::Result<
5558                ::std::vec::Vec<Self>,
5559                #root::sql::ExecError,
5560            > {
5561                use #root::sql::FetcherPool as _;
5562                let _key = ::std::format!("{}__minute", col);
5563                #root::query::QuerySet::<Self>::default()
5564                    .filter(&_key, #root::core::SqlValue::I64(minute))
5565                    .fetch(pool)
5566                    .await
5567            }
5568
5569            /// Fetch every row where `<col> LIKE <pattern>` —
5570            /// caller-supplied pattern (must include `%` / `_`
5571            /// wildcards manually). Eloquent `Model::whereLike`
5572            /// parity.
5573            ///
5574            /// # Errors
5575            /// As [`FetcherPool::fetch`].
5576            ///
5577            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5578            pub async fn where_like(
5579                col: &str,
5580                pattern: impl ::core::convert::Into<::std::string::String>,
5581                pool: &#root::sql::Pool,
5582            ) -> ::core::result::Result<
5583                ::std::vec::Vec<Self>,
5584                #root::sql::ExecError,
5585            > {
5586                use #root::sql::FetcherPool as _;
5587                let _key = ::std::format!("{}__like", col);
5588                #root::query::QuerySet::<Self>::default()
5589                    .filter(
5590                        &_key,
5591                        #root::core::SqlValue::String(pattern.into()),
5592                    )
5593                    .fetch(pool)
5594                    .await
5595            }
5596
5597            /// Fetch every row where `<col> ILIKE <pattern>` —
5598            /// case-insensitive LIKE (PG native, MySQL/SQLite
5599            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5600            ///
5601            /// # Errors
5602            /// As [`FetcherPool::fetch`].
5603            ///
5604            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5605            pub async fn where_ilike(
5606                col: &str,
5607                pattern: impl ::core::convert::Into<::std::string::String>,
5608                pool: &#root::sql::Pool,
5609            ) -> ::core::result::Result<
5610                ::std::vec::Vec<Self>,
5611                #root::sql::ExecError,
5612            > {
5613                use #root::sql::FetcherPool as _;
5614                let _key = ::std::format!("{}__ilike", col);
5615                #root::query::QuerySet::<Self>::default()
5616                    .filter(
5617                        &_key,
5618                        #root::core::SqlValue::String(pattern.into()),
5619                    )
5620                    .fetch(pool)
5621                    .await
5622            }
5623
5624            /// Fetch every row where `<col>` starts with `prefix`
5625            /// (auto-appends `%`). Eloquent
5626            /// `whereLike("col", "$prefix%")` parity.
5627            ///
5628            /// # Errors
5629            /// As [`FetcherPool::fetch`].
5630            ///
5631            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5632            pub async fn where_starts_with(
5633                col: &str,
5634                prefix: impl ::core::convert::Into<::std::string::String>,
5635                pool: &#root::sql::Pool,
5636            ) -> ::core::result::Result<
5637                ::std::vec::Vec<Self>,
5638                #root::sql::ExecError,
5639            > {
5640                use #root::sql::FetcherPool as _;
5641                let _key = ::std::format!("{}__startswith", col);
5642                #root::query::QuerySet::<Self>::default()
5643                    .filter(
5644                        &_key,
5645                        #root::core::SqlValue::String(prefix.into()),
5646                    )
5647                    .fetch(pool)
5648                    .await
5649            }
5650
5651            /// Fetch every row where `<col>` ends with `suffix`
5652            /// (auto-prepends `%`). Eloquent
5653            /// `whereLike("col", "%$suffix")` parity.
5654            ///
5655            /// # Errors
5656            /// As [`FetcherPool::fetch`].
5657            ///
5658            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5659            pub async fn where_ends_with(
5660                col: &str,
5661                suffix: impl ::core::convert::Into<::std::string::String>,
5662                pool: &#root::sql::Pool,
5663            ) -> ::core::result::Result<
5664                ::std::vec::Vec<Self>,
5665                #root::sql::ExecError,
5666            > {
5667                use #root::sql::FetcherPool as _;
5668                let _key = ::std::format!("{}__endswith", col);
5669                #root::query::QuerySet::<Self>::default()
5670                    .filter(
5671                        &_key,
5672                        #root::core::SqlValue::String(suffix.into()),
5673                    )
5674                    .fetch(pool)
5675                    .await
5676            }
5677
5678            /// Fetch every row where `<col>` contains `substr`
5679            /// (auto-wraps with `%`).
5680            /// Eloquent `whereLike("col", "%$substr%")` parity.
5681            ///
5682            /// # Errors
5683            /// As [`FetcherPool::fetch`].
5684            ///
5685            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5686            pub async fn where_contains(
5687                col: &str,
5688                substr: impl ::core::convert::Into<::std::string::String>,
5689                pool: &#root::sql::Pool,
5690            ) -> ::core::result::Result<
5691                ::std::vec::Vec<Self>,
5692                #root::sql::ExecError,
5693            > {
5694                use #root::sql::FetcherPool as _;
5695                let _key = ::std::format!("{}__contains", col);
5696                #root::query::QuerySet::<Self>::default()
5697                    .filter(
5698                        &_key,
5699                        #root::core::SqlValue::String(substr.into()),
5700                    )
5701                    .fetch(pool)
5702                    .await
5703            }
5704
5705            /// Fetch every row where `<col> > val`. Eloquent
5706            /// `Model::where($col, ">", $val)->get()` parity.
5707            ///
5708            /// # Errors
5709            /// As [`FetcherPool::fetch`].
5710            ///
5711            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5712            pub async fn where_gt(
5713                col: &str,
5714                val: impl ::core::convert::Into<#root::core::SqlValue>,
5715                pool: &#root::sql::Pool,
5716            ) -> ::core::result::Result<
5717                ::std::vec::Vec<Self>,
5718                #root::sql::ExecError,
5719            > {
5720                use #root::sql::FetcherPool as _;
5721                let _key = ::std::format!("{}__gt", col);
5722                #root::query::QuerySet::<Self>::default()
5723                    .filter(&_key, val)
5724                    .fetch(pool)
5725                    .await
5726            }
5727
5728            /// Fetch every row where `<col> >= val`. Eloquent
5729            /// `Model::where($col, ">=", $val)->get()` parity.
5730            ///
5731            /// # Errors
5732            /// As [`FetcherPool::fetch`].
5733            ///
5734            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5735            pub async fn where_gte(
5736                col: &str,
5737                val: impl ::core::convert::Into<#root::core::SqlValue>,
5738                pool: &#root::sql::Pool,
5739            ) -> ::core::result::Result<
5740                ::std::vec::Vec<Self>,
5741                #root::sql::ExecError,
5742            > {
5743                use #root::sql::FetcherPool as _;
5744                let _key = ::std::format!("{}__gte", col);
5745                #root::query::QuerySet::<Self>::default()
5746                    .filter(&_key, val)
5747                    .fetch(pool)
5748                    .await
5749            }
5750
5751            /// Fetch every row where `<col> < val`. Eloquent
5752            /// `Model::where($col, "<", $val)->get()` parity.
5753            ///
5754            /// # Errors
5755            /// As [`FetcherPool::fetch`].
5756            ///
5757            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5758            pub async fn where_lt(
5759                col: &str,
5760                val: impl ::core::convert::Into<#root::core::SqlValue>,
5761                pool: &#root::sql::Pool,
5762            ) -> ::core::result::Result<
5763                ::std::vec::Vec<Self>,
5764                #root::sql::ExecError,
5765            > {
5766                use #root::sql::FetcherPool as _;
5767                let _key = ::std::format!("{}__lt", col);
5768                #root::query::QuerySet::<Self>::default()
5769                    .filter(&_key, val)
5770                    .fetch(pool)
5771                    .await
5772            }
5773
5774            /// Fetch every row where `<col> <= val`. Eloquent
5775            /// `Model::where($col, "<=", $val)->get()` parity.
5776            ///
5777            /// # Errors
5778            /// As [`FetcherPool::fetch`].
5779            ///
5780            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5781            pub async fn where_lte(
5782                col: &str,
5783                val: impl ::core::convert::Into<#root::core::SqlValue>,
5784                pool: &#root::sql::Pool,
5785            ) -> ::core::result::Result<
5786                ::std::vec::Vec<Self>,
5787                #root::sql::ExecError,
5788            > {
5789                use #root::sql::FetcherPool as _;
5790                let _key = ::std::format!("{}__lte", col);
5791                #root::query::QuerySet::<Self>::default()
5792                    .filter(&_key, val)
5793                    .fetch(pool)
5794                    .await
5795            }
5796
5797            /// Fetch every row where `<col> <> val`. Eloquent
5798            /// `Model::where($col, "!=", $val)->get()` parity.
5799            ///
5800            /// # Errors
5801            /// As [`FetcherPool::fetch`].
5802            ///
5803            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5804            pub async fn where_ne(
5805                col: &str,
5806                val: impl ::core::convert::Into<#root::core::SqlValue>,
5807                pool: &#root::sql::Pool,
5808            ) -> ::core::result::Result<
5809                ::std::vec::Vec<Self>,
5810                #root::sql::ExecError,
5811            > {
5812                use #root::sql::FetcherPool as _;
5813                let _key = ::std::format!("{}__ne", col);
5814                #root::query::QuerySet::<Self>::default()
5815                    .filter(&_key, val)
5816                    .fetch(pool)
5817                    .await
5818            }
5819
5820            /// Fetch every row matching `<col> = val` for ANY of the
5821            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5822            /// parity. Empty `cols` returns no rows.
5823            ///
5824            /// Resolves each `&str` column to its SCHEMA-registered
5825            /// `&'static str` once and builds a single OR-composed
5826            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5827            ///
5828            /// # Errors
5829            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5830            /// when any column is not declared on the model.
5831            ///
5832            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5833            pub async fn where_any(
5834                cols: &[&str],
5835                val: impl ::core::convert::Into<#root::core::SqlValue>,
5836                pool: &#root::sql::Pool,
5837            ) -> ::core::result::Result<
5838                ::std::vec::Vec<Self>,
5839                #root::sql::ExecError,
5840            > {
5841                Self::__where_multi(cols, val, false, pool).await
5842            }
5843
5844            /// Fetch every row matching `<col> = val` for ALL listed
5845            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5846            /// Empty `cols` returns every row (vacuous AND).
5847            ///
5848            /// # Errors
5849            /// As [`Self::where_any`].
5850            pub async fn where_all(
5851                cols: &[&str],
5852                val: impl ::core::convert::Into<#root::core::SqlValue>,
5853                pool: &#root::sql::Pool,
5854            ) -> ::core::result::Result<
5855                ::std::vec::Vec<Self>,
5856                #root::sql::ExecError,
5857            > {
5858                Self::__where_multi(cols, val, true, pool).await
5859            }
5860
5861            /// Internal: build a Q expression composing `cols` via
5862            /// AND (`all`) or OR (`!all`), then fetch. Backs
5863            /// `where_any` / `where_all`.
5864            #[doc(hidden)]
5865            pub async fn __where_multi(
5866                cols: &[&str],
5867                val: impl ::core::convert::Into<#root::core::SqlValue>,
5868                all: bool,
5869                pool: &#root::sql::Pool,
5870            ) -> ::core::result::Result<
5871                ::std::vec::Vec<Self>,
5872                #root::sql::ExecError,
5873            > {
5874                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5875                    .await
5876            }
5877
5878            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5879            /// parity. PK-ordered
5880            /// is NOT guaranteed without an explicit `order_by` —
5881            /// drop into `Self::query()` for that.
5882            ///
5883            /// # Errors
5884            /// As [`FetcherPool::fetch`].
5885            ///
5886            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5887            pub async fn take(
5888                n: i64,
5889                pool: &#root::sql::Pool,
5890            ) -> ::core::result::Result<
5891                ::std::vec::Vec<Self>,
5892                #root::sql::ExecError,
5893            > {
5894                use #root::sql::FetcherPool as _;
5895                #root::query::QuerySet::<Self>::default()
5896                    .limit(n)
5897                    .fetch(pool)
5898                    .await
5899            }
5900
5901            /// Fetch the page-th window of `per_page` rows
5902            /// (1-indexed). Eloquent
5903            /// `Model::query()->forPage($page, $perPage)->get()`
5904            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5905            /// LIMIT per_page`; for large offsets this is O(N) —
5906            /// prefer keyset pagination via PK on hot paths.
5907            ///
5908            /// # Errors
5909            /// As [`FetcherPool::fetch`].
5910            ///
5911            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5912            pub async fn for_page(
5913                page: i64,
5914                per_page: i64,
5915                pool: &#root::sql::Pool,
5916            ) -> ::core::result::Result<
5917                ::std::vec::Vec<Self>,
5918                #root::sql::ExecError,
5919            > {
5920                use #root::sql::FetcherPool as _;
5921                let _offset = #root::list_params::page_offset(page, per_page);
5922                #root::query::QuerySet::<Self>::default()
5923                    .limit(per_page)
5924                    .offset(_offset)
5925                    .fetch(pool)
5926                    .await
5927            }
5928
5929            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5930            /// one page of rows AND the total row count in one
5931            /// call. Returns `(rows, total)`. Two queries: a
5932            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5933            /// for the total.
5934            ///
5935            /// Useful for paginated UIs that need both the visible
5936            /// rows AND a "Page X of Y" / total-count widget. For
5937            /// large tables prefer keyset pagination + cached count;
5938            /// every call to `paginate` re-counts the full table.
5939            ///
5940            /// Same 1-indexed `page` convention as [`Self::for_page`].
5941            ///
5942            /// # Errors
5943            /// As [`Self::for_page`] and [`Self::count`].
5944            pub async fn paginate(
5945                page: i64,
5946                per_page: i64,
5947                pool: &#root::sql::Pool,
5948            ) -> ::core::result::Result<
5949                (::std::vec::Vec<Self>, i64),
5950                #root::sql::ExecError,
5951            > {
5952                // Route through the queryset rather than `Self::count`:
5953                // the `count` inherent method is suppressed on models
5954                // that declare a field named `count` (field/shortcut
5955                // collision guard), and `paginate` must still compile
5956                // for those models.
5957                let total = {
5958                    use #root::sql::CounterPool as _;
5959                    #root::query::QuerySet::<Self>::default()
5960                        .count(pool)
5961                        .await?
5962                };
5963                let rows = Self::for_page(page, per_page, pool).await?;
5964                ::core::result::Result::Ok((rows, total))
5965            }
5966
5967            /// Bulk-update — set `set_col = set_val` on every row
5968            /// matching `where_col = where_val`. Returns affected row
5969            /// count. Eloquent
5970            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5971            /// parity.
5972            ///
5973            /// For multi-column updates drop into the queryset
5974            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5975            ///
5976            /// # Errors
5977            /// As [`UpdaterPool::execute_pool`].
5978            ///
5979            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5980            pub async fn update_where(
5981                where_col: &str,
5982                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5983                set_col: &str,
5984                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5985                pool: &#root::sql::Pool,
5986            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5987                let _query = #root::query::QuerySet::<Self>::default()
5988                    .filter(where_col, where_val)
5989                    .update()
5990                    .set(set_col, set_val)
5991                    .compile()?;
5992                #bulk_update_run
5993            }
5994
5995            /// Bulk-delete — remove every row matching
5996            /// `where_col = where_val`. Returns affected row count.
5997            /// Eloquent
5998            /// `Model::where($where_col, $where_val)->delete()` parity.
5999            ///
6000            /// # Errors
6001            /// As [`#root::sql::delete_pool`].
6002            pub async fn delete_where(
6003                where_col: &str,
6004                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
6005                pool: &#root::sql::Pool,
6006            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6007                let _query = #root::query::QuerySet::<Self>::default()
6008                    .filter_op(where_col, #root::core::Op::Eq, where_val)
6009                    .compile_delete()?;
6010                #bulk_delete_run
6011            }
6012
6013            /// Bulk-update — set `set_col = set_val` on EVERY row of
6014            /// this model's table. **No WHERE clause** — use with
6015            /// care. Eloquent `Model::query()->update([$col => $val])`
6016            /// parity.
6017            ///
6018            /// Use for backfills, one-shot reset flows, etc. The
6019            /// returned count is rows affected.
6020            ///
6021            /// # Errors
6022            /// As [`UpdaterPool::execute_pool`].
6023            ///
6024            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
6025            pub async fn update_all(
6026                set_col: &str,
6027                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
6028                pool: &#root::sql::Pool,
6029            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6030                let _query = #root::query::QuerySet::<Self>::default()
6031                    .update()
6032                    .set(set_col, set_val)
6033                    .compile()?;
6034                #bulk_update_run
6035            }
6036
6037            /// Fetch every row where `<col> NOT LIKE <pattern>`.
6038            /// Eloquent `Model::whereNotLike` parity. Pattern is
6039            /// passed verbatim — caller controls `%` / `_`.
6040            ///
6041            /// # Errors
6042            /// As [`FetcherPool::fetch`].
6043            ///
6044            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6045            pub async fn where_not_like(
6046                col: &str,
6047                pattern: impl ::core::convert::Into<::std::string::String>,
6048                pool: &#root::sql::Pool,
6049            ) -> ::core::result::Result<
6050                ::std::vec::Vec<Self>,
6051                #root::sql::ExecError,
6052            > {
6053                use #root::sql::FetcherPool as _;
6054                let _key = ::std::format!("{}__not_like", col);
6055                #root::query::QuerySet::<Self>::default()
6056                    .filter(
6057                        &_key,
6058                        #root::core::SqlValue::String(pattern.into()),
6059                    )
6060                    .fetch(pool)
6061                    .await
6062            }
6063
6064            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
6065            /// case-insensitive `NOT LIKE` (PG native, MySQL /
6066            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
6067            ///
6068            /// # Errors
6069            /// As [`FetcherPool::fetch`].
6070            ///
6071            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6072            pub async fn where_not_ilike(
6073                col: &str,
6074                pattern: impl ::core::convert::Into<::std::string::String>,
6075                pool: &#root::sql::Pool,
6076            ) -> ::core::result::Result<
6077                ::std::vec::Vec<Self>,
6078                #root::sql::ExecError,
6079            > {
6080                use #root::sql::FetcherPool as _;
6081                let _key = ::std::format!("{}__not_ilike", col);
6082                #root::query::QuerySet::<Self>::default()
6083                    .filter(
6084                        &_key,
6085                        #root::core::SqlValue::String(pattern.into()),
6086                    )
6087                    .fetch(pool)
6088                    .await
6089            }
6090
6091            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
6092            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
6093            /// parity.
6094            ///
6095            /// # Errors
6096            /// As [`FetcherPool::fetch`].
6097            ///
6098            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6099            pub async fn where_not_between(
6100                col: &str,
6101                lo: impl ::core::convert::Into<#root::core::SqlValue>,
6102                hi: impl ::core::convert::Into<#root::core::SqlValue>,
6103                pool: &#root::sql::Pool,
6104            ) -> ::core::result::Result<
6105                ::std::vec::Vec<Self>,
6106                #root::sql::ExecError,
6107            > {
6108                use #root::sql::FetcherPool as _;
6109                let _key = ::std::format!("{}__not_between", col);
6110                let _vals = #root::core::SqlValue::List(::std::vec![
6111                    ::core::convert::Into::into(lo),
6112                    ::core::convert::Into::into(hi),
6113                ]);
6114                #root::query::QuerySet::<Self>::default()
6115                    .filter(&_key, _vals)
6116                    .fetch(pool)
6117                    .await
6118            }
6119
6120            /// Returns the SQL table name for this model. Eloquent
6121            /// `$model->getTable()` parity.
6122            #[must_use]
6123            pub fn table_name() -> &'static str {
6124                <Self as #root::core::Model>::SCHEMA.table
6125            }
6126
6127            /// Returns the SQL column name of this model's primary
6128            /// key, or `None` when the model has no
6129            /// `#[rustango(primary_key)]`. Eloquent
6130            /// `$model->getKeyName()` parity.
6131            #[must_use]
6132            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
6133                <Self as #root::core::Model>::SCHEMA
6134                    .primary_key()
6135                    .map(|f| f.column)
6136            }
6137
6138            /// Fetch every row where `<col> BETWEEN lo AND hi`
6139            /// (inclusive on both ends — same as SQL). Eloquent
6140            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
6141            ///
6142            /// # Errors
6143            /// As [`FetcherPool::fetch`].
6144            ///
6145            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6146            pub async fn where_between(
6147                col: &str,
6148                lo: impl ::core::convert::Into<#root::core::SqlValue>,
6149                hi: impl ::core::convert::Into<#root::core::SqlValue>,
6150                pool: &#root::sql::Pool,
6151            ) -> ::core::result::Result<
6152                ::std::vec::Vec<Self>,
6153                #root::sql::ExecError,
6154            > {
6155                use #root::sql::FetcherPool as _;
6156                let _key = ::std::format!("{}__between", col);
6157                let _vals = #root::core::SqlValue::List(::std::vec![
6158                    ::core::convert::Into::into(lo),
6159                    ::core::convert::Into::into(hi),
6160                ]);
6161                #root::query::QuerySet::<Self>::default()
6162                    .filter(&_key, _vals)
6163                    .fetch(pool)
6164                    .await
6165            }
6166
6167            /// Fetch the first row where `<col> = <val>`. Returns
6168            /// `Ok(None)` when no row matches. Eloquent
6169            /// `Model::firstWhere($col, $val)` parity.
6170            ///
6171            /// Thin wrapper over `QuerySet::<Self>::default()
6172            /// .filter(col, val).first(pool)`. Use this when you
6173            /// want one row identified by a non-PK column (e.g.
6174            /// `User::first_where_pool("email", "x@y.com", &pool)`).
6175            ///
6176            /// `val` accepts any value `Into<SqlValue>` so plain
6177            /// strings, ints, UUIDs, etc. all work.
6178            ///
6179            /// # Errors
6180            /// As `QuerySet::first`.
6181            pub async fn first_where(
6182                col: &str,
6183                val: impl ::core::convert::Into<#root::core::SqlValue>,
6184                pool: &#root::sql::Pool,
6185            ) -> ::core::result::Result<
6186                ::core::option::Option<Self>,
6187                #root::sql::ExecError,
6188            > {
6189                #root::query::QuerySet::<Self>::default()
6190                    .filter(col, val)
6191                    .first(pool)
6192                    .await
6193            }
6194
6195            #value_method
6196
6197            /// Fetch the row with the largest `field` value —
6198            /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
6199            /// `Ok(None)` for an empty table. Eloquent
6200            /// `Model::latest($field)->first()` parity.
6201            /// Thin wrapper over `QuerySet::<Self>::default()
6202            /// .latest(field, pool)`.
6203            ///
6204            /// **Field name** is the Rust field ident as a string
6205            /// (not the SQL column). It is a runtime `&str`, so an
6206            /// unknown one compiles and surfaces as
6207            /// `ExecError::Query(QueryError::UnknownField)` when the
6208            /// query runs.
6209            ///
6210            /// # Errors
6211            /// As `QuerySet::latest`.
6212            pub async fn latest(
6213                field: &str,
6214                pool: &#root::sql::Pool,
6215            ) -> ::core::result::Result<
6216                ::core::option::Option<Self>,
6217                #root::sql::ExecError,
6218            > {
6219                #root::query::QuerySet::<Self>::default()
6220                    .latest(field, pool)
6221                    .await
6222            }
6223
6224            /// Sibling of [`Self::latest`] — fetches the row
6225            /// with the smallest `field` value (`ORDER BY <field>
6226            /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
6227            /// ->first()` parity.
6228            ///
6229            /// # Errors
6230            /// As [`Self::latest`].
6231            pub async fn earliest(
6232                field: &str,
6233                pool: &#root::sql::Pool,
6234            ) -> ::core::result::Result<
6235                ::core::option::Option<Self>,
6236                #root::sql::ExecError,
6237            > {
6238                #root::query::QuerySet::<Self>::default()
6239                    .earliest(field, pool)
6240                    .await
6241            }
6242
6243            #count_method
6244
6245            /// `true` when the table contains at least one row.
6246            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6247            /// over `QuerySet::<Self>::default().exists(pool)`.
6248            ///
6249            /// # Errors
6250            /// As [`ExistsPool::exists`].
6251            ///
6252            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6253            pub async fn exists(
6254                pool: &#root::sql::Pool,
6255            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6256                use #root::sql::ExistsPool as _;
6257                #root::query::QuerySet::<Self>::default()
6258                    .exists(pool)
6259                    .await
6260            }
6261
6262            /// Inverse of [`Self::exists`] — returns `true` when
6263            /// the table has zero rows. Eloquent
6264            /// `Model::doesntExist()` parity.
6265            ///
6266            /// # Errors
6267            /// As [`Self::exists`].
6268            pub async fn doesnt_exist(
6269                pool: &#root::sql::Pool,
6270            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6271                Self::exists(pool).await.map(|e| !e)
6272            }
6273
6274            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6275            /// `true` when a row with primary key `pk` exists in
6276            /// the table. Sugar over
6277            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6278            ///
6279            /// Differs from [`Self::exists`] (which checks "any row
6280            /// in the table") by checking a specific PK existence.
6281            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6282            /// because the row is never materialized — the SQL is
6283            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6284            ///
6285            /// # Errors
6286            /// As [`#root::sql::ExistsPool::contains_pk`].
6287            pub async fn contains_pk(
6288                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6289                pool: &#root::sql::Pool,
6290            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6291                use #root::sql::ExistsPool as _;
6292                #root::query::QuerySet::<Self>::default()
6293                    .contains_pk(pool, pk)
6294                    .await
6295            }
6296
6297            #sum_method
6298            #avg_method
6299            #min_method
6300            #max_method
6301
6302            /// Fetch every row of this model from `pool`. Eloquent
6303            /// `Model::all()` parity — a thin wrapper over
6304            /// `QuerySet::<Self>::default().fetch(pool)`.
6305            ///
6306            /// **Use with care on large tables** — there's no
6307            /// pagination or limit; the entire table is materialized
6308            /// into memory. For anything beyond fixture / lookup
6309            /// tables, page through `QuerySet::<Self>::default()
6310            /// .order_by(...).limit(N).offset(M).fetch(pool)`
6311            /// or stream via `.iterator(chunk_size)`.
6312            ///
6313            /// # Errors
6314            /// As [`FetcherPool::fetch`].
6315            ///
6316            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6317            pub async fn all(
6318                pool: &#root::sql::Pool,
6319            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6320            {
6321                use #root::sql::FetcherPool as _;
6322                #root::query::QuerySet::<Self>::default()
6323                    .fetch(pool)
6324                    .await
6325            }
6326
6327            /// Look up every row whose primary key is in `pks`.
6328            /// Returns the matching rows in **inventory** order — NOT
6329            /// the order of `pks`. Empty `pks` returns an empty
6330            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6331            /// with a list) parity.
6332            ///
6333            /// Thin wrapper over `QuerySet::<Self>::default()
6334            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6335            /// Caller-supplied PKs that don't match a row are
6336            /// silently skipped (the returned vec is shorter than
6337            /// the input list). For an order-preserving / "fail
6338            /// when any missing" variant, build the queryset
6339            /// explicitly with `in_bulk` instead.
6340            ///
6341            /// Accepts any iterable whose elements are
6342            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6343            /// `Vec<Uuid>`, etc.
6344            ///
6345            /// # Errors
6346            /// As [`FetcherPool::fetch`].
6347            ///
6348            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6349            pub async fn find_many<V>(
6350                pks: impl ::core::iter::IntoIterator<Item = V>,
6351                pool: &#root::sql::Pool,
6352            ) -> ::core::result::Result<
6353                ::std::vec::Vec<Self>,
6354                #root::sql::ExecError,
6355            >
6356            where
6357                V: ::core::convert::Into<#root::core::SqlValue>,
6358            {
6359                use #root::sql::FetcherPool as _;
6360                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6361                    pks.into_iter().map(::core::convert::Into::into).collect();
6362                if _values.is_empty() {
6363                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6364                }
6365                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6366                #root::query::QuerySet::<Self>::default()
6367                    .filter(&_key, #root::core::SqlValue::List(_values))
6368                    .fetch(pool)
6369                    .await
6370            }
6371
6372            /// Look up the row whose primary key equals `pk`. Returns
6373            /// `Ok(None)` when no row matches, rather than erroring.
6374            /// Eloquent `Model::find`
6375            /// shape — accepts any value `Into<SqlValue>`.
6376            ///
6377            /// One-liner shortcut for the common
6378            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6379            /// .limit(1).fetch(pool).await?.into_iter().next()`
6380            /// dance.
6381            ///
6382            /// # Errors
6383            /// As [`FetcherPool::fetch`].
6384            ///
6385            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6386            pub async fn find(
6387                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6388                pool: &#root::sql::Pool,
6389            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6390            {
6391                use #root::sql::FetcherPool as _;
6392                let _pk_val: #root::core::SqlValue = pk.into();
6393                let mut _rows: ::std::vec::Vec<Self> =
6394                    #root::query::QuerySet::<Self>::default()
6395                        .filter(::core::stringify!(#pk_ident), _pk_val)
6396                        .limit(1)
6397                        .fetch(pool)
6398                        .await?;
6399                ::core::result::Result::Ok(_rows.into_iter().next())
6400            }
6401
6402            /// Look up the row whose primary key equals `pk`. Errors
6403            /// when no row matches — the throwing counterpart of
6404            /// [`Self::find`]. Eloquent `Model::findOrFail`
6405            /// parity.
6406            ///
6407            /// Translates the miss into
6408            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6409            /// so callers can `?`-bubble straight through the typical
6410            /// `ExecError` error chain.
6411            ///
6412            /// # Errors
6413            /// As [`Self::find`]; additionally
6414            /// [`sqlx::Error::RowNotFound`] when no row matches.
6415            ///
6416            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6417            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6418            pub async fn find_or_fail(
6419                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6420                pool: &#root::sql::Pool,
6421            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6422                match Self::find(pk, pool).await? {
6423                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6424                    ::core::option::Option::None => ::core::result::Result::Err(
6425                        #root::sql::ExecError::Driver(
6426                            #root::sql::sqlx::Error::RowNotFound,
6427                        ),
6428                    ),
6429                }
6430            }
6431
6432            /// Look up multiple rows by primary key, **failing** if
6433            /// any requested PK is missing. Eloquent
6434            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6435            /// inventory order (NOT request order — same as
6436            /// [`Self::find_many`]).
6437            ///
6438            /// Empty `pks` returns an empty `Vec` (no rows requested
6439            /// → nothing to fail on).
6440            ///
6441            /// Differs from [`Self::find_many`] in that this method
6442            /// requires every PK to resolve. If even one is missing,
6443            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6444            /// in `ExecError::Driver`). Rows are deduped at the SQL
6445            /// layer, so passing the same PK twice in `pks` counts as
6446            /// one expected row.
6447            ///
6448            /// # Errors
6449            /// As [`Self::find_many`], plus `RowNotFound` when the
6450            /// returned row count is less than the count of distinct
6451            /// requested PKs.
6452            pub async fn find_many_or_fail<V>(
6453                pks: impl ::core::iter::IntoIterator<Item = V>,
6454                pool: &#root::sql::Pool,
6455            ) -> ::core::result::Result<
6456                ::std::vec::Vec<Self>,
6457                #root::sql::ExecError,
6458            >
6459            where
6460                V: ::core::convert::Into<#root::core::SqlValue>,
6461            {
6462                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6463                    pks.into_iter().map(::core::convert::Into::into).collect();
6464                if _values.is_empty() {
6465                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6466                }
6467                // Dedup the requested PK list before counting so
6468                // duplicate-PK requests don't false-fail (the SQL
6469                // `IN (...)` clause naturally dedups too).
6470                let mut _seen: ::std::collections::HashSet<
6471                    ::std::string::String,
6472                > = ::std::collections::HashSet::new();
6473                for v in &_values {
6474                    _seen.insert(v.to_display_string());
6475                }
6476                let _expected = _seen.len();
6477                let _rows = Self::find_many(_values, pool).await?;
6478                if _rows.len() < _expected {
6479                    return ::core::result::Result::Err(
6480                        #root::sql::ExecError::Driver(
6481                            #root::sql::sqlx::Error::RowNotFound,
6482                        ),
6483                    );
6484                }
6485                ::core::result::Result::Ok(_rows)
6486            }
6487
6488            /// Find by primary key or run `fallback` to produce a
6489            /// default row to return. Eloquent
6490            /// `Model::findOr($pk, fn() => …)` parity.
6491            ///
6492            /// Unlike [`Self::find_or_fail`] (which raises on
6493            /// miss), this is the "give me something sensible"
6494            /// branch: typical use is "fetch the user's row, else
6495            /// fall back to an anonymous/guest stub".
6496            ///
6497            /// `fallback` runs only when no row matches — the DB
6498            /// round-trip happens unconditionally.
6499            ///
6500            /// # Errors
6501            /// As [`Self::find`].
6502            pub async fn find_or<F>(
6503                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6504                pool: &#root::sql::Pool,
6505                fallback: F,
6506            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6507            where
6508                F: ::core::ops::FnOnce() -> Self,
6509            {
6510                ::core::result::Result::Ok(
6511                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6512                )
6513            }
6514
6515            /// Same as [`Self::find_or`] but also returns a `bool`
6516            /// indicating whether the row was found in the DB
6517            /// (`true`) or freshly built from `fallback` (`false`).
6518            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6519            /// in PHP the returned model also exposes `->exists`;
6520            /// here that's surfaced as the second tuple element.
6521            ///
6522            /// Useful for edit-or-create form handlers where the
6523            /// caller needs to know whether to PATCH or POST when
6524            /// the user submits the form.
6525            ///
6526            /// # Errors
6527            /// As [`Self::find`].
6528            pub async fn find_or_new<F>(
6529                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6530                pool: &#root::sql::Pool,
6531                fallback: F,
6532            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6533            where
6534                F: ::core::ops::FnOnce() -> Self,
6535            {
6536                match Self::find(pk, pool).await? {
6537                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6538                    ::core::option::Option::None => {
6539                        ::core::result::Result::Ok((fallback(), false))
6540                    }
6541                }
6542            }
6543
6544            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6545            /// [`Self::find_or_new`] but **persists** the new row
6546            /// when the PK isn't found. Returns `(row, exists: bool)`
6547            /// — `exists=true` when the PK was found,
6548            /// `false` when a fresh row was inserted.
6549            ///
6550            /// ```ignore
6551            /// let (post, found) = Post::find_or_insert(
6552            ///     pk,
6553            ///     &pool,
6554            ///     || Post { id: Auto::default(), title: "new".into() },
6555            /// ).await?;
6556            /// // `found` true → returned existing row;
6557            /// // `found` false → fallback was inserted, `post.id` populated.
6558            /// ```
6559            ///
6560            /// **Caveat**: two concurrent calls that both miss the
6561            /// find can both INSERT, violating uniqueness. Wrap in a
6562            /// transaction or rely on a UNIQUE constraint + handle
6563            /// the conflict error if you need race-free semantics.
6564            ///
6565            /// # Errors
6566            /// As [`Self::find`] and [`Self::save_pool`].
6567            pub async fn find_or_insert<F>(
6568                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6569                pool: &#root::sql::Pool,
6570                fallback: F,
6571            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6572            where
6573                F: ::core::ops::FnOnce() -> Self,
6574            {
6575                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6576                    return ::core::result::Result::Ok((_row, true));
6577                }
6578                let mut _new = fallback();
6579                _new.save_pool(pool).await?;
6580                ::core::result::Result::Ok((_new, false))
6581            }
6582
6583            /// Fetch the first row of the table, or run `fallback`
6584            /// when the table is empty. Eloquent
6585            /// `Model::firstOr(fn() => …)` parity.
6586            ///
6587            /// # Errors
6588            /// As [`Self::first`].
6589            pub async fn first_or<F>(
6590                pool: &#root::sql::Pool,
6591                fallback: F,
6592            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6593            where
6594                F: ::core::ops::FnOnce() -> Self,
6595            {
6596                // Route through the queryset rather than `Self::first`,
6597                // which is suppressed on models with a field named
6598                // `first` (field/shortcut collision guard).
6599                ::core::result::Result::Ok(
6600                    #root::query::QuerySet::<Self>::default()
6601                        .first(pool)
6602                        .await?
6603                        .unwrap_or_else(fallback),
6604                )
6605            }
6606
6607            /// Fetch exactly one row matching `<col> = <val>`. Errors
6608            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6609            /// or more than one matches
6610            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6611            /// Eloquent `Model::sole($col, $val)` parity.
6612            ///
6613            /// # Errors
6614            /// As [`Self::where_`] plus the explicit
6615            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6616            pub async fn sole(
6617                col: &str,
6618                val: impl ::core::convert::Into<#root::core::SqlValue>,
6619                pool: &#root::sql::Pool,
6620            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6621                let mut _rows = Self::where_(col, val, pool).await?;
6622                match _rows.len() {
6623                    0 => ::core::result::Result::Err(
6624                        #root::sql::ExecError::Driver(
6625                            #root::sql::sqlx::Error::RowNotFound,
6626                        ),
6627                    ),
6628                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6629                    n => ::core::result::Result::Err(
6630                        #root::sql::ExecError::MultipleRowsReturned {
6631                            op: "sole",
6632                            table: <Self as #root::core::Model>::SCHEMA.name,
6633                            count: n,
6634                        },
6635                    ),
6636                }
6637            }
6638        }
6639    } else {
6640        quote!()
6641    };
6642
6643    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6644    // the non-audited `_pool` methods but execute against an open
6645    // `PoolTx` so the writes participate in the caller's transaction.
6646    // Auditing inside TX is deferred; these always use the plain
6647    // executor primitives regardless of whether the model is audited.
6648    let tx_insert_method = if fields.has_auto {
6649        let pushes = &fields.insert_pushes;
6650        let returning_cols = &fields.returning_cols;
6651        quote! {
6652            /// Insert this row inside an open transaction, populating
6653            /// any `Auto<T>` PK from the auto-assigned value. Works
6654            /// against any backend that `tx` wraps.
6655            ///
6656            /// # Errors
6657            /// As [`Self::insert_pool`].
6658            pub async fn insert_tx(
6659                &mut self,
6660                tx: &mut #root::sql::PoolTx<'_>,
6661            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6662                let mut _columns: ::std::vec::Vec<&'static str> =
6663                    ::std::vec::Vec::new();
6664                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6665                    ::std::vec::Vec::new();
6666                #( #pushes )*
6667                let _query = #root::core::InsertQuery::new(
6668                    <Self as #root::core::Model>::SCHEMA,
6669                    _columns,
6670                    _values,
6671                )
6672                .returning(::std::vec![ #( #returning_cols ),* ]);
6673                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6674                #root::sql::apply_auto_pk(_result, self)
6675            }
6676        }
6677    } else {
6678        let insert_columns = &fields.insert_columns;
6679        let insert_values = &fields.insert_values;
6680        quote! {
6681            /// Insert this row inside an open transaction.
6682            ///
6683            /// # Errors
6684            /// As [`Self::insert_pool`].
6685            pub async fn insert_tx(
6686                &self,
6687                tx: &mut #root::sql::PoolTx<'_>,
6688            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6689                let _query = #root::core::InsertQuery::new(
6690                    <Self as #root::core::Model>::SCHEMA,
6691                    ::std::vec![ #( #insert_columns ),* ],
6692                    ::std::vec![ #( #insert_values ),* ],
6693                );
6694                #root::sql::insert_tx(tx, &_query).await
6695            }
6696        }
6697    };
6698
6699    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6700        let pk_column_lit = pk_col.as_str();
6701        let assignments = &fields.update_assignments;
6702        let dispatch_unset = if fields.pk_is_auto {
6703            quote! {
6704                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6705                    return self.insert_tx(tx).await.map(|()| 1u64);
6706                }
6707            }
6708        } else {
6709            quote!()
6710        };
6711        let write = restore_auto_now_unless(
6712            &fields.auto_now_idents,
6713            &save_ret,
6714            &save_keep,
6715            &quote! {
6716                let _query = #root::core::UpdateQuery::new(
6717                    <Self as #root::core::Model>::SCHEMA,
6718                    ::std::vec![ #( #assignments ),* ],
6719                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6720                        #pk_column_lit,
6721                        #root::core::Op::Eq,
6722                        ::core::convert::Into::<#root::core::SqlValue>::into(
6723                            ::core::clone::Clone::clone(&self.#pk_ident)
6724                        ),
6725                    )),
6726                );
6727                let _affected = #root::sql::update_tx(tx, &_query).await?;
6728                ::core::result::Result::Ok(_affected)
6729            },
6730        );
6731        quote! {
6732            /// Save this row inside an open transaction. `INSERT` when
6733            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6734            /// PK. Works against any backend that `tx` wraps.
6735            ///
6736            /// # Errors
6737            /// As [`Self::save_pool`].
6738            pub async fn save_tx(
6739                &mut self,
6740                tx: &mut #root::sql::PoolTx<'_>,
6741            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6742                #dispatch_unset
6743                #write
6744            }
6745        }
6746    } else {
6747        quote!()
6748    };
6749
6750    let tx_delete_method = {
6751        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6752        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6753        if let Some(pk_ident) = pk_ident_for_tx {
6754            quote! {
6755                /// Delete the row identified by this instance's PK
6756                /// inside an open transaction. Works against any backend
6757                /// that `tx` wraps.
6758                ///
6759                /// # Errors
6760                /// As [`Self::delete_pool`].
6761                pub async fn delete_tx(
6762                    &self,
6763                    tx: &mut #root::sql::PoolTx<'_>,
6764                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6765                    let _query = #root::core::DeleteQuery::by_pk(
6766                        <Self as #root::core::Model>::SCHEMA,
6767                        #pk_column_lit,
6768                        ::core::convert::Into::<#root::core::SqlValue>::into(
6769                            ::core::clone::Clone::clone(&self.#pk_ident)
6770                        ),
6771                    );
6772                    #root::sql::delete_tx(tx, &_query).await
6773                }
6774            }
6775        } else {
6776            quote!()
6777        }
6778    };
6779
6780    // Update emission captures both BEFORE and AFTER state — runs an
6781    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6782    // each tracked field's prior value, then after the UPDATE diffs
6783    // against the in-memory `&self`. `diff_changes` drops unchanged
6784    // columns so the JSON only contains the actual delta.
6785    //
6786    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6787    // and binds `_audit_before_pairs`; `audit_update_post` runs
6788    // after the UPDATE and emits the PendingEntry.
6789    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6790        audited_fields
6791    {
6792        if tracked.is_empty() {
6793            (quote!(), quote!())
6794        } else {
6795            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6796            let pk_column_lit_for_select =
6797                primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6798            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6799                if fields.pk_is_auto {
6800                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6801                } else {
6802                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6803                }
6804            } else {
6805                quote!(0_i64)
6806            };
6807            let before_pairs = tracked.iter().map(|c| {
6808                let column_lit = c.column.as_str();
6809                let value_ty = &c.value_ty;
6810                quote! {
6811                    (
6812                        #column_lit,
6813                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6814                            &_audit_before_row, #column_lit,
6815                        ) {
6816                            ::core::result::Result::Ok(v) => #root::core::__json_value(&v),
6817                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6818                        },
6819                    )
6820                }
6821            });
6822            let pre = quote! {
6823                let _audit_before_query = #root::audit::before_image_query(
6824                    <Self as #root::core::Model>::SCHEMA,
6825                    #pk_column_lit_for_select,
6826                    ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6827                    &[ #( #tracked_cols ),* ],
6828                );
6829                // A failed pre-read must not let the UPDATE run unaudited (#1907).
6830                let _audit_before_pairs:
6831                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6832                    #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6833                        .await?
6834                        .into_iter()
6835                        .next()
6836                        .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6837            };
6838            let post = quote! {
6839                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6840                    let _audit_after = self.__rustango_audit_pairs();
6841                    if let ::core::option::Option::Some(_audit_entry) =
6842                        #root::audit::PendingEntry::update_diff(
6843                            <Self as #root::core::Model>::SCHEMA.table,
6844                            self.__rustango_audit_pk(),
6845                            &_audit_before,
6846                            &_audit_after,
6847                        )
6848                    {
6849                        #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6850                    }
6851                }
6852            };
6853            (pre, post)
6854        }
6855    } else {
6856        (quote!(), quote!())
6857    };
6858
6859    // Bulk-insert audit: one `Create` entry per row once RETURNING has
6860    // set its PK, written in one batched INSERT.
6861    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6862        quote! {
6863            let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6864                .iter()
6865                .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6866                .collect();
6867            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6868        }
6869    } else {
6870        quote!()
6871    };
6872
6873    let save_method = if fields.pk_is_auto {
6874        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6875        let pk_column_lit = pk_column.as_str();
6876        let assignments = &fields.update_assignments;
6877        let upsert_cols = &fields.upsert_update_columns;
6878        let upsert_pushes = &fields.insert_pushes;
6879        // Read before the pushes, which fill each unset id.
6880        let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6881            fields.uuid_v7_fields.iter().cloned().unzip();
6882        let upsert_returning = &fields.returning_cols;
6883        let upsert_auto_assigns = &fields.auto_assigns;
6884        let auto_now_idents = &fields.auto_now_idents;
6885        // Conflict target: prefer the first declared `unique_together`
6886        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6887        // `BIGSERIAL` and never collide on insert, so a PK-only target
6888        // would silently turn `upsert()` into "always-insert" for
6889        // surrogate-PK models with composite UNIQUE constraints — see
6890        // `RolePermission` / `UserRole` / `UserPermission` in the
6891        // tenancy permission engine. When no `unique_together` is
6892        // declared we keep the PK target (the original behaviour).
6893        let upsert_target_columns: Vec<String> =
6894            upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6895        let upsert_target_lits = upsert_target_columns
6896            .iter()
6897            .map(String::as_str)
6898            .collect::<Vec<_>>();
6899        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6900            quote!(#root::core::ConflictClause::DoNothing)
6901        } else {
6902            quote!(#root::core::ConflictClause::DoUpdate {
6903                target: ::std::vec![ #( #upsert_target_lits ),* ],
6904                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6905            })
6906        };
6907        // Audited: the upsert reports whether it inserted or updated (#1795).
6908        let (upsert_run, audit_upsert_emit) = if audited_fields.is_some() {
6909            (
6910                quote! {
6911                    let (_returning_row_v, _audit_op) =
6912                        #root::audit::upsert_returning_on(&mut *_executor, &query).await?;
6913                },
6914                quote! {
6915                    let _audit_entry = self.__rustango_audit_entry(_audit_op);
6916                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6917                },
6918            )
6919        } else {
6920            (
6921                quote! {
6922                    let _returning_row_v =
6923                        #root::sql::__macro_internals::insert_returning_on(_executor, &query)
6924                            .await?;
6925                },
6926                quote!(),
6927            )
6928        };
6929        let save_on_write = restore_auto_now_unless(
6930            auto_now_idents,
6931            &save_ret,
6932            &save_keep,
6933            &quote! {
6934                #audit_update_pre
6935                let _query = #root::core::UpdateQuery::new(
6936                    <Self as #root::core::Model>::SCHEMA,
6937                    ::std::vec![ #( #assignments ),* ],
6938                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6939                        #pk_column_lit,
6940                        #root::core::Op::Eq,
6941                        ::core::convert::Into::<#root::core::SqlValue>::into(
6942                            ::core::clone::Clone::clone(&self.#pk_ident)
6943                        ),
6944                    )),
6945                );
6946                let _affected = #root::sql::__macro_internals::update_on(
6947                    #executor_passes_to_data_write,
6948                    &_query,
6949                ).await?;
6950                #audit_update_post
6951                ::core::result::Result::Ok(_affected)
6952            },
6953        );
6954        let upsert_write = restore_auto_now_unless(
6955            auto_now_idents,
6956            &quote!(::core::result::Result<(), #root::sql::ExecError>),
6957            &quote!(_saved.is_ok()),
6958            &quote! {
6959                // The UPDATE branch writes `auto_now` too, so stamp it like `save`.
6960                #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
6961                #( #upsert_pushes )*
6962                let query = #root::core::InsertQuery::new(
6963                    <Self as #root::core::Model>::SCHEMA,
6964                    _columns,
6965                    _values,
6966                )
6967                .returning(::std::vec![ #( #upsert_returning ),* ])
6968                .on_conflict(#conflict_clause)
6969                .generated(_generated);
6970                #upsert_run
6971                let _returning_row = &_returning_row_v;
6972                #( #upsert_auto_assigns )*
6973                #audit_upsert_emit
6974                ::core::result::Result::Ok(())
6975            },
6976        );
6977        Some(quote! {
6978            /// Insert this row if its `Auto<T>` primary key is
6979            /// `Unset`, otherwise update the existing row matching the
6980            /// PK. The caller doesn't need to pick `insert` vs the
6981            /// bulk-update path manually.
6982            ///
6983            /// On the insert branch, populates the PK from `RETURNING`
6984            /// (same behavior as `insert`). On the update branch,
6985            /// writes every non-PK column back; if no row matches the
6986            /// PK, returns `Ok(())` silently.
6987            ///
6988            /// Only generated when the primary key is declared as
6989            /// `Auto<T>`. Models with a manually-managed PK must use
6990            /// `insert` or the QuerySet update builder.
6991            ///
6992            /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6993            /// driver-specific `PgPool`, so it does not exist at all on a
6994            /// build without the `postgres` feature: selecting `sqlite`
6995            /// makes the method vanish rather than fail with anything that
6996            /// names the cause.
6997            ///
6998            /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6999            /// all three backends. The naming is inverted — the short name
7000            /// is the narrow one — and that is tracked in #1293.
7001            ///
7002            /// # Errors
7003            /// Returns [`#root::sql::ExecError`] for SQL-writing
7004            /// or driver failures.
7005            #[cfg(feature = "postgres")]
7006            pub async fn save(
7007                &mut self,
7008                pool: &#root::sql::sqlx::PgPool,
7009            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7010                #pool_to_save_on
7011            }
7012
7013            /// Like [`Self::save`] but accepts any sqlx executor —
7014            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
7015            /// escape hatch for tenant-scoped writes: schema-mode
7016            /// tenants share the registry pool but rely on a per-
7017            /// checkout `SET search_path`, so passing `&PgPool` would
7018            /// silently hit the wrong schema. Acquire a connection
7019            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
7020            ///
7021            /// # Errors
7022            /// As [`Self::save`].
7023            #[cfg(feature = "postgres")]
7024            pub async fn save_on #executor_generics (
7025                &mut self,
7026                #executor_param,
7027            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7028            #executor_where
7029            {
7030                // #1029 — INSERT writes exactly one row → 1; UPDATE returns
7031                // the rows-affected count (0 when the PK no longer exists).
7032                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
7033                    return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
7034                }
7035                #save_on_write
7036            }
7037
7038            /// Per-call override for the audit source. Runs
7039            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
7040            /// scope so the resulting audit entry records `source`
7041            /// instead of the task-local default. Useful for seed
7042            /// scripts and one-off CLI tools that don't sit inside an
7043            /// admin handler. The override applies only to this call;
7044            /// no global state changes.
7045            ///
7046            /// # Errors
7047            /// As [`Self::save_on`].
7048            #[cfg(feature = "postgres")]
7049            pub async fn save_on_with #executor_generics (
7050                &mut self,
7051                #executor_param,
7052                source: #root::audit::AuditSource,
7053            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7054            #executor_where
7055            {
7056                #root::audit::with_source(source, self.save_on(_executor)).await
7057            }
7058
7059            /// Insert this row or update it in-place if the primary key already
7060            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
7061            ///
7062            /// With `Auto::Unset` PK the server assigns a new key and no conflict
7063            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
7064            /// inserted if absent or all non-PK columns are overwritten if present.
7065            ///
7066            /// # Errors
7067            /// As [`Self::insert_on`].
7068            #[cfg(feature = "postgres")]
7069            pub async fn upsert(
7070                &mut self,
7071                pool: &#root::sql::sqlx::PgPool,
7072            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7073                #pool_to_upsert_on
7074            }
7075
7076            /// Like [`Self::upsert`] but accepts any sqlx executor.
7077            /// See [`Self::save_on`] for tenancy-scoped rationale.
7078            ///
7079            /// # Errors
7080            /// As [`Self::upsert`].
7081            #[cfg(feature = "postgres")]
7082            pub async fn upsert_on #executor_generics (
7083                &mut self,
7084                #executor_param,
7085            ) -> ::core::result::Result<(), #root::sql::ExecError>
7086            #executor_where
7087            {
7088                let mut _columns: ::std::vec::Vec<&'static str> =
7089                    ::std::vec::Vec::new();
7090                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7091                    ::std::vec::Vec::new();
7092                #[allow(unused_mut)]
7093                let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7094                #(
7095                    if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
7096                        _generated.push(#gen_cols);
7097                    }
7098                )*
7099                #upsert_write
7100            }
7101        })
7102    } else {
7103        None
7104    };
7105
7106    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
7107        let pk_column_lit = pk_column.as_str();
7108        // Optional `soft_delete_on` / `restore_on` companions when the
7109        // model has a `#[rustango(soft_delete)]` column. They land
7110        // alongside the regular `delete_on` so callers have both
7111        // options — a hard delete (audit-tracked as a real DELETE) and
7112        // a logical delete (audit-tracked as an UPDATE setting the
7113        // deleted_at column to NOW()).
7114        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
7115            let col_lit = col;
7116            let sd_field_ident = fields
7117                .soft_delete_field_ident
7118                .clone()
7119                .expect("soft_delete_column without ident");
7120            quote! {
7121                /// Soft-delete this row by setting its
7122                /// `#[rustango(soft_delete)]` column to `NOW()`.
7123                /// The row stays in the table; query helpers can
7124                /// filter it out by checking the column for `IS NOT
7125                /// NULL`.
7126                ///
7127                /// # Errors
7128                /// As [`Self::delete`].
7129                #[cfg(feature = "postgres")]
7130                pub async fn soft_delete_on #executor_generics (
7131                    &self,
7132                    #executor_param,
7133                ) -> ::core::result::Result<u64, #root::sql::ExecError>
7134                #executor_where
7135                {
7136                    let _now = #root::__chrono::Utc::now();
7137                    let _query = #root::soft_delete::__mark_query(
7138                        <Self as #root::core::Model>::SCHEMA,
7139                        #col_lit,
7140                        #pk_column_lit,
7141                        ::core::convert::Into::<#root::core::SqlValue>::into(
7142                            ::core::clone::Clone::clone(&self.#pk_ident)
7143                        ),
7144                        ::core::option::Option::Some(_now),
7145                    );
7146                    let _affected = #root::sql::__macro_internals::update_on(
7147                        #executor_passes_to_data_write,
7148                        &_query,
7149                    ).await?;
7150                    #audit_softdelete_emit
7151                    ::core::result::Result::Ok(_affected)
7152                }
7153
7154                /// Inverse of [`Self::soft_delete_on`] — clears the
7155                /// soft-delete column back to NULL so the row is
7156                /// considered live again.
7157                ///
7158                /// # Errors
7159                /// As [`Self::delete`].
7160                #[cfg(feature = "postgres")]
7161                pub async fn restore_on #executor_generics (
7162                    &self,
7163                    #executor_param,
7164                ) -> ::core::result::Result<u64, #root::sql::ExecError>
7165                #executor_where
7166                {
7167                    let _query = #root::soft_delete::__mark_query(
7168                        <Self as #root::core::Model>::SCHEMA,
7169                        #col_lit,
7170                        #pk_column_lit,
7171                        ::core::convert::Into::<#root::core::SqlValue>::into(
7172                            ::core::clone::Clone::clone(&self.#pk_ident)
7173                        ),
7174                        ::core::option::Option::None,
7175                    );
7176                    let _affected = #root::sql::__macro_internals::update_on(
7177                        #executor_passes_to_data_write,
7178                        &_query,
7179                    ).await?;
7180                    #audit_restore_emit
7181                    ::core::result::Result::Ok(_affected)
7182                }
7183
7184                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
7185                /// — takes [`#root::sql::Pool`] and dispatches per
7186                /// backend. Eloquent `Model::delete()` semantics on
7187                /// soft-delete-enabled models (closes #821 partial).
7188                ///
7189                /// Sets the `#[rustango(soft_delete)]` column to
7190                /// `NOW()` on every backend. Query helpers
7191                /// (`QuerySet::active()` / `only_trashed()`,
7192                /// `soft_delete::active_filter` /
7193                /// `compose_with_active`) filter trashed rows out by
7194                /// reading `IS NULL` on the same column. Keyed on the PK;
7195                /// global scopes do not apply.
7196                ///
7197                /// # Errors
7198                /// As [`#root::sql::update_pool`].
7199                pub async fn soft_delete(
7200                    &self,
7201                    pool: &#root::sql::Pool,
7202                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7203                    let _now = #root::__chrono::Utc::now();
7204                    let _query = #root::soft_delete::__mark_query(
7205                        <Self as #root::core::Model>::SCHEMA,
7206                        #col_lit,
7207                        #pk_column_lit,
7208                        ::core::convert::Into::<#root::core::SqlValue>::into(
7209                            ::core::clone::Clone::clone(&self.#pk_ident)
7210                        ),
7211                        ::core::option::Option::Some(_now),
7212                    );
7213                    #pool_softdelete_run
7214                }
7215
7216                /// Tri-dialect counterpart of [`Self::restore_on`].
7217                /// Clears the `#[rustango(soft_delete)]` column back
7218                /// to `NULL`, marking the row live again. Eloquent
7219                /// `Model::restore()` parity. Keyed on the PK; global
7220                /// scopes do not apply.
7221                ///
7222                /// # Errors
7223                /// As [`#root::sql::update_pool`].
7224                pub async fn restore(
7225                    &self,
7226                    pool: &#root::sql::Pool,
7227                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7228                    let _query = #root::soft_delete::__mark_query(
7229                        <Self as #root::core::Model>::SCHEMA,
7230                        #col_lit,
7231                        #pk_column_lit,
7232                        ::core::convert::Into::<#root::core::SqlValue>::into(
7233                            ::core::clone::Clone::clone(&self.#pk_ident)
7234                        ),
7235                        ::core::option::Option::None,
7236                    );
7237                    #pool_restore_run
7238                }
7239
7240                /// Hard-delete this row, ignoring the soft-delete
7241                /// column. Eloquent `Model::forceDelete()` parity —
7242                /// the escape hatch when you need to actually purge
7243                /// data (GDPR, fixture cleanup, etc.).
7244                ///
7245                /// Equivalent to [`Self::delete_pool`] (the framework's
7246                /// non-soft delete) — exposed under the Eloquent name
7247                /// for muscle-memory + so soft-delete-enabled models
7248                /// have all three operations (soft / restore / force)
7249                /// in one place.
7250                ///
7251                /// # Errors
7252                /// As [`Self::delete_pool`].
7253                pub async fn force_delete(
7254                    &self,
7255                    pool: &#root::sql::Pool,
7256                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7257                    Self::delete_pool(self, pool).await
7258                }
7259
7260                /// Returns `true` when this row is soft-deleted (its
7261                /// `#[rustango(soft_delete)]` column is currently
7262                /// set — Eloquent `$model->trashed()` parity).
7263                ///
7264                /// Pure in-memory predicate over `&self`; does not
7265                /// hit the database. Useful in admin/template code
7266                /// like `{% if post.trashed() %}…{% endif %}` and in
7267                /// guard clauses on restore/force-delete flows.
7268                pub fn trashed(&self) -> bool {
7269                    ::core::option::Option::is_some(&self.#sd_field_ident)
7270                }
7271
7272                /// Fetch every row whose `#[rustango(soft_delete)]`
7273                /// column is `NULL` (a.k.a. the live, non-trashed
7274                /// rows). Eloquent's default `Model::all()` behavior on
7275                /// a soft-delete model (Eloquent auto-scopes trashed
7276                /// rows out; rustango doesn't have auto-scoping yet —
7277                /// see issue #820 — so this is the explicit shortcut).
7278                ///
7279                /// One-liner over `QuerySet::<Self>::default()
7280                /// .active().fetch(pool)`. Closes #821 partial.
7281                ///
7282                /// # Errors
7283                /// As [`#root::sql::FetcherPool::fetch`].
7284                pub async fn active(
7285                    pool: &#root::sql::Pool,
7286                ) -> ::core::result::Result<
7287                    ::std::vec::Vec<Self>,
7288                    #root::sql::ExecError,
7289                > {
7290                    use #root::sql::FetcherPool as _;
7291                    #root::query::QuerySet::<Self>::default()
7292                        .active()
7293                        .fetch(pool)
7294                        .await
7295                }
7296
7297                /// Fetch ONLY soft-deleted rows. Eloquent
7298                /// `Model::onlyTrashed()->get()` parity — drives the
7299                /// admin "Trash" page, restore flows, GDPR purge
7300                /// scans, etc.
7301                ///
7302                /// One-liner over `QuerySet::<Self>::default()
7303                /// .only_trashed().fetch(pool)`. Closes #821
7304                /// partial.
7305                ///
7306                /// # Errors
7307                /// As [`#root::sql::FetcherPool::fetch`].
7308                pub async fn only_trashed(
7309                    pool: &#root::sql::Pool,
7310                ) -> ::core::result::Result<
7311                    ::std::vec::Vec<Self>,
7312                    #root::sql::ExecError,
7313                > {
7314                    use #root::sql::FetcherPool as _;
7315                    #root::query::QuerySet::<Self>::default()
7316                        .only_trashed()
7317                        .fetch(pool)
7318                        .await
7319                }
7320
7321                /// Fetch every row, both live and soft-deleted.
7322                /// Eloquent `Model::withTrashed()->get()` parity.
7323                ///
7324                /// Today every queryset already includes trashed rows
7325                /// (rustango has no global-scope tracking yet — issue
7326                /// #820), so this is functionally equivalent to
7327                /// [`Self::all`]. Exposed as a named shortcut so
7328                /// soft-delete-aware code reads `Model::with_trashed_pool`
7329                /// rather than `Model::all_pool` — keeps intent visible
7330                /// in callers and stays correct when auto-scoping lands.
7331                ///
7332                /// Closes #821 partial.
7333                ///
7334                /// # Errors
7335                /// As [`#root::sql::FetcherPool::fetch`].
7336                pub async fn with_trashed(
7337                    pool: &#root::sql::Pool,
7338                ) -> ::core::result::Result<
7339                    ::std::vec::Vec<Self>,
7340                    #root::sql::ExecError,
7341                > {
7342                    use #root::sql::FetcherPool as _;
7343                    #root::query::QuerySet::<Self>::default()
7344                        .with_trashed()
7345                        .fetch(pool)
7346                        .await
7347                }
7348
7349            }
7350        } else {
7351            quote!()
7352        };
7353        quote! {
7354            /// Delete the row identified by this instance's primary key.
7355            ///
7356            /// Returns the number of rows affected (0 or 1).
7357            ///
7358            /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7359            /// a driver-specific `PgPool`, so it does not exist on a build
7360            /// without the `postgres` feature. [`Self::delete_pool`] takes
7361            /// `rustango::sql::Pool` and works on all three. The naming is
7362            /// inverted; tracked in #1293.
7363            ///
7364            /// # Errors
7365            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7366            /// driver failures.
7367            #[cfg(feature = "postgres")]
7368            pub async fn delete(
7369                &self,
7370                pool: &#root::sql::sqlx::PgPool,
7371            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7372                #pool_to_delete_on
7373            }
7374
7375            /// Like [`Self::delete`] but accepts any sqlx executor —
7376            /// for tenant-scoped deletes against an explicitly-acquired
7377            /// connection. See [`Self::save_on`] for the rationale.
7378            ///
7379            /// # Errors
7380            /// As [`Self::delete`].
7381            #[cfg(feature = "postgres")]
7382            pub async fn delete_on #executor_generics (
7383                &self,
7384                #executor_param,
7385            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7386            #executor_where
7387            {
7388                let query = #root::core::DeleteQuery::by_pk(
7389                    <Self as #root::core::Model>::SCHEMA,
7390                    #pk_column_lit,
7391                    ::core::convert::Into::<#root::core::SqlValue>::into(
7392                        ::core::clone::Clone::clone(&self.#pk_ident)
7393                    ),
7394                );
7395                let _affected = #root::sql::__macro_internals::delete_on(
7396                    #executor_passes_to_data_write,
7397                    &query,
7398                ).await?;
7399                #audit_delete_emit
7400                ::core::result::Result::Ok(_affected)
7401            }
7402
7403            /// Per-call audit-source override for [`Self::delete_on`].
7404            /// See [`Self::save_on_with`] for shape rationale.
7405            ///
7406            /// # Errors
7407            /// As [`Self::delete_on`].
7408            #[cfg(feature = "postgres")]
7409            pub async fn delete_on_with #executor_generics (
7410                &self,
7411                #executor_param,
7412                source: #root::audit::AuditSource,
7413            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7414            #executor_where
7415            {
7416                #root::audit::with_source(source, self.delete_on(_executor)).await
7417            }
7418            #pool_delete_method
7419            #pool_insert_method
7420            #audit_entry_method
7421            #pool_save_method
7422            #refresh_replicate_methods
7423            #tx_delete_method
7424            #tx_insert_method
7425            #tx_save_method
7426            #soft_delete_methods
7427
7428            /// Returns `true` when `other` represents the same DB
7429            /// row as `self` — i.e. their primary keys compare
7430            /// equal. Eloquent `$model->is($other)` parity.
7431            ///
7432            /// Because both arguments are typed `&Self`, the
7433            /// model/table check is automatic — `Post::is` cannot
7434            /// be invoked against a `Comment` at compile time. Only
7435            /// the PK has to be compared at runtime.
7436            pub fn is(&self, other: &Self) -> bool {
7437                self.#pk_ident == other.#pk_ident
7438            }
7439
7440            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7441            /// parity.
7442            pub fn is_not(&self, other: &Self) -> bool {
7443                self.#pk_ident != other.#pk_ident
7444            }
7445
7446            /// Returns this row's primary-key value as an
7447            /// [`#root::core::SqlValue`]. Eloquent
7448            /// `$model->getKey()` parity.
7449            ///
7450            /// Useful when you need to thread the PK through a
7451            /// generic `Into<SqlValue>`-bound API without knowing the
7452            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7453            #[must_use]
7454            pub fn get_key(&self) -> #root::core::SqlValue {
7455                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7456            }
7457
7458        }
7459    });
7460
7461    let insert_method = if fields.has_auto {
7462        let pushes = &fields.insert_pushes;
7463        let returning_cols = &fields.returning_cols;
7464        let auto_assigns = &fields.auto_assigns;
7465        quote! {
7466            /// Insert this row into its table. Skips columns whose
7467            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7468            /// sequence fills them in, then reads each `Auto` column
7469            /// back via `RETURNING` and stores it on `self`.
7470            ///
7471            /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7472            /// a driver-specific `PgPool`, so it does not exist on a build
7473            /// without the `postgres` feature. [`Self::insert_pool`] takes
7474            /// `rustango::sql::Pool` and works on all three. The naming is
7475            /// inverted; tracked in #1293.
7476            ///
7477            /// # Errors
7478            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7479            /// driver failures.
7480            #[cfg(feature = "postgres")]
7481            pub async fn insert(
7482                &mut self,
7483                pool: &#root::sql::sqlx::PgPool,
7484            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7485                #pool_to_insert_on
7486            }
7487
7488            /// Like [`Self::insert`] but accepts any sqlx executor.
7489            /// See [`Self::save_on`] for tenancy-scoped rationale.
7490            ///
7491            /// # Errors
7492            /// As [`Self::insert`].
7493            #[cfg(feature = "postgres")]
7494            pub async fn insert_on #executor_generics (
7495                &mut self,
7496                #executor_param,
7497            ) -> ::core::result::Result<(), #root::sql::ExecError>
7498            #executor_where
7499            {
7500                let mut _columns: ::std::vec::Vec<&'static str> =
7501                    ::std::vec::Vec::new();
7502                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7503                    ::std::vec::Vec::new();
7504                #( #pushes )*
7505                let query = #root::core::InsertQuery::new(
7506                    <Self as #root::core::Model>::SCHEMA,
7507                    _columns,
7508                    _values,
7509                )
7510                .returning(::std::vec![ #( #returning_cols ),* ]);
7511                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7512                    #executor_passes_to_data_write,
7513                    &query,
7514                ).await?;
7515                let _returning_row = &_returning_row_v;
7516                #( #auto_assigns )*
7517                #audit_insert_emit
7518                ::core::result::Result::Ok(())
7519            }
7520
7521            /// Per-call audit-source override for [`Self::insert_on`].
7522            /// See [`Self::save_on_with`] for shape rationale.
7523            ///
7524            /// # Errors
7525            /// As [`Self::insert_on`].
7526            #[cfg(feature = "postgres")]
7527            pub async fn insert_on_with #executor_generics (
7528                &mut self,
7529                #executor_param,
7530                source: #root::audit::AuditSource,
7531            ) -> ::core::result::Result<(), #root::sql::ExecError>
7532            #executor_where
7533            {
7534                #root::audit::with_source(source, self.insert_on(_executor)).await
7535            }
7536        }
7537    } else {
7538        let insert_columns = &fields.insert_columns;
7539        let insert_values = &fields.insert_values;
7540        quote! {
7541            /// Insert this row into its table.
7542            ///
7543            /// # Errors
7544            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7545            /// driver failures.
7546            #[cfg(feature = "postgres")]
7547            pub async fn insert(
7548                &self,
7549                pool: &#root::sql::sqlx::PgPool,
7550            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7551                self.insert_on(pool).await
7552            }
7553
7554            /// Like [`Self::insert`] but accepts any sqlx executor.
7555            /// See [`Self::save_on`] for tenancy-scoped rationale.
7556            ///
7557            /// # Errors
7558            /// As [`Self::insert`].
7559            #[cfg(feature = "postgres")]
7560            pub async fn insert_on<'_c, _E>(
7561                &self,
7562                _executor: _E,
7563            ) -> ::core::result::Result<(), #root::sql::ExecError>
7564            where
7565                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7566            {
7567                let query = #root::core::InsertQuery::new(
7568                    <Self as #root::core::Model>::SCHEMA,
7569                    ::std::vec![ #( #insert_columns ),* ],
7570                    ::std::vec![ #( #insert_values ),* ],
7571                );
7572                #root::sql::__macro_internals::insert_on(_executor, &query).await
7573            }
7574        }
7575    };
7576
7577    let bulk_insert_method = if fields.has_auto {
7578        let cols_no_auto = &fields.bulk_columns_no_auto;
7579        let cols_all = &fields.bulk_columns_all;
7580        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7581        let pushes_all = &fields.bulk_pushes_all;
7582        let returning_cols = &fields.returning_cols;
7583        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7584        let uniformity = &fields.bulk_auto_uniformity;
7585        let fill_in_place = &fields.bulk_fill_in_place;
7586        // Nothing to read back when every Auto field is filled Rust-side (#1934).
7587        let read_back = if fields.returning_cols.is_empty() {
7588            quote!()
7589        } else {
7590            quote! {
7591                if _returned.len() != rows.len() {
7592                    return ::core::result::Result::Err(
7593                        #root::sql::ExecError::Sql(
7594                            #root::sql::SqlError::BulkInsertReturningMismatch {
7595                                expected: rows.len(),
7596                                actual: _returned.len(),
7597                            }
7598                        )
7599                    );
7600                }
7601                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7602                    #auto_assigns_for_row
7603                }
7604            }
7605        };
7606        // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7607        let first_auto_ident = fields
7608            .first_db_auto
7609            .as_ref()
7610            .map(|(ident, _)| ident)
7611            .or(fields.first_auto_ident.as_ref())
7612            .expect("has_auto implies first_auto_ident is Some");
7613        quote! {
7614            /// Bulk-insert `rows` in a single round-trip. Every row's
7615            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7616            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7617            /// (caller-supplied values). Mixed Set/Unset is rejected
7618            /// — call `insert` per row for that case.
7619            ///
7620            /// Empty slice is a no-op. Each row's `Auto` fields are
7621            /// populated from the `RETURNING` clause in input order
7622            /// before this returns.
7623            ///
7624            /// # Errors
7625            /// Returns [`#root::sql::ExecError`] for validation,
7626            /// SQL-writing, mixed-Auto rejection, or driver failures.
7627            #[cfg(feature = "postgres")]
7628            pub async fn bulk_insert(
7629                rows: &mut [Self],
7630                pool: &#root::sql::sqlx::PgPool,
7631            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7632                #pool_to_bulk_insert_on
7633            }
7634
7635            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7636            /// See [`Self::save_on`] for tenancy-scoped rationale.
7637            ///
7638            /// # Errors
7639            /// As [`Self::bulk_insert`].
7640            #[cfg(feature = "postgres")]
7641            pub async fn bulk_insert_on #executor_generics (
7642                rows: &mut [Self],
7643                #executor_param,
7644            ) -> ::core::result::Result<(), #root::sql::ExecError>
7645            #executor_where
7646            {
7647                if rows.is_empty() {
7648                    return ::core::result::Result::Ok(());
7649                }
7650                let _first_unset = matches!(
7651                    rows[0].#first_auto_ident,
7652                    #root::sql::Auto::Unset
7653                );
7654                #( #uniformity )*
7655                for _row in rows.iter_mut() {
7656                    #( #fill_in_place )*
7657                }
7658
7659                let mut _all_rows: ::std::vec::Vec<
7660                    ::std::vec::Vec<#root::core::SqlValue>,
7661                > = ::std::vec::Vec::with_capacity(rows.len());
7662                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7663                    for _row in rows.iter() {
7664                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7665                            ::std::vec::Vec::new();
7666                        #( #pushes_no_auto )*
7667                        _all_rows.push(_row_vals);
7668                    }
7669                    ::std::vec![ #( #cols_no_auto ),* ]
7670                } else {
7671                    for _row in rows.iter() {
7672                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7673                            ::std::vec::Vec::new();
7674                        #( #pushes_all )*
7675                        _all_rows.push(_row_vals);
7676                    }
7677                    ::std::vec![ #( #cols_all ),* ]
7678                };
7679
7680                let _query = #root::core::BulkInsertQuery::new(
7681                    <Self as #root::core::Model>::SCHEMA,
7682                    _columns,
7683                    _all_rows,
7684                )
7685                .returning(::std::vec![ #( #returning_cols ),* ]);
7686                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7687                    #executor_passes_to_data_write,
7688                    &_query,
7689                ).await?;
7690                #read_back
7691                #audit_bulk_insert_emit
7692                ::core::result::Result::Ok(())
7693            }
7694        }
7695    } else {
7696        let cols_all = &fields.bulk_columns_all;
7697        let pushes_all = &fields.bulk_pushes_all;
7698        quote! {
7699            /// Bulk-insert `rows` in a single round-trip. Every row's
7700            /// fields are written verbatim — there are no `Auto<T>`
7701            /// fields on this model.
7702            ///
7703            /// Empty slice is a no-op.
7704            ///
7705            /// # Errors
7706            /// Returns [`#root::sql::ExecError`] for validation,
7707            /// SQL-writing, or driver failures.
7708            #[cfg(feature = "postgres")]
7709            pub async fn bulk_insert(
7710                rows: &[Self],
7711                pool: &#root::sql::sqlx::PgPool,
7712            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7713                #pool_to_bulk_insert_on
7714            }
7715
7716            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7717            /// See [`Self::save_on`] for tenancy-scoped rationale.
7718            ///
7719            /// # Errors
7720            /// As [`Self::bulk_insert`].
7721            #[cfg(feature = "postgres")]
7722            pub async fn bulk_insert_on #executor_generics (
7723                rows: &[Self],
7724                #executor_param,
7725            ) -> ::core::result::Result<(), #root::sql::ExecError>
7726            #executor_where
7727            {
7728                if rows.is_empty() {
7729                    return ::core::result::Result::Ok(());
7730                }
7731                let mut _all_rows: ::std::vec::Vec<
7732                    ::std::vec::Vec<#root::core::SqlValue>,
7733                > = ::std::vec::Vec::with_capacity(rows.len());
7734                for _row in rows.iter() {
7735                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7736                        ::std::vec::Vec::new();
7737                    #( #pushes_all )*
7738                    _all_rows.push(_row_vals);
7739                }
7740                let _query = #root::core::BulkInsertQuery::new(
7741                    <Self as #root::core::Model>::SCHEMA,
7742                    ::std::vec![ #( #cols_all ),* ],
7743                    _all_rows,
7744                );
7745                let _ = #root::sql::__macro_internals::bulk_insert_on(
7746                    #executor_passes_to_data_write,
7747                    &_query,
7748                ).await?;
7749                #audit_bulk_insert_emit
7750                ::core::result::Result::Ok(())
7751            }
7752        }
7753    };
7754
7755    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7756    // (no postgres-feature gate); routes through the existing
7757    // `bulk_insert_pool` + per-dialect conflict writer.
7758    //
7759    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7760    // sequence picks the PK for fresh inserts; the UPDATE branch never
7761    // touches the Auto column.
7762    // Audited models audit each written row; MySQL, with no RETURNING,
7763    // is refused there (#1747, #1795).
7764    let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7765        (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7766        (true, true) => quote! {
7767            #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7768                _r.__rustango_audit_entry(_op)
7769            })
7770            .await
7771        },
7772        (true, false) => quote! {
7773            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7774                table: <Self as #root::core::Model>::SCHEMA.table,
7775            })
7776        },
7777    };
7778    let bulk_upsert_pool_method = {
7779        let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7780            fields.uuid_v7_fields.iter().cloned().unzip();
7781        // Pick the "no Auto" columns when the model has Auto fields,
7782        // else every column.
7783        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7784            (
7785                fields.bulk_columns_no_auto.clone(),
7786                fields.bulk_pushes_no_auto.clone(),
7787            )
7788        } else {
7789            (
7790                fields.bulk_columns_all.clone(),
7791                fields.bulk_pushes_all.clone(),
7792            )
7793        };
7794        quote! {
7795            /// Tri-dialect bulk insert that updates on conflict — the
7796            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7797            ///
7798            /// Per-row values are extracted and lowered into a
7799            /// [`#root::core::BulkInsertQuery`] with
7800            /// `on_conflict = DoUpdate { target, update_columns }`. The
7801            /// writer dispatches per-dialect:
7802            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7803            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7804            ///   `target` is not the only unique key, since MySQL matches every UNIQUE index
7805            ///
7806            /// `target` names the column(s) whose unique constraint
7807            /// defines the conflict (typically a `unique` or
7808            /// `unique_together` natural-key column, NOT the `Auto<T>`
7809            /// PK). `update_cols` names the columns to overwrite on
7810            /// conflict — every other column is left untouched on the
7811            /// existing row.
7812            ///
7813            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7814            /// picks the PK on insert; the update path never touches
7815            /// the Auto column). Auto-set rows trigger a hard error.
7816            /// Empty slice is a no-op. On an audited model each written
7817            /// row gets a `create` or `update` audit row; MySQL refuses it.
7818            ///
7819            /// # Errors
7820            /// Returns [`#root::sql::ExecError`] for validation,
7821            /// SQL-writing, or driver failures.
7822            pub async fn bulk_upsert_pool(
7823                rows: &[Self],
7824                target: &[&'static str],
7825                update_cols: &[&'static str],
7826                pool: &#root::sql::Pool,
7827            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7828                if rows.is_empty() {
7829                    return ::core::result::Result::Ok(());
7830                }
7831                let mut _all_rows: ::std::vec::Vec<
7832                    ::std::vec::Vec<#root::core::SqlValue>,
7833                > = ::std::vec::Vec::with_capacity(rows.len());
7834                for _row in rows.iter() {
7835                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7836                        ::std::vec::Vec::new();
7837                    #( #upsert_pushes )*
7838                    _all_rows.push(_row_vals);
7839                }
7840                #[allow(unused_mut)]
7841                let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7842                #(
7843                    if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7844                        _generated.push(#gen_cols);
7845                    }
7846                )*
7847                let _query = #root::core::BulkInsertQuery::new(
7848                    <Self as #root::core::Model>::SCHEMA,
7849                    ::std::vec![ #( #upsert_cols ),* ],
7850                    _all_rows,
7851                )
7852                .on_conflict_do_update(target, update_cols)
7853                .generated(_generated);
7854                #run_conflict_bulk_insert
7855            }
7856
7857            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7858            /// skip rows that would violate a unique constraint. Issue
7859            /// #267 / T1.5. Same per-dialect dispatch as
7860            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7861            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7862            /// <pivot> = <pivot>` (MySQL no-op write).
7863            ///
7864            /// # Errors
7865            /// As [`Self::bulk_upsert_pool`].
7866            pub async fn bulk_insert_or_ignore_pool(
7867                rows: &[Self],
7868                pool: &#root::sql::Pool,
7869            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7870                if rows.is_empty() {
7871                    return ::core::result::Result::Ok(());
7872                }
7873                let mut _all_rows: ::std::vec::Vec<
7874                    ::std::vec::Vec<#root::core::SqlValue>,
7875                > = ::std::vec::Vec::with_capacity(rows.len());
7876                for _row in rows.iter() {
7877                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7878                        ::std::vec::Vec::new();
7879                    #( #upsert_pushes )*
7880                    _all_rows.push(_row_vals);
7881                }
7882                let _query = #root::core::BulkInsertQuery::new(
7883                    <Self as #root::core::Model>::SCHEMA,
7884                    ::std::vec![ #( #upsert_cols ),* ],
7885                    _all_rows,
7886                )
7887                .on_conflict_do_nothing();
7888                #run_conflict_bulk_insert
7889            }
7890        }
7891    };
7892
7893    // Ergonomic `Model::bulk_update(objs, fields)`.
7894    // The SQL/IR/executor stack
7895    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7896    // `write_bulk_update_*` writers) already existed; what was missing
7897    // was the per-model constructor that maps `&[Self]` + a runtime
7898    // column list into rows of `[pk, col_vals…]` so callers don't
7899    // hand-build the IR. Emitted only when the model has a primary key
7900    // (the PK is the join key and can't itself be updated).
7901    let bulk_update_rows_run = if audited_fields.is_some() {
7902        quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7903            _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7904        })
7905        .await)
7906    } else {
7907        quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7908    };
7909    let bulk_update_method = match &fields.primary_key {
7910        None => quote! {},
7911        Some((pk_ident, pk_col)) => {
7912            // One pair of match arms per non-PK column: a validation arm
7913            // resolving the runtime name to its `&'static str` column,
7914            // and a value arm pushing that field off the row.
7915            let mut col_arms: Vec<TokenStream2> = Vec::new();
7916            let mut val_arms: Vec<TokenStream2> = Vec::new();
7917            for entry in &fields.column_entries {
7918                if &entry.column == pk_col {
7919                    continue;
7920                }
7921                let col = &entry.column;
7922                let ident = &entry.ident;
7923                col_arms.push(quote! { #col => #col, });
7924                val_arms.push(quote! {
7925                    #col => _row_vals.push(
7926                        ::core::convert::Into::<#root::core::SqlValue>::into(
7927                            ::core::clone::Clone::clone(&_o.#ident)
7928                        )
7929                    ),
7930                });
7931            }
7932            quote! {
7933                /// Write per-row-different values for the named `fields`
7934                /// across every object in `objs`, matched by primary key.
7935                /// Past the backend's bind cap the rows go in batches inside
7936                /// one transaction, so the update is all or none.
7937                ///
7938                /// `fields` names the **columns** to update. The primary
7939                /// key identifies each row and cannot itself be updated
7940                /// (pass it and you get
7941                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7942                /// Empty `objs` or `fields` is a no-op returning `0`.
7943                /// Objects whose PK matches no row are simply not updated.
7944                /// Returns the number of rows affected.
7945                ///
7946                /// Tri-dialect: lowers to a
7947                /// [`#root::core::BulkUpdateQuery`] and dispatches
7948                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7949                /// a CTE + correlated subquery on SQLite, an inner
7950                /// `JOIN (VALUES …)` on MySQL.
7951                ///
7952                /// # Errors
7953                /// [`#root::core::QueryError::UnknownField`] for a name
7954                /// that isn't a column on this model,
7955                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7956                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7957                /// SQL-writing / driver failures.
7958                pub async fn bulk_update(
7959                    objs: &[Self],
7960                    fields: &[&str],
7961                    pool: &#root::sql::Pool,
7962                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7963                    if objs.is_empty() || fields.is_empty() {
7964                        return ::core::result::Result::Ok(0);
7965                    }
7966                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7967                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7968                        ::std::vec::Vec::with_capacity(fields.len());
7969                    for &_f in fields {
7970                        let _col: &'static str = match _f {
7971                            #pk_col => {
7972                                return ::core::result::Result::Err(
7973                                    ::core::convert::Into::into(
7974                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7975                                            model: _model_name,
7976                                            field: ::std::string::ToString::to_string(_f),
7977                                        }
7978                                    )
7979                                );
7980                            }
7981                            #( #col_arms )*
7982                            _ => {
7983                                return ::core::result::Result::Err(
7984                                    ::core::convert::Into::into(
7985                                        #root::core::QueryError::UnknownField {
7986                                            model: _model_name,
7987                                            field: ::std::string::ToString::to_string(_f),
7988                                        }
7989                                    )
7990                                );
7991                            }
7992                        };
7993                        // A model whose only field is the primary key emits no
7994                        // non-PK arms, so every arm above diverges and this push
7995                        // is genuinely unreachable — correct, since such a model
7996                        // has nothing to bulk-update. Scoped to the statement
7997                        // rather than the fn so a real unreachability elsewhere
7998                        // in this body still warns. Without it every PK-only
7999                        // model warns in the *user's* crate, which they cannot
8000                        // act on (#1290).
8001                        #[allow(unreachable_code)]
8002                        _update_columns.push(_col);
8003                    }
8004                    let mut _rows: ::std::vec::Vec<
8005                        ::std::vec::Vec<#root::core::SqlValue>,
8006                    > = ::std::vec::Vec::with_capacity(objs.len());
8007                    for _o in objs.iter() {
8008                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
8009                            ::std::vec::Vec::with_capacity(fields.len() + 1);
8010                        // PK first — the writers expect `[pk, …update cols]`.
8011                        _row_vals.push(
8012                            ::core::convert::Into::<#root::core::SqlValue>::into(
8013                                ::core::clone::Clone::clone(&_o.#pk_ident)
8014                            )
8015                        );
8016                        for &_f in fields {
8017                            match _f {
8018                                #( #val_arms )*
8019                                // Unreachable: every name was validated
8020                                // against the same arm set above.
8021                                _ => {}
8022                            }
8023                        }
8024                        _rows.push(_row_vals);
8025                    }
8026                    let _query = #root::core::BulkUpdateQuery::new(
8027                        <Self as #root::core::Model>::SCHEMA,
8028                        _update_columns,
8029                        _rows,
8030                    );
8031                    #bulk_update_rows_run
8032                }
8033            }
8034        }
8035    };
8036
8037    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
8038        quote! {
8039            /// Hidden runtime accessor for the primary-key value as a
8040            /// [`SqlValue`]. Used by reverse-relation helpers
8041            /// (`<parent>::<child>_set`) emitted from sibling models'
8042            /// FK fields. Not part of the public API.
8043            #[doc(hidden)]
8044            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
8045                ::core::convert::Into::<#root::core::SqlValue>::into(
8046                    ::core::clone::Clone::clone(&self.#pk_ident)
8047                )
8048            }
8049        }
8050    });
8051
8052    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
8053        let column_arms = fields.column_entries.iter().map(|entry| {
8054            let col = &entry.column;
8055            let ident = &entry.ident;
8056            quote! {
8057                #col => ::core::option::Option::Some(
8058                    ::core::convert::Into::<#root::core::SqlValue>::into(
8059                        ::core::clone::Clone::clone(&self.#ident)
8060                    )
8061                ),
8062            }
8063        });
8064        quote! {
8065            impl #root::sql::HasPkValue for #struct_name {
8066                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
8067                    ::core::convert::Into::<#root::core::SqlValue>::into(
8068                        ::core::clone::Clone::clone(&self.#pk_ident)
8069                    )
8070                }
8071                fn __rustango_column_value(
8072                    &self,
8073                    column: &str,
8074                ) -> ::core::option::Option<#root::core::SqlValue> {
8075                    match column {
8076                        #( #column_arms )*
8077                        _ => ::core::option::Option::None,
8078                    }
8079                }
8080            }
8081        }
8082    });
8083
8084    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
8085
8086    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
8087    // dispatch to the right per-backend body without the macro emitting
8088    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
8089    // so audited models with non-Auto PKs (which still go through
8090    // `insert_one_with_audit` → `apply_auto_pk`) link.
8091    let assign_auto_pk_pool_impl = {
8092        let auto_assigns = &fields.auto_assigns;
8093        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
8094        // so the body is structurally identical to `auto_assigns` —
8095        // only the helper name swaps from `try_get_returning` to
8096        // `try_get_returning_sqlite` so the closure typechecks against
8097        // a `SqliteRow` instead of a `PgRow`.
8098        let auto_assigns_sqlite: Vec<TokenStream2> = fields
8099            .auto_field_idents
8100            .iter()
8101            .map(|(ident, column, uuid)| {
8102                // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
8103                if *uuid {
8104                    return quote! {
8105                        self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
8106                            _returning_row, #column
8107                        )?);
8108                    };
8109                }
8110                quote! {
8111                    self.#ident = #root::sql::try_get_returning_sqlite(
8112                        _returning_row, #column
8113                    )?;
8114                }
8115            })
8116            .collect();
8117        // #1028 — decode each `generated_as` column from the same
8118        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
8119        // own type drives the decode (no `Auto<T>` wrapper).
8120        let generated_assigns: Vec<TokenStream2> = fields
8121            .generated_field_idents
8122            .iter()
8123            .map(|(ident, column)| {
8124                quote! {
8125                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
8126                }
8127            })
8128            .collect();
8129        let generated_assigns_sqlite: Vec<TokenStream2> = fields
8130            .generated_field_idents
8131            .iter()
8132            .map(|(ident, column)| {
8133                quote! {
8134                    self.#ident = #root::sql::try_get_returning_sqlite(
8135                        _returning_row, #column
8136                    )?;
8137                }
8138            })
8139            .collect();
8140        let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
8141            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
8142            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
8143            // Auto<Uuid>/etc. fail to link against MySQL (intended —
8144            // those models can't use AUTO_INCREMENT). The trait is only
8145            // touched on the MySQL arm at runtime, so PG-only consumers
8146            // never see the bound failure.
8147            //
8148            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
8149            // Auto<i64> PK + auto_now_add timestamp) errored hard at
8150            // runtime with "multi-column RETURNING". MySQL has no
8151            // multi-column RETURNING semantic and a follow-up SELECT
8152            // would need cross-trait plumbing. Pragmatic shape: succeed
8153            // with the FIRST Auto field populated from LAST_INSERT_ID();
8154            // any other Auto fields stay `Auto::Unset`. Callers that
8155            // need the DB-defaulted timestamp / UUID can re-fetch the
8156            // row by PK after `save_pool`. Fixes the cookbook chapter
8157            // 12 dialect divergence.
8158            quote! {
8159                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
8160                    ::rustango_from_mysql_auto_id(_id)?;
8161                self.#first = #root::sql::Auto::Set(_converted);
8162                ::core::result::Result::Ok(())
8163            }
8164        } else {
8165            quote! {
8166                let _ = _id;
8167                ::core::result::Result::Ok(())
8168            }
8169        };
8170        quote! {
8171            impl #root::sql::AssignAutoPkPool for #struct_name {
8172                fn __rustango_assign_from_pg_row(
8173                    &mut self,
8174                    _returning_row: &#root::sql::PgReturningRow,
8175                ) -> ::core::result::Result<(), #root::sql::ExecError> {
8176                    #( #auto_assigns )*
8177                    #( #generated_assigns )*
8178                    ::core::result::Result::Ok(())
8179                }
8180                fn __rustango_assign_from_mysql_id(
8181                    &mut self,
8182                    _id: i64,
8183                ) -> ::core::result::Result<(), #root::sql::ExecError> {
8184                    #mysql_body
8185                }
8186                fn __rustango_assign_from_sqlite_row(
8187                    &mut self,
8188                    _returning_row: &#root::sql::SqliteReturningRow,
8189                ) -> ::core::result::Result<(), #root::sql::ExecError> {
8190                    #( #auto_assigns_sqlite )*
8191                    #( #generated_assigns_sqlite )*
8192                    ::core::result::Result::Ok(())
8193                }
8194            }
8195        }
8196    };
8197
8198    let from_aliased_row_inits = &fields.from_aliased_row_inits;
8199    let aliased_row_helper = quote! {
8200        /// Decode a row's aliased target columns (produced by
8201        /// `select_related`'s LEFT JOIN) into a fresh instance of
8202        /// this model. Reads each column via
8203        /// `format!("{prefix}__{col}")`, matching the alias the
8204        /// SELECT writer emitted. Slice 9.0d.
8205        #[doc(hidden)]
8206        #[cfg(feature = "postgres")]
8207        pub fn __rustango_from_aliased_row(
8208            row: &#root::sql::sqlx::postgres::PgRow,
8209            prefix: &str,
8210        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8211            ::core::result::Result::Ok(Self {
8212                #( #from_aliased_row_inits ),*
8213            })
8214        }
8215    };
8216    // v0.23.0-batch8 — MySQL counterpart, gated through the
8217    // cfg-aware macro_rules so PG-only builds expand to nothing.
8218    let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
8219    let aliased_row_helper_my = quote! {
8220        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
8221            #( #from_aliased_row_inits_my ),*
8222        });
8223    };
8224
8225    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
8226    // pattern + cfg gate on the `sqlite` feature.
8227    let aliased_row_helper_sqlite = quote! {
8228        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
8229            #( #from_aliased_row_inits ),*
8230        });
8231    };
8232
8233    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
8234    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
8235    let load_related_impl_sqlite =
8236        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
8237
8238    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
8239    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
8240    // default `Self::objects()`. Each accessor returns a fresh
8241    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
8242    // methods the user defined.
8243    let extra_manager_fns: Vec<TokenStream2> = manager_fns
8244        .iter()
8245        .map(|fn_ident| {
8246            let model_name_str = struct_name.to_string();
8247            let fn_name_str = fn_ident.to_string();
8248            let doc = format!(
8249                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
8250                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
8251                 equivalent to `Self::objects()`. Chains with any \
8252                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
8253                 extension methods."
8254            );
8255            quote! {
8256                #[doc = #doc]
8257                #[must_use]
8258                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
8259                    #root::query::QuerySet::new()
8260                }
8261            }
8262        })
8263        .collect();
8264
8265    quote! {
8266        impl #struct_name {
8267            /// Start a new `QuerySet` over this model.
8268            #[must_use]
8269            pub fn objects() -> #root::query::QuerySet<#struct_name> {
8270                #root::query::QuerySet::new()
8271            }
8272
8273            /// Eloquent-shape alias of [`Self::objects`]. Returns
8274            /// a fresh `QuerySet<Self>` ready for `.filter()` /
8275            /// `.where_()` / etc. Matches Laravel muscle-memory:
8276            ///
8277            /// ```ignore
8278            /// // Eloquent:    Post::query()->where('published', true)
8279            /// // rustango:    Post::query().filter("published", true)
8280            /// //         or:  Post::objects().filter("published", true)
8281            /// ```
8282            ///
8283            /// Both names point at the same underlying constructor;
8284            /// neither is preferred.
8285            #[must_use]
8286            pub fn query() -> #root::query::QuerySet<#struct_name> {
8287                #root::query::QuerySet::new()
8288            }
8289
8290            #( #extra_manager_fns )*
8291
8292            #insert_method
8293
8294            #bulk_insert_method
8295
8296            #bulk_upsert_pool_method
8297
8298            #bulk_update_method
8299
8300            #save_method
8301
8302            #pk_methods
8303
8304            #pk_value_helper
8305
8306            #aliased_row_helper
8307
8308            #column_consts
8309        }
8310
8311        #aliased_row_helper_my
8312
8313        #aliased_row_helper_sqlite
8314
8315        #load_related_impl
8316
8317        #load_related_impl_my
8318
8319        #load_related_impl_sqlite
8320
8321        #has_pk_value_impl
8322
8323        #fk_pk_access_impl
8324
8325        #assign_auto_pk_pool_impl
8326    }
8327}
8328
8329/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8330/// `auto_assigns` but reading from `_returning_row` and writing to
8331/// `_row_mut` instead of `self`.
8332fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8333    let root = rustango_root();
8334    let lines = fields.auto_field_idents.iter().map(|(ident, column, _)| {
8335        let col_lit = column.as_str();
8336        quote! {
8337            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8338                _returning_row,
8339                #col_lit,
8340            )?;
8341        }
8342    });
8343    quote! { #( #lines )* }
8344}
8345
8346/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8347fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8348    let lines = entries.iter().map(|e| {
8349        let ident = &e.ident;
8350        let col_ty = column_type_ident(ident);
8351        quote! {
8352            #[allow(non_upper_case_globals)]
8353            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8354        }
8355    });
8356    quote! { #(#lines)* }
8357}
8358
8359/// Emit a hidden per-model module carrying one zero-sized type per field,
8360/// each with a `Column` impl pointing back at the model.
8361fn column_module_tokens(
8362    module_ident: &syn::Ident,
8363    struct_name: &syn::Ident,
8364    entries: &[ColumnEntry],
8365) -> TokenStream2 {
8366    let root = rustango_root();
8367    let items = entries.iter().map(|e| {
8368        let col_ty = column_type_ident(&e.ident);
8369        let value_ty = &e.value_ty;
8370        let name = &e.name;
8371        let column = &e.column;
8372        let field_type_tokens = &e.field_type_tokens;
8373        quote! {
8374            #[derive(::core::clone::Clone, ::core::marker::Copy)]
8375            pub struct #col_ty;
8376
8377            impl #root::core::Column for #col_ty {
8378                type Model = super::#struct_name;
8379                type Value = #value_ty;
8380                const NAME: &'static str = #name;
8381                const COLUMN: &'static str = #column;
8382                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8383            }
8384        }
8385    });
8386    quote! {
8387        #[doc(hidden)]
8388        #[allow(non_camel_case_types, non_snake_case)]
8389        pub mod #module_ident {
8390            // Re-import the parent scope so field types referencing
8391            // sibling models (e.g. `ForeignKey<Author>`) resolve
8392            // inside this submodule. Without this we'd hit
8393            // `proc_macro_derive_resolution_fallback` warnings.
8394            #[allow(unused_imports)]
8395            use super::*;
8396            #(#items)*
8397        }
8398    }
8399}
8400
8401fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8402    let name = ident_name(field_ident);
8403    syn::Ident::new(&format!("{name}_col"), field_ident.span())
8404}
8405
8406/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8407/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8408fn restore_auto_now_unless(
8409    idents: &[syn::Ident],
8410    ret: &TokenStream2,
8411    keep: &TokenStream2,
8412    body: &TokenStream2,
8413) -> TokenStream2 {
8414    if idents.is_empty() {
8415        return body.clone();
8416    }
8417    let prev: Vec<syn::Ident> = (0..idents.len())
8418        .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8419        .collect();
8420    quote! {
8421        #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8422        let _saved: #ret = async { #body }.await;
8423        if !(#keep) {
8424            #( self.#idents = #prev; )*
8425        }
8426        _saved
8427    }
8428}
8429
8430/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8431fn ident_name(ident: &syn::Ident) -> String {
8432    syn::ext::IdentExt::unraw(ident).to_string()
8433}
8434
8435/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8436fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8437    match syn::parse_str::<syn::Ident>(name) {
8438        Ok(mut ident) => {
8439            ident.set_span(span);
8440            ident
8441        }
8442        Err(_) => syn::Ident::new_raw(name, span),
8443    }
8444}
8445
8446fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8447    syn::Ident::new(
8448        &format!("__rustango_cols_{struct_name}"),
8449        struct_name.span(),
8450    )
8451}
8452
8453fn from_row_impl_tokens(
8454    struct_name: &syn::Ident,
8455    from_row_inits: &[TokenStream2],
8456    from_row_inits_my: &[TokenStream2],
8457) -> TokenStream2 {
8458    let root = rustango_root();
8459    // The Postgres impl is always emitted — every rustango build pulls in
8460    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8461    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8462    // macro_rules whose body collapses to nothing when rustango is built
8463    // without the `mysql` feature. No user-facing feature shim required.
8464    //
8465    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8466    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8467    // back into a comma-separated list inside square brackets. Since each
8468    // entry is already in `field: expr` shape, the existing tokens slot in.
8469    quote! {
8470        #[cfg(feature = "postgres")]
8471        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8472            for #struct_name
8473        {
8474            fn from_row(
8475                row: &'r #root::sql::sqlx::postgres::PgRow,
8476            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8477                ::core::result::Result::Ok(Self {
8478                    #( #from_row_inits ),*
8479                })
8480            }
8481        }
8482
8483        #root::__impl_my_from_row!(#struct_name, |row| {
8484            #( #from_row_inits_my ),*
8485        });
8486
8487        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8488            #( #from_row_inits ),*
8489        });
8490    }
8491}
8492
8493struct ContainerAttrs {
8494    table: Option<String>,
8495    display: Option<(String, proc_macro2::Span)>,
8496    /// Explicit app label from `#[rustango(app = "blog")]`.
8497    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8498    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8499    /// at runtime — this attribute is the override for cases where
8500    /// the inference is wrong (e.g. a model that conceptually belongs
8501    /// to one app but is physically in another module).
8502    app: Option<String>,
8503    /// Per-model admin knobs from
8504    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8505    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8506    /// admin code falls back to `AdminConfig::DEFAULT`.
8507    admin: Option<AdminAttrs>,
8508    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8509    /// `None` when the model isn't audited — write paths emit no
8510    /// audit entries. When present, single-row writes capture
8511    /// before/after for the listed fields and bulk writes batch
8512    /// snapshots into one INSERT into `rustango_audit_log`.
8513    audit: Option<AuditAttrs>,
8514    /// `true` when `#[rustango(permissions)]` is present. Signals that
8515    /// `auto_create_permissions` should seed the four CRUD codenames for
8516    /// this model.
8517    permissions: bool,
8518    /// Many-to-many relations declared via
8519    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8520    ///                 src = "post_id", dst = "tag_id"))]`.
8521    m2m: Vec<M2MAttr>,
8522    /// Polymorphic M2M relations declared via
8523    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8524    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8525    ///   related_column = "tag_id"))]` (issue #818).
8526    generic_m2m: Vec<GenericM2MAttr>,
8527    /// Composite indexes declared via
8528    /// `#[rustango(index("col1, col2"))]` or
8529    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8530    /// Single-column indexes from `#[rustango(index)]` on fields are
8531    /// accumulated here during field collection.
8532    indexes: Vec<IndexAttr>,
8533    /// Table-level CHECK constraints declared via
8534    /// `#[rustango(check(name = "…", expr = "…"))]`.
8535    checks: Vec<CheckAttr>,
8536    /// Table-level PG `EXCLUDE` constraints declared via
8537    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8538    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8539    /// migration writer renders them on Postgres and skips with a
8540    /// warning on MySQL/SQLite. Issue #319.
8541    excludes: Vec<ExcludeAttr>,
8542    /// Composite (multi-column) FKs declared via
8543    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8544    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8545    composite_fks: Vec<CompositeFkAttr>,
8546    /// Generic ("any model") FKs declared via
8547    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8548    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8549    generic_fks: Vec<GenericFkAttr>,
8550    /// Where this model lives in a tenancy deployment, declared via
8551    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8552    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8553    /// to partition diff output between registry-scoped and
8554    /// tenant-scoped migration files.
8555    scope: Option<String>,
8556    /// Extra QuerySet accessor names from
8557    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8558    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8559    /// the default `Self::objects()`. Multiple attributes allowed.
8560    manager_fns: Vec<syn::Ident>,
8561    /// Default ordering declared via `#[rustango(default_order =
8562    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8563    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8564    /// `-` prefix means descending; the `+` prefix or no prefix means
8565    /// ascending.
8566    default_order: Vec<(String, bool, proc_macro2::Span)>,
8567    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8568    /// Routes the emitted schema's `is_view = true` so the migration
8569    /// snapshot skips this model (its underlying SQL view is operator-
8570    /// managed, not rustango-managed).
8571    is_view: bool,
8572    /// `#[rustango(managed = false)]` — who owns the table.
8573    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8574    /// the migration snapshot skips this model so `makemigrations` /
8575    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8576    /// TABLE` against it (operator-managed schema).
8577    managed: bool,
8578    /// Name of the model's base manager, from
8579    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8580    /// `ModelSchema::base_manager_name`. Declarative-only today.
8581    base_manager_name: Option<String>,
8582    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8583    /// field this model's instances are ordered relative to.
8584    /// Declarative-only today; threaded onto
8585    /// `ModelSchema::order_with_respect_to`.
8586    order_with_respect_to: Option<String>,
8587    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8588    /// Marks the model as a proxy that
8589    /// shares its DB table with another struct. Threaded into
8590    /// `ModelSchema::proxy` so future codegen can skip table-owning
8591    /// behavior for proxies.
8592    proxy: bool,
8593    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8594    /// Each comma-separated capability token surfaces on
8595    /// `ModelSchema::required_db_features` so `manage check --deploy`
8596    /// can warn when the active dialect lacks one.
8597    required_db_features: Vec<String>,
8598    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8599    /// Normalized to the dialect name `manage check --deploy`
8600    /// compares against `Settings.database.backend`. Aliases
8601    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8602    /// stored under the canonical name.
8603    required_db_vendor: Option<String>,
8604    /// `#[rustango(default_related_name = "...")]`. Threaded into
8605    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8606    /// name to use when an FK / M2M field doesn't override it.
8607    /// Today rustango doesn't auto-emit reverse managers; the
8608    /// metadata is the foundation for that work.
8609    default_related_name: Option<String>,
8610    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8611    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8612    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8613    /// inline `COMMENT='...'`, SQLite: no-op).
8614    db_table_comment: Option<String>,
8615    /// `#[rustango(get_latest_by = "created_at")]` /
8616    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8617    /// `(column, descending)` where `descending = true` when the
8618    /// attribute value starts with `-`. Threaded into
8619    /// `ModelSchema::get_latest_by`.
8620    get_latest_by: Option<(String, bool, proc_macro2::Span)>,
8621    /// Extra `(codename, label)` permissions
8622    /// from `#[rustango(extra_permissions = "approve:Can approve,
8623    /// archive:Can archive")]`. Comma-separated `codename:label`
8624    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8625    extra_permissions: Vec<(String, String)>,
8626    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8627    /// `"view"`) the framework
8628    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8629    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8630    /// Validated at parse time; unknown actions fail with a span-pointing
8631    /// error.
8632    default_permissions: Vec<String>,
8633    /// `#[rustango(verbose_name = "blog post")]` — the
8634    /// human-readable singular label for the model. Threaded into
8635    /// `ModelSchema::verbose_name` so admin section headers /
8636    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8637    /// over the Rust struct identifier.
8638    verbose_name: Option<String>,
8639    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8640    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8641    /// When unset, `display_label_plural()` auto-suffixes `s`.
8642    verbose_name_plural: Option<String>,
8643    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8644    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8645    /// a name (used by `QuerySet::without_global_scope`) with a
8646    /// `fn() -> WhereExpr` path that the macro emits into
8647    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8648    global_scopes: Vec<GlobalScopeAttr>,
8649    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8650    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8651    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8652    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8653    /// entry emits an inherent `<name>_through(&self)` accessor that
8654    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8655    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8656    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8657    through_relations: Vec<ThroughAttr>,
8658    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8659    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8660    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8661    /// Each entry emits two associated functions on the parent —
8662    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8663    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8664    /// over a correlated subquery against the child table. Users
8665    /// drop the result into `QuerySet::where_raw(...)`.
8666    reverse_has_relations: Vec<ReverseHasAttr>,
8667    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8668    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8669    /// entry so the relation-existence family resolves polymorphic
8670    /// children by name.
8671    generic_has_relations: Vec<GenericHasAttr>,
8672}
8673
8674/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8675/// declaration. Each entry becomes one `core::GlobalScope` in the
8676/// emitted schema literal; `apply` resolves at macro-expand time
8677/// against the consumer's scope so the function must be in scope at
8678/// the use site. Issue #820.
8679struct GlobalScopeAttr {
8680    name: String,
8681    apply: syn::Path,
8682}
8683
8684/// Parsed `#[rustango(through(...))]` declaration. Issue
8685/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8686/// `hasManyThrough` / `hasOneThrough` parity.
8687///
8688/// `Country hasManyThrough Post via User` declares as:
8689///
8690/// ```ignore
8691/// #[rustango(through(
8692///     name                   = "posts",
8693///     far                    = "Post",
8694///     far_fk_column          = "author_id",
8695///     intermediate           = "User",
8696///     intermediate_fk_column = "country_id",
8697/// ))]
8698/// struct Country { ... }
8699/// ```
8700///
8701/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8702/// which returns a queryset filtered via a correlated subquery —
8703/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8704/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8705/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8706/// because the subquery lives inside a `WhereExpr::InSubquery` node
8707/// and the outer queryset's pending list stays empty.
8708///
8709/// All four required arguments use **SQL column / table names**
8710/// (not Rust field names) to sidestep the multi-hop-filter substrate
8711/// gap. Once that substrate lands, a higher-level Rust-field-name
8712/// shorthand can be added without breaking this surface.
8713struct ThroughAttr {
8714    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8715    name: String,
8716    /// Far model type identifier. `far = "Post"` → returns
8717    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8718    /// the derive expands.
8719    far: syn::Ident,
8720    /// SQL column on the far model's table that references the
8721    /// intermediate model's primary key. For `Post`'s
8722    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8723    /// default `<field>_id` convention) or whatever the user
8724    /// declared via `#[rustango(db_column = "...")]`.
8725    far_fk_column: String,
8726    /// Intermediate model type identifier. Needed to look up its
8727    /// `SCHEMA` so the subquery's `FROM` clause points at the
8728    /// intermediate table. `intermediate = "User"`.
8729    intermediate: syn::Ident,
8730    /// SQL column on the intermediate's table that references the
8731    /// source (this) model's primary key. For `User`'s
8732    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8733    intermediate_fk_column: String,
8734    /// SQL primary-key column on the intermediate's table — the
8735    /// column the subquery projects. Optional; defaults to `"id"`
8736    /// (rustango's default PK column name). Override when the
8737    /// intermediate declares a custom PK column.
8738    intermediate_pk_column: String,
8739}
8740
8741/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8742/// child_fk_column = "..."))]` declaration. Issue
8743/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8744/// `whereHas` / `whereDoesntHave` parity.
8745///
8746/// `Post hasMany Comment` declares as:
8747///
8748/// ```ignore
8749/// #[rustango(reverse_has(
8750///     name             = "comments",
8751///     child            = "Comment",
8752///     child_fk_column  = "post_id",
8753/// ))]
8754/// struct Post { ... }
8755/// ```
8756///
8757/// The macro emits two associated functions on `Post`:
8758///
8759/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8760///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8761///   <outer>.<self_pk_column>)`.
8762/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8763///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8764///
8765/// User code:
8766///
8767/// ```ignore
8768/// // Posts with at least one comment:
8769/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8770/// // Posts with no comments:
8771/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8772/// ```
8773///
8774/// As with #817, all column / table identifiers are **SQL names**
8775/// (not Rust field names) so the substrate is independent of the
8776/// outstanding multi-hop filter resolver gap. The emitted
8777/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8778/// SQL-emit time via the writer's scope stack.
8779struct ReverseHasAttr {
8780    /// Accessor name. `name = "comments"` → emits
8781    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8782    name: String,
8783    /// Child model type identifier. `child = "Comment"` — needed to
8784    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8785    /// points at the child table.
8786    child: syn::Ident,
8787    /// SQL column on the child's table that references this model's
8788    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8789    /// column is `"post_id"`.
8790    child_fk_column: String,
8791    /// Column on **this** model's table the child FK references — the
8792    /// `OuterRef` target. Defaults to the PK column (`"id"` without one).
8793    self_pk_column: Option<String>,
8794}
8795
8796impl ReverseHasAttr {
8797    fn self_pk_column(&self) -> &str {
8798        self.self_pk_column.as_deref().unwrap_or("id")
8799    }
8800}
8801
8802/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8803/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8804/// relation-existence family (issue #830). The child is a polymorphic,
8805/// content-type-discriminated model; emits a `Model::
8806/// generic_reverse_relations()` entry the queryset resolves by name.
8807struct GenericHasAttr {
8808    /// Accessor name, e.g. `name = "tags"`.
8809    name: String,
8810    /// Child model type identifier (`child = "Tag"`) — looked up for its
8811    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8812    child: syn::Ident,
8813    /// Column on the child table holding the parent's content-type id.
8814    /// Optional; defaults to `"content_type_id"`.
8815    ct_column: String,
8816    /// Column on the child table holding the parent's PK value.
8817    /// Optional; defaults to `"object_pk"`.
8818    pk_column: String,
8819    /// SQL primary-key column on **this** (parent) model's table.
8820    /// Defaults to the PK column (`"id"` without one).
8821    self_pk_column: Option<String>,
8822}
8823
8824impl GenericHasAttr {
8825    fn self_pk_column(&self) -> &str {
8826        self.self_pk_column.as_deref().unwrap_or("id")
8827    }
8828}
8829
8830/// Parsed form of one index declaration (field-level or container-level).
8831struct IndexAttr {
8832    /// The declaring attribute, for diagnostics.
8833    span: proc_macro2::Span,
8834    /// Index name; auto-derived when `None` at parse time.
8835    name: Option<String>,
8836    /// Column names in the index.
8837    columns: Vec<String>,
8838    /// `true` for `CREATE UNIQUE INDEX`.
8839    unique: bool,
8840    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8841    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8842    /// `"btree"` when the attribute is absent — the DDL writer omits
8843    /// the `USING` clause and the backend uses its own default
8844    /// (btree on every supported dialect).
8845    method: String,
8846    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8847    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8848    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8849    where_clause: Option<String>,
8850    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8851    /// Empty `Vec` (the default) means "no covering columns".
8852    include: Vec<String>,
8853}
8854
8855/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8856struct CheckAttr {
8857    name: String,
8858    expr: String,
8859}
8860
8861/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8862/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8863/// PG-only — surfaced on every backend in the macro emit; the migration
8864/// writer skips the op on MySQL/SQLite. Issue #319.
8865struct ExcludeAttr {
8866    /// Constraint name (free-form Rust identifier).
8867    name: String,
8868    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8869    using: String,
8870    /// Comma-separated `(column, operator)` pairs, in declaration
8871    /// order. Parsed from `"col WITH op, col WITH op"`.
8872    elements: Vec<(String, String)>,
8873    /// Optional `WHERE` predicate (raw SQL).
8874    where_clause: Option<String>,
8875}
8876
8877/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8878/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8879/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8880/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8881/// the model, not the field.
8882struct CompositeFkAttr {
8883    /// Logical relation name (free-form Rust identifier).
8884    name: String,
8885    /// SQL table name of the target.
8886    to: String,
8887    /// Source-side column names, in declaration order.
8888    from: Vec<String>,
8889    /// Target-side column names, same length / order as `from`.
8890    on: Vec<String>,
8891}
8892
8893/// Parsed form of one `#[rustango(generic_fk(name = "target",
8894/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8895/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8896/// generic ("any model") FKs live on the model, not the field.
8897struct GenericFkAttr {
8898    /// Logical relation name (free-form Rust identifier).
8899    name: String,
8900    /// Source-side column carrying the `content_type_id` value.
8901    ct_column: String,
8902    /// Source-side column carrying the target row's primary key.
8903    pk_column: String,
8904}
8905
8906/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8907struct M2MAttr {
8908    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8909    name: String,
8910    /// Target table (e.g. `"app_tags"`).
8911    to: String,
8912    /// Junction table (e.g. `"post_tags"`).
8913    through: String,
8914    /// Source FK column in the junction table (e.g. `"post_id"`).
8915    src: String,
8916    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8917    dst: String,
8918    /// Whether the migration writer should auto-create the junction
8919    /// table. Default `true`. Set `auto_create = false` (#324) when
8920    /// the operator declares the through table as its own
8921    /// `#[derive(Model)]` struct with extra columns.
8922    auto_create: bool,
8923}
8924
8925/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8926/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8927/// junction carries a ContentType discriminator so unrelated models
8928/// share one pivot + related set.
8929struct GenericM2MAttr {
8930    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8931    name: String,
8932    /// Polymorphic junction table (e.g. `"taggables"`).
8933    through: String,
8934    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8935    pk_column: String,
8936    /// Junction column holding the owning model's `content_type_id`
8937    /// discriminator (e.g. `"taggable_type"`).
8938    ct_column: String,
8939    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8940    related_column: String,
8941}
8942
8943/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8944/// "user"))]`. `track` is a comma-separated list of field names whose
8945/// before/after values land in the JSONB `changes` column. `source`
8946/// is informational only — it pins a default source when the model
8947/// is written outside any `audit::with_source(...)` scope (rare).
8948#[derive(Default)]
8949struct AuditAttrs {
8950    /// Field names to capture in the `changes` JSONB. Validated
8951    /// against declared scalar fields at compile time. Empty means
8952    /// "track every scalar field".
8953    track: Option<(Vec<String>, proc_macro2::Span)>,
8954}
8955
8956/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8957/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8958/// comma-separated strings; we validate each ident against the model's
8959/// declared fields at compile time.
8960#[derive(Default)]
8961struct AdminAttrs {
8962    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8963    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8964    list_per_page: Option<usize>,
8965    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8966    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8967    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8968    /// Bulk action names. No field-validation against model fields —
8969    /// these are action handlers, not column references.
8970    actions: Option<(Vec<String>, proc_macro2::Span)>,
8971    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8972    /// sections, comma-separated fields per section, optional
8973    /// `Title:` prefix. Empty title omits the `<legend>`.
8974    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8975    /// `admin(list_display_links = "title")`. Names
8976    /// from `list_display` whose cells should link to detail/edit.
8977    /// Issue #350.
8978    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8979    /// `admin(search_help_text = "...")`. Short
8980    /// caption rendered beside the admin list view's search box.
8981    /// Issue #353.
8982    search_help_text: Option<String>,
8983    /// `admin(actions_on_top = false)`. Hides the
8984    /// action-bar above the table. Default `true`. Issue #354.
8985    actions_on_top: Option<bool>,
8986    /// `admin(actions_on_bottom = true)`. Renders an
8987    /// additional action-bar below the table. Default `false`.
8988    /// Issue #354.
8989    actions_on_bottom: Option<bool>,
8990    /// `admin(date_hierarchy = "created_at")`. Name of
8991    /// a date / datetime field whose values render as a clickable
8992    /// year / month / day drill-down strip above the list table.
8993    /// Empty / unset disables the strip. Issue #355.
8994    date_hierarchy: Option<String>,
8995    /// `admin(prepopulated_fields = "slug:title")`.
8996    /// Each entry is `target:source[+source2]`; multiple entries are
8997    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8998    /// The admin change-form emits JS that slugifies the source values
8999    /// into the target field on every keystroke. Issue #356.
9000    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
9001    /// `admin(raw_id_fields = "parent, owner")`. Names
9002    /// of FK fields whose change-form widget renders a lookup link
9003    /// next to the input. Issue #357.
9004    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
9005    /// `admin(autocomplete_fields = "author_id")`.
9006    /// Names of FK fields whose change-form widget renders an
9007    /// Ajax-driven typeahead populated from a `__autocomplete`
9008    /// endpoint on the target model. Issue #358.
9009    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
9010    /// `admin(list_select_related = "all" | "none" | "author, …")`
9011    /// — tunes the admin list view's FK auto-JOIN
9012    /// policy. Default `"all"` matches rustango's join-everything
9013    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
9014    list_select_related: Option<String>,
9015    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
9016    /// Each entry is `field_name:widget_name`; multiple
9017    /// entries comma-separated. Empty / unset → no overrides. The
9018    /// list of widget names supported is documented on
9019    /// `AdminConfig::formfield_overrides`. Issue #359.
9020    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
9021}
9022
9023fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
9024    let mut out = ContainerAttrs {
9025        table: None,
9026        display: None,
9027        app: None,
9028        admin: None,
9029        audit: None,
9030        // Default `permissions = true` so every `#[derive(Model)]`
9031        // gets the four CRUD codenames seeded by `auto_create_permissions`
9032        // and is visible to non-superusers in the tenant admin without
9033        // manual per-model annotation. Models that intentionally don't
9034        // want permission rows (registry-internal types, framework
9035        // tables operators shouldn't manage directly) opt out via
9036        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
9037        // out-of-the-box admin invisibility regression (#62).
9038        permissions: true,
9039        m2m: Vec::new(),
9040        generic_m2m: Vec::new(),
9041        indexes: Vec::new(),
9042        checks: Vec::new(),
9043        excludes: Vec::new(),
9044        composite_fks: Vec::new(),
9045        generic_fks: Vec::new(),
9046        scope: None,
9047        manager_fns: Vec::new(),
9048        default_order: Vec::new(),
9049        is_view: false,
9050        managed: true,
9051        verbose_name: None,
9052        verbose_name_plural: None,
9053        base_manager_name: None,
9054        order_with_respect_to: None,
9055        proxy: false,
9056        required_db_features: Vec::new(),
9057        required_db_vendor: None,
9058        default_related_name: None,
9059        db_table_comment: None,
9060        get_latest_by: None,
9061        extra_permissions: Vec::new(),
9062        default_permissions: Vec::new(),
9063        global_scopes: Vec::new(),
9064        through_relations: Vec::new(),
9065        reverse_has_relations: Vec::new(),
9066        generic_has_relations: Vec::new(),
9067    };
9068    for attr in &input.attrs {
9069        if !attr.path().is_ident("rustango") {
9070            continue;
9071        }
9072        attr.parse_nested_meta(|meta| {
9073            if meta.path.is_ident("table") {
9074                let s: LitStr = meta.value()?.parse()?;
9075                let name = s.value();
9076                // v0.27.3 (#65) — macro-time guard against table names
9077                // that compile but break SQL downstream. Hyphens are
9078                // the common footgun: PostgreSQL accepts a quoted
9079                // `"intermediate-region"` in CREATE TABLE, but the
9080                // FK / index name derivation in `migrate::ddl`
9081                // emits `intermediate-region_field_fkey` unquoted,
9082                // which then fails the SQL parser. Same shape rule
9083                // as Postgres regular identifiers so the safe path
9084                // is the only path.
9085                validate_table_name(&name, s.span())?;
9086                out.table = Some(name);
9087                return Ok(());
9088            }
9089            if meta.path.is_ident("display") {
9090                let s: LitStr = meta.value()?.parse()?;
9091                out.display = Some((s.value(), s.span()));
9092                return Ok(());
9093            }
9094            if meta.path.is_ident("app") {
9095                let s: LitStr = meta.value()?.parse()?;
9096                out.app = Some(s.value());
9097                return Ok(());
9098            }
9099            if meta.path.is_ident("scope") {
9100                let s: LitStr = meta.value()?.parse()?;
9101                let val = s.value();
9102                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
9103                    return Err(meta.error(format!(
9104                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
9105                    )));
9106                }
9107                out.scope = Some(val);
9108                return Ok(());
9109            }
9110            if meta.path.is_ident("admin") {
9111                let mut admin = AdminAttrs::default();
9112                meta.parse_nested_meta(|inner| {
9113                    if inner.path.is_ident("list_display") {
9114                        let s: LitStr = inner.value()?.parse()?;
9115                        admin.list_display =
9116                            Some((split_field_list(&s.value()), s.span()));
9117                        return Ok(());
9118                    }
9119                    if inner.path.is_ident("search_fields") {
9120                        let s: LitStr = inner.value()?.parse()?;
9121                        admin.search_fields =
9122                            Some((split_field_list(&s.value()), s.span()));
9123                        return Ok(());
9124                    }
9125                    if inner.path.is_ident("readonly_fields") {
9126                        let s: LitStr = inner.value()?.parse()?;
9127                        admin.readonly_fields =
9128                            Some((split_field_list(&s.value()), s.span()));
9129                        return Ok(());
9130                    }
9131                    if inner.path.is_ident("list_per_page") {
9132                        let lit: syn::LitInt = inner.value()?.parse()?;
9133                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
9134                        return Ok(());
9135                    }
9136                    if inner.path.is_ident("ordering") {
9137                        let s: LitStr = inner.value()?.parse()?;
9138                        admin.ordering = Some((
9139                            parse_ordering_list(&s.value()),
9140                            s.span(),
9141                        ));
9142                        return Ok(());
9143                    }
9144                    if inner.path.is_ident("list_filter") {
9145                        let s: LitStr = inner.value()?.parse()?;
9146                        admin.list_filter =
9147                            Some((split_field_list(&s.value()), s.span()));
9148                        return Ok(());
9149                    }
9150                    if inner.path.is_ident("actions") {
9151                        let s: LitStr = inner.value()?.parse()?;
9152                        admin.actions =
9153                            Some((split_field_list(&s.value()), s.span()));
9154                        return Ok(());
9155                    }
9156                    if inner.path.is_ident("fieldsets") {
9157                        let s: LitStr = inner.value()?.parse()?;
9158                        admin.fieldsets =
9159                            Some((parse_fieldset_list(&s.value()), s.span()));
9160                        return Ok(());
9161                    }
9162                    if inner.path.is_ident("list_display_links") {
9163                        let s: LitStr = inner.value()?.parse()?;
9164                        admin.list_display_links =
9165                            Some((split_field_list(&s.value()), s.span()));
9166                        return Ok(());
9167                    }
9168                    if inner.path.is_ident("search_help_text") {
9169                        let s: LitStr = inner.value()?.parse()?;
9170                        admin.search_help_text = Some(s.value());
9171                        return Ok(());
9172                    }
9173                    if inner.path.is_ident("actions_on_top") {
9174                        let lit: syn::LitBool = inner.value()?.parse()?;
9175                        admin.actions_on_top = Some(lit.value);
9176                        return Ok(());
9177                    }
9178                    if inner.path.is_ident("actions_on_bottom") {
9179                        let lit: syn::LitBool = inner.value()?.parse()?;
9180                        admin.actions_on_bottom = Some(lit.value);
9181                        return Ok(());
9182                    }
9183                    if inner.path.is_ident("date_hierarchy") {
9184                        let s: LitStr = inner.value()?.parse()?;
9185                        admin.date_hierarchy = Some(s.value());
9186                        return Ok(());
9187                    }
9188                    if inner.path.is_ident("prepopulated_fields") {
9189                        let s: LitStr = inner.value()?.parse()?;
9190                        admin.prepopulated_fields =
9191                            Some((parse_prepopulated_list(&s.value()), s.span()));
9192                        return Ok(());
9193                    }
9194                    if inner.path.is_ident("raw_id_fields") {
9195                        let s: LitStr = inner.value()?.parse()?;
9196                        admin.raw_id_fields =
9197                            Some((split_field_list(&s.value()), s.span()));
9198                        return Ok(());
9199                    }
9200                    if inner.path.is_ident("autocomplete_fields") {
9201                        let s: LitStr = inner.value()?.parse()?;
9202                        admin.autocomplete_fields =
9203                            Some((split_field_list(&s.value()), s.span()));
9204                        return Ok(());
9205                    }
9206                    if inner.path.is_ident("list_select_related") {
9207                        let s: LitStr = inner.value()?.parse()?;
9208                        admin.list_select_related = Some(s.value());
9209                        return Ok(());
9210                    }
9211                    if inner.path.is_ident("formfield_overrides") {
9212                        let s: LitStr = inner.value()?.parse()?;
9213                        admin.formfield_overrides =
9214                            Some((parse_formfield_overrides(&s.value()), s.span()));
9215                        return Ok(());
9216                    }
9217                    Err(inner.error(
9218                        "unknown admin attribute (supported: \
9219                         `list_display`, `list_display_links`, \
9220                         `search_fields`, `search_help_text`, \
9221                         `readonly_fields`, \
9222                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
9223                         `actions_on_top`, `actions_on_bottom`, \
9224                         `date_hierarchy`, \
9225                         `prepopulated_fields`, \
9226                         `raw_id_fields`, \
9227                         `autocomplete_fields`, \
9228                         `list_select_related`, \
9229                         `formfield_overrides`, \
9230                         `fieldsets`)",
9231                    ))
9232                })?;
9233                out.admin = Some(admin);
9234                return Ok(());
9235            }
9236            if meta.path.is_ident("manager") {
9237                // Its empty trait could not take methods (#2132).
9238                return Err(meta.error(
9239                    "`manager(ext = ...)` was removed (#2132): declare your own \
9240                     `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
9241                ));
9242            }
9243            if meta.path.is_ident("manager_fn") {
9244                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
9245                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
9246                // next to the default `Self::objects()`. Multiple
9247                // attributes accumulate.
9248                let s: LitStr = meta.value()?.parse()?;
9249                let name = s.value();
9250                if name.is_empty() {
9251                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
9252                }
9253                if name == "objects" {
9254                    return Err(meta.error(
9255                        "`manager_fn = \"objects\"` collides with the default \
9256                         accessor — pick a different name",
9257                    ));
9258                }
9259                let ident = syn::Ident::new(&name, s.span());
9260                if out.manager_fns.iter().any(|prev| *prev == ident) {
9261                    return Err(meta.error(format!(
9262                        "duplicate `manager_fn = \"{name}\"`"
9263                    )));
9264                }
9265                out.manager_fns.push(ident);
9266                return Ok(());
9267            }
9268            if meta.path.is_ident("default_order") {
9269                // `#[rustango(default_order = "-created_at, status")]`
9270                // — issue #291 / T2.5. Comma-separated list; `-prefix`
9271                // means descending, `+prefix` or bare name means ascending.
9272                // Per-query opt-in via `QuerySet::with_default_order()`.
9273                let s: LitStr = meta.value()?.parse()?;
9274                let raw = s.value();
9275                let span = s.span();
9276                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9277                    Vec::new();
9278                for entry in raw.split(',') {
9279                    let trimmed = entry.trim();
9280                    if trimmed.is_empty() {
9281                        return Err(syn::Error::new(
9282                            span,
9283                            "`default_order = \"...\"` has an empty entry — \
9284                             check for a stray comma",
9285                        ));
9286                    }
9287                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9288                        (true, rest.trim().to_owned())
9289                    } else if let Some(rest) = trimmed.strip_prefix('+') {
9290                        (false, rest.trim().to_owned())
9291                    } else {
9292                        (false, trimmed.to_owned())
9293                    };
9294                    if name.is_empty() {
9295                        return Err(syn::Error::new(
9296                            span,
9297                            "`default_order` entry has no column name after the prefix",
9298                        ));
9299                    }
9300                    if parsed.iter().any(|(n, _, _)| *n == name) {
9301                        return Err(syn::Error::new(
9302                            span,
9303                            format!("duplicate column `{name}` in `default_order`"),
9304                        ));
9305                    }
9306                    parsed.push((name, desc, span));
9307                }
9308                if parsed.is_empty() {
9309                    return Err(syn::Error::new(
9310                        span,
9311                        "`default_order = \"...\"` cannot be empty",
9312                    ));
9313                }
9314                out.default_order = parsed;
9315                return Ok(());
9316            }
9317            if meta.path.is_ident("global_scope") {
9318                // `#[rustango(global_scope(name = "active", apply =
9319                //  path::to::fn))]` — issue #820. The apply function
9320                // path resolves at macro-expand time in the consumer's
9321                // scope; the macro re-emits it verbatim into the
9322                // `ModelSchema::global_scopes` slice literal.
9323                let span = meta.path.span();
9324                let mut scope_name: Option<String> = None;
9325                let mut apply_path: Option<syn::Path> = None;
9326                meta.parse_nested_meta(|inner| {
9327                    if inner.path.is_ident("name") {
9328                        let s: LitStr = inner.value()?.parse()?;
9329                        let raw = s.value();
9330                        if raw.trim().is_empty() {
9331                            return Err(syn::Error::new(
9332                                s.span(),
9333                                "`global_scope(name = \"...\")` must not be empty",
9334                            ));
9335                        }
9336                        scope_name = Some(raw);
9337                        return Ok(());
9338                    }
9339                    if inner.path.is_ident("apply") {
9340                        let p: syn::Path = inner.value()?.parse()?;
9341                        apply_path = Some(p);
9342                        return Ok(());
9343                    }
9344                    Err(inner.error(
9345                        "unknown `global_scope` attribute (supported: \
9346                         `name`, `apply`)",
9347                    ))
9348                })?;
9349                let Some(name) = scope_name else {
9350                    return Err(syn::Error::new(
9351                        span,
9352                        "`global_scope` requires `name = \"...\"`",
9353                    ));
9354                };
9355                let Some(apply) = apply_path else {
9356                    return Err(syn::Error::new(
9357                        span,
9358                        "`global_scope` requires `apply = fn_path`",
9359                    ));
9360                };
9361                if out.global_scopes.iter().any(|s| s.name == name) {
9362                    return Err(syn::Error::new(
9363                        span,
9364                        format!(
9365                            "duplicate global scope name `{name}` — \
9366                             pick a unique identifier so \
9367                             `QuerySet::without_global_scope(\"{name}\")` \
9368                             is unambiguous"
9369                        ),
9370                    ));
9371                }
9372                out.global_scopes.push(GlobalScopeAttr { name, apply });
9373                return Ok(());
9374            }
9375            if meta.path.is_ident("through") {
9376                // `#[rustango(through(name = "posts", far = "Post",
9377                //  far_fk_column = "author_id", intermediate = "User",
9378                //  intermediate_fk_column = "country_id"
9379                //  [, intermediate_pk_column = "id"]))]` — issue #817.
9380                let span = meta.path.span();
9381                let mut accessor_name: Option<String> = None;
9382                let mut far_ident: Option<syn::Ident> = None;
9383                let mut far_fk_column: Option<String> = None;
9384                let mut intermediate_ident: Option<syn::Ident> = None;
9385                let mut intermediate_fk_column: Option<String> = None;
9386                let mut intermediate_pk_column: Option<String> = None;
9387                fn parse_nonempty_string(
9388                    inner: &syn::meta::ParseNestedMeta<'_>,
9389                    field: &str,
9390                ) -> syn::Result<String> {
9391                    let s: LitStr = inner.value()?.parse()?;
9392                    let raw = s.value();
9393                    let trimmed = raw.trim();
9394                    if trimmed.is_empty() {
9395                        return Err(syn::Error::new(
9396                            s.span(),
9397                            format!("`through({field} = \"...\")` must not be empty"),
9398                        ));
9399                    }
9400                    Ok(trimmed.to_owned())
9401                }
9402                meta.parse_nested_meta(|inner| {
9403                    if inner.path.is_ident("name") {
9404                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9405                        return Ok(());
9406                    }
9407                    if inner.path.is_ident("far") {
9408                        // Far model name accepted as a string literal
9409                        // so the attribute fits inside the existing
9410                        // `parse_nested_meta` shape; parsed back into a
9411                        // `syn::Ident` so the emitted accessor can
9412                        // reference the type directly.
9413                        let s: LitStr = inner.value()?.parse()?;
9414                        let raw = s.value();
9415                        let trimmed = raw.trim();
9416                        if trimmed.is_empty() {
9417                            return Err(syn::Error::new(
9418                                s.span(),
9419                                "`through(far = \"...\")` must not be empty",
9420                            ));
9421                        }
9422                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9423                        return Ok(());
9424                    }
9425                    if inner.path.is_ident("far_fk_column") {
9426                        far_fk_column =
9427                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9428                        return Ok(());
9429                    }
9430                    if inner.path.is_ident("intermediate") {
9431                        let s: LitStr = inner.value()?.parse()?;
9432                        let raw = s.value();
9433                        let trimmed = raw.trim();
9434                        if trimmed.is_empty() {
9435                            return Err(syn::Error::new(
9436                                s.span(),
9437                                "`through(intermediate = \"...\")` must not be empty",
9438                            ));
9439                        }
9440                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9441                        return Ok(());
9442                    }
9443                    if inner.path.is_ident("intermediate_fk_column") {
9444                        intermediate_fk_column =
9445                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9446                        return Ok(());
9447                    }
9448                    if inner.path.is_ident("intermediate_pk_column") {
9449                        intermediate_pk_column =
9450                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9451                        return Ok(());
9452                    }
9453                    Err(inner.error(
9454                        "unknown `through` attribute (supported: \
9455                         `name`, `far`, `far_fk_column`, \
9456                         `intermediate`, `intermediate_fk_column`, \
9457                         `intermediate_pk_column`)",
9458                    ))
9459                })?;
9460                let Some(name) = accessor_name else {
9461                    return Err(syn::Error::new(
9462                        span,
9463                        "`through` requires `name = \"...\"`",
9464                    ));
9465                };
9466                let Some(far) = far_ident else {
9467                    return Err(syn::Error::new(
9468                        span,
9469                        "`through` requires `far = \"FarModelType\"`",
9470                    ));
9471                };
9472                let Some(far_fk_column) = far_fk_column else {
9473                    return Err(syn::Error::new(
9474                        span,
9475                        "`through` requires `far_fk_column = \"<column>\"`",
9476                    ));
9477                };
9478                let Some(intermediate) = intermediate_ident else {
9479                    return Err(syn::Error::new(
9480                        span,
9481                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9482                    ));
9483                };
9484                let Some(intermediate_fk_column) = intermediate_fk_column else {
9485                    return Err(syn::Error::new(
9486                        span,
9487                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9488                    ));
9489                };
9490                let intermediate_pk_column =
9491                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9492                if out.through_relations.iter().any(|t| t.name == name) {
9493                    return Err(syn::Error::new(
9494                        span,
9495                        format!(
9496                            "duplicate `through(name = \"{name}\")` — \
9497                             pick a unique accessor name"
9498                        ),
9499                    ));
9500                }
9501                out.through_relations.push(ThroughAttr {
9502                    name,
9503                    far,
9504                    far_fk_column,
9505                    intermediate,
9506                    intermediate_fk_column,
9507                    intermediate_pk_column,
9508                });
9509                return Ok(());
9510            }
9511            if meta.path.is_ident("reverse_has") {
9512                // `#[rustango(reverse_has(name = "comments",
9513                //  child = "Comment", child_fk_column = "post_id"
9514                //  [, self_pk_column = "id"]))]` — issue #830.
9515                let span = meta.path.span();
9516                let mut accessor_name: Option<String> = None;
9517                let mut child_ident: Option<syn::Ident> = None;
9518                let mut child_fk_column: Option<String> = None;
9519                let mut self_pk_column: Option<String> = None;
9520                meta.parse_nested_meta(|inner| {
9521                    if inner.path.is_ident("name") {
9522                        let s: LitStr = inner.value()?.parse()?;
9523                        let raw = s.value();
9524                        if raw.trim().is_empty() {
9525                            return Err(syn::Error::new(
9526                                s.span(),
9527                                "`reverse_has(name = \"...\")` must not be empty",
9528                            ));
9529                        }
9530                        accessor_name = Some(raw);
9531                        return Ok(());
9532                    }
9533                    if inner.path.is_ident("child") {
9534                        let s: LitStr = inner.value()?.parse()?;
9535                        let raw = s.value();
9536                        let trimmed = raw.trim();
9537                        if trimmed.is_empty() {
9538                            return Err(syn::Error::new(
9539                                s.span(),
9540                                "`reverse_has(child = \"...\")` must not be empty",
9541                            ));
9542                        }
9543                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9544                        return Ok(());
9545                    }
9546                    if inner.path.is_ident("child_fk_column") {
9547                        let s: LitStr = inner.value()?.parse()?;
9548                        let raw = s.value();
9549                        let trimmed = raw.trim();
9550                        if trimmed.is_empty() {
9551                            return Err(syn::Error::new(
9552                                s.span(),
9553                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9554                            ));
9555                        }
9556                        child_fk_column = Some(trimmed.to_owned());
9557                        return Ok(());
9558                    }
9559                    if inner.path.is_ident("self_pk_column") {
9560                        let s: LitStr = inner.value()?.parse()?;
9561                        let raw = s.value();
9562                        let trimmed = raw.trim();
9563                        if trimmed.is_empty() {
9564                            return Err(syn::Error::new(
9565                                s.span(),
9566                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9567                            ));
9568                        }
9569                        self_pk_column = Some(trimmed.to_owned());
9570                        return Ok(());
9571                    }
9572                    Err(inner.error(
9573                        "unknown `reverse_has` attribute (supported: \
9574                         `name`, `child`, `child_fk_column`, \
9575                         `self_pk_column`)",
9576                    ))
9577                })?;
9578                let Some(name) = accessor_name else {
9579                    return Err(syn::Error::new(
9580                        span,
9581                        "`reverse_has` requires `name = \"...\"`",
9582                    ));
9583                };
9584                let Some(child) = child_ident else {
9585                    return Err(syn::Error::new(
9586                        span,
9587                        "`reverse_has` requires `child = \"ChildModelType\"`",
9588                    ));
9589                };
9590                let Some(child_fk_column) = child_fk_column else {
9591                    return Err(syn::Error::new(
9592                        span,
9593                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9594                    ));
9595                };
9596                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9597                    return Err(syn::Error::new(
9598                        span,
9599                        format!(
9600                            "duplicate `reverse_has(name = \"{name}\")` — \
9601                             pick a unique accessor name"
9602                        ),
9603                    ));
9604                }
9605                out.reverse_has_relations.push(ReverseHasAttr {
9606                    name,
9607                    child,
9608                    child_fk_column,
9609                    self_pk_column,
9610                });
9611                return Ok(());
9612            }
9613            if meta.path.is_ident("generic_has") {
9614                // `#[rustango(generic_has(name = "tags",
9615                //  child = "Tag", ct_column = "content_type_id",
9616                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9617                //  — issue #830, the reverse generic-FK (GFK) arm of the
9618                //  relation-existence family.
9619                let span = meta.path.span();
9620                let mut accessor_name: Option<String> = None;
9621                let mut child_ident: Option<syn::Ident> = None;
9622                let mut ct_column: Option<String> = None;
9623                let mut pk_column: Option<String> = None;
9624                let mut self_pk_column: Option<String> = None;
9625                meta.parse_nested_meta(|inner| {
9626                    if inner.path.is_ident("name") {
9627                        let s: LitStr = inner.value()?.parse()?;
9628                        let raw = s.value();
9629                        if raw.trim().is_empty() {
9630                            return Err(syn::Error::new(
9631                                s.span(),
9632                                "`generic_has(name = \"...\")` must not be empty",
9633                            ));
9634                        }
9635                        accessor_name = Some(raw);
9636                        return Ok(());
9637                    }
9638                    if inner.path.is_ident("child") {
9639                        let s: LitStr = inner.value()?.parse()?;
9640                        let trimmed = s.value().trim().to_owned();
9641                        if trimmed.is_empty() {
9642                            return Err(syn::Error::new(
9643                                s.span(),
9644                                "`generic_has(child = \"...\")` must not be empty",
9645                            ));
9646                        }
9647                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9648                        return Ok(());
9649                    }
9650                    if inner.path.is_ident("ct_column") {
9651                        let s: LitStr = inner.value()?.parse()?;
9652                        let trimmed = s.value().trim().to_owned();
9653                        if trimmed.is_empty() {
9654                            return Err(syn::Error::new(
9655                                s.span(),
9656                                "`generic_has(ct_column = \"...\")` must not be empty",
9657                            ));
9658                        }
9659                        ct_column = Some(trimmed);
9660                        return Ok(());
9661                    }
9662                    if inner.path.is_ident("pk_column") {
9663                        let s: LitStr = inner.value()?.parse()?;
9664                        let trimmed = s.value().trim().to_owned();
9665                        if trimmed.is_empty() {
9666                            return Err(syn::Error::new(
9667                                s.span(),
9668                                "`generic_has(pk_column = \"...\")` must not be empty",
9669                            ));
9670                        }
9671                        pk_column = Some(trimmed);
9672                        return Ok(());
9673                    }
9674                    if inner.path.is_ident("self_pk_column") {
9675                        let s: LitStr = inner.value()?.parse()?;
9676                        let trimmed = s.value().trim().to_owned();
9677                        if trimmed.is_empty() {
9678                            return Err(syn::Error::new(
9679                                s.span(),
9680                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9681                            ));
9682                        }
9683                        self_pk_column = Some(trimmed);
9684                        return Ok(());
9685                    }
9686                    Err(inner.error(
9687                        "unknown `generic_has` attribute (supported: \
9688                         `name`, `child`, `ct_column`, `pk_column`, \
9689                         `self_pk_column`)",
9690                    ))
9691                })?;
9692                let Some(name) = accessor_name else {
9693                    return Err(syn::Error::new(
9694                        span,
9695                        "`generic_has` requires `name = \"...\"`",
9696                    ));
9697                };
9698                let Some(child) = child_ident else {
9699                    return Err(syn::Error::new(
9700                        span,
9701                        "`generic_has` requires `child = \"ChildModelType\"`",
9702                    ));
9703                };
9704                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9705                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9706                if out.generic_has_relations.iter().any(|r| r.name == name) {
9707                    return Err(syn::Error::new(
9708                        span,
9709                        format!(
9710                            "duplicate `generic_has(name = \"{name}\")` — \
9711                             pick a unique accessor name"
9712                        ),
9713                    ));
9714                }
9715                out.generic_has_relations.push(GenericHasAttr {
9716                    name,
9717                    child,
9718                    ct_column,
9719                    pk_column,
9720                    self_pk_column,
9721                });
9722                return Ok(());
9723            }
9724            if meta.path.is_ident("audit") {
9725                let mut audit = AuditAttrs::default();
9726                meta.parse_nested_meta(|inner| {
9727                    if inner.path.is_ident("track") {
9728                        let s: LitStr = inner.value()?.parse()?;
9729                        audit.track =
9730                            Some((split_field_list(&s.value()), s.span()));
9731                        return Ok(());
9732                    }
9733                    Err(inner.error(
9734                        "unknown audit attribute (supported: `track`)",
9735                    ))
9736                })?;
9737                out.audit = Some(audit);
9738                return Ok(());
9739            }
9740            if meta.path.is_ident("permissions") {
9741                // Two forms accepted:
9742                //   #[rustango(permissions)]          — flag form, true
9743                //   #[rustango(permissions = false)]  — explicit opt-out
9744                //   #[rustango(permissions = true)]   — explicit opt-in
9745                if let Ok(v) = meta.value() {
9746                    let lit: syn::LitBool = v.parse()?;
9747                    out.permissions = lit.value;
9748                } else {
9749                    out.permissions = true;
9750                }
9751                return Ok(());
9752            }
9753            if meta.path.is_ident("view") {
9754                // Issue #293 / T2.10. Two forms accepted, matching
9755                // the `permissions` flag pattern:
9756                //   #[rustango(view)]          — flag form, true
9757                //   #[rustango(view = false)]  — explicit opt-out
9758                //   #[rustango(view = true)]   — explicit opt-in
9759                if let Ok(v) = meta.value() {
9760                    let lit: syn::LitBool = v.parse()?;
9761                    out.is_view = lit.value;
9762                } else {
9763                    out.is_view = true;
9764                }
9765                return Ok(());
9766            }
9767            if meta.path.is_ident("managed") {
9768                // Who owns the table. Issue #321.
9769                //   #[rustango(managed = false)]  — operator-managed table
9770                //   #[rustango(managed = true)]   — rustango-managed (the default)
9771                // Bare-flag form is intentionally not accepted: writing
9772                // `#[rustango(managed)]` reads as "yes please manage it"
9773                // which is already the default. The opt-out is the only
9774                // useful state, so it must be explicit.
9775                let v = meta.value()?;
9776                let lit: syn::LitBool = v.parse()?;
9777                out.managed = lit.value;
9778                return Ok(());
9779            }
9780            if meta.path.is_ident("verbose_name") {
9781                let s: LitStr = meta.value()?.parse()?;
9782                out.verbose_name = Some(s.value());
9783                return Ok(());
9784            }
9785            if meta.path.is_ident("verbose_name_plural") {
9786                let s: LitStr = meta.value()?.parse()?;
9787                out.verbose_name_plural = Some(s.value());
9788                return Ok(());
9789            }
9790            if meta.path.is_ident("db_table_comment") {
9791                // Free-form table-level comment attached to the DB
9792                // catalog.
9793                let s: LitStr = meta.value()?.parse()?;
9794                out.db_table_comment = Some(s.value());
9795                return Ok(());
9796            }
9797            if meta.path.is_ident("proxy") {
9798                // Declarative flag
9799                // marking the struct as a proxy of another model that
9800                // shares its DB table. Stored on `ModelSchema::proxy`
9801                // so future codegen can skip `CreateTable` emission
9802                // for proxies (parent owns the table) and route
9803                // per-instance method resolution to the proxy class.
9804                //
9805                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9806                let value = if meta.input.peek(syn::Token![=]) {
9807                    meta.value()?.parse::<syn::LitBool>()?.value
9808                } else {
9809                    true
9810                };
9811                out.proxy = value;
9812                return Ok(());
9813            }
9814            if meta.path.is_ident("order_with_respect_to") {
9815                // `order_with_respect_to = "parent_fk"` — the model's
9816                // instances are intrinsically ordered relative to
9817                // their parent FK.
9818                //
9819                // rustango stores the FK field name on
9820                // `ModelSchema::order_with_respect_to`. Declarative-only
9821                // today: the migration writer + admin surfaces still
9822                // treat every model identically. Future codegen will
9823                // key off the metadata to auto-emit the `_order`
9824                // column and reorder helpers.
9825                //
9826                // Validated as a Rust-shape identifier so the macro
9827                // can reject typos at derive time.
9828                let s: LitStr = meta.value()?.parse()?;
9829                let raw = s.value();
9830                if raw.is_empty() {
9831                    return Err(syn::Error::new(
9832                        s.span(),
9833                        "`order_with_respect_to` must be a non-empty FK field name",
9834                    ));
9835                }
9836                let valid = raw
9837                    .chars()
9838                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9839                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9840                if !valid {
9841                    return Err(syn::Error::new(
9842                        s.span(),
9843                        format!(
9844                            "`order_with_respect_to` must be a valid Rust \
9845                             identifier (letters / digits / underscores, \
9846                             not starting with a digit); got `{raw}`"
9847                        ),
9848                    ));
9849                }
9850                out.order_with_respect_to = Some(raw);
9851                return Ok(());
9852            }
9853            if meta.path.is_ident("required_db_features") {
9854                // Capability tokens the model needs (e.g.
9855                // `"json_extract"`, `"window_functions"`,
9856                // `"row_security"`). Comma-separated.
9857                // `manage check --deploy` walks every model and warns
9858                // when the active backend doesn't advertise the
9859                // capability.
9860                //
9861                // rustango ships a small registry of capability tokens
9862                // each dialect supports — see `Dialect::supports`.
9863                // Unknown tokens still parse (they end up on the
9864                // schema and show up in the warning) so projects can
9865                // declare aspirational capabilities and the check
9866                // verb will keep nagging until the dialect implements
9867                // them.
9868                let s: LitStr = meta.value()?.parse()?;
9869                let raw = s.value();
9870                let features: Vec<String> = raw
9871                    .split(',')
9872                    .map(str::trim)
9873                    .filter(|s| !s.is_empty())
9874                    .map(str::to_owned)
9875                    .collect();
9876                if features.is_empty() {
9877                    return Err(syn::Error::new(
9878                        s.span(),
9879                        "`required_db_features` must list at least one \
9880                         comma-separated capability token",
9881                    ));
9882                }
9883                out.required_db_features = features;
9884                return Ok(());
9885            }
9886            if meta.path.is_ident("required_db_vendor") {
9887                // The model is only meant to run against the named DB
9888                // backend. `manage check --deploy` flags a mismatch so
9889                // ops catches "I forgot to switch DATABASE_URL" at
9890                // deploy time rather than runtime.
9891                //
9892                // Restricted to the three backends rustango ships
9893                // dialects for so the check verb can compare reliably.
9894                let s: LitStr = meta.value()?.parse()?;
9895                let raw = s.value().to_ascii_lowercase();
9896                match raw.as_str() {
9897                    "postgresql" | "postgres" | "pg" => {
9898                        out.required_db_vendor = Some("postgres".to_owned());
9899                    }
9900                    "mysql" | "mariadb" => {
9901                        out.required_db_vendor = Some("mysql".to_owned());
9902                    }
9903                    "sqlite" | "sqlite3" => {
9904                        out.required_db_vendor = Some("sqlite".to_owned());
9905                    }
9906                    _ => {
9907                        return Err(syn::Error::new(
9908                            s.span(),
9909                            format!(
9910                                "unknown required_db_vendor `{raw}` — \
9911                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9912                                 `mysql` (alias: `mariadb`), or `sqlite` \
9913                                 (alias: `sqlite3`)"
9914                            ),
9915                        ));
9916                    }
9917                }
9918                return Ok(());
9919            }
9920            if meta.path.is_ident("base_manager_name") {
9921                // Name of the manager that `<instance>.<relation>_set`
9922                // uses when resolving reverse-relation managers.
9923                // Distinct from `default_manager_name` (what
9924                // `Self::objects()` returns). Stored on
9925                // `ModelSchema::base_manager_name`.
9926                //
9927                // Validated as a Rust identifier so it stays safe to
9928                // re-emit as code in future reverse-manager codegen.
9929                let s: LitStr = meta.value()?.parse()?;
9930                let raw = s.value();
9931                if raw.is_empty() {
9932                    return Err(syn::Error::new(
9933                        s.span(),
9934                        "`base_manager_name` must be a non-empty string",
9935                    ));
9936                }
9937                let valid = raw
9938                    .chars()
9939                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9940                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9941                if !valid {
9942                    return Err(syn::Error::new(
9943                        s.span(),
9944                        format!(
9945                            "`base_manager_name` must be a valid Rust \
9946                             identifier (letters / digits / underscores, \
9947                             not starting with a digit); got `{raw}`"
9948                        ),
9949                    ));
9950                }
9951                out.base_manager_name = Some(raw);
9952                return Ok(());
9953            }
9954            if meta.path.is_ident("default_related_name") {
9955                // The name reverse-relation accessors use when callers
9956                // don't override `related_name = "..."` on the FK / M2M
9957                // field. Stored on `ModelSchema::default_related_name`
9958                // so future reverse-manager codegen / schema emit /
9959                // admin templates can pick the right accessor name
9960                // (today rustango doesn't auto-emit reverse managers;
9961                // the metadata is the foundation for that work).
9962                //
9963                // Enforced non-empty + ASCII identifier-shape so the
9964                // string is safe to use as a Rust ident later.
9965                let s: LitStr = meta.value()?.parse()?;
9966                let raw = s.value();
9967                if raw.is_empty() {
9968                    return Err(syn::Error::new(
9969                        s.span(),
9970                        "`default_related_name` must be a non-empty string",
9971                    ));
9972                }
9973                let valid = raw
9974                    .chars()
9975                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9976                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9977                if !valid {
9978                    return Err(syn::Error::new(
9979                        s.span(),
9980                        format!(
9981                            "`default_related_name` must be snake_case ASCII \
9982                             (lowercase letters / digits / underscores, not \
9983                             starting with a digit); got `{raw}`"
9984                        ),
9985                    ));
9986                }
9987                out.default_related_name = Some(raw);
9988                return Ok(());
9989            }
9990            if meta.path.is_ident("extra_permissions") {
9991                // Comma-separated `codename:label` pairs.
9992                let s: LitStr = meta.value()?.parse()?;
9993                let raw = s.value();
9994                let mut pairs = Vec::new();
9995                for entry in raw.split(',') {
9996                    let entry = entry.trim();
9997                    if entry.is_empty() {
9998                        continue;
9999                    }
10000                    let (codename, label) = match entry.split_once(':') {
10001                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
10002                        None => (entry.to_owned(), entry.to_owned()),
10003                    };
10004                    if codename.is_empty() {
10005                        return Err(meta.error(
10006                            "`extra_permissions` entries must be `codename:label` pairs",
10007                        ));
10008                    }
10009                    pairs.push((codename, label));
10010                }
10011                if pairs.is_empty() {
10012                    return Err(meta
10013                        .error("`extra_permissions = \"…\"` must list at least one pair"));
10014                }
10015                out.extra_permissions = pairs;
10016                return Ok(());
10017            }
10018            if meta.path.is_ident("default_permissions") {
10019                // Comma-separated subset of the CRUD action set.
10020                // Empty means all four (the framework default).
10021                let s: LitStr = meta.value()?.parse()?;
10022                let raw = s.value();
10023                let mut actions: Vec<String> = Vec::new();
10024                for entry in raw.split(',') {
10025                    let action = entry.trim().to_ascii_lowercase();
10026                    if action.is_empty() {
10027                        continue;
10028                    }
10029                    match action.as_str() {
10030                        "add" | "change" | "delete" | "view" => {}
10031                        other => {
10032                            return Err(syn::Error::new(
10033                                s.span(),
10034                                format!(
10035                                    "unknown default_permissions action `{other}` — \
10036                                     expected one of `add`, `change`, `delete`, `view`"
10037                                ),
10038                            ));
10039                        }
10040                    }
10041                    if !actions.contains(&action) {
10042                        actions.push(action);
10043                    }
10044                }
10045                if actions.is_empty() {
10046                    return Err(syn::Error::new(
10047                        s.span(),
10048                        "`default_permissions = \"…\"` must list at least one action; \
10049                         use `permissions = false` on the container if you want NO \
10050                         permissions seeded for this model.",
10051                    ));
10052                }
10053                out.default_permissions = actions;
10054                return Ok(());
10055            }
10056            if meta.path.is_ident("get_latest_by") {
10057                // The `-` prefix selects descending order.
10058                let s: LitStr = meta.value()?.parse()?;
10059                let raw = s.value();
10060                let trimmed = raw.trim();
10061                if trimmed.is_empty() {
10062                    return Err(meta.error("`get_latest_by` must name a column"));
10063                }
10064                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
10065                    (stripped.to_owned(), true)
10066                } else if let Some(stripped) = trimmed.strip_prefix('+') {
10067                    (stripped.to_owned(), false)
10068                } else {
10069                    (trimmed.to_owned(), false)
10070                };
10071                if col.is_empty() {
10072                    return Err(meta.error("`get_latest_by` must name a column"));
10073                }
10074                out.get_latest_by = Some((col, desc, s.span()));
10075                return Ok(());
10076            }
10077            if meta.path.is_ident("unique_together") {
10078                // Composite UNIQUE index. Two syntaxes:
10079                //
10080                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
10081                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
10082                //
10083                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
10084                // (col1, col2)`, where <name> defaults to
10085                // `<table>_<col1>_<col2>_uq` when not supplied.
10086                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
10087                out.indexes.push(IndexAttr {
10088                    span: meta.path.span(),
10089                    name,
10090                    columns,
10091                    unique: true,
10092                    method: "btree".to_owned(),
10093                    where_clause: None,
10094                include: Vec::new(),
10095                });
10096                return Ok(());
10097            }
10098            if meta.path.is_ident("index_together") {
10099                // Composite (non-unique) index. Two syntaxes
10100                // mirroring `unique_together`.
10101                //
10102                //   #[rustango(index_together = "created_at, status")]
10103                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
10104                let (columns, name) = parse_together_attr(&meta, "index_together")?;
10105                out.indexes.push(IndexAttr {
10106                    span: meta.path.span(),
10107                    name,
10108                    columns,
10109                    unique: false,
10110                    method: "btree".to_owned(),
10111                    where_clause: None,
10112                include: Vec::new(),
10113                });
10114                return Ok(());
10115            }
10116            if meta.path.is_ident("unique_when") {
10117                // Partial unique index. Issue #265 / T1.3.
10118                //
10119                //   #[rustango(unique_when(
10120                //       columns   = "email",
10121                //       condition = "deleted_at IS NULL",
10122                //       name      = "unique_active_email"
10123                //   ))]
10124                //
10125                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
10126                // on PG / SQLite (both ship partial indexes natively).
10127                // MySQL falls back to a plain UNIQUE index — the
10128                // condition is lost; document the limitation in the
10129                // generated migration.
10130                let mut columns: Option<Vec<String>> = None;
10131                let mut condition: Option<String> = None;
10132                let mut name: Option<String> = None;
10133                let mut include: Vec<String> = Vec::new();
10134                meta.parse_nested_meta(|inner| {
10135                    if inner.path.is_ident("columns") {
10136                        let s: LitStr = inner.value()?.parse()?;
10137                        columns = Some(split_field_list(&s.value()));
10138                        return Ok(());
10139                    }
10140                    if inner.path.is_ident("condition") {
10141                        let s: LitStr = inner.value()?.parse()?;
10142                        condition = Some(s.value());
10143                        return Ok(());
10144                    }
10145                    if inner.path.is_ident("name") {
10146                        let s: LitStr = inner.value()?.parse()?;
10147                        name = Some(s.value());
10148                        return Ok(());
10149                    }
10150                    if inner.path.is_ident("include") {
10151                        // PG 11+ covering-index columns. Non-key columns
10152                        // travel with the index leaf for index-only
10153                        // scans. Dropped on MySQL/SQLite by the writer.
10154                        let s: LitStr = inner.value()?.parse()?;
10155                        include = split_field_list(&s.value());
10156                        return Ok(());
10157                    }
10158                    Err(inner.error(
10159                        "unknown unique_when attribute (supported: \
10160                         `columns = \"...\"`, `condition = \"...\"`, \
10161                         `name = \"...\"`, `include = \"...\"`)",
10162                    ))
10163                })?;
10164                let columns = columns.ok_or_else(|| {
10165                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
10166                })?;
10167                let condition = condition.ok_or_else(|| {
10168                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
10169                })?;
10170                if columns.is_empty() {
10171                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
10172                }
10173                out.indexes.push(IndexAttr {
10174                    span: meta.path.span(),
10175                    name,
10176                    columns,
10177                    unique: true,
10178                    method: "btree".to_owned(),
10179                    where_clause: Some(condition),
10180                    include,
10181                });
10182                return Ok(());
10183            }
10184            if meta.path.is_ident("index_when") {
10185                // Non-unique partial index. Sibling of `unique_when`
10186                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
10187                //
10188                //   #[rustango(index_when(
10189                //       columns   = "status, created_at",
10190                //       condition = "deleted_at IS NULL",
10191                //       name      = "active_status_created_idx"
10192                //   ))]
10193                //
10194                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
10195                // on PG / SQLite (both ship partial indexes natively).
10196                // MySQL has no native partial-index support — the writer
10197                // emits a plain CREATE INDEX and the condition is lost;
10198                // operators wanting that selectivity on MySQL should
10199                // declare a covering index plus an application-level
10200                // filter.
10201                let mut columns: Option<Vec<String>> = None;
10202                let mut condition: Option<String> = None;
10203                let mut name: Option<String> = None;
10204                let mut method: String = "btree".to_owned();
10205                let mut include: Vec<String> = Vec::new();
10206                meta.parse_nested_meta(|inner| {
10207                    if inner.path.is_ident("columns") {
10208                        let s: LitStr = inner.value()?.parse()?;
10209                        columns = Some(split_field_list(&s.value()));
10210                        return Ok(());
10211                    }
10212                    if inner.path.is_ident("condition") {
10213                        let s: LitStr = inner.value()?.parse()?;
10214                        condition = Some(s.value());
10215                        return Ok(());
10216                    }
10217                    if inner.path.is_ident("name") {
10218                        let s: LitStr = inner.value()?.parse()?;
10219                        name = Some(s.value());
10220                        return Ok(());
10221                    }
10222                    if inner.path.is_ident("method") {
10223                        let s: LitStr = inner.value()?.parse()?;
10224                        method = s.value();
10225                        return Ok(());
10226                    }
10227                    if inner.path.is_ident("include") {
10228                        // PG 11+ covering-index columns; non-key
10229                        // columns travel with the index leaf. Dropped
10230                        // on MySQL/SQLite.
10231                        let s: LitStr = inner.value()?.parse()?;
10232                        include = split_field_list(&s.value());
10233                        return Ok(());
10234                    }
10235                    Err(inner.error(
10236                        "unknown index_when attribute (supported: \
10237                         `columns = \"...\"`, `condition = \"...\"`, \
10238                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
10239                         `include = \"...\"`)",
10240                    ))
10241                })?;
10242                let columns = columns
10243                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
10244                let condition = condition.ok_or_else(|| {
10245                    meta.error("`index_when(...)` requires `condition = \"...\"`")
10246                })?;
10247                if columns.is_empty() {
10248                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
10249                }
10250                out.indexes.push(IndexAttr {
10251                    span: meta.path.span(),
10252                    name,
10253                    columns,
10254                    unique: false,
10255                    method,
10256                    where_clause: Some(condition),
10257                    include,
10258                });
10259                return Ok(());
10260            }
10261            if meta.path.is_ident("index") {
10262                // Container-level composite index. Two syntaxes:
10263                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
10264                //   #[rustango(index("col1, col2"))]    — call form (same result)
10265                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
10266                // The call form takes the column list as a leading string
10267                // literal, then optional `unique` / `name = "..."` /
10268                // `method = "..."` flags (a leading literal can't compose
10269                // under `parse_nested_meta`, so the paren body is parsed by
10270                // hand). `unique_together` / `index_together` remain
10271                // aliases for the same feature.
10272                let cols_lit: LitStr;
10273                let mut unique = false;
10274                let mut name: Option<String> = None;
10275                let mut method = "btree".to_owned();
10276                if meta.input.peek(syn::token::Paren) {
10277                    let content;
10278                    syn::parenthesized!(content in meta.input);
10279                    cols_lit = content.parse()?;
10280                    while content.peek(syn::Token![,]) {
10281                        content.parse::<syn::Token![,]>()?;
10282                        if content.is_empty() {
10283                            break;
10284                        }
10285                        let flag: syn::Ident = content.parse()?;
10286                        if flag == "unique" {
10287                            unique = true;
10288                        } else if flag == "name" {
10289                            content.parse::<syn::Token![=]>()?;
10290                            let s: LitStr = content.parse()?;
10291                            name = Some(s.value());
10292                        } else if flag == "method" {
10293                            content.parse::<syn::Token![=]>()?;
10294                            let s: LitStr = content.parse()?;
10295                            let v = s.value();
10296                            match v.as_str() {
10297                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10298                                | "bloom" => method = v,
10299                                other => {
10300                                    return Err(syn::Error::new(
10301                                        s.span(),
10302                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10303                                    ));
10304                                }
10305                            }
10306                        } else {
10307                            return Err(syn::Error::new(
10308                                flag.span(),
10309                                "unknown index flag (supported: `unique`, `name`, `method`)",
10310                            ));
10311                        }
10312                    }
10313                } else {
10314                    cols_lit = meta.value()?.parse()?;
10315                }
10316                let columns = split_field_list(&cols_lit.value());
10317                out.indexes.push(IndexAttr {
10318                    span: meta.path.span(),
10319                    name,
10320                    columns,
10321                    unique,
10322                    method,
10323                    where_clause: None,
10324                    include: Vec::new(),
10325                });
10326                return Ok(());
10327            }
10328            if meta.path.is_ident("check") {
10329                // #[rustango(check(name = "…", expr = "…"))]
10330                let mut name: Option<String> = None;
10331                let mut expr: Option<String> = None;
10332                meta.parse_nested_meta(|inner| {
10333                    if inner.path.is_ident("name") {
10334                        let s: LitStr = inner.value()?.parse()?;
10335                        name = Some(s.value());
10336                        return Ok(());
10337                    }
10338                    if inner.path.is_ident("expr") {
10339                        let s: LitStr = inner.value()?.parse()?;
10340                        expr = Some(s.value());
10341                        return Ok(());
10342                    }
10343                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10344                })?;
10345                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10346                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10347                out.checks.push(CheckAttr { name, expr });
10348                return Ok(());
10349            }
10350            if meta.path.is_ident("exclude") {
10351                // #[rustango(exclude(name = "…", using = "gist",
10352                //                    elements = "col WITH op, col WITH op",
10353                //                    where = "…"))]
10354                let mut name: Option<String> = None;
10355                let mut using: Option<String> = None;
10356                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10357                let mut where_clause: Option<String> = None;
10358                meta.parse_nested_meta(|inner| {
10359                    if inner.path.is_ident("name") {
10360                        let s: LitStr = inner.value()?.parse()?;
10361                        name = Some(s.value());
10362                        return Ok(());
10363                    }
10364                    if inner.path.is_ident("using") {
10365                        let s: LitStr = inner.value()?.parse()?;
10366                        using = Some(s.value());
10367                        return Ok(());
10368                    }
10369                    if inner.path.is_ident("elements") {
10370                        let s: LitStr = inner.value()?.parse()?;
10371                        elements_raw = Some((s.value(), s.span()));
10372                        return Ok(());
10373                    }
10374                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10375                        let s: LitStr = inner.value()?.parse()?;
10376                        where_clause = Some(s.value());
10377                        return Ok(());
10378                    }
10379                    Err(inner.error(
10380                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10381                    ))
10382                })?;
10383                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10384                let using = using.unwrap_or_else(|| "gist".to_owned());
10385                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10386                    meta.error(
10387                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
10388                    )
10389                })?;
10390                // Parse `col WITH op` pairs separated by commas.
10391                let mut elements: Vec<(String, String)> = Vec::new();
10392                for pair in elements_str.split(',') {
10393                    let pair = pair.trim();
10394                    if pair.is_empty() {
10395                        continue;
10396                    }
10397                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10398                    let col = split.next().unwrap_or("").trim();
10399                    let rest = split.next().unwrap_or("").trim();
10400                    // `WITH op` — case-insensitive on `WITH`, then op.
10401                    let rest_lc = rest.to_ascii_lowercase();
10402                    let op = rest_lc
10403                        .strip_prefix("with")
10404                        .map(|r| r.trim_start())
10405                        .filter(|r| !r.is_empty())
10406                        .map(|_| {
10407                            // Pull the original-case op from `rest` after the
10408                            // `WITH ` token (5 chars).
10409                            rest[4..].trim_start().to_owned()
10410                        });
10411                    let Some(op) = op else {
10412                        return Err(syn::Error::new(
10413                            elements_span,
10414                            format!(
10415                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10416                                 (e.g. `room_id WITH =` or `during WITH &&`)"
10417                            ),
10418                        ));
10419                    };
10420                    if col.is_empty() || op.is_empty() {
10421                        return Err(syn::Error::new(
10422                            elements_span,
10423                            format!(
10424                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10425                                 (both sides non-empty)"
10426                            ),
10427                        ));
10428                    }
10429                    elements.push((col.to_owned(), op));
10430                }
10431                if elements.is_empty() {
10432                    return Err(syn::Error::new(
10433                        elements_span,
10434                        "exclude requires at least one `col WITH op` element",
10435                    ));
10436                }
10437                out.excludes.push(ExcludeAttr {
10438                    name,
10439                    using,
10440                    elements,
10441                    where_clause,
10442                });
10443                return Ok(());
10444            }
10445            if meta.path.is_ident("generic_fk") {
10446                let mut gfk = GenericFkAttr {
10447                    name: String::new(),
10448                    ct_column: String::new(),
10449                    pk_column: String::new(),
10450                };
10451                meta.parse_nested_meta(|inner| {
10452                    if inner.path.is_ident("name") {
10453                        let s: LitStr = inner.value()?.parse()?;
10454                        gfk.name = s.value();
10455                        return Ok(());
10456                    }
10457                    if inner.path.is_ident("ct_column") {
10458                        let s: LitStr = inner.value()?.parse()?;
10459                        gfk.ct_column = s.value();
10460                        return Ok(());
10461                    }
10462                    if inner.path.is_ident("pk_column") {
10463                        let s: LitStr = inner.value()?.parse()?;
10464                        gfk.pk_column = s.value();
10465                        return Ok(());
10466                    }
10467                    Err(inner.error(
10468                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10469                    ))
10470                })?;
10471                if gfk.name.is_empty() {
10472                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10473                }
10474                if gfk.ct_column.is_empty() {
10475                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10476                }
10477                if gfk.pk_column.is_empty() {
10478                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10479                }
10480                out.generic_fks.push(gfk);
10481                return Ok(());
10482            }
10483            if meta.path.is_ident("fk_composite") {
10484                let mut fk = CompositeFkAttr {
10485                    name: String::new(),
10486                    to: String::new(),
10487                    from: Vec::new(),
10488                    on: Vec::new(),
10489                };
10490                meta.parse_nested_meta(|inner| {
10491                    if inner.path.is_ident("name") {
10492                        let s: LitStr = inner.value()?.parse()?;
10493                        fk.name = s.value();
10494                        return Ok(());
10495                    }
10496                    if inner.path.is_ident("to") {
10497                        let s: LitStr = inner.value()?.parse()?;
10498                        fk.to = s.value();
10499                        return Ok(());
10500                    }
10501                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10502                    // comma-list of string literals.
10503                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10504                        let value = inner.value()?;
10505                        let content;
10506                        syn::parenthesized!(content in value);
10507                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10508                            content.parse_terminated(
10509                                |p| p.parse::<syn::LitStr>(),
10510                                syn::Token![,],
10511                            )?;
10512                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10513                        if inner.path.is_ident("on") {
10514                            fk.on = cols;
10515                        } else {
10516                            fk.from = cols;
10517                        }
10518                        return Ok(());
10519                    }
10520                    Err(inner.error(
10521                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10522                    ))
10523                })?;
10524                if fk.name.is_empty() {
10525                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10526                }
10527                if fk.to.is_empty() {
10528                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10529                }
10530                if fk.from.is_empty() || fk.on.is_empty() {
10531                    return Err(meta.error(
10532                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10533                    ));
10534                }
10535                if fk.from.len() != fk.on.len() {
10536                    return Err(meta.error(format!(
10537                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10538                        fk.from.len(),
10539                        fk.on.len(),
10540                    )));
10541                }
10542                out.composite_fks.push(fk);
10543                return Ok(());
10544            }
10545            if meta.path.is_ident("m2m") {
10546                let mut m2m = M2MAttr {
10547                    name: String::new(),
10548                    to: String::new(),
10549                    through: String::new(),
10550                    src: String::new(),
10551                    dst: String::new(),
10552                    auto_create: true,
10553                };
10554                meta.parse_nested_meta(|inner| {
10555                    if inner.path.is_ident("name") {
10556                        let s: LitStr = inner.value()?.parse()?;
10557                        m2m.name = s.value();
10558                        return Ok(());
10559                    }
10560                    if inner.path.is_ident("to") {
10561                        let s: LitStr = inner.value()?.parse()?;
10562                        m2m.to = s.value();
10563                        return Ok(());
10564                    }
10565                    if inner.path.is_ident("through") {
10566                        let s: LitStr = inner.value()?.parse()?;
10567                        m2m.through = s.value();
10568                        return Ok(());
10569                    }
10570                    if inner.path.is_ident("src") {
10571                        let s: LitStr = inner.value()?.parse()?;
10572                        m2m.src = s.value();
10573                        return Ok(());
10574                    }
10575                    if inner.path.is_ident("dst") {
10576                        let s: LitStr = inner.value()?.parse()?;
10577                        m2m.dst = s.value();
10578                        return Ok(());
10579                    }
10580                    if inner.path.is_ident("auto_create") {
10581                        let lit: syn::LitBool = inner.value()?.parse()?;
10582                        m2m.auto_create = lit.value;
10583                        return Ok(());
10584                    }
10585                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10586                })?;
10587                if m2m.name.is_empty() {
10588                    return Err(meta.error("m2m requires `name = \"...\"`"));
10589                }
10590                if m2m.to.is_empty() {
10591                    return Err(meta.error("m2m requires `to = \"...\"`"));
10592                }
10593                if m2m.through.is_empty() {
10594                    return Err(meta.error("m2m requires `through = \"...\"`"));
10595                }
10596                if m2m.src.is_empty() {
10597                    return Err(meta.error("m2m requires `src = \"...\"`"));
10598                }
10599                if m2m.dst.is_empty() {
10600                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10601                }
10602                out.m2m.push(m2m);
10603                return Ok(());
10604            }
10605            if meta.path.is_ident("generic_m2m") {
10606                let mut gm = GenericM2MAttr {
10607                    name: String::new(),
10608                    through: String::new(),
10609                    pk_column: String::new(),
10610                    ct_column: String::new(),
10611                    related_column: String::new(),
10612                };
10613                meta.parse_nested_meta(|inner| {
10614                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10615                        let s: LitStr = inner.value()?.parse()?;
10616                        Ok(s.value())
10617                    };
10618                    if inner.path.is_ident("name") {
10619                        gm.name = field(&inner)?;
10620                        return Ok(());
10621                    }
10622                    if inner.path.is_ident("through") {
10623                        gm.through = field(&inner)?;
10624                        return Ok(());
10625                    }
10626                    if inner.path.is_ident("pk_column") {
10627                        gm.pk_column = field(&inner)?;
10628                        return Ok(());
10629                    }
10630                    if inner.path.is_ident("ct_column") {
10631                        gm.ct_column = field(&inner)?;
10632                        return Ok(());
10633                    }
10634                    if inner.path.is_ident("related_column") {
10635                        gm.related_column = field(&inner)?;
10636                        return Ok(());
10637                    }
10638                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10639                })?;
10640                for (val, label) in [
10641                    (&gm.name, "name"),
10642                    (&gm.through, "through"),
10643                    (&gm.pk_column, "pk_column"),
10644                    (&gm.ct_column, "ct_column"),
10645                    (&gm.related_column, "related_column"),
10646                ] {
10647                    if val.is_empty() {
10648                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10649                    }
10650                }
10651                out.generic_m2m.push(gm);
10652                return Ok(());
10653            }
10654            Err(meta.error("unknown rustango container attribute"))
10655        })?;
10656    }
10657    Ok(out)
10658}
10659
10660/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10661/// owned field names, trimming whitespace and skipping empty entries.
10662/// Field-name validation against the model is done by the caller.
10663fn split_field_list(raw: &str) -> Vec<String> {
10664    raw.split(',')
10665        .map(str::trim)
10666        .filter(|s| !s.is_empty())
10667        .map(str::to_owned)
10668        .collect()
10669}
10670
10671/// Shared parser for `unique_together` and `index_together` container
10672/// attrs. Accepts both shapes:
10673///
10674///   * `attr = "col1, col2"`              — auto-derived index name.
10675///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10676///
10677/// Returns `(columns, name)`.
10678fn parse_together_attr(
10679    meta: &syn::meta::ParseNestedMeta<'_>,
10680    attr: &str,
10681) -> syn::Result<(Vec<String>, Option<String>)> {
10682    // Disambiguate by whether the next token is `=` (key-value) or
10683    // `(` (parenthesized).
10684    if meta.input.peek(syn::Token![=]) {
10685        let cols_lit: LitStr = meta.value()?.parse()?;
10686        let columns = split_field_list(&cols_lit.value());
10687        check_together_columns(meta, attr, &columns)?;
10688        return Ok((columns, None));
10689    }
10690    let mut columns: Option<Vec<String>> = None;
10691    let mut name: Option<String> = None;
10692    meta.parse_nested_meta(|inner| {
10693        if inner.path.is_ident("columns") {
10694            let s: LitStr = inner.value()?.parse()?;
10695            columns = Some(split_field_list(&s.value()));
10696            return Ok(());
10697        }
10698        if inner.path.is_ident("name") {
10699            let s: LitStr = inner.value()?.parse()?;
10700            name = Some(s.value());
10701            return Ok(());
10702        }
10703        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10704    })?;
10705    let columns = columns.ok_or_else(|| {
10706        meta.error(format!(
10707            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10708        ))
10709    })?;
10710    check_together_columns(meta, attr, &columns)?;
10711    Ok((columns, name))
10712}
10713
10714fn check_together_columns(
10715    meta: &syn::meta::ParseNestedMeta<'_>,
10716    attr: &str,
10717    columns: &[String],
10718) -> syn::Result<()> {
10719    if columns.len() < 2 {
10720        let single = if attr == "unique_together" {
10721            "#[rustango(unique)] on the field"
10722        } else {
10723            "#[rustango(index)] on the field"
10724        };
10725        return Err(meta.error(format!(
10726            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10727        )));
10728    }
10729    Ok(())
10730}
10731
10732/// Parse the fieldsets DSL: pipe-separated sections, optional
10733/// `"Title:"` prefix on each, comma-separated field names after.
10734/// Examples:
10735/// * `"name, office"` → one untitled section with two fields
10736/// * `"Identity: name, office | Metadata: created_at"` → two titled
10737///   sections
10738///
10739/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10740fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10741    raw.split('|')
10742        .map(str::trim)
10743        .filter(|s| !s.is_empty())
10744        .map(|section| {
10745            // Split off an optional `Title:` prefix (first colon).
10746            let (title, rest) = match section.split_once(':') {
10747                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10748                _ => (String::new(), section),
10749            };
10750            let fields = split_field_list(rest);
10751            (title, fields)
10752        })
10753        .collect()
10754}
10755
10756/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10757/// comma-separated entry maps a target field to one or more source
10758/// fields joined with `+`. Whitespace around tokens is trimmed.
10759/// Entries missing `:` or with empty target/source lists are dropped.
10760fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10761    raw.split(',')
10762        .map(str::trim)
10763        .filter(|s| !s.is_empty())
10764        .filter_map(|entry| {
10765            let (target, sources_raw) = entry.split_once(':')?;
10766            let target = target.trim().to_owned();
10767            if target.is_empty() {
10768                return None;
10769            }
10770            let sources: Vec<String> = sources_raw
10771                .split('+')
10772                .map(|s| s.trim().to_owned())
10773                .filter(|s| !s.is_empty())
10774                .collect();
10775            if sources.is_empty() {
10776                return None;
10777            }
10778            Some((target, sources))
10779        })
10780        .collect()
10781}
10782
10783/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10784/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10785/// and empty halves drop silently — the macro layer only enforces shape,
10786/// not field-name vs. widget-name validity (those checks happen at
10787/// `AdminConfig` consumption time). Issue #359.
10788fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10789    raw.split(',')
10790        .map(str::trim)
10791        .filter(|s| !s.is_empty())
10792        .filter_map(|entry| {
10793            let (field, widget) = entry.split_once(':')?;
10794            let field = field.trim().to_owned();
10795            let widget = widget.trim().to_owned();
10796            if field.is_empty() || widget.is_empty() {
10797                return None;
10798            }
10799            Some((field, widget))
10800        })
10801        .collect()
10802}
10803
10804/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10805/// Returns `(field_name, desc)` pairs in the same order as the input.
10806fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10807    raw.split(',')
10808        .map(str::trim)
10809        .filter(|s| !s.is_empty())
10810        .map(|spec| {
10811            spec.strip_prefix('-')
10812                .map_or((spec.to_owned(), false), |rest| {
10813                    (rest.trim().to_owned(), true)
10814                })
10815        })
10816        .collect()
10817}
10818
10819struct FieldAttrs {
10820    column: Option<String>,
10821    primary_key: bool,
10822    fk: Option<String>,
10823    o2o: Option<String>,
10824    on: Option<String>,
10825    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10826    /// "set_default" | "no_action")]` — what happens to this row when
10827    /// the row it points at is deleted.
10828    /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10829    /// set; the macro errors at compile time if applied to a
10830    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10831    /// DDL writer renders `ON DELETE <action>` after the constraint
10832    /// clause when this is `Some`; `None` falls back to the database
10833    /// default (NO ACTION on every backend rustango supports).
10834    on_delete: Option<String>,
10835    /// `#[rustango(related_name = "...")]` — per-FK
10836    /// reverse-accessor override. When set, the derive emits
10837    /// `Parent::<related_name>[_pool]` instead of the container-level
10838    /// `default_related_name` or the `<child_snake>_set[_pool]`
10839    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10840    /// silently ignored on non-FK fields. Follow-up to #816.
10841    /// `"+"` emits none, for a parent defined in another crate.
10842    related_name: Option<String>,
10843    max_length: Option<u32>,
10844    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10845    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10846    /// unconstrained `vector` column.
10847    vector_dims: Option<u32>,
10848    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10849    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10850    /// unconstrained `geometry(Point)` column (SRID 0).
10851    geometry_srid: Option<u32>,
10852    min: Option<i64>,
10853    max: Option<i64>,
10854    default: Option<String>,
10855    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10856    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10857    /// "gen_random_uuid()"`. The Rust field type must be
10858    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10859    /// INSERTs so the DB DEFAULT fires.
10860    auto_uuid: bool,
10861    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10862    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10863    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10864    /// the field is `Auto::Unset`, then bound as a normal parameter
10865    /// rather than relying on a per-dialect DB function. Issue #823
10866    /// (Eloquent `HasUuids`).
10867    ///
10868    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10869    /// Composes with every backend (PG / MySQL / SQLite) — no
10870    /// `gen_random_uuid()` requirement on the database.
10871    default_uuid_v7: bool,
10872    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10873    /// Server-set on insert, immutable from app code afterwards.
10874    /// Implies `auto + default = "now()"`. Field type must be
10875    /// `DateTime<Utc>`.
10876    auto_now_add: bool,
10877    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10878    /// every insert AND every update. Implies `auto + default =
10879    /// "now()"`; the macro additionally rewrites `update_on` /
10880    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10881    /// field value.
10882    auto_now: bool,
10883    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10884    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10885    /// `soft_delete_on(executor)` and `restore_on(executor)`
10886    /// methods on the model.
10887    soft_delete: bool,
10888    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10889    /// the column in the generated DDL.
10890    unique: bool,
10891    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10892    /// generates a `CREATE INDEX` for this column. `unique` here means
10893    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10894    index: bool,
10895    index_unique: bool,
10896    index_name: Option<String>,
10897    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10898    /// `"btree"`. Issue #34.
10899    index_method: String,
10900    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10901    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10902    /// the macro skips this column from every INSERT and UPDATE
10903    /// path, so the database always recomputes the value from
10904    /// `EXPR`. Backlog item #35.
10905    generated_as: Option<String>,
10906    /// `#[rustango(help_text = "…")]` — help text
10907    /// rendered below the admin form's input. Threaded into
10908    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10909    /// layers can read it.
10910    help_text: Option<String>,
10911    /// `#[rustango(choices = "value:Label, value:Label")]` — the
10912    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10913    /// as a `&'static [(&'static str, &'static str)]` slice. When
10914    /// present, the admin form renders a `<select>` instead of `<input>`
10915    /// and the validator rejects values not in the list. Only meaningful
10916    /// for `FieldType::String`; the macro errors at compile time if
10917    /// applied to a non-string field.
10918    choices: Option<Vec<(String, String)>>,
10919    /// `#[rustango(db_comment = "…")]` — DB-side column
10920    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10921    /// the comment in CREATE TABLE; Postgres emits a separate
10922    /// `COMMENT ON COLUMN` statement after the table is created;
10923    /// SQLite silently drops the value (no native column comments).
10924    db_comment: Option<String>,
10925    /// `#[rustango(verbose_name = "…")]` — human-readable
10926    /// label for the field. Threaded into `FieldSchema::verbose_name`
10927    /// so admin column headers, form labels, and other display
10928    /// surfaces can prefer the friendly caption over the Rust
10929    /// identifier. `None` means renderers fall back to the field name.
10930    verbose_name: Option<String>,
10931    /// `#[rustango(editable = false)]` — opt out of
10932    /// auto-generated form rendering. Defaults to `true` so existing
10933    /// fields keep their current admin / form behavior; setting
10934    /// `false` removes the field from the admin change-form entirely
10935    /// (the value is still visible on detail / list views, just not
10936    /// editable).
10937    editable: bool,
10938    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10939    /// "form may submit empty even when DB is NOT NULL". Threaded into
10940    /// `FieldSchema::blank`. Defaults to `false`.
10941    blank: bool,
10942    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10943    /// case-insensitive text column. Threaded into
10944    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10945    /// fields; the macro errors at derive time if applied elsewhere.
10946    case_insensitive: bool,
10947    /// `#[rustango(validators = "email,url")]` — the
10948    /// model-level validator chain. Comma-separated names that
10949    /// dispatch to the `validators::*` family in `validate_value`.
10950    /// Empty by default; fires on every typed INSERT/UPDATE.
10951    validators: Vec<String>,
10952}
10953
10954fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10955    let mut out = FieldAttrs {
10956        column: None,
10957        primary_key: false,
10958        fk: None,
10959        o2o: None,
10960        on: None,
10961        on_delete: None,
10962        related_name: None,
10963        max_length: None,
10964        vector_dims: None,
10965        geometry_srid: None,
10966        min: None,
10967        max: None,
10968        default: None,
10969        auto_uuid: false,
10970        default_uuid_v7: false,
10971        auto_now_add: false,
10972        auto_now: false,
10973        soft_delete: false,
10974        unique: false,
10975        index: false,
10976        index_unique: false,
10977        index_name: None,
10978        index_method: "btree".to_owned(),
10979        generated_as: None,
10980        help_text: None,
10981        choices: None,
10982        db_comment: None,
10983        verbose_name: None,
10984        editable: true,
10985        blank: false,
10986        case_insensitive: false,
10987        validators: Vec::new(),
10988    };
10989    for attr in &field.attrs {
10990        if !attr.path().is_ident("rustango") {
10991            continue;
10992        }
10993        attr.parse_nested_meta(|meta| {
10994            if meta.path.is_ident("column") {
10995                let s: LitStr = meta.value()?.parse()?;
10996                let name = s.value();
10997                validate_sql_identifier(&name, "column", s.span())?;
10998                out.column = Some(name);
10999                return Ok(());
11000            }
11001            if meta.path.is_ident("primary_key") {
11002                out.primary_key = true;
11003                return Ok(());
11004            }
11005            if meta.path.is_ident("fk") {
11006                let s: LitStr = meta.value()?.parse()?;
11007                out.fk = Some(s.value());
11008                return Ok(());
11009            }
11010            if meta.path.is_ident("o2o") {
11011                let s: LitStr = meta.value()?.parse()?;
11012                out.o2o = Some(s.value());
11013                return Ok(());
11014            }
11015            if meta.path.is_ident("on") {
11016                let s: LitStr = meta.value()?.parse()?;
11017                out.on = Some(s.value());
11018                return Ok(());
11019            }
11020            if meta.path.is_ident("on_delete") {
11021                let s: LitStr = meta.value()?.parse()?;
11022                let raw = s.value();
11023                let normalized = raw.trim().to_ascii_lowercase();
11024                // Validate at parse time so the user gets a clear span
11025                // rather than a downstream compile error in the emit.
11026                match normalized.as_str() {
11027                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
11028                    _ => {
11029                        return Err(syn::Error::new(
11030                            s.span(),
11031                            format!(
11032                                "unknown on_delete action `{raw}`; expected one of \
11033                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
11034                            ),
11035                        ));
11036                    }
11037                }
11038                out.on_delete = Some(normalized);
11039                return Ok(());
11040            }
11041            if meta.path.is_ident("related_name") {
11042                let s: LitStr = meta.value()?.parse()?;
11043                let raw = s.value();
11044                if raw.trim().is_empty() {
11045                    return Err(syn::Error::new(
11046                        s.span(),
11047                        "`related_name` must be a non-empty identifier",
11048                    ));
11049                }
11050                // Validate as a Rust-ident shape — the value becomes
11051                // a method name on the parent type. Same rule as
11052                // `default_related_name` so the two surfaces match.
11053                // `"+"` emits no accessor at all.
11054                let snake = raw
11055                    .chars()
11056                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
11057                    && !raw.starts_with(char::is_numeric);
11058                if !snake && raw != NO_REVERSE_ACCESSOR {
11059                    return Err(syn::Error::new(
11060                        s.span(),
11061                        "`related_name` must be snake_case ASCII (lowercase letters, \
11062                         digits, underscores; no leading digit), or \"+\" for none",
11063                    ));
11064                }
11065                out.related_name = Some(raw);
11066                return Ok(());
11067            }
11068            if meta.path.is_ident("max_length") {
11069                let lit: syn::LitInt = meta.value()?.parse()?;
11070                out.max_length = Some(lit.base10_parse::<u32>()?);
11071                return Ok(());
11072            }
11073            // `#[rustango(vector(dims = N))]` — pgvector column
11074            // dimension (#824). Nested-meta form so it reads like the
11075            // other typed-column attrs.
11076            if meta.path.is_ident("vector") {
11077                meta.parse_nested_meta(|inner| {
11078                    if inner.path.is_ident("dims") {
11079                        let lit: syn::LitInt = inner.value()?.parse()?;
11080                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
11081                        return Ok(());
11082                    }
11083                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
11084                })?;
11085                return Ok(());
11086            }
11087            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
11088            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
11089            if meta.path.is_ident("geometry") {
11090                meta.parse_nested_meta(|inner| {
11091                    if inner.path.is_ident("srid") {
11092                        let lit: syn::LitInt = inner.value()?.parse()?;
11093                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
11094                        return Ok(());
11095                    }
11096                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
11097                })?;
11098                return Ok(());
11099            }
11100            if meta.path.is_ident("min") {
11101                out.min = Some(parse_signed_i64(&meta)?);
11102                return Ok(());
11103            }
11104            if meta.path.is_ident("max") {
11105                out.max = Some(parse_signed_i64(&meta)?);
11106                return Ok(());
11107            }
11108            if meta.path.is_ident("default") {
11109                let s: LitStr = meta.value()?.parse()?;
11110                out.default = Some(s.value());
11111                return Ok(());
11112            }
11113            if meta.path.is_ident("generated_as") {
11114                let s: LitStr = meta.value()?.parse()?;
11115                out.generated_as = Some(s.value());
11116                return Ok(());
11117            }
11118            if meta.path.is_ident("help_text") {
11119                let s: LitStr = meta.value()?.parse()?;
11120                out.help_text = Some(s.value());
11121                return Ok(());
11122            }
11123            if meta.path.is_ident("choices") {
11124                let s: LitStr = meta.value()?.parse()?;
11125                let raw = s.value();
11126                let mut pairs: Vec<(String, String)> = Vec::new();
11127                for chunk in raw.split(',') {
11128                    let chunk = chunk.trim();
11129                    if chunk.is_empty() {
11130                        continue;
11131                    }
11132                    let (value, label) = match chunk.split_once(':') {
11133                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
11134                        None => (chunk.to_owned(), chunk.to_owned()),
11135                    };
11136                    if value.is_empty() {
11137                        return Err(syn::Error::new(
11138                            s.span(),
11139                            "`choices` entry has empty value before `:`",
11140                        ));
11141                    }
11142                    pairs.push((value, label));
11143                }
11144                if pairs.is_empty() {
11145                    return Err(syn::Error::new(
11146                        s.span(),
11147                        "`choices = \"…\"` must contain at least one value",
11148                    ));
11149                }
11150                out.choices = Some(pairs);
11151                return Ok(());
11152            }
11153            if meta.path.is_ident("db_comment") {
11154                let s: LitStr = meta.value()?.parse()?;
11155                out.db_comment = Some(s.value());
11156                return Ok(());
11157            }
11158            if meta.path.is_ident("verbose_name") {
11159                let s: LitStr = meta.value()?.parse()?;
11160                out.verbose_name = Some(s.value());
11161                return Ok(());
11162            }
11163            if meta.path.is_ident("editable") {
11164                // Two forms accepted:
11165                //   #[rustango(editable = false)] / true — explicit
11166                //   #[rustango(editable)] — flag form (= true, the
11167                //   default, so harmless; included for symmetry)
11168                if let Ok(v) = meta.value() {
11169                    let lit: syn::LitBool = v.parse()?;
11170                    out.editable = lit.value;
11171                } else {
11172                    out.editable = true;
11173                }
11174                return Ok(());
11175            }
11176            if meta.path.is_ident("blank") {
11177                // Two forms accepted:
11178                //   #[rustango(blank)] — flag form, true
11179                //   #[rustango(blank = false)] / true — explicit
11180                if let Ok(v) = meta.value() {
11181                    let lit: syn::LitBool = v.parse()?;
11182                    out.blank = lit.value;
11183                } else {
11184                    out.blank = true;
11185                }
11186                return Ok(());
11187            }
11188            if meta.path.is_ident("citext") {
11189                // Case-insensitive text column (#344). Two forms:
11190                //   #[rustango(citext)]          — flag form, true
11191                //   #[rustango(citext = true)]   — explicit
11192                //   #[rustango(citext = false)]  — explicit opt-out
11193                // String-only validation lives in the field-type
11194                // emitter (the FieldType discriminant is computed in
11195                // `detect_type`); the macro records the flag and the
11196                // DDL writer emits dialect-specific COLLATE / CITEXT.
11197                if let Ok(v) = meta.value() {
11198                    let lit: syn::LitBool = v.parse()?;
11199                    out.case_insensitive = lit.value;
11200                } else {
11201                    out.case_insensitive = true;
11202                }
11203                return Ok(());
11204            }
11205            if meta.path.is_ident("validators") {
11206                let s: LitStr = meta.value()?.parse()?;
11207                let raw = s.value();
11208                out.validators = raw
11209                    .split(',')
11210                    .map(str::trim)
11211                    .filter(|s| !s.is_empty())
11212                    .map(str::to_owned)
11213                    .collect();
11214                if out.validators.is_empty() {
11215                    return Err(syn::Error::new(
11216                        s.span(),
11217                        "`validators = \"…\"` must list at least one name",
11218                    ));
11219                }
11220                return Ok(());
11221            }
11222            if meta.path.is_ident("auto_uuid") {
11223                out.auto_uuid = true;
11224                // Implied: PK + auto + DEFAULT gen_random_uuid().
11225                // Each is also explicitly settable; the explicit
11226                // value wins if conflicting.
11227                out.primary_key = true;
11228                if out.default.is_none() {
11229                    out.default = Some("gen_random_uuid()".into());
11230                }
11231                return Ok(());
11232            }
11233            if meta.path.is_ident("default_uuid_v7") {
11234                // Backend-neutral counterpart of `auto_uuid` — issue #823.
11235                // No SQL DEFAULT (the macro fills the value Rust-side
11236                // before binding); just mark the field as PK + Auto
11237                // so the insert path is the `Auto::Unset → generate`
11238                // branch.
11239                out.default_uuid_v7 = true;
11240                out.primary_key = true;
11241                return Ok(());
11242            }
11243            if meta.path.is_ident("auto_now_add") {
11244                out.auto_now_add = true;
11245                if out.default.is_none() {
11246                    out.default = Some("now()".into());
11247                }
11248                return Ok(());
11249            }
11250            if meta.path.is_ident("auto_now") {
11251                out.auto_now = true;
11252                if out.default.is_none() {
11253                    out.default = Some("now()".into());
11254                }
11255                return Ok(());
11256            }
11257            if meta.path.is_ident("soft_delete") {
11258                out.soft_delete = true;
11259                return Ok(());
11260            }
11261            if meta.path.is_ident("unique") {
11262                out.unique = true;
11263                return Ok(());
11264            }
11265            if meta.path.is_ident("index") {
11266                out.index = true;
11267                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11268                if meta.input.peek(syn::token::Paren) {
11269                    meta.parse_nested_meta(|inner| {
11270                        if inner.path.is_ident("unique") {
11271                            out.index_unique = true;
11272                            return Ok(());
11273                        }
11274                        if inner.path.is_ident("name") {
11275                            let s: LitStr = inner.value()?.parse()?;
11276                            out.index_name = Some(s.value());
11277                            return Ok(());
11278                        }
11279                        if inner.path.is_ident("method") {
11280                            let s: LitStr = inner.value()?.parse()?;
11281                            let v = s.value();
11282                            match v.as_str() {
11283                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11284                                    out.index_method = v;
11285                                }
11286                                other => {
11287                                    return Err(inner.error(format!(
11288                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11289                                    )));
11290                                }
11291                            }
11292                            return Ok(());
11293                        }
11294                        Err(inner.error(
11295                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11296                        ))
11297                    })?;
11298                }
11299                return Ok(());
11300            }
11301            Err(meta.error("unknown rustango field attribute"))
11302        })?;
11303    }
11304    Ok(out)
11305}
11306
11307/// Parse a signed integer literal, accepting optional leading `-`.
11308fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11309    let expr: syn::Expr = meta.value()?.parse()?;
11310    match expr {
11311        syn::Expr::Lit(syn::ExprLit {
11312            lit: syn::Lit::Int(lit),
11313            ..
11314        }) => lit.base10_parse::<i64>(),
11315        syn::Expr::Unary(syn::ExprUnary {
11316            op: syn::UnOp::Neg(_),
11317            expr,
11318            ..
11319        }) => {
11320            if let syn::Expr::Lit(syn::ExprLit {
11321                lit: syn::Lit::Int(lit),
11322                ..
11323            }) = *expr
11324            {
11325                let v: i64 = lit.base10_parse()?;
11326                Ok(-v)
11327            } else {
11328                Err(syn::Error::new_spanned(expr, "expected integer literal"))
11329            }
11330        }
11331        other => Err(syn::Error::new_spanned(
11332            other,
11333            "expected integer literal (signed)",
11334        )),
11335    }
11336}
11337
11338struct FieldInfo<'a> {
11339    ident: &'a syn::Ident,
11340    column: String,
11341    primary_key: bool,
11342    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11343    /// skip this column when `Auto::Unset` and emit it under
11344    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11345    auto: bool,
11346    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11347    /// the `Column::Value` associated type for typed-column tokens.
11348    value_ty: &'a Type,
11349    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11350    field_type_tokens: TokenStream2,
11351    schema: TokenStream2,
11352    from_row_init: TokenStream2,
11353    /// Variant of [`Self::from_row_init`] that reads the column via
11354    /// `format!("{prefix}__{col}")` so a model can be decoded out of
11355    /// the aliased columns of a JOINed row. Drives slice 9.0d's
11356    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11357    /// helper that `select_related` calls when stitching loaded FKs.
11358    from_aliased_row_init: TokenStream2,
11359    /// MySQL twins of the two inits above. They differ only for a UUID
11360    /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11361    from_row_init_my: TokenStream2,
11362    from_aliased_row_init_my: TokenStream2,
11363    /// A bare or optional `Uuid` field.
11364    uuid_column: bool,
11365    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11366    /// relation helper emit (`Author::<child>_set`) needs to know `T`
11367    /// to point the generated method at the right child model.
11368    fk_inner: Option<Type>,
11369    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11370    /// `kind` (since ForeignKey detection sets `kind` to K's
11371    /// underlying type) but stored separately for clarity at the
11372    /// `FkRelation` construction site, which only sees the FK's
11373    /// surface fields.
11374    fk_pk_kind: DetectedKind,
11375    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11376    /// the bare `ForeignKey<T, K>`. Routes the load_related and
11377    /// fk_pk_access emitters to wrap assignments / accessors in
11378    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11379    /// FK column compiles end-to-end. The DDL writer reads this off
11380    /// the field schema (`nullable` flag); the macro just needs to
11381    /// keep the Rust-side codegen consistent.
11382    nullable: bool,
11383    /// `true` when this column was marked `#[rustango(auto_now)]` —
11384    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11385    /// column instead of the user-supplied value, so `updated_at`
11386    /// always reflects the latest write without the caller having
11387    /// to remember to set it.
11388    auto_now: bool,
11389    /// `true` when this column was marked `#[rustango(auto_now_add)]`
11390    /// — the column is server-set on INSERT (DB DEFAULT) and
11391    /// **immutable** afterwards. `update_on` / `save_on` skip the
11392    /// column entirely so a stale `created_at` value in memory never
11393    /// rewrites the persisted timestamp.
11394    auto_now_add: bool,
11395    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11396    /// Triggers emission of `soft_delete_on(executor)` and
11397    /// `restore_on(executor)` on the model's inherent impl. There is
11398    /// at most one such column per model — emission asserts this.
11399    soft_delete: bool,
11400    /// `Some` when this column was marked
11401    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11402    /// every INSERT and UPDATE path; the database recomputes the
11403    /// value from `EXPR`. Backlog item #35.
11404    generated_as: Option<String>,
11405    /// `true` when this column was marked
11406    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11407    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11408    /// with `Uuid::now_v7()` before binding, so the PK is generated
11409    /// Rust-side and the column is always present in the INSERT
11410    /// statement (no DB DEFAULT requirement). Issue #823.
11411    default_uuid_v7: bool,
11412    /// `Some` when this FK field carried `#[rustango(related_name =
11413    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11414    /// consumed by [`reverse_helper_tokens`] to override the default
11415    /// `<child_snake>_set` accessor name. Follow-up to #816.
11416    related_name: Option<String>,
11417}
11418
11419/// Reject table names that won't survive SQL identifier
11420/// derivation downstream. Postgres' regular-identifier rule
11421/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11422/// through the framework's unquoted FK / index / constraint name
11423/// emission without surprises. We also disallow leading-digit and
11424/// the empty string for clarity.
11425///
11426/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11427/// here — those produce a runtime error from the SQL parser,
11428/// which is loud enough; statically enumerating reserved words
11429/// across the three supported dialects is more friction than help.
11430///
11431/// Backlog item #65.
11432fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11433    validate_sql_identifier(name, "table", span)
11434}
11435
11436/// Reject SQL identifiers that compile but break downstream SQL
11437/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11438/// `kind` is "table" / "column" — used for the error message so users
11439/// see which attribute caused the failure.
11440fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11441    if name.is_empty() {
11442        return Err(syn::Error::new(
11443            span,
11444            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11445        ));
11446    }
11447    let mut chars = name.chars();
11448    let first = chars.next().unwrap();
11449    if !(first.is_ascii_alphabetic() || first == '_') {
11450        return Err(syn::Error::new(
11451            span,
11452            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11453        ));
11454    }
11455    for c in chars {
11456        if !(c.is_ascii_alphanumeric() || c == '_') {
11457            return Err(syn::Error::new(
11458                span,
11459                format!(
11460                    "{kind} name `{name}` contains invalid character {c:?} — \
11461                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11462                     Hyphens in particular break FK / index name derivation \
11463                     downstream; use underscores instead (e.g. `{}`)",
11464                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11465                ),
11466            ));
11467        }
11468    }
11469    Ok(())
11470}
11471
11472fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11473    let root = rustango_root();
11474    let attrs = parse_field_attrs(field)?;
11475    let ident = field
11476        .ident
11477        .as_ref()
11478        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11479    let name = ident_name(ident);
11480    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11481    let primary_key = attrs.primary_key;
11482    let DetectedType {
11483        kind,
11484        nullable,
11485        auto: detected_auto,
11486        fk_inner,
11487    } = detect_type(&field.ty)?;
11488    check_bound_compatibility(field, &attrs, kind)?;
11489    let auto = detected_auto;
11490    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11491    // INSERT path: the user must wrap the field in `Auto<T>`, which
11492    // marks the column as DB-default-supplied. The mixin attrs then
11493    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11494    // for `auto_now`, force the value on UPDATE too.
11495    if attrs.auto_uuid {
11496        if kind != DetectedKind::Uuid {
11497            return Err(syn::Error::new_spanned(
11498                field,
11499                "`#[rustango(auto_uuid)]` requires the field type to be \
11500                 `Auto<uuid::Uuid>`",
11501            ));
11502        }
11503        if !detected_auto {
11504            return Err(syn::Error::new_spanned(
11505                field,
11506                "`#[rustango(auto_uuid)]` requires the field type to be \
11507                 wrapped in `Auto<...>` so the macro skips the column on \
11508                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11509            ));
11510        }
11511    }
11512    if attrs.default_uuid_v7 {
11513        if kind != DetectedKind::Uuid {
11514            return Err(syn::Error::new_spanned(
11515                field,
11516                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11517                 `Auto<uuid::Uuid>`",
11518            ));
11519        }
11520        if !detected_auto {
11521            return Err(syn::Error::new_spanned(
11522                field,
11523                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11524                 wrapped in `Auto<...>` so the macro can detect the \
11525                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11526            ));
11527        }
11528        if attrs.auto_uuid {
11529            return Err(syn::Error::new_spanned(
11530                field,
11531                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11532                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11533                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11534                 Pick one.",
11535            ));
11536        }
11537    }
11538    if attrs.auto_now_add || attrs.auto_now {
11539        if kind != DetectedKind::DateTime {
11540            return Err(syn::Error::new_spanned(
11541                field,
11542                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11543                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11544            ));
11545        }
11546        if !detected_auto {
11547            return Err(syn::Error::new_spanned(
11548                field,
11549                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11550                 the field type to be wrapped in `Auto<...>` so the macro skips \
11551                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11552            ));
11553        }
11554    }
11555    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11556        return Err(syn::Error::new_spanned(
11557            field,
11558            "`#[rustango(soft_delete)]` requires the field type to be \
11559             `Option<chrono::DateTime<chrono::Utc>>`",
11560        ));
11561    }
11562    let is_mixin_auto =
11563        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11564    if detected_auto && !primary_key && !is_mixin_auto {
11565        return Err(syn::Error::new_spanned(
11566            field,
11567            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11568             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11569             `auto_now`",
11570        ));
11571    }
11572    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11573        return Err(syn::Error::new_spanned(
11574            field,
11575            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11576             SERIAL / BIGSERIAL already supplies a default sequence.",
11577        ));
11578    }
11579    if fk_inner.is_some() && primary_key {
11580        return Err(syn::Error::new_spanned(
11581            field,
11582            "`ForeignKey<T>` is not allowed on a primary-key field — \
11583             a row's PK is its own identity, not a reference to a parent.",
11584        ));
11585    }
11586    if attrs.generated_as.is_some() {
11587        if primary_key {
11588            return Err(syn::Error::new_spanned(
11589                field,
11590                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11591                 primary-key field — a PK must be writable so the row \
11592                 has an identity at INSERT time.",
11593            ));
11594        }
11595        if attrs.default.is_some() {
11596            return Err(syn::Error::new_spanned(
11597                field,
11598                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11599                 `default = \"…\"` — Postgres rejects DEFAULT on \
11600                 generated columns. The expression IS the default.",
11601            ));
11602        }
11603        if detected_auto {
11604            return Err(syn::Error::new_spanned(
11605                field,
11606                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11607                 an `Auto<T>` field — generated columns are computed \
11608                 by the DB, not server-assigned via a sequence. Use a \
11609                 plain Rust type (e.g. `f64`).",
11610            ));
11611        }
11612        if fk_inner.is_some() {
11613            return Err(syn::Error::new_spanned(
11614                field,
11615                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11616                 ForeignKey field.",
11617            ));
11618        }
11619    }
11620    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11621    let column_lit = column.as_str();
11622    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11623    // dimension that the bare `Vector` Rust type can't carry, so emit
11624    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11625    // fallback of `Vector(0)`.
11626    let field_type_tokens = if kind == DetectedKind::Vector {
11627        let root = rustango_root();
11628        let dims = attrs.vector_dims.unwrap_or(0);
11629        quote!(#root::core::FieldType::Vector(#dims))
11630    } else if kind == DetectedKind::Geometry {
11631        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11632        // SRID that the bare `Point` Rust type can't carry, so emit
11633        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11634        let root = rustango_root();
11635        let srid = attrs.geometry_srid.unwrap_or(0);
11636        quote!(#root::core::FieldType::Geometry(#srid))
11637    } else {
11638        kind.variant_tokens()
11639    };
11640    let max_length = optional_u32(attrs.max_length);
11641    let min = optional_i64(attrs.min);
11642    let max = optional_i64(attrs.max);
11643    let default = optional_str(attrs.default.as_deref());
11644
11645    let unique = attrs.unique;
11646    let generated_as = optional_str(attrs.generated_as.as_deref());
11647    let help_text = optional_str(attrs.help_text.as_deref());
11648    let choices = optional_choices(attrs.choices.as_deref());
11649    let db_comment = optional_str(attrs.db_comment.as_deref());
11650    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11651    let editable = attrs.editable;
11652    let blank = attrs.blank;
11653    let case_insensitive = attrs.case_insensitive;
11654    let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11655    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11656    {
11657        return Err(syn::Error::new_spanned(
11658            field,
11659            "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11660             or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11661             has no meaning on a non-FK column.",
11662        ));
11663    }
11664    if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11665        return Err(syn::Error::new_spanned(
11666            field,
11667            "`on_delete = \"set_null\"` needs a nullable column: use \
11668             `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11669             alias for `Option` is not seen).",
11670        ));
11671    }
11672    let fk_on_delete = match attrs.on_delete.as_deref() {
11673        None => quote!(::core::option::Option::None),
11674        Some(action) => {
11675            let variant = match action {
11676                "cascade" => quote!(Cascade),
11677                "restrict" => quote!(Restrict),
11678                "set_null" => quote!(SetNull),
11679                "set_default" => quote!(SetDefault),
11680                "no_action" => quote!(NoAction),
11681                // parse_field_attrs already validated this — guard against future drift.
11682                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11683            };
11684            quote!(::core::option::Option::Some(
11685                #root::core::OnDeleteAction::#variant
11686            ))
11687        }
11688    };
11689    let auto_now_flag = attrs.auto_now;
11690    let schema = quote! {{
11691        #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11692            name: #name,
11693            column: #column_lit,
11694            ty: #field_type_tokens,
11695            nullable: #nullable,
11696            primary_key: #primary_key,
11697            relation: #relation,
11698            max_length: #max_length,
11699            min: #min,
11700            max: #max,
11701            default: #default,
11702            auto: #auto,
11703            auto_now: #auto_now_flag,
11704            unique: #unique,
11705            generated_as: #generated_as,
11706            help_text: #help_text,
11707            choices: #choices,
11708            db_comment: #db_comment,
11709            verbose_name: #verbose_name,
11710            editable: #editable,
11711            blank: #blank,
11712            case_insensitive: #case_insensitive,
11713            fk_on_delete: #fk_on_delete,
11714            validators: &[ #(#validators_lits),* ],
11715        })
11716    }};
11717
11718    let from_row_init = quote! {
11719        #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11720    };
11721    let from_aliased_row_init = quote! {
11722        #ident: #root::sql::sqlx::Row::try_get(
11723            row,
11724            #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11725        )?
11726    };
11727    // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11728    // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11729    let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11730    let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11731        (
11732            quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11733            quote! {
11734                #ident: #root::sql::try_get_flat_my(
11735                    row,
11736                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11737                )?
11738            },
11739        )
11740    } else {
11741        (from_row_init.clone(), from_aliased_row_init.clone())
11742    };
11743
11744    Ok(FieldInfo {
11745        ident,
11746        column,
11747        primary_key,
11748        auto,
11749        value_ty: &field.ty,
11750        field_type_tokens,
11751        schema,
11752        from_row_init,
11753        from_aliased_row_init,
11754        from_row_init_my,
11755        from_aliased_row_init_my,
11756        uuid_column,
11757        fk_inner: fk_inner.cloned(),
11758        fk_pk_kind: kind,
11759        nullable,
11760        auto_now: attrs.auto_now,
11761        auto_now_add: attrs.auto_now_add,
11762        soft_delete: attrs.soft_delete,
11763        generated_as: attrs.generated_as.clone(),
11764        default_uuid_v7: attrs.default_uuid_v7,
11765        related_name: attrs.related_name.clone(),
11766    })
11767}
11768
11769fn check_bound_compatibility(
11770    field: &syn::Field,
11771    attrs: &FieldAttrs,
11772    kind: DetectedKind,
11773) -> syn::Result<()> {
11774    if attrs.max_length.is_some() && kind != DetectedKind::String {
11775        return Err(syn::Error::new_spanned(
11776            field,
11777            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11778        ));
11779    }
11780    // These were silently dropped on the wrong type (#1937).
11781    if attrs.case_insensitive && kind != DetectedKind::String {
11782        return Err(syn::Error::new_spanned(
11783            field,
11784            "`citext` is only valid on `String` fields (or `Option<String>`)",
11785        ));
11786    }
11787    if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11788        return Err(syn::Error::new_spanned(
11789            field,
11790            "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11791        ));
11792    }
11793    if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11794        return Err(syn::Error::new_spanned(
11795            field,
11796            "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11797        ));
11798    }
11799    if attrs.choices.is_some() && kind != DetectedKind::String {
11800        return Err(syn::Error::new_spanned(
11801            field,
11802            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11803             integer-valued enumerations should be modeled with a Rust enum and \
11804             custom (de)serializer for now",
11805        ));
11806    }
11807    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11808        return Err(syn::Error::new_spanned(
11809            field,
11810            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11811        ));
11812    }
11813    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11814        if min > max {
11815            return Err(syn::Error::new_spanned(
11816                field,
11817                format!("`min` ({min}) is greater than `max` ({max})"),
11818            ));
11819        }
11820    }
11821    Ok(())
11822}
11823
11824/// A `&'static [ty]` of `const fn` results. The slice needs its own
11825/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11826fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11827    quote!({
11828        const ITEMS: &[#ty] = &[ #(#items),* ];
11829        ITEMS
11830    })
11831}
11832
11833fn optional_u32(value: Option<u32>) -> TokenStream2 {
11834    if let Some(v) = value {
11835        quote!(::core::option::Option::Some(#v))
11836    } else {
11837        quote!(::core::option::Option::None)
11838    }
11839}
11840
11841fn optional_i64(value: Option<i64>) -> TokenStream2 {
11842    if let Some(v) = value {
11843        quote!(::core::option::Option::Some(#v))
11844    } else {
11845        quote!(::core::option::Option::None)
11846    }
11847}
11848
11849fn optional_str(value: Option<&str>) -> TokenStream2 {
11850    if let Some(v) = value {
11851        quote!(::core::option::Option::Some(#v))
11852    } else {
11853        quote!(::core::option::Option::None)
11854    }
11855}
11856
11857fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11858    let Some(pairs) = pairs else {
11859        return quote!(::core::option::Option::None);
11860    };
11861    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11862    quote!(::core::option::Option::Some(&[#(#entries),*]))
11863}
11864
11865fn relation_tokens(
11866    field: &syn::Field,
11867    attrs: &FieldAttrs,
11868    fk_inner: Option<&syn::Type>,
11869    table: &str,
11870) -> syn::Result<TokenStream2> {
11871    let root = rustango_root();
11872    if let Some(inner) = fk_inner {
11873        if attrs.fk.is_some() || attrs.o2o.is_some() {
11874            return Err(syn::Error::new_spanned(
11875                field,
11876                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11877                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11878            ));
11879        }
11880        // No `on`: the target's PK column, which may be renamed.
11881        let on = match attrs.on.as_deref() {
11882            Some(on) => quote!(#on),
11883            None => quote! {
11884                match <#inner as #root::core::Model>::SCHEMA.pk_column() {
11885                    ::core::option::Option::Some(c) => c,
11886                    ::core::option::Option::None => "id",
11887                }
11888            },
11889        };
11890        return Ok(quote! {
11891            ::core::option::Option::Some(#root::core::Relation::fk(
11892                <#inner as #root::core::Model>::SCHEMA.table,
11893                #on,
11894            ))
11895        });
11896    }
11897    match (&attrs.fk, &attrs.o2o) {
11898        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11899            field,
11900            "`fk` and `o2o` are mutually exclusive",
11901        )),
11902        (Some(to), None) => {
11903            let on = attrs.on.as_deref().unwrap_or("id");
11904            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11905            // the model's own table. Threaded as a literal string at
11906            // macro-expansion time to sidestep the const-eval cycle
11907            // that `Self::SCHEMA.table` would create when referenced
11908            // inside Self::SCHEMA's own initializer.
11909            let resolved = if to == "self" { table } else { to };
11910            Ok(quote! {
11911                ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11912            })
11913        }
11914        (None, Some(to)) => {
11915            let on = attrs.on.as_deref().unwrap_or("id");
11916            let resolved = if to == "self" { table } else { to };
11917            Ok(quote! {
11918                ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11919            })
11920        }
11921        (None, None) => {
11922            if attrs.on.is_some() {
11923                return Err(syn::Error::new_spanned(
11924                    field,
11925                    "`on` requires `fk` or `o2o`",
11926                ));
11927            }
11928            Ok(quote!(::core::option::Option::None))
11929        }
11930    }
11931}
11932
11933/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11934/// about kinds without depending on `rustango-core` (which would require a
11935/// proc-macro/normal split it doesn't have today).
11936#[derive(Clone, Copy, PartialEq, Eq)]
11937enum DetectedKind {
11938    I16,
11939    I32,
11940    I64,
11941    F32,
11942    F64,
11943    Bool,
11944    String,
11945    DateTime,
11946    Date,
11947    Time,
11948    Uuid,
11949    Json,
11950    Decimal,
11951    Binary,
11952    /// `Array<String>` → PG `text[]` (#341).
11953    ArrayText,
11954    /// `Array<i32>` → PG `integer[]` (#341).
11955    ArrayInt,
11956    /// `Array<i64>` → PG `bigint[]` (#341).
11957    ArrayBigInt,
11958    /// `Range<i32>` → PG `int4range` (#343).
11959    RangeInt,
11960    /// `Range<i64>` → PG `int8range` (#343).
11961    RangeBigInt,
11962    /// `Range<Decimal>` → PG `numrange` (#343).
11963    RangeNumeric,
11964    /// `Range<NaiveDate>` → PG `daterange` (#343).
11965    RangeDate,
11966    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11967    RangeDateTime,
11968    /// `HStore` → PG `hstore` (#342).
11969    HStore,
11970    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11971    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11972    /// in at the `FieldType` emission site (not carried on this enum).
11973    Vector,
11974    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11975    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11976    /// threaded in at the `FieldType` emission site.
11977    Geometry,
11978}
11979
11980impl DetectedKind {
11981    fn variant_tokens(self) -> TokenStream2 {
11982        let root = rustango_root();
11983        match self {
11984            Self::I16 => quote!(#root::core::FieldType::I16),
11985            Self::I32 => quote!(#root::core::FieldType::I32),
11986            Self::I64 => quote!(#root::core::FieldType::I64),
11987            Self::F32 => quote!(#root::core::FieldType::F32),
11988            Self::F64 => quote!(#root::core::FieldType::F64),
11989            Self::Bool => quote!(#root::core::FieldType::Bool),
11990            Self::String => quote!(#root::core::FieldType::String),
11991            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11992            Self::Date => quote!(#root::core::FieldType::Date),
11993            Self::Time => quote!(#root::core::FieldType::Time),
11994            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11995            Self::Json => quote!(#root::core::FieldType::Json),
11996            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11997            Self::Binary => quote!(#root::core::FieldType::Binary),
11998            Self::ArrayText => {
11999                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
12000            }
12001            Self::ArrayInt => {
12002                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
12003            }
12004            Self::ArrayBigInt => {
12005                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
12006            }
12007            Self::RangeInt => {
12008                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
12009            }
12010            Self::RangeBigInt => {
12011                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
12012            }
12013            Self::RangeNumeric => {
12014                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
12015            }
12016            Self::RangeDate => {
12017                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
12018            }
12019            Self::RangeDateTime => {
12020                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
12021            }
12022            Self::HStore => quote!(#root::core::FieldType::HStore),
12023            // Dimension comes from the `vector(dims = N)` attribute,
12024            // applied at the emission site; `0` here is just a fallback.
12025            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
12026            // SRID comes from the `geometry(srid = N)` attribute, applied
12027            // at the emission site; `0` here is just a fallback.
12028            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
12029        }
12030    }
12031
12032    fn is_integer(self) -> bool {
12033        matches!(self, Self::I16 | Self::I32 | Self::I64)
12034    }
12035}
12036
12037/// Result of walking a field's Rust type. `kind` is the underlying
12038/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
12039/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
12040/// `Some(<T>)` when the field was `ForeignKey<T>` (or
12041/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
12042#[derive(Clone, Copy)]
12043struct DetectedType<'a> {
12044    kind: DetectedKind,
12045    nullable: bool,
12046    auto: bool,
12047    fk_inner: Option<&'a syn::Type>,
12048}
12049
12050/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
12051/// non-`Auto` types — the caller should already have routed Auto-only
12052/// codegen through this helper, so a `None` indicates a macro-internal
12053/// invariant break.
12054fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
12055    let Type::Path(TypePath { path, qself: None }) = ty else {
12056        return None;
12057    };
12058    let last = path.segments.last()?;
12059    if last.ident != "Auto" {
12060        return None;
12061    }
12062    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12063        return None;
12064    };
12065    args.args.iter().find_map(|a| match a {
12066        syn::GenericArgument::Type(t) => Some(t),
12067        _ => None,
12068    })
12069}
12070
12071fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
12072    let Type::Path(TypePath { path, qself: None }) = ty else {
12073        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
12074    };
12075    let last = path
12076        .segments
12077        .last()
12078        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
12079
12080    if last.ident == "Option" {
12081        let inner = generic_inner(ty, &last.arguments, "Option")?;
12082        let inner_det = detect_type(inner)?;
12083        if inner_det.nullable {
12084            return Err(syn::Error::new_spanned(
12085                ty,
12086                "nested Option is not supported",
12087            ));
12088        }
12089        if inner_det.auto {
12090            return Err(syn::Error::new_spanned(
12091                ty,
12092                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12093            ));
12094        }
12095        return Ok(DetectedType {
12096            nullable: true,
12097            ..inner_det
12098        });
12099    }
12100
12101    if last.ident == "Auto" {
12102        let inner = generic_inner(ty, &last.arguments, "Auto")?;
12103        let inner_det = detect_type(inner)?;
12104        if inner_det.auto {
12105            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
12106        }
12107        if inner_det.nullable {
12108            return Err(syn::Error::new_spanned(
12109                ty,
12110                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12111            ));
12112        }
12113        if inner_det.fk_inner.is_some() {
12114            return Err(syn::Error::new_spanned(
12115                ty,
12116                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
12117            ));
12118        }
12119        if !matches!(
12120            inner_det.kind,
12121            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
12122        ) {
12123            return Err(syn::Error::new_spanned(
12124                ty,
12125                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
12126                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
12127                 (DEFAULT now())",
12128            ));
12129        }
12130        return Ok(DetectedType {
12131            auto: true,
12132            ..inner_det
12133        });
12134    }
12135
12136    if last.ident == "ForeignKey" {
12137        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
12138        // Resolve the FK column's underlying SQL type from `K`. When the
12139        // user wrote `ForeignKey<T>` without a key parameter, the type
12140        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
12141        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
12142        // recurse into K so the column DDL emits the right SQL type
12143        // (VARCHAR for String, UUID for Uuid, …) and the load_related
12144        // emitter knows which `SqlValue` variant to match.
12145        let kind = match key_ty {
12146            Some(k) => detect_type(k)?.kind,
12147            None => DetectedKind::I64,
12148        };
12149        return Ok(DetectedType {
12150            kind,
12151            nullable: false,
12152            auto: false,
12153            fk_inner: Some(inner),
12154        });
12155    }
12156
12157    let kind = match last.ident.to_string().as_str() {
12158        "i16" => DetectedKind::I16,
12159        "i32" => DetectedKind::I32,
12160        "i64" => DetectedKind::I64,
12161        "f32" => DetectedKind::F32,
12162        "f64" => DetectedKind::F64,
12163        "bool" => DetectedKind::Bool,
12164        "String" => DetectedKind::String,
12165        "DateTime" => DetectedKind::DateTime,
12166        "NaiveDate" => DetectedKind::Date,
12167        "NaiveTime" => DetectedKind::Time,
12168        "Uuid" => DetectedKind::Uuid,
12169        "Value" => DetectedKind::Json,
12170        "Decimal" => DetectedKind::Decimal,
12171        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
12172        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
12173        // — that would emit Binary DDL and decode-fail at runtime.
12174        "Vec" => {
12175            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
12176            if let Type::Path(TypePath { path, qself: None }) = inner {
12177                if let Some(seg) = path.segments.last() {
12178                    if seg.ident == "u8" && seg.arguments.is_empty() {
12179                        return Ok(DetectedType {
12180                            kind: DetectedKind::Binary,
12181                            nullable: false,
12182                            auto: false,
12183                            fk_inner: None,
12184                        });
12185                    }
12186                }
12187            }
12188            return Err(syn::Error::new_spanned(
12189                ty,
12190                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
12191                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
12192            ));
12193        }
12194        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
12195        // `integer[]` / `bigint[]` (#341).
12196        "Array" => {
12197            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
12198            let elem = match inner {
12199                Type::Path(TypePath { path, qself: None }) => {
12200                    path.segments.last().map(|s| s.ident.to_string())
12201                }
12202                _ => None,
12203            };
12204            let kind = match elem.as_deref() {
12205                Some("String") => DetectedKind::ArrayText,
12206                Some("i32") => DetectedKind::ArrayInt,
12207                Some("i64") => DetectedKind::ArrayBigInt,
12208                _ => {
12209                    return Err(syn::Error::new_spanned(
12210                        ty,
12211                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12212                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12213                    ));
12214                }
12215            };
12216            return Ok(DetectedType {
12217                kind,
12218                nullable: false,
12219                auto: false,
12220                fk_inner: None,
12221            });
12222        }
12223        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12224        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12225        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12226        "Range" => {
12227            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12228            let elem = match inner {
12229                Type::Path(TypePath { path, qself: None }) => {
12230                    path.segments.last().map(|s| s.ident.to_string())
12231                }
12232                _ => None,
12233            };
12234            let kind = match elem.as_deref() {
12235                Some("i32") => DetectedKind::RangeInt,
12236                Some("i64") => DetectedKind::RangeBigInt,
12237                Some("Decimal") => DetectedKind::RangeNumeric,
12238                Some("NaiveDate") => DetectedKind::RangeDate,
12239                Some("DateTime") => DetectedKind::RangeDateTime,
12240                _ => {
12241                    return Err(syn::Error::new_spanned(
12242                        ty,
12243                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12244                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12245                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12246                         (→ tstzrange) are supported (#343)",
12247                    ));
12248                }
12249            };
12250            return Ok(DetectedType {
12251                kind,
12252                nullable: false,
12253                auto: false,
12254                fk_inner: None,
12255            });
12256        }
12257        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12258        // (the `CastValue` impl bridges logical↔stored); the field's own
12259        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12260        // schema just needs `FieldType::String`.
12261        "Cast" => {
12262            return Ok(DetectedType {
12263                kind: DetectedKind::String,
12264                nullable: false,
12265                auto: false,
12266                fk_inner: None,
12267            });
12268        }
12269        // `HStore` → PG `hstore` (#342). No generic
12270        // parameter — always a string→string map.
12271        "HStore" => {
12272            return Ok(DetectedType {
12273                kind: DetectedKind::HStore,
12274                nullable: false,
12275                auto: false,
12276                fk_inner: None,
12277            });
12278        }
12279        // `Vector` → pgvector `vector(N)` (#824). The dimension is
12280        // supplied by `#[rustango(vector(dims = N))]`, not the type.
12281        "Vector" => {
12282            return Ok(DetectedType {
12283                kind: DetectedKind::Vector,
12284                nullable: false,
12285                auto: false,
12286                fk_inner: None,
12287            });
12288        }
12289        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12290        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12291        "Point" => {
12292            return Ok(DetectedType {
12293                kind: DetectedKind::Geometry,
12294                nullable: false,
12295                auto: false,
12296                fk_inner: None,
12297            });
12298        }
12299        other => {
12300            return Err(syn::Error::new_spanned(
12301                ty,
12302                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)"),
12303            ));
12304        }
12305    };
12306    Ok(DetectedType {
12307        kind,
12308        nullable: false,
12309        auto: false,
12310        fk_inner: None,
12311    })
12312}
12313
12314fn generic_inner<'a>(
12315    ty: &'a Type,
12316    arguments: &'a PathArguments,
12317    wrapper: &str,
12318) -> syn::Result<&'a Type> {
12319    let PathArguments::AngleBracketed(args) = arguments else {
12320        return Err(syn::Error::new_spanned(
12321            ty,
12322            format!("{wrapper} requires a generic argument"),
12323        ));
12324    };
12325    args.args
12326        .iter()
12327        .find_map(|a| match a {
12328            GenericArgument::Type(t) => Some(t),
12329            _ => None,
12330        })
12331        .ok_or_else(|| {
12332            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12333        })
12334}
12335
12336/// Like [`generic_inner`] but pulls *two* type args — the first is
12337/// required, the second is optional. Used by the `ForeignKey<T, K>`
12338/// detection where K defaults to `i64` when omitted.
12339fn generic_pair<'a>(
12340    ty: &'a Type,
12341    arguments: &'a PathArguments,
12342    wrapper: &str,
12343) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12344    let PathArguments::AngleBracketed(args) = arguments else {
12345        return Err(syn::Error::new_spanned(
12346            ty,
12347            format!("{wrapper} requires a generic argument"),
12348        ));
12349    };
12350    let mut types = args.args.iter().filter_map(|a| match a {
12351        GenericArgument::Type(t) => Some(t),
12352        _ => None,
12353    });
12354    let first = types.next().ok_or_else(|| {
12355        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12356    })?;
12357    let second = types.next();
12358    Ok((first, second))
12359}
12360
12361fn to_snake_case(s: &str) -> String {
12362    let mut out = String::with_capacity(s.len() + 4);
12363    for (i, ch) in s.chars().enumerate() {
12364        if ch.is_ascii_uppercase() {
12365            if i > 0 {
12366                out.push('_');
12367            }
12368            out.push(ch.to_ascii_lowercase());
12369        } else {
12370            out.push(ch);
12371        }
12372    }
12373    out
12374}
12375
12376// ============================================================
12377//  #[derive(Form)]  —  slice 8.4B
12378// ============================================================
12379
12380/// Per-field `#[form(...)]` attributes recognised by the derive.
12381#[derive(Default)]
12382struct FormFieldAttrs {
12383    min: Option<i64>,
12384    max: Option<i64>,
12385    min_length: Option<u32>,
12386    max_length: Option<u32>,
12387    /// `#[form(clean = "fn_name")]` — per-field clean hook.
12388    /// The named static method on the form struct is called after the
12389    /// field's typed parse + length/range checks; it gets the parsed
12390    /// value by reference and returns `Result<<FieldType>, String>`.
12391    /// On Ok, the returned value replaces the parsed one; on Err, the
12392    /// message is attached to the field error list. Issue #372.
12393    clean: Option<syn::Ident>,
12394}
12395
12396/// Container-level `#[form(...)]` attributes. Currently only the
12397/// cross-field `validate` hook (issue #373).
12398#[derive(Default)]
12399struct FormContainerAttrs {
12400    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12401    /// After every per-field parse succeeds, the named method on the
12402    /// form struct is called with `&self` and may return
12403    /// `Result<(), FormErrors>`. Errors merge into the field error
12404    /// list. Issue #373.
12405    validate: Option<syn::Ident>,
12406}
12407
12408/// Detected shape of a form field's Rust type.
12409#[derive(Clone, Copy)]
12410enum FormFieldKind {
12411    String,
12412    I16,
12413    I32,
12414    I64,
12415    F32,
12416    F64,
12417    Bool,
12418}
12419
12420impl FormFieldKind {
12421    fn parse_method(self) -> &'static str {
12422        match self {
12423            Self::I16 => "i16",
12424            Self::I32 => "i32",
12425            Self::I64 => "i64",
12426            Self::F32 => "f32",
12427            Self::F64 => "f64",
12428            // String + Bool don't go through `str::parse`; the codegen
12429            // handles them inline.
12430            Self::String | Self::Bool => "",
12431        }
12432    }
12433}
12434
12435/// A generated `derive(Form)` local; mixed-site so a same-named field can't shadow it.
12436fn form_local(name: &str) -> syn::Ident {
12437    syn::Ident::new(name, proc_macro2::Span::mixed_site())
12438}
12439
12440fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12441    let root = rustango_root();
12442    let (errs, cleaned, data_param) = (
12443        form_local("__errors"),
12444        form_local("__cleaned"),
12445        form_local("data"),
12446    );
12447    let struct_name = &input.ident;
12448
12449    let Data::Struct(data) = &input.data else {
12450        return Err(syn::Error::new_spanned(
12451            struct_name,
12452            "Form can only be derived on structs",
12453        ));
12454    };
12455    let Fields::Named(named) = &data.fields else {
12456        return Err(syn::Error::new_spanned(
12457            struct_name,
12458            "Form requires a struct with named fields",
12459        ));
12460    };
12461
12462    // #373 — container-level `#[form(validate = "fn")]` hook.
12463    let container = parse_form_container_attrs(input)?;
12464    let post_field_clean: Vec<TokenStream2> = Vec::new();
12465    let _ = post_field_clean;
12466
12467    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12468    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12469
12470    for field in &named.named {
12471        let ident = field
12472            .ident
12473            .as_ref()
12474            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12475        let attrs = parse_form_field_attrs(field)?;
12476        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12477        let is_string = matches!(kind, FormFieldKind::String);
12478        let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12479        if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12480            return Err(syn::Error::new(
12481                field.span(),
12482                "`min_length` / `max_length` need a String field",
12483            ));
12484        }
12485        if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12486            return Err(syn::Error::new(
12487                field.span(),
12488                "`min` / `max` need a numeric field",
12489            ));
12490        }
12491
12492        let name_lit = ident_name(ident);
12493        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12494        // #372 — append the per-field `clean_<field>` call right after
12495        // the parse block when the attribute is set. The clean fn
12496        // takes &T and returns Result<T, String>; on Err we attach
12497        // the message to the field error list without aborting, so
12498        // the caller sees every field error at once.
12499        let clean_block = if let Some(clean_fn) = &attrs.clean {
12500            quote! {
12501                if #errs.fields().get(#name_lit).is_none() {
12502                    match Self::#clean_fn(&#ident) {
12503                        ::core::result::Result::Ok(#cleaned) => { #ident = #cleaned; }
12504                        ::core::result::Result::Err(__msg) => {
12505                            #errs.add(#name_lit, __msg);
12506                        }
12507                    }
12508                }
12509            }
12510        } else {
12511            quote! {}
12512        };
12513        field_blocks.push(quote! {
12514            #parse_block
12515            #clean_block
12516        });
12517        field_idents.push(ident);
12518    }
12519
12520    // #373 — after every per-field parse + clean succeeds, call the
12521    // cross-field validator if declared. Errors merge into the
12522    // outgoing FormErrors via the existing `FormErrors::merge` helper
12523    // (same primitive the serializer cross-field hook uses).
12524    let cross_field_call = if let Some(validate_fn) = &container.validate {
12525        quote! {
12526            if #errs.is_empty() {
12527                let __candidate = Self { #( #field_idents ),* };
12528                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12529                    #errs.merge(__other);
12530                }
12531                if !#errs.is_empty() {
12532                    return ::core::result::Result::Err(#errs);
12533                }
12534                return ::core::result::Result::Ok(__candidate);
12535            }
12536        }
12537    } else {
12538        quote! {}
12539    };
12540
12541    Ok(quote! {
12542        impl #root::forms::Form for #struct_name {
12543            fn parse(
12544                #data_param: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12545            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12546                let mut #errs = #root::forms::FormErrors::default();
12547                #( #field_blocks )*
12548                #cross_field_call
12549                if !#errs.is_empty() {
12550                    return ::core::result::Result::Err(#errs);
12551                }
12552                ::core::result::Result::Ok(Self {
12553                    #( #field_idents ),*
12554                })
12555            }
12556        }
12557    })
12558}
12559
12560fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12561    let mut out = FormContainerAttrs::default();
12562    for attr in &input.attrs {
12563        if !attr.path().is_ident("form") {
12564            continue;
12565        }
12566        attr.parse_nested_meta(|meta| {
12567            if meta.path.is_ident("validate") {
12568                let s: LitStr = meta.value()?.parse()?;
12569                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12570                return Ok(());
12571            }
12572            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12573        })?;
12574    }
12575    Ok(out)
12576}
12577
12578fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12579    let mut out = FormFieldAttrs::default();
12580    for attr in &field.attrs {
12581        if !attr.path().is_ident("form") {
12582            continue;
12583        }
12584        attr.parse_nested_meta(|meta| {
12585            if meta.path.is_ident("min") {
12586                let lit: syn::LitInt = meta.value()?.parse()?;
12587                out.min = Some(lit.base10_parse::<i64>()?);
12588                return Ok(());
12589            }
12590            if meta.path.is_ident("max") {
12591                let lit: syn::LitInt = meta.value()?.parse()?;
12592                out.max = Some(lit.base10_parse::<i64>()?);
12593                return Ok(());
12594            }
12595            if meta.path.is_ident("min_length") {
12596                let lit: syn::LitInt = meta.value()?.parse()?;
12597                out.min_length = Some(lit.base10_parse::<u32>()?);
12598                return Ok(());
12599            }
12600            if meta.path.is_ident("max_length") {
12601                let lit: syn::LitInt = meta.value()?.parse()?;
12602                out.max_length = Some(lit.base10_parse::<u32>()?);
12603                return Ok(());
12604            }
12605            if meta.path.is_ident("clean") {
12606                let s: LitStr = meta.value()?.parse()?;
12607                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12608                return Ok(());
12609            }
12610            Err(meta.error(
12611                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12612            ))
12613        })?;
12614    }
12615    Ok(out)
12616}
12617
12618fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12619    let Type::Path(TypePath { path, qself: None }) = ty else {
12620        return Err(syn::Error::new(
12621            span,
12622            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12623        ));
12624    };
12625    let last = path
12626        .segments
12627        .last()
12628        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12629
12630    if last.ident == "Option" {
12631        let inner = generic_inner(ty, &last.arguments, "Option")?;
12632        let (kind, nested) = detect_form_field(inner, span)?;
12633        if nested {
12634            return Err(syn::Error::new(
12635                span,
12636                "nested Option in Form fields is not supported",
12637            ));
12638        }
12639        return Ok((kind, true));
12640    }
12641
12642    let kind = match last.ident.to_string().as_str() {
12643        "String" => FormFieldKind::String,
12644        "i16" => FormFieldKind::I16,
12645        "i32" => FormFieldKind::I32,
12646        "i64" => FormFieldKind::I64,
12647        "f32" => FormFieldKind::F32,
12648        "f64" => FormFieldKind::F64,
12649        "bool" => FormFieldKind::Bool,
12650        other => {
12651            return Err(syn::Error::new(
12652                span,
12653                format!(
12654                    "Form field type `{other}` is not supported in v0.8 — use String / \
12655                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12656                ),
12657            ));
12658        }
12659    };
12660    Ok((kind, false))
12661}
12662
12663#[allow(clippy::too_many_lines)]
12664fn render_form_field_parse(
12665    ident: &syn::Ident,
12666    name_lit: &str,
12667    kind: FormFieldKind,
12668    nullable: bool,
12669    attrs: &FormFieldAttrs,
12670) -> TokenStream2 {
12671    let (errs, data_param) = (form_local("__errors"), form_local("data"));
12672    // Pull the raw &str from the payload `Form::parse(data: &HashMap<…>)`.
12673    let lookup = quote! {
12674        let __raw: ::core::option::Option<&::std::string::String> = #data_param.get(#name_lit);
12675    };
12676
12677    let parsed_value = match kind {
12678        // `Option<bool>`: absent or blank is `None`, as for the other types.
12679        FormFieldKind::Bool if nullable => quote! {
12680            let __v: ::core::option::Option<bool> = match __raw {
12681                ::core::option::Option::None => ::core::option::Option::None,
12682                ::core::option::Option::Some(__s) if __s.is_empty() => {
12683                    ::core::option::Option::None
12684                }
12685                ::core::option::Option::Some(__s) => ::core::option::Option::Some(!matches!(
12686                    __s.to_ascii_lowercase().as_str(),
12687                    "false" | "0" | "off" | "no"
12688                )),
12689            };
12690        },
12691        FormFieldKind::Bool => quote! {
12692            let __v: bool = match __raw {
12693                ::core::option::Option::None => false,
12694                ::core::option::Option::Some(__s) => !matches!(
12695                    __s.to_ascii_lowercase().as_str(),
12696                    "" | "false" | "0" | "off" | "no"
12697                ),
12698            };
12699        },
12700        FormFieldKind::String => {
12701            if nullable {
12702                quote! {
12703                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12704                        ::core::option::Option::None => ::core::option::Option::None,
12705                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12706                            ::core::option::Option::None
12707                        }
12708                        ::core::option::Option::Some(__s) => {
12709                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12710                        }
12711                    };
12712                }
12713            } else {
12714                quote! {
12715                    let __v: ::std::string::String = match __raw {
12716                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12717                            ::core::clone::Clone::clone(__s)
12718                        }
12719                        _ => {
12720                            #errs.add(#name_lit, "This field is required.");
12721                            ::std::string::String::new()
12722                        }
12723                    };
12724                }
12725            }
12726        }
12727        FormFieldKind::I16
12728        | FormFieldKind::I32
12729        | FormFieldKind::I64
12730        | FormFieldKind::F32
12731        | FormFieldKind::F64 => {
12732            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12733            let ty_lit = kind.parse_method();
12734            let default_val = match kind {
12735                FormFieldKind::I16 => quote! { 0i16 },
12736                FormFieldKind::I32 => quote! { 0i32 },
12737                FormFieldKind::I64 => quote! { 0i64 },
12738                FormFieldKind::F32 => quote! { 0f32 },
12739                FormFieldKind::F64 => quote! { 0f64 },
12740                _ => quote! { Default::default() },
12741            };
12742            // NaN and inf parse, then pass every min/max compare.
12743            let finite = if matches!(kind, FormFieldKind::F32 | FormFieldKind::F64) {
12744                quote! {
12745                    .and_then(|__n| if __n.is_finite() {
12746                        ::core::result::Result::Ok(__n)
12747                    } else {
12748                        ::core::result::Result::Err(::std::string::String::from("not a finite number"))
12749                    })
12750                }
12751            } else {
12752                quote! {}
12753            };
12754            if nullable {
12755                quote! {
12756                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12757                        ::core::option::Option::None => ::core::option::Option::None,
12758                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12759                            ::core::option::Option::None
12760                        }
12761                        ::core::option::Option::Some(__s) => {
12762                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12763                                ::core::result::Result::Ok(__n) => {
12764                                    ::core::option::Option::Some(__n)
12765                                }
12766                                ::core::result::Result::Err(__e) => {
12767                                    #errs.add(
12768                                        #name_lit,
12769                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12770                                    );
12771                                    ::core::option::Option::None
12772                                }
12773                            }
12774                        }
12775                    };
12776                }
12777            } else {
12778                quote! {
12779                    let __v: #parse_ty = match __raw {
12780                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12781                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12782                                ::core::result::Result::Ok(__n) => __n,
12783                                ::core::result::Result::Err(__e) => {
12784                                    #errs.add(
12785                                        #name_lit,
12786                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12787                                    );
12788                                    #default_val
12789                                }
12790                            }
12791                        }
12792                        _ => {
12793                            #errs.add(#name_lit, "This field is required.");
12794                            #default_val
12795                        }
12796                    };
12797                }
12798            }
12799        }
12800    };
12801
12802    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12803
12804    quote! {
12805        // `mut` so the per-field `clean` hook (#372) can rewrite the
12806        // parsed value in-place when it returns Ok with a normalized
12807        // form (e.g. trim / lowercase).
12808        let mut #ident = {
12809            #lookup
12810            #parsed_value
12811            #validators
12812            __v
12813        };
12814    }
12815}
12816
12817fn render_form_validators(
12818    name_lit: &str,
12819    kind: FormFieldKind,
12820    nullable: bool,
12821    attrs: &FormFieldAttrs,
12822) -> TokenStream2 {
12823    let errs = form_local("__errors");
12824    let mut checks: Vec<TokenStream2> = Vec::new();
12825
12826    let val_ref = if nullable {
12827        quote! { __v.as_ref() }
12828    } else {
12829        quote! { ::core::option::Option::Some(&__v) }
12830    };
12831
12832    let is_string = matches!(kind, FormFieldKind::String);
12833    let is_numeric = matches!(
12834        kind,
12835        FormFieldKind::I16
12836            | FormFieldKind::I32
12837            | FormFieldKind::I64
12838            | FormFieldKind::F32
12839            | FormFieldKind::F64
12840    );
12841
12842    if is_string {
12843        if let Some(min_len) = attrs.min_length {
12844            let min_len_usize = min_len as usize;
12845            checks.push(quote! {
12846                if let ::core::option::Option::Some(__s) = #val_ref {
12847                    if __s.chars().count() < #min_len_usize {
12848                        #errs.add(
12849                            #name_lit,
12850                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12851                        );
12852                    }
12853                }
12854            });
12855        }
12856        if let Some(max_len) = attrs.max_length {
12857            let max_len_usize = max_len as usize;
12858            checks.push(quote! {
12859                if let ::core::option::Option::Some(__s) = #val_ref {
12860                    if __s.chars().count() > #max_len_usize {
12861                        #errs.add(
12862                            #name_lit,
12863                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12864                        );
12865                    }
12866                }
12867            });
12868        }
12869    }
12870
12871    if is_numeric {
12872        if let Some(min) = attrs.min {
12873            checks.push(quote! {
12874                if let ::core::option::Option::Some(__n) = #val_ref {
12875                    if (*__n as f64) < (#min as f64) {
12876                        #errs.add(
12877                            #name_lit,
12878                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12879                        );
12880                    }
12881                }
12882            });
12883        }
12884        if let Some(max) = attrs.max {
12885            checks.push(quote! {
12886                if let ::core::option::Option::Some(__n) = #val_ref {
12887                    if (*__n as f64) > (#max as f64) {
12888                        #errs.add(
12889                            #name_lit,
12890                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12891                        );
12892                    }
12893                }
12894            });
12895        }
12896    }
12897
12898    quote! { #( #checks )* }
12899}
12900
12901// ============================================================
12902//  #[derive(ViewSet)]
12903// ============================================================
12904
12905struct ViewSetAttrs {
12906    model: syn::Path,
12907    fields: Vec<String>,
12908    filter_fields: Vec<String>,
12909    search_fields: Vec<String>,
12910    /// (field_name, desc)
12911    ordering: Vec<(String, bool)>,
12912    page_size: Option<usize>,
12913    read_only: bool,
12914    allow_anonymous: bool,
12915    perms: ViewSetPermsAttrs,
12916    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12917    /// retrieve / create responses through this `#[derive(Serializer)]`
12918    /// type instead of the default field-level projection (requires the
12919    /// `serializer` feature). Tri-dialect.
12920    serializer: Option<syn::Path>,
12921}
12922
12923#[derive(Default)]
12924struct ViewSetPermsAttrs {
12925    list: Vec<String>,
12926    retrieve: Vec<String>,
12927    create: Vec<String>,
12928    update: Vec<String>,
12929    destroy: Vec<String>,
12930}
12931
12932fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12933    let root = rustango_root();
12934    let struct_name = &input.ident;
12935
12936    // Must be a unit struct or an empty named struct.
12937    match &input.data {
12938        Data::Struct(s) => match &s.fields {
12939            Fields::Unit | Fields::Named(_) => {}
12940            Fields::Unnamed(_) => {
12941                return Err(syn::Error::new_spanned(
12942                    struct_name,
12943                    "ViewSet can only be derived on a unit struct or an empty named struct",
12944                ));
12945            }
12946        },
12947        _ => {
12948            return Err(syn::Error::new_spanned(
12949                struct_name,
12950                "ViewSet can only be derived on a struct",
12951            ));
12952        }
12953    }
12954
12955    let attrs = parse_viewset_attrs(input)?;
12956    let model_path = &attrs.model;
12957
12958    let field_lits = attrs.fields.iter().map(|f| f.as_str());
12959
12960    let filter_fields_call = if attrs.filter_fields.is_empty() {
12961        quote!()
12962    } else {
12963        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12964        quote!(.filter_fields(&[ #(#lits),* ]))
12965    };
12966
12967    let search_fields_call = if attrs.search_fields.is_empty() {
12968        quote!()
12969    } else {
12970        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12971        quote!(.search_fields(&[ #(#lits),* ]))
12972    };
12973
12974    let ordering_call = if attrs.ordering.is_empty() {
12975        quote!()
12976    } else {
12977        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12978            let f = f.as_str();
12979            quote!((#f, #desc))
12980        });
12981        quote!(.ordering(&[ #(#pairs),* ]))
12982    };
12983
12984    let page_size_call = if let Some(n) = attrs.page_size {
12985        quote!(.page_size(#n))
12986    } else {
12987        quote!()
12988    };
12989
12990    let read_only_call = if attrs.read_only {
12991        quote!(.read_only())
12992    } else {
12993        quote!()
12994    };
12995    let allow_anonymous_call = if attrs.allow_anonymous {
12996        quote!(.allow_anonymous())
12997    } else {
12998        quote!()
12999    };
13000
13001    // `.serializer::<S>()` — reshape responses through a derived
13002    // serializer. Requires the downstream crate to enable the
13003    // `serializer` feature (the method is gated on it).
13004    let serializer_call = if let Some(ref ser) = attrs.serializer {
13005        quote!(.serializer::<#ser>())
13006    } else {
13007        quote!()
13008    };
13009
13010    let perms = &attrs.perms;
13011    let perms_call = if perms.list.is_empty()
13012        && perms.retrieve.is_empty()
13013        && perms.create.is_empty()
13014        && perms.update.is_empty()
13015        && perms.destroy.is_empty()
13016    {
13017        quote!()
13018    } else {
13019        let list_lits = perms.list.iter().map(|s| s.as_str());
13020        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
13021        let create_lits = perms.create.iter().map(|s| s.as_str());
13022        let update_lits = perms.update.iter().map(|s| s.as_str());
13023        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
13024        quote! {
13025            .permissions(#root::viewset::ViewSetPerms {
13026                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
13027                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
13028                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
13029                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
13030                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
13031            })
13032        }
13033    };
13034
13035    Ok(quote! {
13036        impl #struct_name {
13037            /// Build an `axum::Router` with the six standard REST endpoints
13038            /// for this ViewSet, mounted at `prefix`.
13039            ///
13040            /// Accepts any backend's pool — a `sqlx::PgPool`,
13041            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
13042            /// enum — because they all `Into<Pool>` (#1273). This is why
13043            /// the derive no longer names `PgPool`, which would not
13044            /// exist on a project built without the `postgres` feature.
13045            pub fn router(
13046                prefix: &str,
13047                pool: impl ::core::convert::Into<#root::sql::Pool>,
13048            ) -> #root::__axum::Router {
13049                #root::viewset::ViewSet::for_model(
13050                    <#model_path as #root::core::Model>::SCHEMA,
13051                    &[ #(#field_lits),* ],
13052                )
13053                    #filter_fields_call
13054                    #search_fields_call
13055                    #ordering_call
13056                    #page_size_call
13057                    #perms_call
13058                    #read_only_call
13059                    #allow_anonymous_call
13060                    #serializer_call
13061                    .router_pool(prefix, pool.into())
13062            }
13063        }
13064    })
13065}
13066
13067fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
13068    let mut model: Option<syn::Path> = None;
13069    let mut fields: Option<Vec<String>> = None;
13070    let mut filter_fields: Vec<String> = Vec::new();
13071    let mut search_fields: Vec<String> = Vec::new();
13072    let mut ordering: Vec<(String, bool)> = Vec::new();
13073    let mut page_size: Option<usize> = None;
13074    let mut read_only = false;
13075    let mut allow_anonymous = false;
13076    let mut perms = ViewSetPermsAttrs::default();
13077    let mut serializer: Option<syn::Path> = None;
13078
13079    for attr in &input.attrs {
13080        if !attr.path().is_ident("viewset") {
13081            continue;
13082        }
13083        attr.parse_nested_meta(|meta| {
13084            if meta.path.is_ident("model") {
13085                let path: syn::Path = meta.value()?.parse()?;
13086                model = Some(path);
13087                return Ok(());
13088            }
13089            if meta.path.is_ident("serializer") {
13090                let path: syn::Path = meta.value()?.parse()?;
13091                serializer = Some(path);
13092                return Ok(());
13093            }
13094            if meta.path.is_ident("fields") {
13095                let s: LitStr = meta.value()?.parse()?;
13096                fields = Some(split_field_list(&s.value()));
13097                return Ok(());
13098            }
13099            if meta.path.is_ident("filter_fields") {
13100                let s: LitStr = meta.value()?.parse()?;
13101                filter_fields = split_field_list(&s.value());
13102                return Ok(());
13103            }
13104            if meta.path.is_ident("search_fields") {
13105                let s: LitStr = meta.value()?.parse()?;
13106                search_fields = split_field_list(&s.value());
13107                return Ok(());
13108            }
13109            if meta.path.is_ident("ordering") {
13110                let s: LitStr = meta.value()?.parse()?;
13111                ordering = parse_ordering_list(&s.value());
13112                return Ok(());
13113            }
13114            if meta.path.is_ident("page_size") {
13115                let lit: syn::LitInt = meta.value()?.parse()?;
13116                page_size = Some(lit.base10_parse::<usize>()?);
13117                return Ok(());
13118            }
13119            if meta.path.is_ident("read_only") {
13120                read_only = true;
13121                return Ok(());
13122            }
13123            if meta.path.is_ident("allow_anonymous") {
13124                allow_anonymous = true;
13125                return Ok(());
13126            }
13127            if meta.path.is_ident("permissions") {
13128                meta.parse_nested_meta(|inner| {
13129                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
13130                        let s: LitStr = inner.value()?.parse()?;
13131                        Ok(split_field_list(&s.value()))
13132                    };
13133                    if inner.path.is_ident("list") {
13134                        perms.list = parse_codenames(&inner)?;
13135                    } else if inner.path.is_ident("retrieve") {
13136                        perms.retrieve = parse_codenames(&inner)?;
13137                    } else if inner.path.is_ident("create") {
13138                        perms.create = parse_codenames(&inner)?;
13139                    } else if inner.path.is_ident("update") {
13140                        perms.update = parse_codenames(&inner)?;
13141                    } else if inner.path.is_ident("destroy") {
13142                        perms.destroy = parse_codenames(&inner)?;
13143                    } else {
13144                        return Err(inner.error(
13145                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
13146                        ));
13147                    }
13148                    Ok(())
13149                })?;
13150                return Ok(());
13151            }
13152            Err(meta.error(
13153                "unknown viewset attribute (supported: model, fields, filter_fields, \
13154                 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
13155                 permissions(...))",
13156            ))
13157        })?;
13158    }
13159
13160    let model = model.ok_or_else(|| {
13161        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
13162    })?;
13163    // The field list is the write allow-list, so it is never implied.
13164    let fields = fields.ok_or_else(|| {
13165        syn::Error::new_spanned(
13166            &input.ident,
13167            "`#[viewset(fields = \"id, title\")]` is required: the fields the API renders and writes",
13168        )
13169    })?;
13170
13171    Ok(ViewSetAttrs {
13172        model,
13173        fields,
13174        filter_fields,
13175        search_fields,
13176        ordering,
13177        page_size,
13178        read_only,
13179        allow_anonymous,
13180        perms,
13181        serializer,
13182    })
13183}
13184
13185// ============================================================ #[derive(Serializer)]
13186
13187struct SerializerContainerAttrs {
13188    model: syn::Path,
13189    /// `#[serializer(validate = "fn_name")]` on the struct — the
13190    /// cross-field validation hook (#436). The named inherent method
13191    /// must take `&self` and return
13192    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
13193    /// `validate()` runs every per-field validator first; then calls
13194    /// the cross-field method if declared; aggregates all errors into
13195    /// one `FormErrors`.
13196    cross_validate: Option<syn::Ident>,
13197}
13198
13199#[derive(Default)]
13200struct SerializerFieldAttrs {
13201    read_only: bool,
13202    write_only: bool,
13203    source: Option<String>,
13204    skip: bool,
13205    /// `#[serializer(method = "fn_name")]` — computed field. The macro
13206    /// emits a `from_model` initializer that calls
13207    /// `Self::fn_name(&model)` and stores the return value.
13208    method: Option<String>,
13209    /// `#[serializer(validate = "fn_name")]` — per-field validator
13210    /// callable run by `Self::validate(&self)`. Must return
13211    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
13212    /// the field name.
13213    validate: Option<String>,
13214    /// `#[serializer(nested)]` on a field whose type is another
13215    /// `Serializer` — the macro emits `from_model` initializer that
13216    /// reads the parent via `model.<source>.value()` then calls the
13217    /// child serializer's `from_model(parent)`. When the FK is
13218    /// unloaded the field falls back to `Default::default()` (does
13219    /// NOT panic) so a missing prefetch in prod degrades gracefully.
13220    /// Source field on the model defaults to the field name; override
13221    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
13222    /// panic-on-unloaded behavior for tests.
13223    nested: bool,
13224    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
13225    /// strict behavior: panic when the FK isn't loaded. Useful in
13226    /// test code where forgetting select_related must trip a hard
13227    /// failure rather than render a blank nested object.
13228    nested_strict: bool,
13229    /// `#[serializer(many = TagSerializer)]` — declare the field as
13230    /// a list of nested serializers. Field type must be `Vec<S>`
13231    /// where `S` is the inner serializer. The macro initializes the
13232    /// field to `Vec::new()` in `from_model` and emits a typed
13233    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13234    /// maps each model row through `S::from_model`. Auto-load isn't
13235    /// possible (the M2M / one-to-many accessor is async); callers
13236    /// fetch the children + call the setter post-from_model.
13237    many: Option<syn::Type>,
13238    /// `#[serializer(slug = "name")]` — render a relation as one of
13239    /// its fields.
13240    /// Source field on the model must be a `ForeignKey<T>`; the
13241    /// macro emits `from_model` glue that walks
13242    /// `model.<source>.value()?.<slug>` and clones it. Field type on
13243    /// the serializer is typically `String` (whatever type the slug
13244    /// column has). When the FK is unloaded the field falls back to
13245    /// `Default::default()`, same graceful-degrade contract as
13246    /// `nested`. Source defaults to the field name; override with
13247    /// `source = "..."`. v0.44.
13248    slug: Option<String>,
13249    /// `#[serializer(max_length = N)]` — caps
13250    /// the character count of a string field on write. Overrides the
13251    /// model's `max_length`; when absent the model value is inherited.
13252    max_length: Option<u64>,
13253    /// `#[serializer(min_length = N)]` — minimum character count.
13254    /// Serializer-only (the model has no `min_length` column).
13255    min_length: Option<u64>,
13256    /// `#[serializer(min = N)]` — inclusive
13257    /// integer lower bound; overrides the model's `min` when given.
13258    min: Option<i64>,
13259    /// `#[serializer(max = N)]` — inclusive
13260    /// integer upper bound; overrides the model's `max` when given.
13261    max: Option<i64>,
13262}
13263
13264fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13265    let mut model: Option<syn::Path> = None;
13266    let mut cross_validate: Option<syn::Ident> = None;
13267    for attr in &input.attrs {
13268        if !attr.path().is_ident("serializer") {
13269            continue;
13270        }
13271        attr.parse_nested_meta(|meta| {
13272            if meta.path.is_ident("model") {
13273                let _eq: syn::Token![=] = meta.input.parse()?;
13274                model = Some(meta.input.parse()?);
13275                return Ok(());
13276            }
13277            if meta.path.is_ident("validate") {
13278                // #436 — container-level `validate = "fn_name"` for
13279                // cross-field validation. Field-level
13280                // `#[serializer(validate = "...")]` on a field is
13281                // parsed separately in `parse_serializer_field_attrs`.
13282                let s: LitStr = meta.value()?.parse()?;
13283                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13284                return Ok(());
13285            }
13286            Err(meta.error(
13287                "unknown serializer container attribute \
13288                 (supported: `model`, `validate`)",
13289            ))
13290        })?;
13291    }
13292    let model = model.ok_or_else(|| {
13293        syn::Error::new_spanned(
13294            &input.ident,
13295            "`#[serializer(model = SomeModel)]` is required",
13296        )
13297    })?;
13298    Ok(SerializerContainerAttrs {
13299        model,
13300        cross_validate,
13301    })
13302}
13303
13304fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13305    let mut out = SerializerFieldAttrs::default();
13306    for attr in &field.attrs {
13307        if !attr.path().is_ident("serializer") {
13308            continue;
13309        }
13310        attr.parse_nested_meta(|meta| {
13311            if meta.path.is_ident("read_only") {
13312                out.read_only = true;
13313                return Ok(());
13314            }
13315            if meta.path.is_ident("write_only") {
13316                out.write_only = true;
13317                return Ok(());
13318            }
13319            if meta.path.is_ident("skip") {
13320                out.skip = true;
13321                return Ok(());
13322            }
13323            if meta.path.is_ident("source") {
13324                let s: LitStr = meta.value()?.parse()?;
13325                out.source = Some(s.value());
13326                return Ok(());
13327            }
13328            if meta.path.is_ident("method") {
13329                let s: LitStr = meta.value()?.parse()?;
13330                out.method = Some(s.value());
13331                return Ok(());
13332            }
13333            if meta.path.is_ident("validate") {
13334                let s: LitStr = meta.value()?.parse()?;
13335                out.validate = Some(s.value());
13336                return Ok(());
13337            }
13338            if meta.path.is_ident("many") {
13339                let _eq: syn::Token![=] = meta.input.parse()?;
13340                out.many = Some(meta.input.parse()?);
13341                return Ok(());
13342            }
13343            if meta.path.is_ident("nested") {
13344                out.nested = true;
13345                // Optional strict flag inside parentheses:
13346                //   #[serializer(nested(strict))]
13347                if meta.input.peek(syn::token::Paren) {
13348                    meta.parse_nested_meta(|inner| {
13349                        if inner.path.is_ident("strict") {
13350                            out.nested_strict = true;
13351                            return Ok(());
13352                        }
13353                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13354                    })?;
13355                }
13356                return Ok(());
13357            }
13358            if meta.path.is_ident("slug") {
13359                let s: LitStr = meta.value()?.parse()?;
13360                out.slug = Some(s.value());
13361                return Ok(());
13362            }
13363            if meta.path.is_ident("max_length") {
13364                let lit: syn::LitInt = meta.value()?.parse()?;
13365                out.max_length = Some(lit.base10_parse::<u64>()?);
13366                return Ok(());
13367            }
13368            if meta.path.is_ident("min_length") {
13369                let lit: syn::LitInt = meta.value()?.parse()?;
13370                out.min_length = Some(lit.base10_parse::<u64>()?);
13371                return Ok(());
13372            }
13373            if meta.path.is_ident("min") {
13374                let lit: syn::LitInt = meta.value()?.parse()?;
13375                out.min = Some(lit.base10_parse::<i64>()?);
13376                return Ok(());
13377            }
13378            if meta.path.is_ident("max") {
13379                let lit: syn::LitInt = meta.value()?.parse()?;
13380                out.max = Some(lit.base10_parse::<i64>()?);
13381                return Ok(());
13382            }
13383            Err(meta.error(
13384                "unknown serializer field attribute (supported: \
13385                 `read_only`, `write_only`, `source`, `skip`, `method`, \
13386                 `validate`, `nested`, `many`, `slug`, `max_length`, \
13387                 `min_length`, `min`, `max`)",
13388            ))
13389        })?;
13390    }
13391    // Validate: read_only + write_only is nonsensical
13392    if out.read_only && out.write_only {
13393        return Err(syn::Error::new_spanned(
13394            field,
13395            "a field cannot be both `read_only` and `write_only`",
13396        ));
13397    }
13398    if out.method.is_some() && out.source.is_some() {
13399        return Err(syn::Error::new_spanned(
13400            field,
13401            "`method` and `source` are mutually exclusive — `method` computes \
13402             the value from a method, `source` reads it from a different model field",
13403        ));
13404    }
13405    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13406        return Err(syn::Error::new_spanned(
13407            field,
13408            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13409             — pick one strategy for populating the field",
13410        ));
13411    }
13412    Ok(out)
13413}
13414
13415fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13416    let root = rustango_root();
13417    let struct_name = &input.ident;
13418    let struct_name_lit = struct_name.to_string();
13419
13420    let Data::Struct(data) = &input.data else {
13421        return Err(syn::Error::new_spanned(
13422            struct_name,
13423            "Serializer can only be derived on structs",
13424        ));
13425    };
13426    let Fields::Named(named) = &data.fields else {
13427        return Err(syn::Error::new_spanned(
13428            struct_name,
13429            "Serializer requires a struct with named fields",
13430        ));
13431    };
13432
13433    let container = parse_serializer_container_attrs(input)?;
13434    let model_path = &container.model;
13435
13436    // Classify each field. `ty` is only consumed by the
13437    // `#[cfg(feature = "openapi")]` block below, but we always
13438    // capture it to keep the field-info build a single pass.
13439    #[allow(dead_code)]
13440    struct FieldInfo {
13441        ident: syn::Ident,
13442        ty: syn::Type,
13443        attrs: SerializerFieldAttrs,
13444    }
13445    let mut fields_info: Vec<FieldInfo> = Vec::new();
13446    for field in &named.named {
13447        let ident = field.ident.clone().expect("named field has ident");
13448        let attrs = parse_serializer_field_attrs(field)?;
13449        fields_info.push(FieldInfo {
13450            ident,
13451            ty: field.ty.clone(),
13452            attrs,
13453        });
13454    }
13455
13456    // Generate from_model body: struct literal with each field assigned.
13457    let from_model_fields = fields_info.iter().map(|fi| {
13458        let ident = &fi.ident;
13459        if let Some(_inner) = &fi.attrs.many {
13460            // Many — collection field. Initialize empty; caller
13461            // populates via the macro-emitted set_<field> helper
13462            // after fetching the M2M children.
13463            quote! { #ident: ::std::vec::Vec::new() }
13464        } else if let Some(method) = &fi.attrs.method {
13465            // SerializerMethodField: call Self::<method>(&model) to
13466            // compute the value. Method signature must be
13467            // `fn <method>(model: &T) -> <field type>`.
13468            let method_ident = syn::Ident::new(method, ident.span());
13469            quote! { #ident: Self::#method_ident(model) }
13470        } else if let Some(slug_field) = &fi.attrs.slug {
13471            // v0.44 — SlugRelatedField. Source defaults to the field
13472            // name on this struct; override via `source = "..."`. The
13473            // source field on the model is expected to be a
13474            // `ForeignKey<T>`; the slug field on the parent is named
13475            // by the attribute value. When the FK is unloaded the
13476            // field falls back to `Default::default()` — same
13477            // graceful-degrade contract as `nested`; a NULL
13478            // `Option<ForeignKey>` renders `None`.
13479            let src_name = fi
13480                .attrs
13481                .source
13482                .as_deref()
13483                .unwrap_or(&ident_name(&fi.ident))
13484                .to_owned();
13485            let src_ident = ident_from_name(&src_name, ident.span());
13486            let slug_ident = syn::Ident::new(slug_field, ident.span());
13487            quote! {
13488                #ident: #root::serializer::LoadedRelation::render(
13489                    &model.#src_ident,
13490                    |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
13491                    ::core::default::Default::default,
13492                )
13493            }
13494        } else if fi.attrs.nested {
13495            // Nested serializer. Source defaults to the field name on
13496            // this struct; override via `source = "..."`. The source
13497            // field on the model is a `ForeignKey<T>` or
13498            // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
13499            //
13500            // Behavior matrix (tweakable per-field):
13501            //   * FK loaded   → nested object materializes via
13502            //                   ChildSerializer::from_model(parent).
13503            //   * FK unloaded → fall back to ChildSerializer::default()
13504            //                   (so prod doesn't crash on a missing
13505            //                   prefetch — just renders a blank nested
13506            //                   object). Add `#[serializer(nested(
13507            //                   strict))]` to keep the v0.18.1
13508            //                   panic-on-unloaded behavior for tests
13509            //                   that want hard guardrails.
13510            //   * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
13511            //                   field (JSON null), strict or not.
13512            let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13513            let src_ident = ident_from_name(&src_name, ident.span());
13514            let unloaded = if fi.attrs.nested_strict {
13515                let panic_msg = format!(
13516                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13517                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13518                );
13519                quote! { || ::core::panic!("{}", #panic_msg) }
13520            } else {
13521                quote! { ::core::default::Default::default }
13522            };
13523            quote! {
13524                #ident: #root::serializer::LoadedRelation::render(
13525                    &model.#src_ident,
13526                    |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
13527                    #unloaded,
13528                )
13529            }
13530        } else if fi.attrs.write_only || fi.attrs.skip {
13531            // Not read from model — use default
13532            quote! { #ident: ::core::default::Default::default() }
13533        } else if let Some(src) = &fi.attrs.source {
13534            let src_ident = ident_from_name(src, ident.span());
13535            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13536        } else {
13537            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13538        }
13539    });
13540
13541    // is_writable predicate (also used by writable_lits /
13542    // writable_source_lits below).
13543    let is_writable = |fi: &&FieldInfo| {
13544        !fi.attrs.read_only
13545            && !fi.attrs.skip
13546            && fi.attrs.method.is_none()
13547            && !fi.attrs.nested
13548            && fi.attrs.many.is_none()
13549            && fi.attrs.slug.is_none()
13550    };
13551
13552    // Declarative field constraints: one block
13553    // per writable field, run inside `validate()`. Each resolves its
13554    // bounds as the serializer attr when given (`#[serializer(max_length
13555    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13556    // `max` / `choices`), then dispatches on the field's JSON value via
13557    // `forms::validators::check_value` (string → length/choices, integer
13558    // → min/max).
13559    let opt_usize = |v: Option<u64>| match v {
13560        Some(n) => {
13561            let n = n as usize;
13562            quote!(::core::option::Option::Some(#n))
13563        }
13564        None => quote!(::core::option::Option::None),
13565    };
13566    let opt_i64 = |v: Option<i64>| match v {
13567        Some(n) => quote!(::core::option::Option::Some(#n)),
13568        None => quote!(::core::option::Option::None),
13569    };
13570    let constraint_blocks: Vec<_> = fields_info
13571        .iter()
13572        .filter(is_writable)
13573        .map(|fi| {
13574            let ident = &fi.ident;
13575            let fname = ident_name(ident);
13576            let mname = fi
13577                .attrs
13578                .source
13579                .clone()
13580                .unwrap_or_else(|| ident_name(&fi.ident));
13581            let attr_max_len = opt_usize(fi.attrs.max_length);
13582            let attr_min_len = opt_usize(fi.attrs.min_length);
13583            let attr_min = opt_i64(fi.attrs.min);
13584            let attr_max = opt_i64(fi.attrs.max);
13585            // A bound the field's type cannot take is a compile error, not a no-op.
13586            let ty = &fi.ty;
13587            let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
13588                .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
13589            let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
13590                .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
13591            quote! {
13592                #len_check
13593                #num_check
13594                if __sent(#fname) {
13595                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13596                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13597                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13598                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13599                    let __min: ::core::option::Option<i64> =
13600                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13601                    let __max: ::core::option::Option<i64> =
13602                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13603                    let __choices = __sf.and_then(|__f| __f.choices);
13604                    let __v = #root::__serde_json::to_value(&self.#ident)
13605                        .unwrap_or(#root::__serde_json::Value::Null);
13606                    #root::forms::validators::check_value(
13607                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13608                        &mut __errors,
13609                    );
13610                }
13611            }
13612        })
13613        .collect();
13614    let has_constraints = !constraint_blocks.is_empty();
13615
13616    // Per-field validators. Emit a
13617    // `validate(&self)` method that runs each user-defined validator
13618    // and aggregates errors into `FormErrors`.
13619    let validator_calls: Vec<_> = fields_info
13620        .iter()
13621        .filter_map(|fi| {
13622            let ident = &fi.ident;
13623            let name_lit = ident_name(ident);
13624            let method = fi.attrs.validate.as_ref()?;
13625            let method_ident = syn::Ident::new(method, ident.span());
13626            Some(quote! {
13627                if __sent(#name_lit) {
13628                    if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13629                        __errors.add(#name_lit.to_owned(), __e);
13630                    }
13631                }
13632            })
13633        })
13634        .collect();
13635    // #436 — cross-field `validate(&self)`. If the
13636    // container declared `#[serializer(validate = "fn_name")]`,
13637    // the macro-generated `validate(&self)` runs every per-field
13638    // validator first, then calls the user's cross-field method,
13639    // merging its `FormErrors` into the per-field errors. Either
13640    // alone is enough to emit the wrapper.
13641    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13642        quote! {
13643            // Merge cross-field errors into the per-field bucket so
13644            // a single .validate() call surfaces both layers.
13645            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13646                __errors.merge(__cross);
13647            }
13648        }
13649    });
13650    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13651    // Anything to run? Declarative constraints (for any writable field) +
13652    // per-field validators + cross-field hook.
13653    let has_run_validations = has_validators || has_constraints;
13654    // Shared body: declarative field constraints first (length / range /
13655    // choices, serializer-attr-or-model), then per-field validators, then
13656    // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13657    let checks = quote! {
13658        let mut __errors = #root::forms::FormErrors::default();
13659        #( #constraint_blocks )*
13660        #( #validator_calls )*
13661        #cross_validate_call
13662        if __errors.is_empty() {
13663            ::core::result::Result::Ok(())
13664        } else {
13665            ::core::result::Result::Err(__errors)
13666        }
13667    };
13668    let validate_body = quote! {
13669        let __sent = |_: &str| true;
13670        #checks
13671    };
13672    // PATCH: only the keys the body carries.
13673    let validate_patch_body = quote! {
13674        let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13675        #checks
13676    };
13677    // Inherent `validate(&self)` — kept for back-compat with direct
13678    // `serializer.validate()` calls that don't import `ModelSerializer`.
13679    // Emitted only when the serializer declares per-field/cross-field
13680    // validators (unchanged rule) so it never collides with a
13681    // hand-written inherent `validate`.
13682    let validate_method = if has_validators {
13683        quote! {
13684            impl #struct_name {
13685                /// Run the declarative field constraints, every
13686                /// `#[serializer(validate = "...")]` per-field validator,
13687                /// and (when declared) the container-level cross-field
13688                /// validator. Aggregates errors into `FormErrors` keyed by
13689                /// the field name. Returns `Ok(())` when all pass.
13690                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13691                    #validate_body
13692                }
13693            }
13694        }
13695    } else {
13696        quote! {}
13697    };
13698    // Trait-method override so the type-erased ViewSet write path can
13699    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13700    // there's anything to run (declarative constraints inherited from the
13701    // model count); otherwise the trait's default no-op is used.
13702    let trait_validate_override = if has_run_validations {
13703        quote! {
13704            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13705                #validate_body
13706            }
13707        }
13708    } else {
13709        quote! {}
13710    };
13711
13712    // For every `#[serializer(many = S)]` field, emit a
13713    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13714    // helper that maps the parents through `S::from_model`.
13715    let many_setters: Vec<_> = fields_info
13716        .iter()
13717        .filter_map(|fi| {
13718            let many_ty = fi.attrs.many.as_ref()?;
13719            let ident = &fi.ident;
13720            let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13721            Some(quote! {
13722                /// Populate this `many` field by mapping each parent model
13723                /// through the inner serializer's `from_model`. Call after
13724                /// fetching the M2M / one-to-many children since
13725                /// `from_model` itself can't await an SQL query.
13726                pub fn #setter(
13727                    &mut self,
13728                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13729                ) -> &mut Self {
13730                    self.#ident = models.iter()
13731                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13732                        .collect();
13733                    self
13734                }
13735            })
13736        })
13737        .collect();
13738    let many_setters_impl = if many_setters.is_empty() {
13739        quote! {}
13740    } else {
13741        quote! {
13742            impl #struct_name {
13743                #( #many_setters )*
13744            }
13745        }
13746    };
13747
13748    // Generate custom Serialize: skip write_only fields
13749    let output_fields: Vec<_> = fields_info
13750        .iter()
13751        .filter(|fi| !fi.attrs.write_only)
13752        .collect();
13753    let output_field_count = output_fields.len();
13754    let serialize_fields = output_fields.iter().map(|fi| {
13755        let ident = &fi.ident;
13756        let name_lit = ident_name(ident);
13757        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13758    });
13759
13760    // writable_fields: normal + write_only.
13761    // Exclude:
13762    //   - `read_only` — server-computed.
13763    //   - `skip` — caller sets manually post-from_model.
13764    //   - `method` — computed from a Self::fn(&model) call; accepting
13765    //     it on write is meaningless.
13766    //   - `nested` / `many` — populated from related-model data, not
13767    //     from a field on the wire body.
13768    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13769    // `many` in `writable_fields()`, which made the ViewSet write
13770    // path accept those fields from the JSON body and try to bind
13771    // them to the SQL UPDATE — a silent no-op at best, a type
13772    // mismatch at worst.
13773    // (`is_writable` is defined above, near the constraint codegen.)
13774    let writable_lits: Vec<_> = fields_info
13775        .iter()
13776        .filter(is_writable)
13777        .map(|fi| ident_name(&fi.ident))
13778        .collect();
13779
13780    // `writable_source_fields`: the MODEL field names of writable
13781    // serializer fields (source-resolved). The ViewSet write path skips
13782    // every model column NOT in this set, so `read_only` / `method` /
13783    // computed fields a client posts are ignored instead of written.
13784    let writable_source_lits: Vec<String> = fields_info
13785        .iter()
13786        .filter(is_writable)
13787        .map(|fi| {
13788            fi.attrs
13789                .source
13790                .clone()
13791                .unwrap_or_else(|| ident_name(&fi.ident))
13792        })
13793        .collect();
13794
13795    // `readable_source_fields`: model columns the JSON output copies
13796    // straight from the model, so `?ordering=` cannot sort on a column
13797    // the API never shows (#1845).
13798    let readable_source_lits: Vec<String> = fields_info
13799        .iter()
13800        .filter(|fi| {
13801            !fi.attrs.write_only
13802                && !fi.attrs.skip
13803                && fi.attrs.method.is_none()
13804                && !fi.attrs.nested
13805                && fi.attrs.many.is_none()
13806                && fi.attrs.slug.is_none()
13807        })
13808        .map(|fi| {
13809            fi.attrs
13810                .source
13811                .clone()
13812                .unwrap_or_else(|| ident_name(&fi.ident))
13813        })
13814        .collect();
13815
13816    // `from_writable_json`: build a partial instance for input
13817    // validation. Writable fields are parsed from the JSON body (keyed
13818    // by serializer field name); every other field defaults. Per-field
13819    // type errors collect into `FormErrors` keyed by the field name.
13820    // Construction is field-by-field (not `Self::default()`) so it works
13821    // for serializers regardless of a struct-level `Default`.
13822    let from_writable_json_inits: Vec<_> = fields_info
13823        .iter()
13824        .map(|fi| {
13825            let ident = &fi.ident;
13826            let fname = ident_name(ident);
13827            let ty = &fi.ty;
13828            if is_writable(&fi) {
13829                quote! {
13830                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13831                        ::core::option::Option::Some(__v) => {
13832                            match #root::__serde_json::from_value::<#ty>(
13833                                ::core::clone::Clone::clone(__v),
13834                            ) {
13835                                ::core::result::Result::Ok(__x) => __x,
13836                                ::core::result::Result::Err(__e) => {
13837                                    __errors.add(#fname.to_owned(), __e.to_string());
13838                                    ::core::default::Default::default()
13839                                }
13840                            }
13841                        }
13842                        ::core::option::Option::None => ::core::default::Default::default(),
13843                    }
13844                }
13845            } else {
13846                quote! { #ident: ::core::default::Default::default() }
13847            }
13848        })
13849        .collect();
13850    // `validate_patch`: overlay each sent writable field on the stored row.
13851    let patch_overlays: Vec<_> = fields_info
13852        .iter()
13853        .filter(is_writable)
13854        .map(|fi| {
13855            let ident = &fi.ident;
13856            let fname = ident_name(ident);
13857            let ty = &fi.ty;
13858            quote! {
13859                if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
13860                    match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
13861                        ::core::result::Result::Ok(__x) => __merged.#ident = __x,
13862                        ::core::result::Result::Err(__e) => {
13863                            __errors.add(#fname.to_owned(), __e.to_string());
13864                        }
13865                    }
13866                }
13867            }
13868        })
13869        .collect();
13870    let patch_validate_call = if has_run_validations {
13871        quote! { __merged.__rustango_validate_patch(__obj) }
13872    } else {
13873        quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
13874    };
13875    let patch_validate_helper = if has_run_validations {
13876        quote! {
13877            impl #struct_name {
13878                #[doc(hidden)]
13879                fn __rustango_validate_patch(
13880                    &self,
13881                    __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
13882                ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13883                    #validate_patch_body
13884                }
13885            }
13886        }
13887    } else {
13888        quote! {}
13889    };
13890
13891    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13892    // Only includes fields shown in JSON output (skips write_only). For each
13893    // `Option<T>` field, omit from `required` and add `.nullable()`.
13894    let openapi_impl = {
13895        #[cfg(feature = "openapi")]
13896        {
13897            let property_calls = output_fields.iter().map(|fi| {
13898                let ident = &fi.ident;
13899                let name_lit = ident_name(ident);
13900                let ty = &fi.ty;
13901                let nullable_call = if is_option(ty) {
13902                    quote! { .nullable() }
13903                } else {
13904                    quote! {}
13905                };
13906                quote! {
13907                    .property(
13908                        #name_lit,
13909                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13910                            #nullable_call,
13911                    )
13912                }
13913            });
13914            let required_lits: Vec<_> = output_fields
13915                .iter()
13916                .filter(|fi| !is_option(&fi.ty))
13917                .map(|fi| ident_name(&fi.ident))
13918                .collect();
13919            quote! {
13920                impl #root::openapi::OpenApiSchema for #struct_name {
13921                    fn openapi_schema() -> #root::openapi::Schema {
13922                        #root::openapi::Schema::object()
13923                            #( #property_calls )*
13924                            .required([ #( #required_lits ),* ])
13925                    }
13926                }
13927            }
13928        }
13929        #[cfg(not(feature = "openapi"))]
13930        {
13931            quote! {}
13932        }
13933    };
13934
13935    Ok(quote! {
13936        impl #root::serializer::ModelSerializer for #struct_name {
13937            type Model = #model_path;
13938
13939            fn from_model(model: &Self::Model) -> Self {
13940                Self {
13941                    #( #from_model_fields ),*
13942                }
13943            }
13944
13945            fn writable_fields() -> &'static [&'static str] {
13946                &[ #( #writable_lits ),* ]
13947            }
13948
13949            fn writable_source_fields() -> &'static [&'static str] {
13950                &[ #( #writable_source_lits ),* ]
13951            }
13952
13953            fn readable_source_fields() -> &'static [&'static str] {
13954                &[ #( #readable_source_lits ),* ]
13955            }
13956
13957            fn from_writable_json(
13958                __body: &#root::__serde_json::Value,
13959            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13960                let mut __errors = #root::forms::FormErrors::default();
13961                let __obj = __body.as_object();
13962                let __out = Self {
13963                    #( #from_writable_json_inits ),*
13964                };
13965                if __errors.is_empty() {
13966                    ::core::result::Result::Ok(__out)
13967                } else {
13968                    ::core::result::Result::Err(__errors)
13969                }
13970            }
13971
13972            fn validate_patch(
13973                __stored: &Self::Model,
13974                __body: &#root::__serde_json::Value,
13975            ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13976                let mut __errors = #root::forms::FormErrors::default();
13977                let __obj = __body.as_object();
13978                #[allow(unused_mut)]
13979                let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
13980                #( #patch_overlays )*
13981                if !__errors.is_empty() {
13982                    return ::core::result::Result::Err(__errors);
13983                }
13984                #patch_validate_call
13985            }
13986
13987            #trait_validate_override
13988        }
13989
13990        #patch_validate_helper
13991
13992        impl #root::__serde::Serialize for #struct_name {
13993            fn serialize<S>(&self, serializer: S)
13994                -> ::core::result::Result<S::Ok, S::Error>
13995            where
13996                S: #root::__serde::Serializer,
13997            {
13998                use #root::__serde::ser::SerializeStruct;
13999                let mut __state = serializer.serialize_struct(
14000                    #struct_name_lit,
14001                    #output_field_count,
14002                )?;
14003                #( #serialize_fields )*
14004                __state.end()
14005            }
14006        }
14007
14008        #openapi_impl
14009
14010        #validate_method
14011
14012        #many_setters_impl
14013    })
14014}
14015
14016/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
14017/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
14018/// when the feature is off.
14019#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
14020fn is_option(ty: &syn::Type) -> bool {
14021    if let syn::Type::Path(p) = ty {
14022        if let Some(last) = p.path.segments.last() {
14023            return last.ident == "Option";
14024        }
14025    }
14026    false
14027}
14028
14029#[cfg(test)]
14030mod main_attr_tests {
14031    use super::*;
14032
14033    fn expand(args: &str) -> String {
14034        let body = quote! { async fn main() { run().await } };
14035        expand_main(args.parse().unwrap(), body)
14036            .expect("expansion failed")
14037            .to_string()
14038    }
14039
14040    /// Asserts on the expansion rather than on `parse_logging`,
14041    /// because the defect was a subscriber reaching the output, not
14042    /// an argument being misread (#1465).
14043    #[test]
14044    fn logging_false_emits_no_subscriber() {
14045        let out = expand("logging = false");
14046        assert!(
14047            !out.contains("logging :: Setup"),
14048            "`logging = false` must not install a subscriber:\n{out}",
14049        );
14050        assert!(
14051            out.contains("block_on"),
14052            "the runtime must still be built:\n{out}",
14053        );
14054    }
14055
14056    #[test]
14057    fn default_still_installs_a_subscriber() {
14058        // The control. Without it, an `expand_main` that dropped the
14059        // prologue unconditionally would pass the test above while
14060        // leaving every default project with no logging at all.
14061        let out = expand("");
14062        assert!(
14063            out.contains("logging :: Setup"),
14064            "the default entrypoint must still install one:\n{out}",
14065        );
14066        // `./app serve` or `./app --port 8080` must keep logging to stdout:
14067        // only `Cli`, which knows its verbs, moves logs to stderr.
14068        assert!(
14069            !out.contains("args ()"),
14070            "the entrypoint must not route logs by argv:\n{out}",
14071        );
14072    }
14073
14074    #[test]
14075    fn logging_false_composes_with_flavor() {
14076        let out = expand("flavor = \"current_thread\", logging = false");
14077        assert!(!out.contains("logging :: Setup"), "{out}");
14078        assert!(out.contains("new_current_thread"), "{out}");
14079    }
14080
14081    #[test]
14082    fn logging_true_is_the_default_shape() {
14083        // `logging = true` is accepted and means the default, so the
14084        // match must not fire on the substring `logging`.
14085        assert!(parse_logging(&"logging = true".parse().unwrap()));
14086        assert!(!parse_logging(&"logging=false".parse().unwrap()));
14087    }
14088}
14089
14090#[cfg(test)]
14091mod main_arg_warning_tests {
14092    use super::*;
14093
14094    fn expand(args: &str) -> String {
14095        let body = quote! { async fn main() { run().await } };
14096        expand_main(args.parse().unwrap(), body)
14097            .expect("expansion failed")
14098            .to_string()
14099    }
14100
14101    /// A typo or unknown key used to be dropped without a word.
14102    #[test]
14103    fn unknown_or_misspelt_args_warn() {
14104        for args in ["logging = flase", "multi_thread", "workers = 4"] {
14105            let out = expand(args);
14106            assert!(out.contains("unknown argument"), "{args}:\n{out}");
14107        }
14108    }
14109
14110    #[test]
14111    fn supported_args_do_not_warn() {
14112        for args in [
14113            "",
14114            r#"flavor = "current_thread""#,
14115            r#"flavor = "multi_thread", logging = false"#,
14116            "logging = true",
14117            "worker_threads = 4, start_paused = true",
14118            r#"crate = "tokio", unhandled_panic = "shutdown_runtime""#,
14119            r#"flavor = "curent_thread""#,
14120        ] {
14121            assert!(!expand(args).contains("RUSTANGO_WARNING"), "{args}");
14122        }
14123    }
14124}
14125
14126#[cfg(test)]
14127mod embed_migrations_tests {
14128    use super::*;
14129
14130    fn expand(path: &str) -> String {
14131        expand_embed_migrations(quote! { #path })
14132            .expect("expansion failed")
14133            .to_string()
14134    }
14135
14136    #[test]
14137    fn missing_dir_warns() {
14138        let out = expand("./no_such_migrations_dir");
14139        assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
14140        assert!(out.contains("no_such_migrations_dir"), "{out}");
14141    }
14142
14143    #[test]
14144    fn existing_dir_does_not_warn() {
14145        let out = expand("./src");
14146        assert!(!out.contains("deprecated"), "{out}");
14147    }
14148}
14149
14150#[cfg(test)]
14151mod field_attr_tests {
14152    use super::*;
14153
14154    fn error_for(field: syn::Field) -> Option<String> {
14155        let attrs = parse_field_attrs(&field).unwrap();
14156        let kind = detect_type(&field.ty).unwrap().kind;
14157        check_bound_compatibility(&field, &attrs, kind)
14158            .err()
14159            .map(|e| e.to_string())
14160    }
14161
14162    /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
14163    #[test]
14164    fn type_specific_attrs_reject_other_types() {
14165        let f: syn::FieldsNamed = syn::parse_quote!({
14166            #[rustango(citext)] a: i64,
14167            #[rustango(vector(dims = 3))] b: String,
14168            #[rustango(geometry(srid = 4326))] c: String,
14169            #[rustango(citext)] d: String,
14170        });
14171        let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
14172        assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
14173        assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
14174        assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
14175        assert_eq!(errs[3], None);
14176    }
14177
14178    /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
14179    #[test]
14180    fn manager_ext_is_refused() {
14181        let input: DeriveInput = syn::parse_quote! {
14182            #[rustango(table = "p", manager(ext = "PostManagerExt"))]
14183            struct Post { id: i64 }
14184        };
14185        let err = parse_container_attrs(&input).err().expect("refused");
14186        assert!(err.to_string().contains("#2132"), "{err}");
14187    }
14188
14189    /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
14190    #[test]
14191    fn set_null_needs_an_optional_foreign_key() {
14192        let input: DeriveInput = syn::parse_quote! {
14193            #[rustango(table = "c")]
14194            struct Child {
14195                #[rustango(primary_key)] id: i64,
14196                #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
14197            }
14198        };
14199        let err = expand(&input).err().expect("refused");
14200        assert!(err.to_string().contains("set_null"), "{err}");
14201        let input: DeriveInput = syn::parse_quote! {
14202            #[rustango(table = "c")]
14203            struct Child {
14204                #[rustango(primary_key)] id: i64,
14205                #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
14206            }
14207        };
14208        assert!(expand(&input).is_ok());
14209    }
14210
14211    /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
14212    #[test]
14213    fn set_null_needs_a_nullable_fk_column() {
14214        let input: DeriveInput = syn::parse_quote! {
14215            #[rustango(table = "c")]
14216            struct Child {
14217                #[rustango(primary_key)] id: i64,
14218                #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
14219                #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
14220            }
14221        };
14222        let err = expand(&input).err().expect("refused");
14223        assert!(err.to_string().contains("set_null"), "{err}");
14224        let input: DeriveInput = syn::parse_quote! {
14225            #[rustango(table = "c")]
14226            struct Child {
14227                #[rustango(primary_key)] id: i64,
14228                #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
14229                #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
14230                #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
14231            }
14232        };
14233        assert!(expand(&input).is_ok());
14234    }
14235
14236    /// An index or `get_latest_by` naming no column warns; it does not fail.
14237    #[test]
14238    fn unknown_index_columns_warn() {
14239        let input: DeriveInput = syn::parse_quote! {
14240            #[rustango(
14241                table = "w",
14242                unique_together = "a, nope",
14243                index_together = "a, b_typo",
14244                index("missing"),
14245                get_latest_by = "-when_typo"
14246            )]
14247            struct W { #[rustango(primary_key)] id: i64, a: i64, b: i64 }
14248        };
14249        let out = expand(&input).expect("still compiles").to_string();
14250        for name in ["nope", "b_typo", "missing", "when_typo"] {
14251            assert!(out.contains(&format!("`{name}`")), "{name}:\n{out}");
14252        }
14253    }
14254
14255    /// Real columns, field names for `get_latest_by` and expression
14256    /// indexes emit nothing new.
14257    #[test]
14258    fn known_index_columns_do_not_warn() {
14259        let input: DeriveInput = syn::parse_quote! {
14260            #[rustango(
14261                table = "w",
14262                unique_together = "a, b_col",
14263                index("lower(a)"),
14264                get_latest_by = "-b"
14265            )]
14266            struct W {
14267                #[rustango(primary_key)] id: i64,
14268                a: i64,
14269                #[rustango(column = "b_col")] b: i64,
14270            }
14271        };
14272        let out = expand(&input).expect("compiles").to_string();
14273        assert!(!out.contains("RUSTANGO_WARNING"), "{out}");
14274    }
14275
14276    /// Two FKs to one parent would emit the same reverse accessor twice.
14277    #[test]
14278    fn reverse_accessor_clash_names_related_name() {
14279        let input: DeriveInput = syn::parse_quote! {
14280            #[rustango(table = "m")]
14281            struct Message {
14282                #[rustango(primary_key)] id: i64,
14283                sender: ForeignKey<User>,
14284                recipient: ForeignKey<User>,
14285            }
14286        };
14287        let err = expand(&input).err().expect("refused");
14288        assert!(err.to_string().contains("related_name"), "{err}");
14289    }
14290
14291    /// `related_name = "+"` emits no reverse accessor, so a parent from
14292    /// another crate, which cannot take an inherent impl, still derives.
14293    #[test]
14294    fn related_name_plus_skips_the_reverse_accessor() {
14295        let input: DeriveInput = syn::parse_quote! {
14296            #[rustango(table = "m")]
14297            struct Message {
14298                #[rustango(primary_key)] id: i64,
14299                sender: ForeignKey<User>,
14300                #[rustango(related_name = "+")] recipient: ForeignKey<User>,
14301                #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
14302            }
14303        };
14304        let out = expand(&input).expect("derives").to_string();
14305        assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
14306        assert!(!out.contains("impl other :: Account {"), "{out}");
14307    }
14308
14309    /// A raw ident derives names without `r#`; this used to panic (#1937).
14310    #[test]
14311    fn raw_idents_derive_plain_names() {
14312        let ident: syn::Ident = syn::parse_quote!(r#type);
14313        assert_eq!(column_type_ident(&ident).to_string(), "type_col");
14314        assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
14315    }
14316}