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, rgo))]
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; requests are capped
87///   at `ViewSet::max_page_size`, default 100).
88/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
89/// * `allow_anonymous` — flag; write actions without codenames are
90///   intended, so the mount warning is skipped.
91/// * `serializer = SomeSerializer` — render list / retrieve / create
92///   responses through a `#[derive(Serializer)]` type instead of the
93///   default field-level projection (`read_only` / `source` / `method`
94///   / `write_only` overrides apply). Tri-dialect. Requires the
95///   `serializer` feature.
96/// * `permissions(list = "...", retrieve = "...", create = "...",
97///   update = "...", destroy = "...")` — codenames required per action.
98#[proc_macro_derive(ViewSet, attributes(viewset))]
99pub fn derive_viewset(input: TokenStream) -> TokenStream {
100    let input = parse_macro_input!(input as DeriveInput);
101    expand_viewset(&input)
102        .unwrap_or_else(syn::Error::into_compile_error)
103        .into()
104}
105
106/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
107/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
108/// that walks every named field and:
109///
110/// * Parses the string value into the field's Rust type (`String`,
111///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
112///   nullable case).
113/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
114///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
115///   validators in declaration order, returning `FormError::Parse`
116///   on the first failure.
117///
118/// Example:
119///
120/// ```ignore
121/// #[derive(Form)]
122/// pub struct CreateItemForm {
123///     #[form(min_length = 1, max_length = 64)]
124///     pub name: String,
125///     #[form(min = 0, max = 150)]
126///     pub age: i32,
127///     pub active: bool,
128///     pub email: Option<String>,
129/// }
130///
131/// let parsed = CreateItemForm::parse(&form_map)?;
132/// ```
133#[proc_macro_derive(Form, attributes(form))]
134pub fn derive_form(input: TokenStream) -> TokenStream {
135    let input = parse_macro_input!(input as DeriveInput);
136    expand_form(&input)
137        .unwrap_or_else(syn::Error::into_compile_error)
138        .into()
139}
140
141/// Derive `rustango::serializer::ModelSerializer` for a struct.
142/// (intra-doc link disabled — the macro crate doesn't depend on
143/// `rustango` itself, so rustdoc can't resolve the path.)
144///
145/// # Container attribute (required)
146/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
147///
148/// # Field attributes
149/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
150/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
151/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
152/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
153/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
154/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
155/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
156/// - `#[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)
157/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
158/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
159///   write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
160///   no attr is given, override it when given (`min_length` is serializer-only)
161///
162/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
163/// It honours `#[serde(rename, rename_all, skip, skip_serializing, skip_serializing_if,
164/// skip_deserializing)]`; other `serde` keys are ignored.
165#[proc_macro_derive(Serializer, attributes(serializer, serde))]
166pub fn derive_serializer(input: TokenStream) -> TokenStream {
167    let input = parse_macro_input!(input as DeriveInput);
168    expand_serializer(&input)
169        .unwrap_or_else(syn::Error::into_compile_error)
170        .into()
171}
172
173/// Bake every `*.json` migration file in a directory into the binary
174/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
175/// of `(name, json_content)` pairs, lex-sorted by file stem.
176///
177/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
178/// behaviour as `migrate(pool, dir)` but with no filesystem access.
179/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
180/// (i.e. the crate that invokes the macro). Default is
181/// `"./migrations"` if no argument is supplied.
182///
183/// ```ignore
184/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
185/// // or:
186/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
187///
188/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
189/// ```
190///
191/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
192/// file's `name` field must equal its file stem, every `prev`
193/// reference must point to another migration in the same directory,
194/// and the JSON must parse. A broken chain — orphan `prev`, missing
195/// predecessor, malformed file — fails at macro-expansion time with
196/// a clear `compile_error!`. *No other Rust web framework
197/// validates migration chains at compile time*: Cot's migrations are
198/// imperative Rust code (no static chain), Loco's are SeaORM
199/// up/down (same), Rwf's are raw SQL (no chain at all).
200///
201/// Each migration is included via `include_str!` so cargo's rebuild
202/// detection picks up file *content* changes. Cargo does not watch
203/// directory listings, so a new migration file is missed until the
204/// crate rebuilds. Add a `build.rs` that tracks the directory:
205///
206/// ```ignore
207/// fn main() { println!("cargo:rerun-if-changed=migrations"); }
208/// ```
209///
210/// A missing directory embeds nothing and emits a deprecation warning.
211#[proc_macro]
212pub fn embed_migrations(input: TokenStream) -> TokenStream {
213    expand_embed_migrations(input.into())
214        .unwrap_or_else(syn::Error::into_compile_error)
215        .into()
216}
217
218/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
219/// compile time against typed columns. Issue #269 / T1.7.
220///
221/// Each invocation lowers to the equivalent typed-column method call:
222///
223/// ```ignore
224/// // These expand identically:
225/// Q!(User.email__icontains = "alice")
226/// User::email.ilike("%alice%")
227/// ```
228///
229/// Field-name typos fail the build (the macro emits `User::no_such_field`
230/// which doesn't exist) — the headline ergonomic win of this slice over
231/// filters keyed by a plain string.
232///
233/// # Supported lookup suffixes
234///
235/// * bare `=` / `__exact` → `.eq(value)`
236/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
237/// * `__ne` → `.ne(value)`
238/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
239/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
240/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
241/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
242/// * `__in` → `.is_in(iterable)`
243/// * `__not_in` → `.not_in(iterable)`
244/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
245/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
246/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
247///
248/// Unknown suffixes fail the build with a `compile_error!` pointing at
249/// the lookup token.
250///
251/// # Combine
252///
253/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
254/// `.and()` / `.or()` / `.not()` methods:
255///
256/// ```ignore
257/// User::objects()
258///     .where_(
259///         Q!(User.active = true)
260///             .and(Q!(User.email__icontains = "alice"))
261///     )
262///     .fetch(&pool).await?;
263/// ```
264///
265/// All emitted code routes through existing per-dialect writers — no new
266/// SQL emission machinery. Tri-dialect support is inherent.
267#[allow(non_snake_case)]
268#[proc_macro]
269pub fn Q(input: TokenStream) -> TokenStream {
270    expand_q(input.into())
271        .unwrap_or_else(syn::Error::into_compile_error)
272        .into()
273}
274
275/// `#[rustango::main]` — the runserver entrypoint. Wraps
276/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
277/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
278/// functions are zero-boilerplate. `RUST_LOG` and
279/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
280/// environment, else from `./.env` (only those keys are read):
281///
282/// ```ignore
283/// #[rustango::main]
284/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
285///     rustango::server::Builder::from_env().await?
286///         .migrate("migrations").await?
287///         .api(my_app::urls::api())
288///         .seed_with(my_app::seed::run).await?
289///         .serve("0.0.0.0:8080").await
290/// }
291/// ```
292///
293/// Optional `flavor = "current_thread"` passes through to
294/// `#[tokio::main]`; default is the multi-threaded runtime.
295///
296/// `logging = false` suppresses the default subscriber, for apps
297/// that install their own. Needed whenever the body reaches
298/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
299/// macro installs gets there first, and `tracing` keeps the first
300/// one, so the `[logging]` section would otherwise be read and
301/// discarded (#1465).
302///
303/// ```ignore
304/// #[rustango::main(logging = false)]
305/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
306///     rustango::manage::Cli::new()
307///         .with_settings_from_env()
308///         .with_logging()          // now the first installer
309///         .run().await
310/// }
311/// ```
312///
313/// Pulls `tracing-subscriber` into the rustango crate behind the
314/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
315/// out get plain `#[tokio::main]` ergonomics without the dependency.
316#[proc_macro_attribute]
317pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
318    expand_main(args.into(), item.into())
319        .unwrap_or_else(syn::Error::into_compile_error)
320        .into()
321}
322
323fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
324    let mut input: syn::ItemFn = syn::parse2(item)?;
325    if input.sig.asyncness.is_none() {
326        return Err(syn::Error::new(
327            input.sig.ident.span(),
328            "`#[rustango::main]` must wrap an `async fn`",
329        ));
330    }
331
332    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
333    // to `#[tokio::main]`. Tokio's proc-macro internally emits
334    // `::tokio::*` paths that resolve against the user crate's deps,
335    // so calling it through the rustango re-export still requires the
336    // user to add tokio to their own Cargo.toml. Building the
337    // runtime ourselves keeps the dep transitive through the
338    // `runtime` feature on rustango.
339    //
340    // Parse optional `flavor = "current_thread"` / `flavor =
341    // "multi_thread"` from the attribute args. Unknown args still
342    // compile, with a warning that they are ignored.
343    let root = rustango_root();
344    let arg_warnings = main_arg_warnings(&args);
345    let flavor = parse_flavor(&args);
346    let builder_call = match flavor {
347        Flavor::CurrentThread => quote! {
348            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
349        },
350        Flavor::MultiThread => quote! {
351            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
352        },
353    };
354
355    // Detach the user body and rewrite `main` as a sync fn that
356    // builds the runtime and blocks on the async body.
357    let user_body = input.block.clone();
358    input.sig.asyncness = None;
359
360    // `logging = false` drops the block entirely, so a later
361    // `Cli::with_logging()` is the first installer and its
362    // `[logging]` settings actually take effect (#1465).
363    let logging_prologue = if parse_logging(&args) {
364        quote! {
365            // `Setup`'s defaults, not a copy of them: filter, format and
366            // the `Color::Auto` rule live in one place (#1493).
367            // `logging` and this macro share the `runtime` gate.
368            // The real `RUST_LOG` still wins over `./.env`'s (#2204).
369            let _ = #root::logging::Setup::new()
370                .with_default_env_filter(#root::__private_runtime::main_default_filter())
371                .with_format(#root::__private_runtime::main_format())
372                .install();
373        }
374    } else {
375        quote! {}
376    };
377
378    input.block = syn::parse2(quote! {{
379        #arg_warnings
380        { #logging_prologue }
381        let __rt = #builder_call
382            .enable_all()
383            .build()
384            .expect("failed to build tokio runtime");
385        __rt.block_on(async move #user_body)
386    }})?;
387
388    Ok(quote! {
389        #input
390    })
391}
392
393enum Flavor {
394    MultiThread,
395    CurrentThread,
396}
397
398/// `#[tokio::main]` argument names, accepted without a warning.
399const TOKIO_MAIN_ARGS: &[&str] = &[
400    "flavor",
401    "worker_threads",
402    "start_paused",
403    "crate",
404    "unhandled_panic",
405];
406
407/// A warning per `#[rustango::main]` argument the parsers below ignore.
408fn main_arg_warnings(args: &TokenStream2) -> TokenStream2 {
409    use syn::parse::Parser as _;
410    let parsed = syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
411        .parse2(args.clone());
412    let metas = match parsed {
413        Ok(m) => m,
414        Err(e) => {
415            return deprecation_warning(
416                e.span(),
417                &format!("#[rustango::main]: cannot parse the arguments ({e}); they are ignored"),
418            )
419        }
420    };
421    let lit_bool = |m: &syn::MetaNameValue| match &m.value {
422        syn::Expr::Lit(syn::ExprLit {
423            lit: syn::Lit::Bool(_),
424            ..
425        }) => true,
426        _ => false,
427    };
428    metas
429        .iter()
430        .filter_map(|meta| {
431            // `#[tokio::main]`'s own names stay silent: a `deny(warnings)`
432            // crate passing one must keep compiling in a patch release.
433            let tokio_arg = TOKIO_MAIN_ARGS.iter().any(|n| meta.path().is_ident(n));
434            let ok = match meta {
435                syn::Meta::NameValue(m) if m.path.is_ident("logging") => lit_bool(m),
436                _ => tokio_arg,
437            };
438            (!ok).then(|| {
439                deprecation_warning(
440                    meta.span(),
441                    "#[rustango::main]: unknown argument, ignored (supported: \
442                     `flavor = \"current_thread\" | \"multi_thread\"`, `logging = false`)",
443                )
444            })
445        })
446        .collect()
447}
448
449/// A compile warning at `span`: a proc macro's only stable way to warn
450/// is a `#[deprecated]` item that its own output then uses.
451fn deprecation_warning(span: proc_macro2::Span, note: &str) -> TokenStream2 {
452    quote::quote_spanned! {span=>
453        const _: () = {
454            #[deprecated(note = #note)]
455            const RUSTANGO_WARNING: () = ();
456            RUSTANGO_WARNING
457        };
458    }
459}
460
461fn parse_flavor(args: &TokenStream2) -> Flavor {
462    // Cheap parser: look for the literal token sequence
463    // `flavor = "current_thread"`. Everything else (including
464    // bare `multi_thread` or no args) defaults to multi-thread.
465    let s = args.to_string();
466    if s.contains("current_thread") {
467        Flavor::CurrentThread
468    } else {
469        Flavor::MultiThread
470    }
471}
472
473/// `false` when the attribute carries `logging = false` — the caller
474/// installs their own subscriber, usually via `Cli::with_logging`
475/// (#1465). Same cheap token match as `parse_flavor`.
476fn parse_logging(args: &TokenStream2) -> bool {
477    !args.to_string().replace(' ', "").contains("logging=false")
478}
479
480/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
481struct QInput {
482    base_path: syn::Path,
483    field: syn::Ident,
484    value: syn::Expr,
485}
486
487impl syn::parse::Parse for QInput {
488    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
489        let base_path: syn::Path = input.parse()?;
490        input.parse::<syn::Token![.]>()?;
491        let field: syn::Ident = input.parse()?;
492        input.parse::<syn::Token![=]>()?;
493        let value: syn::Expr = input.parse()?;
494        Ok(QInput {
495            base_path,
496            field,
497            value,
498        })
499    }
500}
501
502fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
503    let q: QInput = syn::parse2(input)?;
504    let root = rustango_root();
505    let field_str = ident_name(&q.field);
506    let field_span = q.field.span();
507    let (base, suffix) = match field_str.find("__") {
508        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
509        None => (field_str.as_str(), ""),
510    };
511    if base.is_empty() {
512        return Err(syn::Error::new(
513            field_span,
514            "Q!(): field name is empty before `__` suffix",
515        ));
516    }
517    let base_ident = ident_from_name(base, field_span);
518    let value = &q.value;
519    let path = &q.base_path;
520
521    // Most suffixes map directly to a Column method with the value
522    // forwarded unchanged. Some need value-shape massaging (wildcards
523    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
524    // ISNULL). Unknown suffixes fail the build.
525    let expanded = match suffix {
526        "" | "exact" => quote! {
527            #root::core::Column::eq(#path::#base_ident, #value)
528        },
529        "ne" => quote! {
530            #root::core::Column::ne(#path::#base_ident, #value)
531        },
532        "gt" => quote! {
533            #root::core::Column::gt(#path::#base_ident, #value)
534        },
535        "gte" => quote! {
536            #root::core::Column::gte(#path::#base_ident, #value)
537        },
538        "lt" => quote! {
539            #root::core::Column::lt(#path::#base_ident, #value)
540        },
541        "lte" => quote! {
542            #root::core::Column::lte(#path::#base_ident, #value)
543        },
544        // The escaped-LIKE lookups delegate to the typed Column
545        // methods, which escape the value and pair it with the
546        // *Escaped ops so `%` / `_` match literally on every dialect
547        // (#1257) — the macro no longer owns any pattern wrapping.
548        "iexact" => quote! {
549            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
550        },
551        "contains" => quote! {
552            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
553        },
554        "icontains" => quote! {
555            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
556        },
557        "startswith" => quote! {
558            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
559        },
560        "istartswith" => quote! {
561            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
562        },
563        "endswith" => quote! {
564            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
565        },
566        "iendswith" => quote! {
567            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
568        },
569        "in" => quote! {
570            #root::core::Column::is_in(#path::#base_ident, #value)
571        },
572        "not_in" => quote! {
573            #root::core::Column::not_in(#path::#base_ident, #value)
574        },
575        "isnull" => {
576            // Must be a bool literal at macro time so we can route to
577            // is_null vs is_not_null without a runtime branch.
578            let b = match value {
579                syn::Expr::Lit(syn::ExprLit {
580                    lit: syn::Lit::Bool(b),
581                    ..
582                }) => b.value(),
583                _ => {
584                    return Err(syn::Error::new_spanned(
585                        value,
586                        "Q!(): `__isnull` requires a `true` or `false` literal",
587                    ));
588                }
589            };
590            if b {
591                quote! { #root::core::Column::is_null(#path::#base_ident) }
592            } else {
593                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
594            }
595        }
596        "between" => {
597            // Accept a tuple literal `(lo, hi)`.
598            let tuple = match value {
599                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
600                _ => {
601                    return Err(syn::Error::new_spanned(
602                        value,
603                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
604                    ));
605                }
606            };
607            let lo = &tuple.elems[0];
608            let hi = &tuple.elems[1];
609            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
610        }
611        "regex" => quote! {
612            #root::core::Column::regex(#path::#base_ident, #value)
613        },
614        "iregex" => quote! {
615            #root::core::Column::iregex(#path::#base_ident, #value)
616        },
617        _ => {
618            return Err(syn::Error::new(
619                field_span,
620                format!(
621                    "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",
622                    suffix
623                ),
624            ));
625        }
626    };
627    Ok(expanded)
628}
629
630fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
631    // Default to "./migrations" if invoked without args.
632    let path_str = if input.is_empty() {
633        "./migrations".to_string()
634    } else {
635        let lit: LitStr = syn::parse2(input)?;
636        lit.value()
637    };
638
639    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
640        syn::Error::new(
641            proc_macro2::Span::call_site(),
642            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
643        )
644    })?;
645    let abs = std::path::Path::new(&manifest).join(&path_str);
646
647    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
648    if abs.is_dir() {
649        let read = std::fs::read_dir(&abs).map_err(|e| {
650            syn::Error::new(
651                proc_macro2::Span::call_site(),
652                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
653            )
654        })?;
655        for entry in read.flatten() {
656            let path = entry.path();
657            if !path.is_file() {
658                continue;
659            }
660            if path.extension().and_then(|s| s.to_str()) != Some("json") {
661                continue;
662            }
663            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
664                continue;
665            };
666            entries.push((stem.to_owned(), path));
667        }
668    }
669    entries.sort_by(|a, b| a.0.cmp(&b.0));
670
671    // Compile-time chain validation: read each migration's JSON,
672    // pull `name` and `prev` (file-stem-keyed for the chain check),
673    // and verify every `prev` points to another migration in the
674    // slice. Mismatches between the file stem and the embedded
675    // `name` field — and broken `prev` chains — fail at MACRO
676    // EXPANSION time so a misshapen migration set never compiles.
677    //
678    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
679    // migrations + a Rust proc-macro that reads them is a combo no
680    // other Rust web framework matches (Cot's are imperative Rust
681    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
682    // a static chain to validate).
683    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
684    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
685    for (stem, path) in &entries {
686        let raw = std::fs::read_to_string(path).map_err(|e| {
687            syn::Error::new(
688                proc_macro2::Span::call_site(),
689                format!(
690                    "embed_migrations!: cannot read {} for chain validation: {e}",
691                    path.display()
692                ),
693            )
694        })?;
695        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
696            syn::Error::new(
697                proc_macro2::Span::call_site(),
698                format!(
699                    "embed_migrations!: {} is not valid JSON: {e}",
700                    path.display()
701                ),
702            )
703        })?;
704        let name = json
705            .get("name")
706            .and_then(|v| v.as_str())
707            .ok_or_else(|| {
708                syn::Error::new(
709                    proc_macro2::Span::call_site(),
710                    format!(
711                        "embed_migrations!: {} is missing the `name` field",
712                        path.display()
713                    ),
714                )
715            })?
716            .to_owned();
717        if name != *stem {
718            return Err(syn::Error::new(
719                proc_macro2::Span::call_site(),
720                format!(
721                    "embed_migrations!: file stem `{stem}` does not match the migration's \
722                     `name` field `{name}` — rename the file or fix the JSON",
723                ),
724            ));
725        }
726        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
727        chain_names.push(name.clone());
728        prev_refs.push((name, prev));
729    }
730
731    let name_set: std::collections::HashSet<&str> =
732        chain_names.iter().map(String::as_str).collect();
733    for (name, prev) in &prev_refs {
734        if let Some(p) = prev {
735            if !name_set.contains(p.as_str()) {
736                return Err(syn::Error::new(
737                    proc_macro2::Span::call_site(),
738                    format!(
739                        "embed_migrations!: broken migration chain — `{name}` declares \
740                         prev=`{p}` but no migration with that name exists in {}",
741                        abs.display()
742                    ),
743                ));
744            }
745        }
746    }
747
748    let pairs: Vec<TokenStream2> = entries
749        .iter()
750        .map(|(name, path)| {
751            let path_lit = path.display().to_string();
752            quote! { (#name, ::core::include_str!(#path_lit)) }
753        })
754        .collect();
755
756    // A wrong path used to embed nothing silently; a deprecation use is
757    // the only stable way for a proc macro to warn without breaking builds.
758    let missing_warning = if abs.is_dir() {
759        quote! {}
760    } else {
761        let note = format!(
762            "embed_migrations!: directory {} does not exist; no migrations are embedded",
763            abs.display()
764        );
765        quote! {
766            #[deprecated(note = #note)]
767            const __RUSTANGO_MIGRATIONS_DIR_MISSING: () = ();
768            #[allow(clippy::let_unit_value)]
769            let _ = __RUSTANGO_MIGRATIONS_DIR_MISSING;
770        }
771    };
772
773    Ok(quote! {
774        {
775            #missing_warning
776            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
777            __RUSTANGO_EMBEDDED
778        }
779    })
780}
781
782fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
783    let root = rustango_root();
784    let struct_name = &input.ident;
785
786    let Data::Struct(data) = &input.data else {
787        return Err(syn::Error::new_spanned(
788            struct_name,
789            "Model can only be derived on structs",
790        ));
791    };
792    let Fields::Named(named) = &data.fields else {
793        return Err(syn::Error::new_spanned(
794            struct_name,
795            "Model requires a struct with named fields",
796        ));
797    };
798
799    let mut container = parse_container_attrs(input)?;
800    let table = container
801        .table
802        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
803    let model_name = struct_name.to_string();
804
805    let collected = collect_fields(named, &table)?;
806    // `reverse_has` / `generic_has` join on the PK column unless they name another.
807    if let Some((_, pk_column)) = &collected.primary_key {
808        for rel in &mut container.reverse_has_relations {
809            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
810        }
811        for rel in &mut container.generic_has_relations {
812            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
813        }
814    }
815
816    // Validate that #[rustango(display = "…")] names a real field.
817    if let Some((ref display, span)) = container.display {
818        if !collected.field_names.iter().any(|n| n == display) {
819            return Err(syn::Error::new(
820                span,
821                format!("`display = \"{display}\"` does not match any field on this struct"),
822            ));
823        }
824    }
825    let display = container.display.map(|(name, _)| name);
826    let app_label = container.app.clone();
827
828    // Validate admin field-name lists against declared field names.
829    // Note: `list_display` is intentionally NOT validated here. As of
830    // v0.32 it may also reference inventory-registered computed
831    // fields (via `register_admin_computed!`) whose existence the
832    // macro can't see at compile time — they're submitted from any
833    // crate that depends on rustango. The runtime list-view resolves
834    // unknown names against the inventory + silently drops the
835    // truly-bogus ones, which is the cheaper trade-off versus
836    // forcing a per-Model attr to opt out.
837    if let Some(admin) = &container.admin {
838        for (label, list) in [
839            ("search_fields", &admin.search_fields),
840            ("readonly_fields", &admin.readonly_fields),
841            ("list_filter", &admin.list_filter),
842        ] {
843            if let Some((names, span)) = list {
844                for name in names {
845                    if !collected.field_names.iter().any(|n| n == name) {
846                        return Err(syn::Error::new(
847                            *span,
848                            format!(
849                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
850                            ),
851                        ));
852                    }
853                }
854            }
855        }
856        if let Some((pairs, span)) = &admin.ordering {
857            for (name, _) in pairs {
858                if !collected.field_names.iter().any(|n| n == name) {
859                    return Err(syn::Error::new(
860                        *span,
861                        format!(
862                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
863                        ),
864                    ));
865                }
866            }
867        }
868        if let Some((groups, span)) = &admin.fieldsets {
869            for (_, fields) in groups {
870                for name in fields {
871                    if !collected.field_names.iter().any(|n| n == name) {
872                        return Err(syn::Error::new(
873                            *span,
874                            format!(
875                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
876                            ),
877                        ));
878                    }
879                }
880            }
881        }
882    }
883    if let Some(audit) = &container.audit {
884        if let Some((names, span)) = &audit.track {
885            for name in names {
886                if !collected.field_names.iter().any(|n| n == name) {
887                    return Err(syn::Error::new(
888                        *span,
889                        format!(
890                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
891                        ),
892                    ));
893                }
894            }
895        }
896    }
897
898    // Issue #291 / T2.5 — validate each `default_order` column name
899    // against the model's collected fields. Typos fail at macro-expand
900    // time, not at the database.
901    for (col, _desc, span) in &container.default_order {
902        if !collected.field_names.iter().any(|n| n == col) {
903            return Err(syn::Error::new(
904                *span,
905                format!(
906                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
907                ),
908            ));
909        }
910    }
911
912    let column_warnings =
913        unknown_column_warnings(&container.indexes, &container.get_latest_by, &collected);
914
915    // Build the audit_track list for ModelSchema: None when no audit attr,
916    // Some(empty) when audit present without track, Some(names) when explicit.
917    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
918        audit
919            .track
920            .as_ref()
921            .map(|(names, _)| names.clone())
922            .unwrap_or_default()
923    });
924
925    // The upsert conflict target: a container-level composite unique,
926    // never a field `index(unique)` or a partial index PG can't target.
927    let upsert_unique: Option<Vec<String>> = container
928        .indexes
929        .iter()
930        .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
931        .map(|i| i.columns.clone());
932
933    // Merge field-level indexes into the container's index list.
934    let mut all_indexes: Vec<IndexAttr> = container.indexes;
935    for field in &named.named {
936        let ident = field.ident.as_ref().expect("named");
937        // The same default column as `process_field` (#1937).
938        let col = ident_name(ident);
939        // Re-parse field attrs to check for index flag
940        if let Ok(fa) = parse_field_attrs(field) {
941            if fa.index {
942                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
943                let auto_name = capped_index_name(if fa.index_unique {
944                    format!("{table}_{col_name}_uq_idx")
945                } else {
946                    format!("{table}_{col_name}_idx")
947                });
948                all_indexes.push(IndexAttr {
949                    span: field.span(),
950                    name: fa.index_name.or(Some(auto_name)),
951                    columns: vec![col_name],
952                    unique: fa.index_unique,
953                    method: fa.index_method,
954                    where_clause: None,
955                    include: Vec::new(),
956                });
957            }
958        }
959    }
960    // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
961    for field in &named.named {
962        let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
963            continue;
964        };
965        if det.kind != DetectedKind::String || fa.max_length.is_some() {
966            continue;
967        }
968        let col = fa
969            .column
970            .clone()
971            .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
972        // MySQL indexes every foreign-key column too.
973        let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
974        if fa.primary_key
975            || fa.unique
976            || relation
977            || all_indexes.iter().any(|i| i.columns.contains(&col))
978        {
979            return Err(syn::Error::new_spanned(
980                field,
981                "a primary-key, unique, indexed or foreign-key `String` needs \
982                 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
983            ));
984        }
985    }
986
987    let model_impl = model_impl_tokens(
988        struct_name,
989        &model_name,
990        &table,
991        display.as_deref(),
992        app_label.as_deref(),
993        container.admin.as_ref(),
994        &container.default_order,
995        &collected.field_schemas,
996        collected.soft_delete_column.as_deref(),
997        container.permissions,
998        audit_track_names.as_deref(),
999        &container.m2m,
1000        &all_indexes,
1001        &container.checks,
1002        &container.excludes,
1003        &container.composite_fks,
1004        &container.generic_fks,
1005        container.scope.as_deref(),
1006        container.is_view,
1007        container.verbose_name.as_deref(),
1008        container.verbose_name_plural.as_deref(),
1009        container.managed,
1010        container.base_manager_name.as_deref(),
1011        container.order_with_respect_to.as_deref(),
1012        container.proxy,
1013        &container.required_db_features,
1014        container.required_db_vendor.as_deref(),
1015        container.default_related_name.as_deref(),
1016        container.db_table_comment.as_deref(),
1017        container
1018            .get_latest_by
1019            .as_ref()
1020            .map(|(c, d, _)| (c.as_str(), *d)),
1021        &container.extra_permissions,
1022        &container.default_permissions,
1023        &container.global_scopes,
1024        &container.reverse_has_relations,
1025        &container.generic_has_relations,
1026    );
1027    let module_ident = column_module_ident(struct_name);
1028    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
1029    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
1030        let track_set: Option<std::collections::HashSet<&str>> = audit
1031            .track
1032            .as_ref()
1033            .map(|(names, _)| names.iter().map(String::as_str).collect());
1034        collected
1035            .column_entries
1036            .iter()
1037            .filter(|c| {
1038                track_set
1039                    .as_ref()
1040                    .map_or(true, |s| s.contains(c.name.as_str()))
1041            })
1042            .collect()
1043    });
1044    let inherent_impl = inherent_impl_tokens(
1045        struct_name,
1046        &collected,
1047        collected.primary_key.as_ref(),
1048        &column_consts,
1049        audited_fields.as_deref(),
1050        upsert_unique.as_deref(),
1051        &container.manager_fns,
1052    );
1053    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
1054    let from_row_impl = from_row_impl_tokens(
1055        struct_name,
1056        &collected.from_row_inits,
1057        &collected.from_row_inits_my,
1058    );
1059    let reverse_helpers = reverse_helper_tokens(
1060        struct_name,
1061        &collected.fk_relations,
1062        container.default_related_name.as_deref(),
1063    )?;
1064    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
1065    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
1066    // Issue #817 — `#[rustango(through(...))]` accessors.
1067    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
1068    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
1069    let reverse_has_accessors = reverse_has_accessor_tokens(
1070        struct_name,
1071        &container.reverse_has_relations,
1072        &collected.column_entries,
1073    );
1074    let generic_fk_accessors = generic_fk_accessor_tokens(
1075        struct_name,
1076        &container.generic_fks,
1077        &collected.column_entries,
1078    );
1079
1080    Ok(quote! {
1081        #column_warnings
1082        #model_impl
1083        #inherent_impl
1084        #from_row_impl
1085        #column_module
1086        #reverse_helpers
1087        #m2m_accessors
1088        #generic_m2m_accessors
1089        #through_accessors
1090        #reverse_has_accessors
1091        #generic_fk_accessors
1092
1093        #root::core::inventory::submit! {
1094            // `module_path!()` evaluates at the registration site,
1095            // so a Model declared in `crate::blog::models` records
1096            // `"<crate>::blog::models"` and `resolved_app_label()`
1097            // can infer "blog" without an explicit attribute.
1098            #root::core::ModelEntry::new(
1099                <#struct_name as #root::core::Model>::SCHEMA,
1100                ::core::module_path!(),
1101            )
1102            .with_audited(
1103                <#struct_name as #root::core::Model>::__rustango_audited_update,
1104                <#struct_name as #root::core::Model>::__rustango_audited_delete,
1105            )
1106            .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1107            .with_audited_update_record(
1108                <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1109            )
1110            .with_audited_cascade(<#struct_name as #root::core::Model>::__rustango_audited_cascade)
1111        }
1112    })
1113}
1114
1115/// A deprecation-style warning per index or `get_latest_by` name that is
1116/// no column of the model: an error would break code that compiles today.
1117fn unknown_column_warnings(
1118    indexes: &[IndexAttr],
1119    get_latest_by: &Option<(String, bool, proc_macro2::Span)>,
1120    collected: &CollectedFields,
1121) -> TokenStream2 {
1122    let is_column = |c: &str| collected.column_entries.iter().any(|e| e.column == c);
1123    // Only plain names: an expression index such as `lower(email)` is legal.
1124    let plain = |c: &str| c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
1125    let mut found: Vec<(proc_macro2::Span, String)> = Vec::new();
1126    for idx in indexes {
1127        for col in idx.columns.iter().filter(|c| plain(c) && !is_column(c)) {
1128            found.push((
1129                idx.span,
1130                format!("index column `{col}` is not a column of this model; its DDL will fail"),
1131            ));
1132        }
1133    }
1134    if let Some((col, _, span)) = get_latest_by {
1135        if !is_column(col) && !collected.field_names.iter().any(|n| n == col) {
1136            found.push((
1137                *span,
1138                format!("`get_latest_by`: `{col}` is not a field of this model"),
1139            ));
1140        }
1141    }
1142    found
1143        .into_iter()
1144        .map(|(span, note)| deprecation_warning(span, &note))
1145        .collect()
1146}
1147
1148/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1149/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1150fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1151    let root = rustango_root();
1152    let parent_ty = &rel.parent_type;
1153    let field_ident = &rel.field_ident;
1154    let null_fk = if rel.nullable {
1155        quote! {
1156            if #receiver.#field_ident.is_none() {
1157                return ::core::result::Result::Ok(false);
1158            }
1159        }
1160    } else {
1161        quote! {}
1162    };
1163    quote! {
1164        #null_fk
1165        #root::sql::__rustango_require_join(
1166            row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1167        )?;
1168    }
1169}
1170
1171/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1172/// reused, and the deeper chain stitched into it; re-decoding it would
1173/// drop what that chain loaded.
1174fn loaded_reuse_tokens(
1175    rel: &FkRelation,
1176    receiver: &TokenStream2,
1177    load: &TokenStream2,
1178) -> TokenStream2 {
1179    let root = rustango_root();
1180    let field_ident = &rel.field_ident;
1181    let pattern = if rel.nullable {
1182        quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1183    } else {
1184        quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1185    };
1186    quote! {
1187        if let #pattern = &mut #receiver.#field_ident {
1188            if let ::core::option::Option::Some(__r) = __rest {
1189                let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1190            }
1191            return ::core::result::Result::Ok(true);
1192        }
1193    }
1194}
1195
1196/// Store the decoded `_parent` on the FK field, keeping the key the row
1197/// holds: with `on = <unique column>` it is not the parent's PK.
1198fn loaded_assign_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1199    let root = rustango_root();
1200    let field_ident = &rel.field_ident;
1201    if rel.nullable {
1202        quote! {
1203            if let ::core::option::Option::Some(__fk) = &mut #receiver.#field_ident {
1204                let _pk = ::core::clone::Clone::clone(__fk.pk_ref());
1205                *__fk = #root::sql::ForeignKey::loaded(_pk, _parent);
1206            }
1207        }
1208    } else {
1209        quote! {
1210            let _pk = ::core::clone::Clone::clone(#receiver.#field_ident.pk_ref());
1211            #receiver.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1212        }
1213    }
1214}
1215
1216/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1217/// matches `field_name` against the model's FK fields and, for a
1218/// match, decodes the FK target via the parent's macro-generated
1219/// `__rustango_from_aliased_row`, and stores `ForeignKey::Loaded` on
1220/// `self` under the key the row already holds.
1221///
1222/// Always emitted (with empty arms for FK-less models, which
1223/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1224/// trait bound on `fetch_on` is universally satisfied — users
1225/// never have to think about implementing it.
1226fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1227    let root = rustango_root();
1228    let arms = fk_relations.iter().map(|rel| {
1229        let parent_ty = &rel.parent_type;
1230        let field_ident = &rel.field_ident;
1231        let fk_name = ident_name(field_ident);
1232        let fk_col = fk_name.as_str();
1233        let guard = join_guard_tokens(rel, &quote!(self));
1234        let reuse = loaded_reuse_tokens(
1235            rel,
1236            &quote!(self),
1237            &quote!(#root::sql::LoadRelated::__rustango_load_related),
1238        );
1239        let assign = loaded_assign_tokens(rel, &quote!(self));
1240        quote! {
1241            #fk_col => {
1242                #guard
1243                #reuse
1244                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1245                // Audit #451 — multi-hop `select_related("a__b__c")`:
1246                // stitch the deeper relation onto this parent first,
1247                // decoding it at the accumulated `__next_alias`. The
1248                // parent type also impls `LoadRelated`, so this recurses
1249                // the FK chain to arbitrary depth.
1250                if let ::core::option::Option::Some(__r) = __rest {
1251                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1252                        &mut _parent, row, __r, &__next_alias,
1253                    )?;
1254                }
1255                #assign
1256                ::core::result::Result::Ok(true)
1257            }
1258        }
1259    });
1260    quote! {
1261        #[cfg(feature = "postgres")]
1262        impl #root::sql::LoadRelated for #struct_name {
1263            #[allow(unused_variables)]
1264            fn __rustango_load_related(
1265                &mut self,
1266                row: &#root::sql::sqlx::postgres::PgRow,
1267                field_name: &str,
1268                alias: &str,
1269            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1270                // Audit #451 — split the multi-hop path: `base` is the FK
1271                // on THIS model, `__rest` (if any) is the remaining chain
1272                // to stitch onto the loaded parent. `__next_alias` is the
1273                // accumulated join alias the parent's columns live under
1274                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1275                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1276                    match field_name.split_once("__") {
1277                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1278                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1279                    };
1280                let __next_alias: ::std::string::String = match __rest {
1281                    ::core::option::Option::Some(__r) => {
1282                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1283                        ::std::format!("{}__{}", alias, __rb)
1284                    }
1285                    ::core::option::Option::None => ::std::string::String::new(),
1286                };
1287                match __base {
1288                    #( #arms )*
1289                    _ => ::core::result::Result::Ok(false),
1290                }
1291            }
1292        }
1293    }
1294}
1295
1296/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1297/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1298/// which expands to a `LoadRelatedMy` impl when rustango is built with
1299/// the `mysql` feature, and to nothing otherwise. The decoded parent
1300/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1301/// decoder, also batch8) so the dual emission is symmetric across
1302/// backends.
1303fn load_related_impl_my_tokens(
1304    struct_name: &syn::Ident,
1305    fk_relations: &[FkRelation],
1306) -> TokenStream2 {
1307    let root = rustango_root();
1308    let arms = fk_relations.iter().map(|rel| {
1309        let parent_ty = &rel.parent_type;
1310        let field_ident = &rel.field_ident;
1311        let fk_name = ident_name(field_ident);
1312        let fk_col = fk_name.as_str();
1313        let guard = join_guard_tokens(rel, &quote!(__self));
1314        let reuse = loaded_reuse_tokens(
1315            rel,
1316            &quote!(__self),
1317            &quote!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1318        );
1319        let assign = loaded_assign_tokens(rel, &quote!(__self));
1320        // `self` IS hygiene-tracked through macro_rules — emitted from
1321        // a different context than the `&mut self` parameter inside
1322        // the macro_rules-expanded fn. Pass it through as `__self`
1323        // and let the macro_rules rebind it to the receiver.
1324        quote! {
1325            #fk_col => {
1326                #guard
1327                #reuse
1328                let mut _parent: #parent_ty =
1329                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1330                // Audit #451 — multi-hop: stitch the deeper relation onto
1331                // the parent at the accumulated alias (see PG twin).
1332                if let ::core::option::Option::Some(__r) = __rest {
1333                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1334                        &mut _parent, row, __r, &__next_alias,
1335                    )?;
1336                }
1337                #assign
1338                ::core::result::Result::Ok(true)
1339            }
1340        }
1341    });
1342    quote! {
1343        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1344            #( #arms )*
1345        });
1346    }
1347}
1348
1349/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1350/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1351/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1352fn load_related_impl_sqlite_tokens(
1353    struct_name: &syn::Ident,
1354    fk_relations: &[FkRelation],
1355) -> TokenStream2 {
1356    let root = rustango_root();
1357    let arms = fk_relations.iter().map(|rel| {
1358        let parent_ty = &rel.parent_type;
1359        let field_ident = &rel.field_ident;
1360        let fk_name = ident_name(field_ident);
1361        let fk_col = fk_name.as_str();
1362        let guard = join_guard_tokens(rel, &quote!(__self));
1363        let reuse = loaded_reuse_tokens(
1364            rel,
1365            &quote!(__self),
1366            &quote!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1367        );
1368        let assign = loaded_assign_tokens(rel, &quote!(__self));
1369        quote! {
1370            #fk_col => {
1371                #guard
1372                #reuse
1373                let mut _parent: #parent_ty =
1374                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1375                // Audit #451 — multi-hop: stitch the deeper relation onto
1376                // the parent at the accumulated alias (see PG twin).
1377                if let ::core::option::Option::Some(__r) = __rest {
1378                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1379                        &mut _parent, row, __r, &__next_alias,
1380                    )?;
1381                }
1382                #assign
1383                ::core::result::Result::Ok(true)
1384            }
1385        }
1386    });
1387    quote! {
1388        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1389            #( #arms )*
1390        });
1391    }
1392}
1393
1394/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1395/// matches `field_name` against the model's FK fields and returns
1396/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1397/// group children by parent PK.
1398///
1399/// Always emitted (with `_ => None` for FK-less models) so the
1400/// trait bound on `fetch_with_prefetch` is universally satisfied.
1401fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1402    let root = rustango_root();
1403    let arms = fk_relations.iter().map(|rel| {
1404        let field_ident = &rel.field_ident;
1405        let fk_name = ident_name(field_ident);
1406        let fk_col = fk_name.as_str();
1407        if rel.pk_kind == DetectedKind::I64 {
1408            // i64 FK — return the stored PK so prefetch_related can
1409            // group children by it. Nullable variant unwraps via
1410            // `as_ref().map(...)`: an unset (NULL) FK column yields
1411            // `None` and that child sits out of the grouping (correct
1412            // semantics — it has no parent to attach to).
1413            if rel.nullable {
1414                quote! {
1415                    #fk_col => self.#field_ident
1416                        .as_ref()
1417                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1418                }
1419            } else {
1420                quote! {
1421                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1422                }
1423            }
1424        } else {
1425            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1426            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1427            // i64-keyed grouping path — the trait signature is
1428            // `Option<i64>` and a non-i64 PK can't lower into it.
1429            // The FK still works for everything else (CRUD, lazy
1430            // load via `.get()`, select_related JOINs); only the
1431            // bulk prefetch grouper needs the integer key.
1432            quote! {
1433                #fk_col => ::core::option::Option::None,
1434            }
1435        }
1436    });
1437    // PK-type-agnostic version: every FK arm emits an
1438    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1439    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1440    // opt OUT of the legacy i64 method (it returns None) but opt IN
1441    // here.
1442    let value_arms = fk_relations.iter().map(|rel| {
1443        let field_ident = &rel.field_ident;
1444        let fk_name = ident_name(field_ident);
1445        let fk_col = fk_name.as_str();
1446        if rel.nullable {
1447            quote! {
1448                #fk_col => self.#field_ident
1449                    .as_ref()
1450                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1451                        #root::sql::ForeignKey::pk(fk)
1452                    )),
1453            }
1454        } else {
1455            quote! {
1456                #fk_col => ::core::option::Option::Some(
1457                    ::core::convert::Into::<#root::core::SqlValue>::into(
1458                        self.#field_ident.pk()
1459                    )
1460                ),
1461            }
1462        }
1463    });
1464    quote! {
1465        impl #root::sql::FkPkAccess for #struct_name {
1466            #[allow(unused_variables)]
1467            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1468                match field_name {
1469                    #( #arms )*
1470                    _ => ::core::option::Option::None,
1471                }
1472            }
1473            #[allow(unused_variables)]
1474            fn __rustango_fk_pk_value(
1475                &self,
1476                field_name: &str,
1477            ) -> ::core::option::Option<#root::core::SqlValue> {
1478                match field_name {
1479                    #( #value_arms )*
1480                    _ => ::core::option::Option::None,
1481                }
1482            }
1483        }
1484    }
1485}
1486
1487/// `related_name = "+"`: the FK gets no reverse accessor on its parent.
1488const NO_REVERSE_ACCESSOR: &str = "+";
1489
1490/// For every `ForeignKey<Parent>` field on `Child`, emit
1491/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1492/// Reads the column the FK points at (`on`, else the PK) and runs one
1493/// `SELECT … WHERE <fk_column> = $1`: one round trip, no N+1.
1494///
1495/// **PG-only emission**: the accessor is bounded on
1496/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1497/// both of which are gated behind the `postgres` cargo feature. The
1498/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1499/// model derive itself compiles on tri-dialect / sqlite-only
1500/// downstream builds — the accessor just isn't materialised. A tri-
1501/// dialect `_set_pool` variant is a separate follow-up.
1502fn reverse_helper_tokens(
1503    child_ident: &syn::Ident,
1504    fk_relations: &[FkRelation],
1505    default_related_name: Option<&str>,
1506) -> syn::Result<TokenStream2> {
1507    let root = rustango_root();
1508    // `related_name = "+"`: no accessor, so a parent from another crate works.
1509    let fk_relations: Vec<&FkRelation> = fk_relations
1510        .iter()
1511        .filter(|rel| rel.related_name.as_deref() != Some(NO_REVERSE_ACCESSOR))
1512        .collect();
1513    if fk_relations.is_empty() {
1514        return Ok(TokenStream2::new());
1515    }
1516    // Method-name resolution per FK (issue #816 + follow-up):
1517    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1518    //      itself — wins over everything else.
1519    //   2. Container-level `default_related_name = "..."` on the
1520    //      child. Applies to every FK on this model that didn't
1521    //      override.
1522    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1523    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1524    //      edge cases.
1525    //
1526    // The PG-on-executor variant keeps the resolved name; the
1527    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1528    // framework's convention for the `&Pool` flavor of every helper).
1529    let default_pg_suffix = default_related_name
1530        .map(str::to_owned)
1531        .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1532    let name_of = |rel: &FkRelation| {
1533        rel.related_name
1534            .clone()
1535            .unwrap_or_else(|| default_pg_suffix.clone())
1536    };
1537    // Two FKs to one parent would define the same method twice (E0592).
1538    for (i, rel) in fk_relations.iter().enumerate() {
1539        let parent_ty = &rel.parent_type;
1540        let parent = quote!(#parent_ty).to_string();
1541        let name = name_of(rel);
1542        if let Some(first) = fk_relations[..i].iter().find(|r| {
1543            let other = &r.parent_type;
1544            quote!(#other).to_string() == parent && name_of(r) == name
1545        }) {
1546            return Err(syn::Error::new_spanned(
1547                &rel.field_ident,
1548                format!(
1549                    "`{}` and `{}` both point at `{parent}` and would both add `{parent}::{name}`; \
1550                     give one `#[rustango(related_name = \"...\")]`, or `related_name = \"+\"` for none",
1551                    first.field_ident, rel.field_ident
1552                ),
1553            ));
1554        }
1555    }
1556    let impls = fk_relations.iter().map(|rel| {
1557        let pg_suffix = name_of(rel);
1558        let pool_suffix = format!("{}_pool", pg_suffix);
1559        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1560        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1561        let parent_ty = &rel.parent_type;
1562        let fk_name = ident_name(&rel.field_ident);
1563        let fk_col = fk_name.as_str();
1564        let doc = format!(
1565            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1566             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1567             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1568             further `{child_ident}::objects()` filters via direct queryset use."
1569        );
1570        let pool_doc = format!(
1571            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1572             [`#root::sql::Pool`] and dispatches per backend so the \
1573             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1574             Use this from framework code that holds a `&Pool` (admin, \
1575             tenancy resolver, viewset handlers); reach for the executor- \
1576             bound variant when you already have a typed `sqlx::Executor`."
1577        );
1578        quote! {
1579            #[cfg(feature = "postgres")]
1580            impl #parent_ty {
1581                #[doc = #doc]
1582                ///
1583                /// # Errors
1584                /// Returns [`#root::sql::ExecError`] for SQL-writing
1585                /// or driver failures.
1586                pub async fn #pg_method_ident<'_c, _E>(
1587                    &self,
1588                    _executor: _E,
1589                ) -> ::core::result::Result<
1590                    ::std::vec::Vec<#child_ident>,
1591                    #root::sql::ExecError,
1592                >
1593                where
1594                    _E: #root::sql::sqlx::Executor<
1595                        '_c,
1596                        Database = #root::sql::sqlx::Postgres,
1597                    >,
1598                {
1599                    let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1600                    #root::query::QuerySet::<#child_ident>::new()
1601                        .filter_op(#fk_col, #root::core::Op::Eq, _key)
1602                        .fetch_on(_executor)
1603                        .await
1604                }
1605            }
1606
1607            impl #parent_ty {
1608                #[doc = #pool_doc]
1609                ///
1610                /// # Errors
1611                /// Returns [`#root::sql::ExecError`] for SQL-writing
1612                /// or driver failures.
1613                pub async fn #pool_method_ident(
1614                    &self,
1615                    pool: &#root::sql::Pool,
1616                ) -> ::core::result::Result<
1617                    ::std::vec::Vec<#child_ident>,
1618                    #root::sql::ExecError,
1619                > {
1620                    use #root::sql::FetcherPool as _;
1621                    let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1622                    #root::query::QuerySet::<#child_ident>::new()
1623                        .filter_op(#fk_col, #root::core::Op::Eq, _key)
1624                        .fetch(pool)
1625                        .await
1626                }
1627            }
1628        }
1629    });
1630    Ok(quote! { #( #impls )* })
1631}
1632
1633/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1634/// relation declared on the model.
1635/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1636/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1637///
1638/// Closes #239 + #240 — `comment.content_object` /
1639/// `comment.content_object = post` ergonomics on top of the existing
1640/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1641///
1642/// `column_entries` is passed so we can resolve each `ct_column` /
1643/// `pk_column` SQL name back to its Rust field ident — the macro
1644/// only sees the column-side strings in the attribute, but the
1645/// emitted accessor needs to read the actual struct field.
1646fn generic_fk_accessor_tokens(
1647    struct_name: &syn::Ident,
1648    generic_fks: &[GenericFkAttr],
1649    column_entries: &[ColumnEntry],
1650) -> TokenStream2 {
1651    let root = rustango_root();
1652    if generic_fks.is_empty() {
1653        return TokenStream2::new();
1654    }
1655    let methods = generic_fks.iter().filter_map(|gfk| {
1656        // Resolve `ct_column` + `pk_column` to the struct's Rust
1657        // field idents. A typo (column name doesn't match any field)
1658        // emits no method for that registration — the user will see
1659        // the compiler reject the SCHEMA literal anyway, so there's
1660        // a clear error path without us double-reporting.
1661        let ct_ident = column_entries
1662            .iter()
1663            .find(|c| c.column == gfk.ct_column)
1664            .map(|c| c.ident.clone())?;
1665        let pk_ident = column_entries
1666            .iter()
1667            .find(|c| c.column == gfk.pk_column)
1668            .map(|c| c.ident.clone())?;
1669
1670        let accessor_ident =
1671            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1672        let setter_ident =
1673            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1674        let name_literal = gfk.name.as_str();
1675
1676        Some(quote! {
1677            #[doc = concat!(
1678                "Resolve the polymorphic `",
1679                #name_literal,
1680                "` relation. Reads `self.",
1681                stringify!(#ct_ident),
1682                "` + `self.",
1683                stringify!(#pk_ident),
1684                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1685                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1686                #name_literal,
1687                "\", ...))]`."
1688            )]
1689            pub async fn #accessor_ident(
1690                &self,
1691                pool: &#root::sql::Pool,
1692            ) -> ::core::result::Result<
1693                ::core::option::Option<#root::__serde_json::Value>,
1694                #root::sql::ExecError,
1695            > {
1696                let gfk = #root::contenttypes::GenericForeignKey::new(
1697                    self.#ct_ident as i64,
1698                    self.#pk_ident as i64,
1699                );
1700                gfk.get_object(pool).await
1701            }
1702
1703            #[doc = concat!(
1704                "Set the polymorphic `",
1705                #name_literal,
1706                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1707                stringify!(#ct_ident),
1708                "` and `self.",
1709                stringify!(#pk_ident),
1710                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1711                #name_literal,
1712                "\", ...))]`."
1713            )]
1714            pub async fn #setter_ident<T: #root::core::Model>(
1715                &mut self,
1716                pool: &#root::sql::Pool,
1717                target_pk: i64,
1718            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1719                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1720                    pool,
1721                    target_pk,
1722                ).await?;
1723                self.#ct_ident = gfk.content_type_id as _;
1724                self.#pk_ident = gfk.object_pk as _;
1725                ::core::result::Result::Ok(())
1726            }
1727        })
1728    });
1729    quote! {
1730        impl #struct_name {
1731            #( #methods )*
1732        }
1733    }
1734}
1735
1736fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1737    let root = rustango_root();
1738    if m2m_relations.is_empty() {
1739        return TokenStream2::new();
1740    }
1741    let methods = m2m_relations.iter().map(|rel| {
1742        let method_name = format!("{}_m2m", rel.name);
1743        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1744        let through = rel.through.as_str();
1745        let src_col = rel.src.as_str();
1746        let dst_col = rel.dst.as_str();
1747        quote! {
1748            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1749                #root::sql::M2MManager {
1750                    src_pk: self.__rustango_pk_value(),
1751                    through: #through,
1752                    src_col: #src_col,
1753                    dst_col: #dst_col,
1754                }
1755            }
1756        }
1757    });
1758    quote! {
1759        impl #struct_name {
1760            #( #methods )*
1761        }
1762    }
1763}
1764
1765/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1766/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1767fn generic_m2m_accessor_tokens(
1768    struct_name: &syn::Ident,
1769    relations: &[GenericM2MAttr],
1770) -> TokenStream2 {
1771    let root = rustango_root();
1772    if relations.is_empty() {
1773        return TokenStream2::new();
1774    }
1775    let methods = relations.iter().map(|rel| {
1776        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1777        let through = rel.through.as_str();
1778        let pk_col = rel.pk_column.as_str();
1779        let ct_col = rel.ct_column.as_str();
1780        let related_col = rel.related_column.as_str();
1781        quote! {
1782            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1783                #root::sql::GenericM2MManager {
1784                    src_pk: self.__rustango_pk_value(),
1785                    src_schema: <Self as #root::core::Model>::SCHEMA,
1786                    through: #through,
1787                    pk_col: #pk_col,
1788                    ct_col: #ct_col,
1789                    dst_col: #related_col,
1790                }
1791            }
1792        }
1793    });
1794    // Lets an owner delete find and clear its junction rows.
1795    let entries = relations.iter().map(|rel| {
1796        let through = rel.through.as_str();
1797        let pk_col = rel.pk_column.as_str();
1798        let ct_col = rel.ct_column.as_str();
1799        quote! {
1800            #root::core::inventory::submit! {
1801                #root::core::GenericM2MEntry::new(
1802                    <#struct_name as #root::core::Model>::SCHEMA,
1803                    #through,
1804                    #pk_col,
1805                    #ct_col,
1806                )
1807            }
1808        }
1809    });
1810    quote! {
1811        impl #struct_name {
1812            #( #methods )*
1813        }
1814        #( #entries )*
1815    }
1816}
1817
1818/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1819/// associated functions for each `#[rustango(reverse_has(...))]`
1820/// attribute. Issue #830.
1821///
1822/// The two emitted functions return ready-to-use `WhereExpr` nodes
1823/// that downstream callers drop into
1824/// `QuerySet::<Self>::where_raw(...)`:
1825///
1826/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1827///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1828///   `whereHas` parity (without the closure-style sub-predicate
1829///   refinement — that's a follow-up; users can layer additional
1830///   predicates by constructing the SelectQuery themselves and
1831///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1832/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1833///   `whereDoesntHave` parity.
1834///
1835/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1836/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1837/// machinery threads the outer-table reference through automatically
1838/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1839fn reverse_has_accessor_tokens(
1840    struct_name: &syn::Ident,
1841    reverse_has_relations: &[ReverseHasAttr],
1842    columns: &[ColumnEntry],
1843) -> TokenStream2 {
1844    let root = rustango_root();
1845    if reverse_has_relations.is_empty() {
1846        return TokenStream2::new();
1847    }
1848    let methods = reverse_has_relations.iter().map(|rel| {
1849        let exists_name = format!("{}_exists_expr", rel.name);
1850        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1851        let count_name = format!("{}_count", rel.name);
1852        let fetch_name = format!("{}_fetch", rel.name);
1853        let first_name = format!("{}_first", rel.name);
1854        let pluck_name = format!("{}_pluck", rel.name);
1855        let accessor_name = rel.name.as_str();
1856        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1857        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1858        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1859        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1860        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1861        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1862        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1863        let child = &rel.child;
1864        let child_fk_column = rel.child_fk_column.as_str();
1865        let self_pk_column = rel.self_pk_column();
1866        // The instance helpers filter on the value the EXISTS form joins on.
1867        let self_value = columns
1868            .iter()
1869            .find(|c| c.column == self_pk_column)
1870            .map_or_else(
1871                || quote! { self.__rustango_pk_value() },
1872                |c| {
1873                    let ident = &c.ident;
1874                    quote! {
1875                        ::core::convert::Into::<#root::core::SqlValue>::into(
1876                            ::core::clone::Clone::clone(&self.#ident)
1877                        )
1878                    }
1879                },
1880            );
1881        let exists_doc = format!(
1882            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1883             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1884             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1885             filter to {struct_name}s with at least one matching child.",
1886        );
1887        let not_exists_doc = format!(
1888            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1889             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1890             <outer>.{self_pk_column})`. Drop into \
1891             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1892             {struct_name}s with **no** matching child.",
1893        );
1894        let count_doc = format!(
1895            "Eloquent `$model->{name}->count()` analog — returns \
1896             the number of `{child}` rows whose `{child_fk_column}` \
1897             matches this `{struct_name}` instance's `{self_pk_column}`. \
1898             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1899             {child_fk_column} = <self.{self_pk_column}>`.",
1900            name = rel.name,
1901        );
1902        let accessor_doc = format!(
1903            "Eloquent `$model->{name}` accessor — returns a \
1904             `QuerySet<{child}>` filtered to rows whose \
1905             `{child_fk_column}` matches this `{struct_name}` \
1906             instance's `{self_pk_column}`. **Chainable**: compose `.filter()` \
1907             / `.order_by()` / `.limit()` etc. on top, then call \
1908             `.fetch(&pool)` (the QuerySet trait method) when \
1909             done. For the simple \"fetch all\" hot path with no \
1910             further composition, prefer the bare-name \
1911             `{name}_fetch(&pool)` companion.",
1912            name = rel.name,
1913        );
1914        let fetch_doc = format!(
1915            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1916             over `{name}(&self).fetch(&pool)`. Use this when \
1917             you don't need further `.filter()` / `.order_by()` \
1918             composition; falls back to the chainable accessor when \
1919             you do. Avoids the `_pool` suffix on the most common \
1920             call-site shape.",
1921            name = rel.name,
1922        );
1923        quote! {
1924            #[doc = #accessor_doc]
1925            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1926                #root::query::QuerySet::<#child>::new()
1927                    .filter(#child_fk_column, #self_value)
1928            }
1929
1930            #[doc = #fetch_doc]
1931            pub async fn #fetch_ident(
1932                &self,
1933                pool: &#root::sql::Pool,
1934            ) -> ::core::result::Result<
1935                ::std::vec::Vec<#child>,
1936                #root::sql::ExecError,
1937            > {
1938                use #root::sql::FetcherPool as _;
1939                self.#accessor_ident().fetch(pool).await
1940            }
1941
1942            /// Eloquent `$model->relation->first()` / `hasOne`
1943            /// semantics — bare-name shortcut over
1944            /// `self.<name>().first(&pool)`. Returns `None` when no
1945            /// child rows match. Useful when the relation is
1946            /// nominally many-to-one in shape but at most one row is
1947            /// expected (latest comment, primary tag, etc.).
1948            pub async fn #first_ident(
1949                &self,
1950                pool: &#root::sql::Pool,
1951            ) -> ::core::result::Result<
1952                ::core::option::Option<#child>,
1953                #root::sql::ExecError,
1954            > {
1955                self.#accessor_ident().first(pool).await
1956            }
1957
1958            /// Eloquent `$model->relation->pluck($col)` — project a
1959            /// single column from the child rows into a `Vec<U>`.
1960            /// Skips the typed `Child` decode, which is cheaper when
1961            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1962            pub async fn #pluck_ident<U>(
1963                &self,
1964                col: &'static str,
1965                pool: &#root::sql::Pool,
1966            ) -> ::core::result::Result<
1967                ::std::vec::Vec<U>,
1968                #root::sql::ExecError,
1969            >
1970            where
1971                U: #root::sql::FlatScalar,
1972            {
1973                self.#accessor_ident()
1974                    .values_list_flat(col)
1975                    .fetch::<U>(pool)
1976                    .await
1977            }
1978
1979            #[doc = #exists_doc]
1980            pub fn #exists_ident() -> #root::core::WhereExpr {
1981                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1982                let child_schema =
1983                    <#child as #root::core::Model>::SCHEMA;
1984                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1985                    lhs: Expr::Column(#child_fk_column),
1986                    op: Op::Eq,
1987                    rhs: Expr::OuterRef(#self_pk_column),
1988                });
1989                WhereExpr::Exists(::std::boxed::Box::new(inner))
1990            }
1991
1992            #[doc = #not_exists_doc]
1993            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1994                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1995                let child_schema =
1996                    <#child as #root::core::Model>::SCHEMA;
1997                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1998                    lhs: Expr::Column(#child_fk_column),
1999                    op: Op::Eq,
2000                    rhs: Expr::OuterRef(#self_pk_column),
2001                });
2002                WhereExpr::NotExists(::std::boxed::Box::new(inner))
2003            }
2004
2005            #[doc = #count_doc]
2006            pub async fn #count_ident(
2007                &self,
2008                pool: &#root::sql::Pool,
2009            ) -> ::core::result::Result<
2010                i64,
2011                #root::sql::ExecError,
2012            > {
2013                use #root::sql::CounterPool as _;
2014                self.#accessor_ident().count(pool).await
2015            }
2016        }
2017    });
2018    quote! {
2019        impl #struct_name {
2020            #( #methods )*
2021        }
2022    }
2023}
2024
2025/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
2026/// `#[rustango(through(...))]` attribute. Issue #817.
2027///
2028/// Each accessor builds a correlated subquery via
2029/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
2030/// intermediate table, filters on the FK column pointing at this
2031/// model, and projects the intermediate PK. The outer queryset
2032/// filters the far model by its FK-to-intermediate column being in
2033/// that set.
2034///
2035/// The returned `QuerySet<Far>` is **chainable** — the subquery
2036/// lives inside a `where_raw` clause so the user's later
2037/// `.filter()` / `.order_by()` / `.limit()` compositions don't
2038/// disturb it.
2039///
2040/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
2041/// SQLite — no LATERAL or backend-specific syntax involved.
2042fn through_accessor_tokens(
2043    struct_name: &syn::Ident,
2044    through_relations: &[ThroughAttr],
2045) -> TokenStream2 {
2046    let root = rustango_root();
2047    if through_relations.is_empty() {
2048        return TokenStream2::new();
2049    }
2050    let methods = through_relations.iter().map(|rel| {
2051        let method_name = format!("{}_through", rel.name);
2052        let count_name = format!("{}_through_count", rel.name);
2053        let fetch_name = format!("{}_through_fetch", rel.name);
2054        let first_name = format!("{}_through_first", rel.name);
2055        let pluck_name = format!("{}_through_pluck", rel.name);
2056        let method_ident = syn::Ident::new(&method_name, struct_name.span());
2057        let count_ident = syn::Ident::new(&count_name, struct_name.span());
2058        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
2059        let first_ident = syn::Ident::new(&first_name, struct_name.span());
2060        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
2061        let far = &rel.far;
2062        let intermediate = &rel.intermediate;
2063        let far_fk_column = rel.far_fk_column.as_str();
2064        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
2065        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
2066        let doc = format!(
2067            "Eloquent `hasManyThrough` accessor — returns a \
2068             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
2069             instance through the intermediate `{intermediate}` table. \
2070             Generated SQL shape: \
2071             `… WHERE {far_fk_column} IN (SELECT \
2072             {intermediate_pk_column} FROM <{intermediate}> WHERE \
2073             {intermediate_fk_column} = self.pk)`. Chainable like any \
2074             other QuerySet — compose `.filter()` / `.order_by()` / \
2075             `.limit()` etc. on top.",
2076        );
2077        let count_doc = format!(
2078            "Eloquent `$model->{name}->count()` analog for the \
2079             through-relation — returns the number of `{far}` rows \
2080             reachable through `{intermediate}`. Equivalent to \
2081             `self.{name}_through().count(pool)` but spelled \
2082             as a bare instance method for parity with the \
2083             `reverse_has` `<name>_count` shape.",
2084            name = rel.name,
2085        );
2086        quote! {
2087            #[doc = #doc]
2088            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
2089                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
2090                let intermediate_schema =
2091                    <#intermediate as #root::core::Model>::SCHEMA;
2092                let sub = SelectQuery::new(intermediate_schema)
2093                    .where_clause(WhereExpr::Predicate(Filter::new(
2094                        #intermediate_fk_column,
2095                        Op::Eq,
2096                        self.__rustango_pk_value(),
2097                    )))
2098                    .projection(::std::vec![#intermediate_pk_column]);
2099                #root::query::QuerySet::<#far>::new().where_raw(
2100                    WhereExpr::InSubquery {
2101                        column: #far_fk_column,
2102                        negated: false,
2103                        subquery: ::std::boxed::Box::new(sub),
2104                    },
2105                )
2106            }
2107
2108            #[doc = #count_doc]
2109            pub async fn #count_ident(
2110                &self,
2111                pool: &#root::sql::Pool,
2112            ) -> ::core::result::Result<
2113                i64,
2114                #root::sql::ExecError,
2115            > {
2116                use #root::sql::CounterPool as _;
2117                self.#method_ident().count(pool).await
2118            }
2119
2120            /// Eloquent `$model->relation->get()` for the
2121            /// through-relation — bare-name hot-path over
2122            /// `self.<name>_through().fetch(&pool)`. Use this
2123            /// when you don't need further `.filter()` /
2124            /// `.order_by()` composition; falls back to the
2125            /// chainable accessor when you do. Avoids the `_pool`
2126            /// suffix on the most common call-site shape.
2127            pub async fn #fetch_ident(
2128                &self,
2129                pool: &#root::sql::Pool,
2130            ) -> ::core::result::Result<
2131                ::std::vec::Vec<#far>,
2132                #root::sql::ExecError,
2133            > {
2134                use #root::sql::FetcherPool as _;
2135                self.#method_ident().fetch(pool).await
2136            }
2137
2138            /// Eloquent `hasOneThrough` analog — bare-name shortcut
2139            /// over `self.<name>_through().first(&pool)`. Returns
2140            /// `None` when no far rows are reachable through the
2141            /// intermediate. Useful when at most one row is
2142            /// expected (latest comment by country, primary tag,
2143            /// etc.).
2144            pub async fn #first_ident(
2145                &self,
2146                pool: &#root::sql::Pool,
2147            ) -> ::core::result::Result<
2148                ::core::option::Option<#far>,
2149                #root::sql::ExecError,
2150            > {
2151                self.#method_ident().first(pool).await
2152            }
2153
2154            /// Pluck a single column from the far rows into `Vec<U>`
2155            /// — cheaper than the typed `<Far>` decode when only one
2156            /// scalar column is needed.
2157            pub async fn #pluck_ident<U>(
2158                &self,
2159                col: &'static str,
2160                pool: &#root::sql::Pool,
2161            ) -> ::core::result::Result<
2162                ::std::vec::Vec<U>,
2163                #root::sql::ExecError,
2164            >
2165            where
2166                U: #root::sql::FlatScalar,
2167            {
2168                self.#method_ident()
2169                    .values_list_flat(col)
2170                    .fetch::<U>(pool)
2171                    .await
2172            }
2173        }
2174    });
2175    quote! {
2176        impl #struct_name {
2177            #( #methods )*
2178        }
2179    }
2180}
2181
2182struct ColumnEntry {
2183    /// The struct field ident, used both for the inherent const name on
2184    /// the model and for the inner column type's name.
2185    ident: syn::Ident,
2186    /// The struct's field type, used as `Column::Value`.
2187    value_ty: Type,
2188    /// Rust-side field name (e.g. `"id"`).
2189    name: String,
2190    /// SQL-side column name (e.g. `"user_id"`).
2191    column: String,
2192    /// `#root::core::FieldType::I64` etc.
2193    field_type_tokens: TokenStream2,
2194    /// A bare or optional `Uuid` field; MySQL decodes it from text.
2195    uuid_column: bool,
2196}
2197
2198struct CollectedFields {
2199    field_schemas: Vec<TokenStream2>,
2200    from_row_inits: Vec<TokenStream2>,
2201    /// Aliased counterparts of `from_row_inits` — read columns via
2202    /// `format!("{prefix}__{col}")` aliases so a Model can be
2203    /// decoded from a JOINed row's projected target columns.
2204    from_aliased_row_inits: Vec<TokenStream2>,
2205    /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2206    from_row_inits_my: Vec<TokenStream2>,
2207    from_aliased_row_inits_my: Vec<TokenStream2>,
2208    /// Static column-name list — used by the simple insert path
2209    /// (no `Auto<T>` fields). Aligned with `insert_values`.
2210    insert_columns: Vec<TokenStream2>,
2211    /// Static `Into<SqlValue>` expressions, one per field. Aligned
2212    /// with `insert_columns`. Used by the simple insert path only.
2213    insert_values: Vec<TokenStream2>,
2214    /// Per-field push expressions for the dynamic (Auto-aware)
2215    /// insert path. Each statement either unconditionally pushes
2216    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2217    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2218    insert_pushes: Vec<TokenStream2>,
2219    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2220    /// when `has_auto == false`.
2221    returning_cols: Vec<TokenStream2>,
2222    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2223    /// field. Run after `insert_returning` to populate the model.
2224    auto_assigns: Vec<TokenStream2>,
2225    /// `(ident, column_literal, is_uuid)` for every Auto field. Used by
2226    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2227    /// instead of `self`.
2228    auto_field_idents: Vec<(syn::Ident, String, bool)>,
2229    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2230    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2231    /// back into the struct after insert (MySQL has no RETURNING → the
2232    /// field stays at its placeholder until the next read).
2233    generated_field_idents: Vec<(syn::Ident, String)>,
2234    /// First `Auto<T>` field the database fills, with its inner `T`: the
2235    /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2236    first_db_auto: Option<(syn::Ident, Type)>,
2237    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2238    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2239    /// `columns`).
2240    bulk_pushes_no_auto: Vec<TokenStream2>,
2241    /// Bulk-insert per-row pushes for **all fields including Auto**.
2242    /// Used by the all-Auto-Set bulk path (Auto col included with the
2243    /// caller-supplied value).
2244    bulk_pushes_all: Vec<TokenStream2>,
2245    /// Column-name literals for non-Auto fields only (paired with
2246    /// `bulk_pushes_no_auto`).
2247    bulk_columns_no_auto: Vec<TokenStream2>,
2248    /// Column-name literals for every field including Auto (paired
2249    /// with `bulk_pushes_all`).
2250    bulk_columns_all: Vec<TokenStream2>,
2251    /// Fills each `default_uuid_v7` field of `_row` in place, so a
2252    /// mutable bulk insert hands the PKs back (#1934).
2253    bulk_fill_in_place: Vec<TokenStream2>,
2254    /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2255    rust_filled_idents: Vec<syn::Ident>,
2256    /// `auto_now` fields: `upsert` stamps them before binding.
2257    auto_now_idents: Vec<syn::Ident>,
2258    /// `default_uuid_v7` fields: an `Unset` one gets a fresh id, so the
2259    /// upsert marks it `generated` for the MySQL key guard.
2260    uuid_v7_fields: Vec<(syn::Ident, String)>,
2261    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2262    /// + the loop that asserts every row matches. One pair per Auto
2263    /// field. Empty when `has_auto == false`.
2264    bulk_auto_uniformity: Vec<TokenStream2>,
2265    /// Identifier of the first Auto field, used as the witness for
2266    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2267    first_auto_ident: Option<syn::Ident>,
2268    /// `true` if any field on the struct is `Auto<T>`.
2269    has_auto: bool,
2270    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2271    /// Gates `save()` codegen — only Auto PKs let us infer
2272    /// insert-vs-update from the in-memory value.
2273    pk_is_auto: bool,
2274    /// `Assignment` constructors for every non-PK column. Drives the
2275    /// UPDATE branch of `save()`.
2276    update_assignments: Vec<TokenStream2>,
2277    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2278    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2279    upsert_update_columns: Vec<TokenStream2>,
2280    primary_key: Option<(syn::Ident, String)>,
2281    column_entries: Vec<ColumnEntry>,
2282    /// Rust-side field names, in declaration order. Used to validate
2283    /// container attributes like `display = "…"`.
2284    field_names: Vec<String>,
2285    /// FK fields on this child model. Drives the reverse-relation
2286    /// helper emit — for each FK, the macro adds an inherent
2287    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2288    /// method on the parent type.
2289    fk_relations: Vec<FkRelation>,
2290    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2291    /// the model has one. Drives emission of the `soft_delete_on` /
2292    /// `restore_on` inherent methods. At most one such column per
2293    /// model is allowed; collect_fields rejects duplicates.
2294    soft_delete_column: Option<String>,
2295    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2296    /// companion to `soft_delete_column` for emitting predicates
2297    /// that need to read the field off `&self` (e.g. `trashed()`).
2298    soft_delete_field_ident: Option<syn::Ident>,
2299}
2300
2301#[derive(Clone)]
2302struct FkRelation {
2303    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2304    /// helper is emitted as `impl <ParentType> { … }`.
2305    parent_type: Type,
2306    /// Rust field ident of the FK. Its name is the key for select_related,
2307    /// prefetch and filters; the SQL column may differ (#1936).
2308    field_ident: syn::Ident,
2309    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2310    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2311    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2312    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2313    pk_kind: DetectedKind,
2314    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2315    /// FK column). Drives the `Some(...)` wrapping in load_related
2316    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2317    /// the codegen matches the field's declared shape.
2318    nullable: bool,
2319    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2320    /// override. When set, the reverse helper picks this name instead
2321    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2322    /// #816 (issue's "Related" note re: per-FK override).
2323    related_name: Option<String>,
2324}
2325
2326fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2327    let root = rustango_root();
2328    let cap = named.named.len();
2329    let mut out = CollectedFields {
2330        field_schemas: Vec::with_capacity(cap),
2331        from_row_inits: Vec::with_capacity(cap),
2332        from_aliased_row_inits: Vec::with_capacity(cap),
2333        from_row_inits_my: Vec::with_capacity(cap),
2334        from_aliased_row_inits_my: Vec::with_capacity(cap),
2335        insert_columns: Vec::with_capacity(cap),
2336        insert_values: Vec::with_capacity(cap),
2337        insert_pushes: Vec::with_capacity(cap),
2338        returning_cols: Vec::new(),
2339        auto_assigns: Vec::new(),
2340        auto_field_idents: Vec::new(),
2341        generated_field_idents: Vec::new(),
2342        first_db_auto: None,
2343        bulk_pushes_no_auto: Vec::with_capacity(cap),
2344        bulk_pushes_all: Vec::with_capacity(cap),
2345        bulk_columns_no_auto: Vec::with_capacity(cap),
2346        bulk_columns_all: Vec::with_capacity(cap),
2347        bulk_fill_in_place: Vec::new(),
2348        rust_filled_idents: Vec::new(),
2349        auto_now_idents: Vec::new(),
2350        uuid_v7_fields: Vec::new(),
2351        bulk_auto_uniformity: Vec::new(),
2352        first_auto_ident: None,
2353        has_auto: false,
2354        pk_is_auto: false,
2355        update_assignments: Vec::with_capacity(cap),
2356        upsert_update_columns: Vec::with_capacity(cap),
2357        primary_key: None,
2358        column_entries: Vec::with_capacity(cap),
2359        field_names: Vec::with_capacity(cap),
2360        fk_relations: Vec::new(),
2361        soft_delete_column: None,
2362        soft_delete_field_ident: None,
2363    };
2364
2365    for field in &named.named {
2366        let info = process_field(field, table)?;
2367        out.field_names.push(ident_name(info.ident));
2368        out.field_schemas.push(info.schema);
2369        out.from_row_inits.push(info.from_row_init);
2370        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2371        out.from_row_inits_my.push(info.from_row_init_my);
2372        out.from_aliased_row_inits_my
2373            .push(info.from_aliased_row_init_my);
2374        if let Some(parent_ty) = info.fk_inner.clone() {
2375            out.fk_relations.push(FkRelation {
2376                parent_type: parent_ty,
2377                field_ident: info.ident.clone(),
2378                pk_kind: info.fk_pk_kind,
2379                nullable: info.nullable,
2380                related_name: info.related_name.clone(),
2381            });
2382        }
2383        if info.soft_delete {
2384            if out.soft_delete_column.is_some() {
2385                return Err(syn::Error::new_spanned(
2386                    field,
2387                    "only one field may be marked `#[rustango(soft_delete)]`",
2388                ));
2389            }
2390            out.soft_delete_column = Some(info.column.clone());
2391            out.soft_delete_field_ident = Some(info.ident.clone());
2392        }
2393        let column = info.column.as_str();
2394        let ident = info.ident;
2395        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2396        // skip every write path — Postgres recomputes the value
2397        // from EXPR. Push only the column-entry record (so typed
2398        // column constants still exist for filtering / projection)
2399        // and the schema literal (already pushed above) and move
2400        // on. No insert_columns/values, no insert_pushes, no
2401        // bulk_*, no update_assignments, no upsert_update_columns,
2402        // no returning_cols.
2403        if info.generated_as.is_some() {
2404            out.column_entries.push(ColumnEntry {
2405                ident: ident.clone(),
2406                value_ty: info.value_ty.clone(),
2407                name: ident_name(ident),
2408                column: info.column.clone(),
2409                field_type_tokens: info.field_type_tokens,
2410                uuid_column: info.uuid_column,
2411            });
2412            // #1028 — refresh the DB-computed value after insert on
2413            // RETURNING-capable backends. The column joins `returning_cols`
2414            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2415            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2416            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2417            // it keeps the placeholder until the next read.
2418            out.returning_cols.push(quote!(#column));
2419            out.generated_field_idents
2420                .push((ident.clone(), info.column.clone()));
2421            continue;
2422        }
2423        out.insert_columns.push(quote!(#column));
2424        out.insert_values.push(quote! {
2425            ::core::convert::Into::<#root::core::SqlValue>::into(
2426                ::core::clone::Clone::clone(&self.#ident)
2427            )
2428        });
2429        if info.auto {
2430            out.has_auto = true;
2431            if out.first_auto_ident.is_none() {
2432                out.first_auto_ident = Some(ident.clone());
2433            }
2434            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2435            // before binding, so the value is already in
2436            // `self.#ident` after the insert_push — RETURNING is
2437            // redundant. Skip adding this column to returning_cols /
2438            // auto_assigns / auto_field_idents to avoid (a) an
2439            // unnecessary RETURNING column on every dialect, and (b)
2440            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2441            // integer PK.
2442            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2443            // for the same reason: they are filled Rust-side below and
2444            // bound, so the value is already in `self` and RETURNING
2445            // would be a redundant column on every dialect.
2446            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2447                if out.first_db_auto.is_none() {
2448                    out.first_db_auto = auto_inner_type(info.value_ty)
2449                        .cloned()
2450                        .map(|ty| (ident.clone(), ty));
2451                }
2452                out.returning_cols.push(quote!(#column));
2453                out.auto_field_idents.push((
2454                    ident.clone(),
2455                    info.column.clone(),
2456                    info.fk_pk_kind == DetectedKind::Uuid,
2457                ));
2458                out.auto_assigns.push(quote! {
2459                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2460                });
2461            }
2462            if info.default_uuid_v7 {
2463                // Rust-side UUIDv7 generation (issue #823, Eloquent
2464                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2465                // and bind; Auto::Set → bind the user's value. The
2466                // column is ALWAYS present in the INSERT statement —
2467                // no RETURNING / no DB DEFAULT needed.
2468                out.insert_pushes.push(quote! {
2469                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2470                        self.#ident = #root::sql::Auto::Set(
2471                            #root::__uuid::Uuid::now_v7(),
2472                        );
2473                    }
2474                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2475                        _columns.push(#column);
2476                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2477                            ::core::clone::Clone::clone(_v)
2478                        ));
2479                    }
2480                });
2481            } else if info.auto_now_add || info.auto_now {
2482                // Rust-side write timestamp (#1464), mirroring
2483                // `default_uuid_v7` above. `auto_now` has always bound
2484                // `Utc::now()` on UPDATE but not on INSERT, so the
2485                // first write of an `updated_at` took the DB default
2486                // while every later one took the clock — the same
2487                // column, filled two different ways.
2488                //
2489                // The default is the problem. `CREATE TABLE IF NOT
2490                // EXISTS` leaves an upgraded table's
2491                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2492                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2493                // statement that changes it — so every insert relying
2494                // on that default wrote the legacy shape forever, while
2495                // the migrate sweep converted the rows around it. The
2496                // column went permanently mixed and `ORDER BY` inverted
2497                // the admin audit log (#1616 rework review,
2498                // correctness-001 / dialects-005 / security-002).
2499                //
2500                // Binding it here makes the stale default unreachable
2501                // rather than trying to migrate it: the timestamp comes
2502                // from the application, not the database.
2503                out.insert_pushes.push(quote! {
2504                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2505                        self.#ident = #root::sql::Auto::Set(
2506                            #root::__chrono::Utc::now(),
2507                        );
2508                    }
2509                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2510                        _columns.push(#column);
2511                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2512                            ::core::clone::Clone::clone(_v)
2513                        ));
2514                    }
2515                });
2516            } else {
2517                out.insert_pushes.push(quote! {
2518                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2519                        _columns.push(#column);
2520                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2521                            ::core::clone::Clone::clone(_v)
2522                        ));
2523                    }
2524                });
2525            }
2526            // …except the timestamp columns, which must appear in BOTH
2527            // paths (#1464). The path is chosen by the *first* Auto
2528            // field — in practice the PK — so a bulk insert of rows
2529            // with an unset PK took the no-auto branch and dropped the
2530            // timestamp column, falling back to the DB default that
2531            // this change exists to stop depending on.
2532            //
2533            // `rows` is borrowed immutably here, so this fills per row
2534            // at push time rather than writing back into the struct. An
2535            // explicitly-Set value is still honoured; only `Unset`
2536            // takes the clock.
2537            // `default_uuid_v7` likewise: its column has no DB default (#1934).
2538            if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2539                out.rust_filled_idents.push(ident.clone());
2540            }
2541            if info.auto_now {
2542                out.auto_now_idents.push(ident.clone());
2543            }
2544            if info.default_uuid_v7 {
2545                out.uuid_v7_fields
2546                    .push((ident.clone(), info.column.clone()));
2547            }
2548            let rust_fill = if info.default_uuid_v7 {
2549                Some(quote!(#root::__uuid::Uuid::now_v7()))
2550            } else if info.auto_now_add || info.auto_now {
2551                Some(quote!(#root::__chrono::Utc::now()))
2552            } else {
2553                None
2554            };
2555            // Bulk: Auto fields appear in the all-Set path. An unset filled
2556            // field takes its fill there too, not NULL (#1950).
2557            out.bulk_columns_all.push(quote!(#column));
2558            if let Some(fill) = &rust_fill {
2559                let push = quote! {
2560                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2561                        match &_row.#ident {
2562                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2563                            #root::sql::Auto::Unset => #fill,
2564                        }
2565                    ));
2566                };
2567                out.bulk_columns_no_auto.push(quote!(#column));
2568                out.bulk_pushes_no_auto.push(push.clone());
2569                out.bulk_pushes_all.push(push);
2570            } else {
2571                out.bulk_pushes_all.push(quote! {
2572                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2573                        ::core::clone::Clone::clone(&_row.#ident)
2574                    ));
2575                });
2576            }
2577            if info.default_uuid_v7 {
2578                out.bulk_fill_in_place.push(quote! {
2579                    if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2580                        _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2581                    }
2582                });
2583            }
2584            // Uniformity check: every row's Auto state must match the
2585            // first row's. Mixed Set/Unset within one bulk_insert is
2586            // rejected here so the column list stays consistent.
2587            // A filled field may be unset on any row, so it is exempt.
2588            let ident_clone = ident.clone();
2589            if rust_fill.is_none() {
2590                out.bulk_auto_uniformity.push(quote! {
2591                    for _r in rows.iter().skip(1) {
2592                        if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2593                            return ::core::result::Result::Err(
2594                                #root::sql::ExecError::Sql(
2595                                    #root::sql::SqlError::BulkAutoMixed
2596                                )
2597                            );
2598                        }
2599                    }
2600                });
2601            }
2602        } else {
2603            out.insert_pushes.push(quote! {
2604                _columns.push(#column);
2605                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2606                    ::core::clone::Clone::clone(&self.#ident)
2607                ));
2608            });
2609            // Bulk: non-Auto fields appear in BOTH paths.
2610            out.bulk_columns_no_auto.push(quote!(#column));
2611            out.bulk_columns_all.push(quote!(#column));
2612            let push_expr = quote! {
2613                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2614                    ::core::clone::Clone::clone(&_row.#ident)
2615                ));
2616            };
2617            out.bulk_pushes_no_auto.push(push_expr.clone());
2618            out.bulk_pushes_all.push(push_expr);
2619        }
2620        if info.primary_key {
2621            if out.primary_key.is_some() {
2622                return Err(syn::Error::new_spanned(
2623                    field,
2624                    "only one field may be marked `#[rustango(primary_key)]`",
2625                ));
2626            }
2627            out.primary_key = Some((ident.clone(), info.column.clone()));
2628            if info.auto {
2629                out.pk_is_auto = true;
2630            }
2631        } else if info.auto_now_add {
2632            // Immutable post-insert: skip from UPDATE entirely.
2633        } else if info.auto_now {
2634            // `auto_now` columns: bind `chrono::Utc::now()` on every
2635            // UPDATE, and write it back so `self` matches the row.
2636            out.update_assignments.push(quote! {
2637                #root::core::Assignment::new(
2638                    #column,
2639                    {
2640                        let _now = #root::__chrono::Utc::now();
2641                        self.#ident = #root::sql::Auto::Set(_now);
2642                        ::core::convert::Into::<#root::core::SqlValue>::into(_now)
2643                    },
2644                )
2645            });
2646            out.upsert_update_columns.push(quote!(#column));
2647        } else {
2648            out.update_assignments.push(quote! {
2649                #root::core::Assignment::new(
2650                    #column,
2651                    ::core::convert::Into::<#root::core::SqlValue>::into(
2652                        ::core::clone::Clone::clone(&self.#ident)
2653                    ),
2654                )
2655            });
2656            out.upsert_update_columns.push(quote!(#column));
2657        }
2658        out.column_entries.push(ColumnEntry {
2659            ident: ident.clone(),
2660            value_ty: info.value_ty.clone(),
2661            name: ident_name(ident),
2662            column: info.column.clone(),
2663            field_type_tokens: info.field_type_tokens,
2664            uuid_column: info.uuid_column,
2665        });
2666    }
2667    Ok(out)
2668}
2669
2670/// A derived index name within PG's 63 bytes (MySQL takes 64); a longer one
2671/// keeps a prefix plus a hash of the whole, so two long names stay distinct.
2672fn capped_index_name(name: String) -> String {
2673    const MAX: usize = 63;
2674    if name.len() <= MAX {
2675        return name;
2676    }
2677    // FNV-1a: stable across builds, unlike `DefaultHasher`.
2678    let hash = name.bytes().fold(0x811c_9dc5_u32, |h, b| {
2679        (h ^ u32::from(b)).wrapping_mul(0x0100_0193)
2680    });
2681    let mut cut = MAX - 9;
2682    while !name.is_char_boundary(cut) {
2683        cut -= 1;
2684    }
2685    format!("{}_{hash:08x}", &name[..cut])
2686}
2687
2688fn model_impl_tokens(
2689    struct_name: &syn::Ident,
2690    model_name: &str,
2691    table: &str,
2692    display: Option<&str>,
2693    app_label: Option<&str>,
2694    admin: Option<&AdminAttrs>,
2695    default_order: &[(String, bool, proc_macro2::Span)],
2696    field_schemas: &[TokenStream2],
2697    soft_delete_column: Option<&str>,
2698    permissions: bool,
2699    audit_track: Option<&[String]>,
2700    m2m_relations: &[M2MAttr],
2701    indexes: &[IndexAttr],
2702    checks: &[CheckAttr],
2703    excludes: &[ExcludeAttr],
2704    composite_fks: &[CompositeFkAttr],
2705    generic_fks: &[GenericFkAttr],
2706    scope: Option<&str>,
2707    is_view: bool,
2708    verbose_name: Option<&str>,
2709    verbose_name_plural: Option<&str>,
2710    managed: bool,
2711    base_manager_name: Option<&str>,
2712    order_with_respect_to: Option<&str>,
2713    proxy: bool,
2714    required_db_features: &[String],
2715    required_db_vendor: Option<&str>,
2716    default_related_name: Option<&str>,
2717    db_table_comment: Option<&str>,
2718    get_latest_by: Option<(&str, bool)>,
2719    extra_permissions: &[(String, String)],
2720    default_permissions: &[String],
2721    global_scopes: &[GlobalScopeAttr],
2722    reverse_has_relations: &[ReverseHasAttr],
2723    generic_has_relations: &[GenericHasAttr],
2724) -> TokenStream2 {
2725    let root = rustango_root();
2726    let display_tokens = if let Some(name) = display {
2727        quote!(::core::option::Option::Some(#name))
2728    } else {
2729        quote!(::core::option::Option::None)
2730    };
2731    let app_label_tokens = if let Some(name) = app_label {
2732        quote!(::core::option::Option::Some(#name))
2733    } else {
2734        quote!(::core::option::Option::None)
2735    };
2736    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2737        quote!(::core::option::Option::Some(#col))
2738    } else {
2739        quote!(::core::option::Option::None)
2740    };
2741    let audit_track_tokens = match audit_track {
2742        None => quote!(::core::option::Option::None),
2743        Some(names) => {
2744            let lits = names.iter().map(|n| n.as_str());
2745            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2746        }
2747    };
2748    let admin_tokens = admin_config_tokens(admin);
2749    // Default `tenant` so single-tenant projects (no `scope` attr
2750    // anywhere) keep the v0.24.x behavior. Container-attr parser
2751    // already validated the value is "registry" or "tenant".
2752    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2753        Some("registry") => quote!(#root::core::ModelScope::Registry),
2754        _ => quote!(#root::core::ModelScope::Tenant),
2755    };
2756    let verbose_name_tokens = optional_str(verbose_name);
2757    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2758    let base_manager_name_tokens = optional_str(base_manager_name);
2759    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2760    let required_db_features_lits: Vec<&str> =
2761        required_db_features.iter().map(String::as_str).collect();
2762    let required_db_vendor_tokens = optional_str(required_db_vendor);
2763    let default_related_name_tokens = optional_str(default_related_name);
2764    let db_table_comment_tokens = optional_str(db_table_comment);
2765    let get_latest_by_tokens = match get_latest_by {
2766        Some((col, desc)) => {
2767            quote!(::core::option::Option::Some((#col, #desc)))
2768        }
2769        None => quote!(::core::option::Option::None),
2770    };
2771    let extra_permission_tokens: Vec<_> = extra_permissions
2772        .iter()
2773        .map(|(c, l)| quote!((#c, #l)))
2774        .collect();
2775    let default_permission_tokens: Vec<_> = default_permissions
2776        .iter()
2777        .map(|action| quote!(#action))
2778        .collect();
2779    let indexes_tokens = indexes.iter().map(|idx| {
2780        // When no explicit `name = "..."` was given, derive a stable,
2781        // collision-free name from the table + columns
2782        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2783        // would clash the moment a model declares two unnamed indexes.
2784        let derived_name = idx
2785            .name
2786            .clone()
2787            .unwrap_or_else(|| capped_index_name(format!("{table}_{}_idx", idx.columns.join("_"))));
2788        let name = derived_name.as_str();
2789        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2790        let unique = idx.unique;
2791        // Map the parsed method string onto the IndexMethod enum
2792        // variant — kept at the codegen layer so the IR doesn't
2793        // carry the string form.
2794        let method_variant = match idx.method.as_str() {
2795            "gin" => quote!(#root::core::IndexMethod::Gin),
2796            "gist" => quote!(#root::core::IndexMethod::Gist),
2797            "brin" => quote!(#root::core::IndexMethod::Brin),
2798            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2799            "hash" => quote!(#root::core::IndexMethod::Hash),
2800            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2801            _ => quote!(#root::core::IndexMethod::BTree),
2802        };
2803        let where_clause = match &idx.where_clause {
2804            Some(s) => quote!(::core::option::Option::Some(#s)),
2805            None => quote!(::core::option::Option::None),
2806        };
2807        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2808        quote! {{
2809            let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2810            i.unique = #unique;
2811            i.method = #method_variant;
2812            i.where_clause = #where_clause;
2813            i.include = &[ #(#include_lits),* ];
2814            i
2815        }}
2816    });
2817    let checks_tokens = checks.iter().map(|c| {
2818        let name = c.name.as_str();
2819        let expr = c.expr.as_str();
2820        quote!(#root::core::CheckConstraint::new(#name, #expr))
2821    });
2822    let excludes_tokens = excludes.iter().map(|e| {
2823        let name = e.name.as_str();
2824        let using = e.using.as_str();
2825        let element_tokens = e.elements.iter().map(|(col, op)| {
2826            let col_s = col.as_str();
2827            let op_s = op.as_str();
2828            quote!((#col_s, #op_s))
2829        });
2830        let where_tokens = match e.where_clause.as_deref() {
2831            Some(w) => quote!(::core::option::Option::Some(#w)),
2832            None => quote!(::core::option::Option::None),
2833        };
2834        quote! {{
2835            let mut e = #root::core::ExclusionConstraint::new(
2836                #name,
2837                #using,
2838                &[ #(#element_tokens),* ],
2839            );
2840            e.where_clause = #where_tokens;
2841            e
2842        }}
2843    });
2844    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2845        let name = rel.name.as_str();
2846        let to = rel.to.as_str();
2847        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2848        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2849        quote! {
2850            #root::core::CompositeFkRelation::new(
2851                #name,
2852                #to,
2853                &[ #(#from_cols),* ],
2854                &[ #(#on_cols),* ],
2855            )
2856        }
2857    });
2858    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2859        let name = rel.name.as_str();
2860        let ct_col = rel.ct_column.as_str();
2861        let pk_col = rel.pk_column.as_str();
2862        quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2863    });
2864    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2865    // no `#[rustango(default_order = "...")]` attribute was supplied.
2866    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2867        let col_lit = col.as_str();
2868        quote! { (#col_lit, #desc) }
2869    });
2870
2871    // Issue #820 — `global_scopes` slice literal. Empty when no
2872    // `#[rustango(global_scope(...))]` attribute was supplied. The
2873    // `apply` path is re-emitted verbatim so it resolves in the
2874    // consumer's scope; the name is stored as a string literal.
2875    let global_scope_tokens = global_scopes.iter().map(|s| {
2876        let name = s.name.as_str();
2877        // The slice sits in a nested `const`, where `Self` does not resolve.
2878        let mut apply = s.apply.clone();
2879        if let Some(first) = apply.segments.first_mut() {
2880            if first.ident == "Self" {
2881                first.ident = struct_name.clone();
2882            }
2883        }
2884        quote!(#root::core::GlobalScope::new(#name, #apply))
2885    });
2886
2887    let m2m_tokens = m2m_relations.iter().map(|rel| {
2888        let name = rel.name.as_str();
2889        let to = rel.to.as_str();
2890        let through = rel.through.as_str();
2891        let src = rel.src.as_str();
2892        let dst = rel.dst.as_str();
2893        let auto_create = rel.auto_create;
2894        quote! {{
2895            let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2896            m.auto_create = #auto_create;
2897            m
2898        }}
2899    });
2900    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2901    // when the model declares `#[rustango(reverse_has(...))]`. Each
2902    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2903    // a const expression. Models without reverse_has fall through to
2904    // the trait's empty default — no override emitted.
2905    let reverse_relations_override = if reverse_has_relations.is_empty() {
2906        quote!()
2907    } else {
2908        let entries = reverse_has_relations.iter().map(|rel| {
2909            let name = rel.name.as_str();
2910            let child = &rel.child;
2911            let child_fk_column = rel.child_fk_column.as_str();
2912            let self_pk_column = rel.self_pk_column();
2913            quote! {
2914                #root::core::ReverseRelation::new(
2915                    #name,
2916                    <#child as #root::core::Model>::SCHEMA,
2917                    #child_fk_column,
2918                    #self_pk_column,
2919                )
2920            }
2921        });
2922        quote! {
2923            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2924                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2925                RELS
2926            }
2927        }
2928    };
2929    // Issue #830 — `Model::generic_reverse_relations()` override for
2930    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2931    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2932        quote!()
2933    } else {
2934        let entries = generic_has_relations.iter().map(|rel| {
2935            let name = rel.name.as_str();
2936            let child = &rel.child;
2937            let ct_column = rel.ct_column.as_str();
2938            let pk_column = rel.pk_column.as_str();
2939            let self_pk_column = rel.self_pk_column();
2940            quote! {
2941                #root::core::GenericReverseRelation::new(
2942                    #name,
2943                    <#child as #root::core::Model>::SCHEMA,
2944                    #ct_column,
2945                    #pk_column,
2946                    #self_pk_column,
2947                )
2948            }
2949        });
2950        quote! {
2951            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2952                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2953                RELS
2954            }
2955        }
2956    };
2957    let fields_slice = const_slice(
2958        &quote!(#root::core::FieldSchema),
2959        field_schemas.iter().cloned(),
2960    );
2961    let m2m_slice = const_slice(&quote!(#root::core::M2MRelation), m2m_tokens);
2962    let indexes_slice = const_slice(&quote!(#root::core::IndexSchema), indexes_tokens);
2963    let checks_slice = const_slice(&quote!(#root::core::CheckConstraint), checks_tokens);
2964    let excludes_slice = const_slice(&quote!(#root::core::ExclusionConstraint), excludes_tokens);
2965    let composite_fk_slice = const_slice(
2966        &quote!(#root::core::CompositeFkRelation),
2967        composite_fk_tokens,
2968    );
2969    let generic_fk_slice = const_slice(&quote!(#root::core::GenericRelation), generic_fk_tokens);
2970    let audited_update_override = if audit_track.is_some() {
2971        quote! {
2972            fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2973                ::core::option::Option::Some(Self::__rustango_update_audited)
2974            }
2975            fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2976                ::core::option::Option::Some(Self::__rustango_delete_audited)
2977            }
2978            fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2979                ::core::option::Option::Some(Self::__rustango_create_audited)
2980            }
2981            fn __rustango_audited_update_record(
2982            ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
2983                ::core::option::Option::Some(Self::__rustango_update_record_audited)
2984            }
2985            fn __rustango_audited_cascade() -> ::core::option::Option<#root::audit::AuditedCascade> {
2986                ::core::option::Option::Some(Self::__rustango_cascade_audited)
2987            }
2988        }
2989    } else {
2990        quote!()
2991    };
2992    let global_scope_slice = const_slice(&quote!(#root::core::GlobalScope), global_scope_tokens);
2993    quote! {
2994        impl #root::core::Model for #struct_name {
2995            const SCHEMA: &'static #root::core::ModelSchema = &{
2996                #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2997                    name: #model_name,
2998                    table: #table,
2999                    fields: #fields_slice,
3000                    display: #display_tokens,
3001                    app_label: #app_label_tokens,
3002                    admin: #admin_tokens,
3003                    soft_delete_column: #soft_delete_tokens,
3004                    permissions: #permissions,
3005                    audit_track: #audit_track_tokens,
3006                    m2m: #m2m_slice,
3007                    indexes: #indexes_slice,
3008                    check_constraints: #checks_slice,
3009                    exclusion_constraints: #excludes_slice,
3010                    composite_relations: #composite_fk_slice,
3011                    generic_relations: #generic_fk_slice,
3012                    scope: #scope_tokens,
3013                    default_order: &[ #(#default_order_tokens),* ],
3014                    is_view: #is_view,
3015                    verbose_name: #verbose_name_tokens,
3016                    verbose_name_plural: #verbose_name_plural_tokens,
3017                    managed: #managed,
3018                    base_manager_name: #base_manager_name_tokens,
3019                    order_with_respect_to: #order_with_respect_to_tokens,
3020                    proxy: #proxy,
3021                    required_db_features: &[ #(#required_db_features_lits),* ],
3022                    required_db_vendor: #required_db_vendor_tokens,
3023                    default_related_name: #default_related_name_tokens,
3024                    db_table_comment: #db_table_comment_tokens,
3025                    get_latest_by: #get_latest_by_tokens,
3026                    extra_permissions: &[ #(#extra_permission_tokens),* ],
3027                    default_permissions: &[ #(#default_permission_tokens),* ],
3028                    global_scopes: #global_scope_slice,
3029                })
3030            };
3031
3032            #reverse_relations_override
3033            #generic_reverse_relations_override
3034            #audited_update_override
3035        }
3036    }
3037}
3038
3039/// Emit the `admin: Option<&'static AdminConfig>` field for the model
3040/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
3041/// otherwise a static reference to a populated `AdminConfig`.
3042fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
3043    let root = rustango_root();
3044    let Some(admin) = admin else {
3045        return quote!(::core::option::Option::None);
3046    };
3047
3048    let list_display = admin
3049        .list_display
3050        .as_ref()
3051        .map(|(v, _)| v.as_slice())
3052        .unwrap_or(&[]);
3053    let list_display_lits = list_display.iter().map(|s| s.as_str());
3054
3055    let search_fields = admin
3056        .search_fields
3057        .as_ref()
3058        .map(|(v, _)| v.as_slice())
3059        .unwrap_or(&[]);
3060    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
3061
3062    let readonly_fields = admin
3063        .readonly_fields
3064        .as_ref()
3065        .map(|(v, _)| v.as_slice())
3066        .unwrap_or(&[]);
3067    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
3068
3069    let list_filter = admin
3070        .list_filter
3071        .as_ref()
3072        .map(|(v, _)| v.as_slice())
3073        .unwrap_or(&[]);
3074    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
3075
3076    let actions = admin
3077        .actions
3078        .as_ref()
3079        .map(|(v, _)| v.as_slice())
3080        .unwrap_or(&[]);
3081    let actions_lits = actions.iter().map(|s| s.as_str());
3082
3083    let fieldsets = admin
3084        .fieldsets
3085        .as_ref()
3086        .map(|(v, _)| v.as_slice())
3087        .unwrap_or(&[]);
3088    let fieldset_tokens = const_slice(
3089        &quote!(#root::core::Fieldset),
3090        fieldsets.iter().map(|(title, fields)| {
3091            let title = title.as_str();
3092            let field_lits = fields.iter().map(String::as_str);
3093            quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
3094        }),
3095    );
3096
3097    let list_display_links = admin
3098        .list_display_links
3099        .as_ref()
3100        .map(|(v, _)| v.as_slice())
3101        .unwrap_or(&[]);
3102    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
3103
3104    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
3105    let actions_on_top = admin.actions_on_top.unwrap_or(true);
3106    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
3107    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
3108
3109    let prepopulated = admin
3110        .prepopulated_fields
3111        .as_ref()
3112        .map(|(v, _)| v.as_slice())
3113        .unwrap_or(&[]);
3114    let prepopulated_tokens = const_slice(
3115        &quote!(#root::core::PrepopulatedField),
3116        prepopulated.iter().map(|(target, sources)| {
3117            let target = target.as_str();
3118            let source_lits = sources.iter().map(String::as_str);
3119            quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
3120        }),
3121    );
3122
3123    let raw_id_fields = admin
3124        .raw_id_fields
3125        .as_ref()
3126        .map(|(v, _)| v.as_slice())
3127        .unwrap_or(&[]);
3128    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
3129
3130    let autocomplete_fields = admin
3131        .autocomplete_fields
3132        .as_ref()
3133        .map(|(v, _)| v.as_slice())
3134        .unwrap_or(&[]);
3135    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
3136
3137    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
3138    let list_select_related_tokens = match admin.list_select_related.as_deref() {
3139        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
3140        Some("none") => quote!(#root::core::ListSelectRelated::None),
3141        Some(raw) => {
3142            let names: Vec<&str> = raw
3143                .split(',')
3144                .map(str::trim)
3145                .filter(|s| !s.is_empty())
3146                .collect();
3147            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
3148        }
3149    };
3150
3151    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
3152    // a Vec<(String, String)>. Empty / unset → no overrides.
3153    let formfield_pairs: Vec<(&str, &str)> = admin
3154        .formfield_overrides
3155        .as_ref()
3156        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
3157        .unwrap_or_default();
3158    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
3159        let field = *field;
3160        let widget = *widget;
3161        quote!((#field, #widget))
3162    });
3163
3164    let list_per_page = admin.list_per_page.unwrap_or(0);
3165
3166    let ordering_pairs = admin
3167        .ordering
3168        .as_ref()
3169        .map(|(v, _)| v.as_slice())
3170        .unwrap_or(&[]);
3171    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3172        let name = name.as_str();
3173        let desc = *desc;
3174        quote!((#name, #desc))
3175    });
3176
3177    quote! {
3178        ::core::option::Option::Some({
3179            const ADMIN: &#root::core::AdminConfig = &{
3180                #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3181                    list_display: &[ #( #list_display_lits ),* ],
3182                    search_fields: &[ #( #search_fields_lits ),* ],
3183                    list_per_page: #list_per_page,
3184                    ordering: &[ #( #ordering_tokens ),* ],
3185                    readonly_fields: &[ #( #readonly_fields_lits ),* ],
3186                    list_filter: &[ #( #list_filter_lits ),* ],
3187                    actions: &[ #( #actions_lits ),* ],
3188                    fieldsets: #fieldset_tokens,
3189                    list_display_links: &[ #( #list_display_links_lits ),* ],
3190                    search_help_text: #search_help_text,
3191                    actions_on_top: #actions_on_top,
3192                    actions_on_bottom: #actions_on_bottom,
3193                    date_hierarchy: #date_hierarchy,
3194                    prepopulated_fields: #prepopulated_tokens,
3195                    raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3196                    autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3197                    list_select_related: #list_select_related_tokens,
3198                    formfield_overrides: &[ #( #formfield_tokens ),* ],
3199                })
3200            };
3201            ADMIN
3202        })
3203    }
3204}
3205
3206fn inherent_impl_tokens(
3207    struct_name: &syn::Ident,
3208    fields: &CollectedFields,
3209    primary_key: Option<&(syn::Ident, String)>,
3210    column_consts: &TokenStream2,
3211    audited_fields: Option<&[&ColumnEntry]>,
3212    upsert_unique: Option<&[String]>,
3213    manager_fns: &[syn::Ident],
3214) -> TokenStream2 {
3215    let root = rustango_root();
3216    // Audit-emit fragments threaded into write paths. Non-empty only
3217    // when the model carries `#[rustango(audit(...))]`. They reborrow
3218    // `_executor` (a `&mut PgConnection` for audited models — the
3219    // macro switches the signature below) so the data write and the
3220    // audit INSERT both run on the same caller-supplied connection.
3221    let executor_passes_to_data_write = if audited_fields.is_some() {
3222        quote!(&mut *_executor)
3223    } else {
3224        quote!(_executor)
3225    };
3226    let executor_param = if audited_fields.is_some() {
3227        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
3228    } else {
3229        quote!(_executor: _E)
3230    };
3231    let executor_generics = if audited_fields.is_some() {
3232        quote!()
3233    } else {
3234        quote!(<'_c, _E>)
3235    };
3236    let executor_where = if audited_fields.is_some() {
3237        quote!()
3238    } else {
3239        quote! {
3240            where
3241                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
3242        }
3243    };
3244    // Audited `_on` methods take `&mut PgConnection`; `&PgPool` is an Executor for the rest.
3245    // One transaction: the before-image's FOR UPDATE must hold through
3246    // the UPDATE and its audit row.
3247    let pool_to_save_on = if audited_fields.is_some() {
3248        // Any failure, COMMIT included, rolls the INSERT back: put the pk and
3249        // stamps back too, or a retry takes the UPDATE path and writes nothing.
3250        let restored: Vec<&syn::Ident> = primary_key
3251            .map(|(pk, _)| pk)
3252            .into_iter()
3253            .chain(&fields.auto_now_idents)
3254            .collect();
3255        let prev: Vec<syn::Ident> = (0..restored.len())
3256            .map(|i| quote::format_ident!("__rustango_save_prev_{}", i))
3257            .collect();
3258        quote! {
3259            #( let #prev = ::core::clone::Clone::clone(&self.#restored); )*
3260            let _saved: ::core::result::Result<u64, #root::sql::ExecError> = async {
3261                let mut _tx = pool.begin().await?;
3262                let _affected = self.save_on(&mut *_tx).await?;
3263                _tx.commit().await?;
3264                ::core::result::Result::Ok(_affected)
3265            }
3266            .await;
3267            if _saved.is_err() {
3268                #( self.#restored = #prev; )*
3269            }
3270            _saved
3271        }
3272    } else {
3273        quote!(self.save_on(pool).await)
3274    };
3275    // One transaction so the write and its audit row commit together.
3276    let pool_to_insert_on = if audited_fields.is_some() {
3277        // A failed COMMIT leaves the RETURNING values on `self`; put them back.
3278        let assigned: Vec<syn::Ident> = fields
3279            .auto_field_idents
3280            .iter()
3281            .map(|(ident, _, _)| ident.clone())
3282            .collect();
3283        restore_auto_now_unless(
3284            &assigned,
3285            &quote!(::core::result::Result<(), #root::sql::ExecError>),
3286            &quote!(_saved.is_ok()),
3287            &quote! {
3288                let mut _tx = pool.begin().await?;
3289                self.insert_on(&mut *_tx).await?;
3290                _tx.commit().await?;
3291                ::core::result::Result::Ok(())
3292            },
3293        )
3294    } else {
3295        quote!(self.insert_on(pool).await)
3296    };
3297    let pool_to_delete_on = if audited_fields.is_some() {
3298        quote! {
3299            let mut _tx = pool.begin().await?;
3300            let _affected = self.delete_on(&mut *_tx).await?;
3301            _tx.commit().await?;
3302            ::core::result::Result::Ok(_affected)
3303        }
3304    } else {
3305        quote!(self.delete_on(pool).await)
3306    };
3307    // Past the bind cap one statement per chunk, all in one transaction.
3308    let bulk_columns = fields.bulk_columns_all.len();
3309    let one_statement = if audited_fields.is_some() {
3310        quote!()
3311    } else {
3312        quote! {
3313            if rows.len() <= _per {
3314                return Self::bulk_insert_on(rows, pool).await;
3315            }
3316        }
3317    };
3318    let pool_to_bulk_insert_on = |chunks: TokenStream2| {
3319        quote! {
3320            let _per = #root::sql::__macro_internals::bulk_insert_rows_per_statement(#bulk_columns);
3321            #one_statement
3322            let mut _tx = pool.begin().await?;
3323            for _chunk in rows.#chunks(_per) {
3324                Self::bulk_insert_on(_chunk, &mut *_tx).await?;
3325            }
3326            _tx.commit().await?;
3327            ::core::result::Result::Ok(())
3328        }
3329    };
3330    let pool_to_bulk_insert_mut = pool_to_bulk_insert_on(quote!(chunks_mut));
3331    let pool_to_bulk_insert_on = pool_to_bulk_insert_on(quote!(chunks));
3332    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
3333    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
3334    // directly, but `upsert_on` for audited models takes
3335    // `&mut PgConnection` (the audit emit needs a real connection).
3336    // Add the missing acquire shim to keep audited Auto-PK upsert
3337    // compiling.
3338    let pool_to_upsert_on = if audited_fields.is_some() {
3339        quote! {
3340            let mut _tx = pool.begin().await?;
3341            self.upsert_on(&mut *_tx).await?;
3342            _tx.commit().await?;
3343            ::core::result::Result::Ok(())
3344        }
3345    } else {
3346        quote!(self.upsert_on(pool).await)
3347    };
3348
3349    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3350    // (audit-on-connection over &Pool needs a bi-dialect transaction
3351    // helper, deferred). Two body shapes:
3352    // - has_auto: build InsertQuery skipping Auto::Unset columns,
3353    //   request Auto cols in `returning`, dispatch via
3354    //   `insert_returning_pool`, then on the returned `PgRow` /
3355    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
3356    //   row OR drop the single i64 into the first Auto field on MySQL
3357    //   (multi-Auto models on MySQL error at runtime since
3358    //   `LAST_INSERT_ID()` only reports one)
3359    // - non-Auto: build InsertQuery with explicit columns/values and
3360    //   call `insert_pool` (no returning needed)
3361    // pool_insert_method body for the audited Auto-PK case is moved
3362    // to after audit_pair_tokens / audit_pk_to_string (they live
3363    // ~150 lines below). This block keeps the non-audited and
3364    // non-Auto branches in place — the audited Auto-PK arm is
3365    // computed below and merged via the dispatch helper variable.
3366    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3367        // Audited models with explicit (non-Auto) PKs go through
3368        // the non-Auto insert path below — the audit emit is one
3369        // round-trip after the INSERT inside the same tx via
3370        // audit::save_one_with_audit? No, INSERT semantics
3371        // differ. For non-Auto PK + audited, route through a
3372        // dedicated insert + audit emit on the same tx, but defer
3373        // the macro emission to the audit-bundle-aware block below
3374        // — this `quote!()` placeholder gets overwritten there.
3375        quote!()
3376    } else if audited_fields.is_some() && fields.has_auto {
3377        // Audited Auto-PK insert_pool — assembled after the audit
3378        // bundles. Placeholder; real emission below.
3379        quote!()
3380    } else if fields.has_auto {
3381        let pushes = &fields.insert_pushes;
3382        let returning_cols = &fields.returning_cols;
3383        // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3384        let filled = &fields.rust_filled_idents;
3385        let was_unset: Vec<syn::Ident> = filled
3386            .iter()
3387            .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3388            .collect();
3389        let insert_or_ignore_call = quote! {
3390            #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3391            let mut _columns: ::std::vec::Vec<&'static str> =
3392                ::std::vec::Vec::new();
3393            let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3394                ::std::vec::Vec::new();
3395            #( #pushes )*
3396            let _query = #root::core::InsertQuery::new(
3397                <Self as #root::core::Model>::SCHEMA,
3398                _columns,
3399                _values,
3400            );
3401            let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3402            if !matches!(_result, ::core::result::Result::Ok(true)) {
3403                #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3404            }
3405            _result
3406        };
3407        // When every `Auto<T>` field is filled Rust-side
3408        // (`default_uuid_v7`, issue #823), there is no column to read
3409        // back from the database — `returning_cols` is empty. Route
3410        // through plain `insert_pool` instead of
3411        // `insert_returning_pool` to skip the redundant RETURNING /
3412        // LAST_INSERT_ID round-trip.
3413        if fields.returning_cols.is_empty() {
3414            quote! {
3415                /// Insert this row against either backend. Every
3416                /// `Auto<T>` PK on this model is filled Rust-side
3417                /// (e.g. `default_uuid_v7`) before binding, so no
3418                /// RETURNING round-trip is needed.
3419                ///
3420                /// # Errors
3421                /// As [`Self::insert`].
3422                pub async fn insert_pool(
3423                    &mut self,
3424                    pool: &#root::sql::Pool,
3425                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3426                    let mut _columns: ::std::vec::Vec<&'static str> =
3427                        ::std::vec::Vec::new();
3428                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3429                        ::std::vec::Vec::new();
3430                    #( #pushes )*
3431                    let _query = #root::core::InsertQuery::new(
3432                        <Self as #root::core::Model>::SCHEMA,
3433                        _columns,
3434                        _values,
3435                    );
3436                    #root::sql::insert_pool(pool, &_query).await
3437                }
3438
3439                /// Eloquent `Model::insertOrIgnore()` — same shape
3440                /// as the auto-PK branch above. Returns `Ok(true)`
3441                /// when inserted, `Ok(false)` when a conflict caused
3442                /// the INSERT to silently skip.
3443                ///
3444                /// # Errors
3445                /// As [`Self::insert`].
3446                pub async fn insert_or_ignore(
3447                    &mut self,
3448                    pool: &#root::sql::Pool,
3449                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3450                    #insert_or_ignore_call
3451                }
3452            }
3453        } else {
3454            quote! {
3455                /// Insert this row against either backend, populating any
3456                /// `Auto<T>` PK from the auto-assigned value.
3457                ///
3458                /// # Errors
3459                /// As [`Self::insert`].
3460                pub async fn insert_pool(
3461                    &mut self,
3462                    pool: &#root::sql::Pool,
3463                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3464                    let mut _columns: ::std::vec::Vec<&'static str> =
3465                        ::std::vec::Vec::new();
3466                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3467                        ::std::vec::Vec::new();
3468                    #( #pushes )*
3469                    let _query = #root::core::InsertQuery::new(
3470                        <Self as #root::core::Model>::SCHEMA,
3471                        _columns,
3472                        _values,
3473                    )
3474                    .returning(::std::vec![ #( #returning_cols ),* ]);
3475                    let _result = #root::sql::insert_returning_pool(
3476                        pool, &_query,
3477                    ).await?;
3478                    #root::sql::apply_auto_pk(_result, self)
3479                }
3480
3481                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3482                /// row or silently skip on unique-constraint
3483                /// violation. Returns `Ok(true)` when a row was
3484                /// inserted, `Ok(false)` when a conflict caused the
3485                /// INSERT to silently skip.
3486                ///
3487                /// **Caveat on auto-PK models**: when the row is
3488                /// skipped (conflict), this instance's `Auto<T>`
3489                /// fields stay `Unset` — no PK is back-populated
3490                /// because the server didn't auto-assign one. For
3491                /// "insert then read back the PK or the existing
3492                /// row's PK", use the `upsert` family or
3493                /// `get_or_create`.
3494                ///
3495                /// # Errors
3496                /// As [`Self::insert`].
3497                pub async fn insert_or_ignore(
3498                    &mut self,
3499                    pool: &#root::sql::Pool,
3500                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3501                    #insert_or_ignore_call
3502                }
3503            }
3504        }
3505    } else {
3506        let insert_columns = &fields.insert_columns;
3507        let insert_values = &fields.insert_values;
3508        quote! {
3509            /// Insert this row into its table against either backend.
3510            /// Equivalent to [`Self::insert`] but takes
3511            /// [`#root::sql::Pool`].
3512            ///
3513            /// # Errors
3514            /// As [`Self::insert`].
3515            pub async fn insert_pool(
3516                &self,
3517                pool: &#root::sql::Pool,
3518            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3519                let _query = #root::core::InsertQuery::new(
3520                    <Self as #root::core::Model>::SCHEMA,
3521                    ::std::vec![ #( #insert_columns ),* ],
3522                    ::std::vec![ #( #insert_values ),* ],
3523                );
3524                #root::sql::insert_pool(pool, &_query).await
3525            }
3526
3527            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3528            /// or silently skip on unique-constraint violation. Maps
3529            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3530            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3531            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3532            /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3533            ///
3534            /// Returns `Ok(true)` when a row was inserted,
3535            /// `Ok(false)` when a conflict caused the INSERT to
3536            /// silently skip.
3537            ///
3538            /// Use for "create-if-absent" patterns where you don't
3539            /// need the row back. For "find-or-create with the row
3540            /// returned", use the queryset-level
3541            /// `crate::sql::get_or_create` free function.
3542            ///
3543            /// # Errors
3544            /// As [`Self::insert`], plus any dialect-specific
3545            /// translation error from the ConflictClause writer.
3546            pub async fn insert_or_ignore(
3547                &self,
3548                pool: &#root::sql::Pool,
3549            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3550                let _query = #root::core::InsertQuery::new(
3551                    <Self as #root::core::Model>::SCHEMA,
3552                    ::std::vec![ #( #insert_columns ),* ],
3553                    ::std::vec![ #( #insert_values ),* ],
3554                );
3555                #root::sql::insert_or_ignore(pool, &_query).await
3556            }
3557        }
3558    };
3559
3560    // The tracked (column, JSON value) pairs, spliced only into
3561    // `__rustango_audit_pairs`. Empty when the model isn't audited.
3562    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3563        .map(|tracked| {
3564            tracked
3565                .iter()
3566                .map(|c| {
3567                    let column_lit = c.column.as_str();
3568                    let ident = &c.ident;
3569                    quote! {
3570                        (
3571                            #column_lit,
3572                            #root::core::__json_value(&self.#ident),
3573                        )
3574                    }
3575                })
3576                .collect()
3577        })
3578        .unwrap_or_default();
3579    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3580        if fields.pk_is_auto {
3581            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3582        } else {
3583            quote!(::std::format!("{}", &self.#pk_ident))
3584        }
3585    } else {
3586        quote!(::std::string::String::new())
3587    };
3588    // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3589    // is built, so every audited write path records the same shape.
3590    let delete_audited_body = if primary_key.is_some() {
3591        quote! {
3592            ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3593                pool,
3594                query,
3595                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3596            ))
3597        }
3598    } else {
3599        quote! {
3600            let _ = (pool, query);
3601            ::std::boxed::Box::pin(async {
3602                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3603                    table: <Self as #root::core::Model>::SCHEMA.table,
3604                })
3605            })
3606        }
3607    };
3608    let cascade_audited_body = if primary_key.is_some() {
3609        quote! {
3610            ::std::boxed::Box::pin(#root::audit::cascade_rows::<Self>(
3611                tx,
3612                <Self as #root::core::Model>::SCHEMA,
3613                rows,
3614                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3615            ))
3616        }
3617    } else {
3618        quote! {
3619            let _ = (tx, rows);
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 update_audited_body = if primary_key.is_some() {
3628        quote! {
3629            ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3630                pool,
3631                query,
3632                move |_r: &Self| _r.__rustango_audit_entry(op),
3633            ))
3634        }
3635    } else {
3636        quote! {
3637            let _ = (pool, query, op);
3638            ::std::boxed::Box::pin(async {
3639                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3640                    table: <Self as #root::core::Model>::SCHEMA.table,
3641                })
3642            })
3643        }
3644    };
3645    let audit_entry_method = if audited_fields.is_some() {
3646        let pairs = audit_pair_tokens.iter();
3647        let pk_str = audit_pk_to_string.clone();
3648        quote! {
3649            /// This row's tracked `(column, value)` pairs.
3650            #[doc(hidden)]
3651            pub fn __rustango_audit_pairs(
3652                &self,
3653            ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3654                ::std::vec![ #( #pairs ),* ]
3655            }
3656
3657            /// This row's audit PK.
3658            #[doc(hidden)]
3659            pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3660                #pk_str
3661            }
3662
3663            /// Snapshot audit entry for `operation` on this row.
3664            #[doc(hidden)]
3665            pub fn __rustango_audit_entry(
3666                &self,
3667                operation: #root::audit::AuditOp,
3668            ) -> #root::audit::PendingEntry {
3669                #root::audit::PendingEntry {
3670                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3671                    entity_pk: self.__rustango_audit_pk(),
3672                    operation,
3673                    source: #root::audit::current_source(),
3674                    changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3675                }
3676            }
3677
3678            /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3679            #[doc(hidden)]
3680            pub fn __rustango_update_audited<'a>(
3681                pool: &'a #root::sql::Pool,
3682                query: &'a #root::core::UpdateQuery,
3683                op: #root::audit::AuditOp,
3684            ) -> ::std::pin::Pin<::std::boxed::Box<
3685                dyn ::core::future::Future<
3686                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3687                > + ::core::marker::Send + 'a,
3688            >> {
3689                #update_audited_body
3690            }
3691
3692            /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3693            #[doc(hidden)]
3694            pub fn __rustango_create_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_create::<Self>(
3703                    tx,
3704                    <Self as #root::core::Model>::SCHEMA,
3705                    pk,
3706                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3707                ))
3708            }
3709
3710            /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3711            #[doc(hidden)]
3712            pub fn __rustango_update_record_audited<'a, 't>(
3713                tx: &'a mut #root::sql::PoolTx<'t>,
3714                pk: #root::core::SqlValue,
3715            ) -> ::std::pin::Pin<::std::boxed::Box<
3716                dyn ::core::future::Future<
3717                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3718                > + ::core::marker::Send + 'a,
3719            >> {
3720                ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3721                    tx,
3722                    <Self as #root::core::Model>::SCHEMA,
3723                    pk,
3724                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3725                ))
3726            }
3727
3728            /// Rows an FK cascade will delete, behind `Model::__rustango_audited_cascade`.
3729            #[doc(hidden)]
3730            pub fn __rustango_cascade_audited<'a, 't>(
3731                tx: &'a mut #root::sql::PoolTx<'t>,
3732                rows: #root::core::WhereExpr,
3733            ) -> ::std::pin::Pin<::std::boxed::Box<
3734                dyn ::core::future::Future<
3735                    Output = ::core::result::Result<
3736                        ::std::vec::Vec<(#root::core::SqlValue, #root::audit::PendingEntry)>,
3737                        #root::sql::ExecError,
3738                    >,
3739                > + ::core::marker::Send + 'a,
3740            >> {
3741                #cascade_audited_body
3742            }
3743
3744            /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3745            #[doc(hidden)]
3746            pub fn __rustango_delete_audited<'a>(
3747                pool: &'a #root::sql::Pool,
3748                query: &'a #root::core::DeleteQuery,
3749            ) -> ::std::pin::Pin<::std::boxed::Box<
3750                dyn ::core::future::Future<
3751                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3752                > + ::core::marker::Send + 'a,
3753            >> {
3754                #delete_audited_body
3755            }
3756        }
3757    } else {
3758        quote!()
3759    };
3760    // Builds `_audit_entry`; `written` is the soft-delete column's new
3761    // value, recorded in place of the stale `self` field.
3762    let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3763        if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3764            quote! {
3765                let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3766                _audit_entry.set_tracked(#col, #v);
3767            }
3768        } else {
3769            quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3770        }
3771    };
3772    let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3773        .unwrap_or(#root::__serde_json::Value::Null));
3774    let restored_json = quote!(#root::__serde_json::Value::Null);
3775    // `_on` emit; `affected` paths skip the row when nothing changed.
3776    let make_op_emit =
3777        |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3778            if audited_fields.is_none() {
3779                return quote!();
3780            }
3781            let entry = make_entry(op_path, written);
3782            let emit = quote! {
3783                #entry
3784                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3785            };
3786            if affected {
3787                quote!(if _affected > 0 { #emit })
3788            } else {
3789                emit
3790            }
3791        };
3792    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3793    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3794    let audit_softdelete_emit = make_op_emit(
3795        quote!(#root::audit::AuditOp::SoftDelete),
3796        Some(soft_deleted_json.clone()),
3797        true,
3798    );
3799    let audit_restore_emit = make_op_emit(
3800        quote!(#root::audit::AuditOp::Restore),
3801        Some(restored_json.clone()),
3802        true,
3803    );
3804    // `&Pool` update runner for soft_delete / restore: one tx with the
3805    // audit row when audited, a plain UPDATE otherwise.
3806    let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3807        if audited_fields.is_some() {
3808            let entry = make_entry(op_path, Some(written));
3809            quote! {
3810                #entry
3811                #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3812            }
3813        } else {
3814            quote!(#root::sql::update_pool(pool, &_query).await)
3815        }
3816    };
3817    let pool_softdelete_run =
3818        make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3819    let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3820
3821    // `save_pool(&Pool)` — emitted for every model with a PK. The
3822    // audited `save_pool` is built below on the diff path; both share
3823    // `save_partial_methods`, which differ only in how they run `_query`.
3824    let save_ret = quote!(::core::result::Result<u64, #root::sql::ExecError>);
3825    let save_keep = quote!(matches!(_saved, ::core::result::Result::Ok(n) if n > 0));
3826    let save_partial_methods = |pk_ident: &syn::Ident,
3827                                pk_column_lit: &str,
3828                                run: TokenStream2|
3829     -> TokenStream2 {
3830        let assignments = &fields.update_assignments;
3831        // Pushed in step with `update_assignments`: one column each.
3832        let assignment_cols = &fields.upsert_update_columns;
3833        let write = restore_auto_now_unless(
3834            &fields.auto_now_idents,
3835            &save_ret,
3836            &save_keep,
3837            &quote! {
3838                // Build only the wanted assignments, so an unnamed
3839                // `auto_now` field is not stamped.
3840                let mut _filtered: ::std::vec::Vec<#root::core::Assignment> =
3841                    ::std::vec::Vec::with_capacity(_wanted_cols.len());
3842                #(
3843                    if _wanted_cols.contains(#assignment_cols) {
3844                        _filtered.push(#assignments);
3845                    }
3846                )*
3847                if _filtered.is_empty() {
3848                    // All field names valid, but they all map to
3849                    // non-assignable slots (PK column, computed/
3850                    // virtual fields, relations without an
3851                    // assignment). Same no-op as an empty list.
3852                    #root::__tracing::warn!(
3853                        target: "rustango::save_partial",
3854                        model = _schema.name,
3855                        "save_partial: every named field maps to a non-assignable column — no-op"
3856                    );
3857                    return ::core::result::Result::Ok(0);
3858                }
3859                let _query = #root::core::UpdateQuery::new(
3860                    _schema,
3861                    _filtered,
3862                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3863                        #pk_column_lit,
3864                        #root::core::Op::Eq,
3865                        ::core::convert::Into::<#root::core::SqlValue>::into(
3866                            ::core::clone::Clone::clone(&self.#pk_ident)
3867                        ),
3868                    )),
3869                );
3870                #run
3871            },
3872        );
3873        quote! {
3874            /// Save (UPDATE) only the listed Rust-side fields,
3875            /// leaving every other column untouched. Issue #66.
3876            ///
3877            /// `fields` are Rust-side struct field names; the macro
3878            /// resolves each to its SQL column. Unknown field
3879            /// names return [`#root::core::QueryError::UnknownField`]
3880            /// wrapped in `ExecError::Query`. An empty list is a
3881            /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3882            /// On an audited model the audit diff covers only these fields.
3883            ///
3884            /// Use this when:
3885            /// * you only mutated a couple of fields on a wide row
3886            ///   (avoid re-writing every column on every save), or
3887            /// * two writers diverged after their initial read and
3888            ///   you want to preserve the other writer's changes to
3889            ///   columns you didn't touch.
3890            ///
3891            /// Auto-PK models with an unset PK return
3892            /// [`#root::core::QueryError::UnknownField`] with
3893            /// field name `<pk>` — `save_partial` is an
3894            /// UPDATE-only path. Call [`Self::insert_pool`]
3895            /// (or [`Self::save_pool`] which dispatches based on
3896            /// PK state) for the INSERT case.
3897            ///
3898            /// # Errors
3899            /// As [`Self::save_pool`], plus `UnknownField` for
3900            /// unknown / empty / Auto-Unset cases.
3901            pub async fn save_partial(
3902                &mut self,
3903                fields: &[&str],
3904                pool: &#root::sql::Pool,
3905            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3906                if fields.is_empty() {
3907                    #root::__tracing::warn!(
3908                        target: "rustango::save_partial",
3909                        model = <Self as #root::core::Model>::SCHEMA.name,
3910                        "save_partial called with empty field list — no-op"
3911                    );
3912                    return ::core::result::Result::Ok(0);
3913                }
3914                let _schema = <Self as #root::core::Model>::SCHEMA;
3915                // Validate field names against the schema.
3916                let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3917                    ::std::collections::HashSet::with_capacity(fields.len());
3918                for f in fields {
3919                    match _schema.field(f) {
3920                        ::core::option::Option::Some(fs) => {
3921                            _wanted_cols.insert(fs.column);
3922                        }
3923                        ::core::option::Option::None => {
3924                            return ::core::result::Result::Err(
3925                                #root::sql::ExecError::Query(
3926                                    #root::core::QueryError::UnknownField {
3927                                        model: _schema.name,
3928                                        field: (*f).to_owned(),
3929                                    }
3930                                )
3931                            );
3932                        }
3933                    }
3934                }
3935                #write
3936            }
3937
3938            /// Typed-column counterpart of [`Self::save_partial`] —
3939            /// issue #67. `fields` is a tuple of [`Column`]
3940            /// constants whose `Model` matches `Self`; typos and
3941            /// model mismatches surface at *compile time*
3942            /// (`Author::name` inside a `Post::save_partial_typed`
3943            /// call is a type error, no runtime check).
3944            ///
3945            /// ```ignore
3946            /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3947            /// ```
3948            ///
3949            /// Lowers to [`Self::save_partial`] under the hood — the
3950            /// tuple is reduced to a `&[&str]` slice of Rust-side
3951            /// field names and forwarded.
3952            ///
3953            /// [`Column`]: #root::core::Column
3954            ///
3955            /// # Errors
3956            /// As [`Self::save_partial`].
3957            pub async fn save_partial_typed<
3958                L: #root::core::TypedFieldList<Self>,
3959            >(
3960                &mut self,
3961                fields: L,
3962                pool: &#root::sql::Pool,
3963            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3964                let _names = fields.rust_field_names();
3965                let _refs: ::std::vec::Vec<&str> =
3966                    _names.iter().copied().collect();
3967                self.save_partial(&_refs, pool).await
3968            }
3969        }
3970    };
3971    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3972        let pk_column_lit = pk_col.as_str();
3973        let assignments = &fields.update_assignments;
3974        if audited_fields.is_some() {
3975            quote!()
3976        } else {
3977            let partial = save_partial_methods(
3978                pk_ident,
3979                pk_column_lit,
3980                quote!(#root::sql::update_pool(pool, &_query).await),
3981            );
3982            let dispatch_unset = if fields.pk_is_auto {
3983                quote! {
3984                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3985                        return self.insert_pool(pool).await.map(|()| 1u64);
3986                    }
3987                }
3988            } else {
3989                quote!()
3990            };
3991            let write = restore_auto_now_unless(
3992                &fields.auto_now_idents,
3993                &save_ret,
3994                &save_keep,
3995                &quote! {
3996                    let _query = #root::core::UpdateQuery::new(
3997                        <Self as #root::core::Model>::SCHEMA,
3998                        ::std::vec![ #( #assignments ),* ],
3999                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4000                            #pk_column_lit,
4001                            #root::core::Op::Eq,
4002                            ::core::convert::Into::<#root::core::SqlValue>::into(
4003                                ::core::clone::Clone::clone(&self.#pk_ident)
4004                            ),
4005                        )),
4006                    );
4007                    let _affected = #root::sql::update_pool(pool, &_query).await?;
4008                    ::core::result::Result::Ok(_affected)
4009                },
4010            );
4011            quote! {
4012                /// Save this row to its table against either backend.
4013                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
4014                /// `UPDATE` keyed on the PK; global scopes do not apply.
4015                ///
4016                /// # Errors
4017                /// As [`Self::save`].
4018                pub async fn save_pool(
4019                    &mut self,
4020                    pool: &#root::sql::Pool,
4021                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4022                    #dispatch_unset
4023                    #write
4024                }
4025
4026                #partial
4027            }
4028        }
4029    } else {
4030        quote!()
4031    };
4032
4033    // Audited `insert_pool` (overrides the placeholder set higher up
4034    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
4035    // audited models get insert_pool routing through
4036    // audit::insert_one_with_audit (per-backend tx wraps INSERT
4037    // + auto-PK readback + audit emit). Snapshot-style audit (the
4038    // PendingEntry's `changes` carries post-write field values).
4039    let pool_insert_method = if audited_fields.is_some() {
4040        if let Some(_) = primary_key {
4041            let pushes = if fields.has_auto {
4042                fields.insert_pushes.clone()
4043            } else {
4044                // For non-Auto-PK models, the macro normally builds
4045                // {columns, values} from fields.insert_columns +
4046                // fields.insert_values rather than insert_pushes.
4047                // Map those into the pushes shape.
4048                fields
4049                    .insert_columns
4050                    .iter()
4051                    .zip(&fields.insert_values)
4052                    .map(|(col, val)| {
4053                        quote! {
4054                            _columns.push(#col);
4055                            _values.push(#val);
4056                        }
4057                    })
4058                    .collect()
4059            };
4060            // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
4061            let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
4062            {
4063                fields.returning_cols.clone()
4064            } else {
4065                // Non-Auto-PK: still need RETURNING something for the
4066                // audit helper's contract (it errors on empty
4067                // returning). Return the PK column so the audit row
4068                // can carry the assigned PK back. Some non-Auto PKs
4069                // are server-side-default (e.g. UUIDv4 default), so
4070                // RETURNING is genuinely useful.
4071                primary_key
4072                    .map(|(_, col)| {
4073                        let lit = col.as_str();
4074                        vec![quote!(#lit)]
4075                    })
4076                    .unwrap_or_default()
4077            };
4078            quote! {
4079                /// Insert this row against either backend with audit
4080                /// emission inside the same transaction. Bi-dialect
4081                /// counterpart of [`Self::insert`] for audited models.
4082                ///
4083                /// Snapshot-style audit (post-write field values).
4084                ///
4085                /// # Errors
4086                /// As [`Self::insert`].
4087                pub async fn insert_pool(
4088                    &mut self,
4089                    pool: &#root::sql::Pool,
4090                ) -> ::core::result::Result<(), #root::sql::ExecError> {
4091                    let mut _columns: ::std::vec::Vec<&'static str> =
4092                        ::std::vec::Vec::new();
4093                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
4094                        ::std::vec::Vec::new();
4095                    #( #pushes )*
4096                    let _query = #root::core::InsertQuery::new(
4097                        <Self as #root::core::Model>::SCHEMA,
4098                        _columns,
4099                        _values,
4100                    )
4101                    .returning(::std::vec![ #( #returning_cols ),* ]);
4102                    #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
4103                        _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
4104                    })
4105                    .await
4106                }
4107            }
4108        } else {
4109            quote!()
4110        }
4111    } else {
4112        // Keep the non-audited pool_insert_method we built earlier.
4113        pool_insert_method
4114    };
4115
4116    // Update audited save_pool: now that insert_pool is wired for
4117    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
4118    // insert_pool. Non-audited save_pool already does this.
4119    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
4120    // Replaces the snapshot-only emission with a per-backend transaction
4121    // body that:
4122    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
4123    //     column), capturing BEFORE values
4124    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
4125    //  3. builds AFTER pairs from &self
4126    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
4127    //     AuditOp::Update + diff_changes(...) on the same tx connection
4128    //  5. commits
4129    //
4130    // Per-backend arms inline the SQL string + placeholder shape, then
4131    // share the `audit_before_pair_tokens` decoder block (Row::try_get
4132    // is polymorphic over Row type — the same tokens work against
4133    // PgRow and MySqlRow as long as the field's Rust type implements
4134    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
4135    // primitives + chrono/uuid/serde_json::Value all do).
4136    let pool_save_method = if let Some(tracked) = audited_fields {
4137        if let Some((pk_ident, pk_col)) = primary_key {
4138            let pk_column_lit = pk_col.as_str();
4139            let partial = save_partial_methods(
4140                pk_ident,
4141                pk_column_lit,
4142                quote! {
4143                    self.__rustango_save_with_diff(
4144                        pool,
4145                        &_query,
4146                        ::core::option::Option::Some(&_wanted_cols),
4147                    )
4148                    .await
4149                },
4150            );
4151            // Per-tracked-column BEFORE-pair token list. Each entry
4152            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
4153            // The Row alias resolves to PgRow / MySqlRow per call site,
4154            // so the same template generates both the PG and MySQL bodies.
4155            let mk_before_pairs =
4156                |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
4157                    tracked
4158                        .iter()
4159                        .map(|c| {
4160                            let getter = getter(c);
4161                            let column_lit = c.column.as_str();
4162                            let value_ty = &c.value_ty;
4163                            quote! {
4164                                (
4165                                    #column_lit,
4166                                    match #getter::<#value_ty>(
4167                                        _audit_before_row, #column_lit,
4168                                    ) {
4169                                        ::core::result::Result::Ok(v) => {
4170                                            #root::core::__json_value(&v)
4171                                        }
4172                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
4173                                    },
4174                                )
4175                            }
4176                        })
4177                        .collect()
4178                };
4179            let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
4180            let before_pairs_my = mk_before_pairs(&|c| {
4181                if c.uuid_column {
4182                    quote!(#root::sql::try_get_flat_my)
4183                } else {
4184                    quote!(#root::sql::try_get_returning_my)
4185                }
4186            });
4187            let before_pairs_sqlite =
4188                mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
4189            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
4190            let pk_value_for_bind = if fields.pk_is_auto {
4191                quote!(self.#pk_ident.get().copied().unwrap_or_default())
4192            } else {
4193                quote!(::core::clone::Clone::clone(&self.#pk_ident))
4194            };
4195            let assignments = &fields.update_assignments;
4196            let unset_dispatch = if fields.has_auto {
4197                quote! {
4198                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
4199                        return self.insert_pool(pool).await.map(|()| 1u64);
4200                    }
4201                }
4202            } else {
4203                quote!()
4204            };
4205            let write = restore_auto_now_unless(
4206                &fields.auto_now_idents,
4207                &save_ret,
4208                &save_keep,
4209                &quote! {
4210                    let _query = #root::core::UpdateQuery::new(
4211                        <Self as #root::core::Model>::SCHEMA,
4212                        ::std::vec![ #( #assignments ),* ],
4213                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4214                            #pk_column_lit,
4215                            #root::core::Op::Eq,
4216                            ::core::convert::Into::<#root::core::SqlValue>::into(
4217                                ::core::clone::Clone::clone(&self.#pk_ident)
4218                            ),
4219                        )),
4220                    );
4221                    self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
4222                        .await
4223                },
4224            );
4225            quote! {
4226                /// Save this row against either backend with audit
4227                /// emission (diff-style: BEFORE+AFTER) inside the
4228                /// same transaction. Auto::Unset PK routes to
4229                /// insert_pool. Bi-dialect counterpart of
4230                /// [`Self::save`] for audited models.
4231                ///
4232                /// The audit row's `changes` JSON contains one
4233                /// `{ "field": { "before": …, "after": … } }` entry
4234                /// per tracked column whose value actually changed
4235                /// — same shape as the existing &PgPool save() emits.
4236                /// Keyed on the PK; global scopes do not apply.
4237                ///
4238                /// # Errors
4239                /// As [`Self::save`].
4240                pub async fn save_pool(
4241                    &mut self,
4242                    pool: &#root::sql::Pool,
4243                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4244                    #unset_dispatch
4245                    #write
4246                }
4247
4248                /// Run `query` and write its diff entry, limited to `only` columns.
4249                #[doc(hidden)]
4250                async fn __rustango_save_with_diff(
4251                    &self,
4252                    pool: &#root::sql::Pool,
4253                    query: &#root::core::UpdateQuery,
4254                    only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
4255                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4256                    let mut _after_pairs = self.__rustango_audit_pairs();
4257                    if let ::core::option::Option::Some(only) = only {
4258                        _after_pairs.retain(|(col, _)| only.contains(col));
4259                    }
4260                    let _before_query = #root::audit::before_image_query(
4261                        <Self as #root::core::Model>::SCHEMA,
4262                        #pk_column_lit,
4263                        ::core::convert::Into::<#root::core::SqlValue>::into(
4264                            #pk_value_for_bind,
4265                        ),
4266                        &[ #( #tracked_cols ),* ],
4267                    );
4268                    #root::audit::save_one_with_diff(
4269                        pool,
4270                        query,
4271                        &_before_query,
4272                        <Self as #root::core::Model>::SCHEMA.table,
4273                        self.__rustango_audit_pk(),
4274                        _after_pairs,
4275                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4276                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4277                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4278                    ).await
4279                }
4280
4281                #partial
4282            }
4283        } else {
4284            quote!()
4285        }
4286    } else {
4287        pool_save_method
4288    };
4289
4290    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4291    // body shapes:
4292    // - non-audited: simple dispatch through `sql::delete_pool`
4293    // - audited: routes through `audit::delete_one_with_audit`,
4294    //   which opens a per-backend transaction wrapping DELETE +
4295    //   audit emit so the data write and audit row commit atomically.
4296    let pool_delete_method = {
4297        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4298        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4299        if let Some(pk_ident) = pk_ident_for_pool {
4300            if audited_fields.is_some() {
4301                quote! {
4302                    /// Delete this row against either backend with audit
4303                    /// emission inside the same transaction. Bi-dialect
4304                    /// counterpart of [`Self::delete`] for audited models.
4305                    /// Keyed on the PK; global scopes do not apply.
4306                    ///
4307                    /// # Errors
4308                    /// As [`Self::delete`].
4309                    pub async fn delete_pool(
4310                        &self,
4311                        pool: &#root::sql::Pool,
4312                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4313                        let _query = #root::core::DeleteQuery::by_pk(
4314                            <Self as #root::core::Model>::SCHEMA,
4315                            #pk_column_lit,
4316                            ::core::convert::Into::<#root::core::SqlValue>::into(
4317                                ::core::clone::Clone::clone(&self.#pk_ident)
4318                            ),
4319                        );
4320                        let _audit_entry =
4321                            self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4322                        #root::audit::delete_one_with_audit(
4323                            pool, &_query, &_audit_entry,
4324                        ).await
4325                    }
4326                }
4327            } else {
4328                quote! {
4329                    /// Delete the row identified by this instance's primary key
4330                    /// against either backend. Equivalent to [`Self::delete`] but
4331                    /// takes [`#root::sql::Pool`] and dispatches per backend.
4332                    /// Global scopes do not apply.
4333                    ///
4334                    /// # Errors
4335                    /// As [`Self::delete`].
4336                    pub async fn delete_pool(
4337                        &self,
4338                        pool: &#root::sql::Pool,
4339                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4340                        let _query = #root::core::DeleteQuery::by_pk(
4341                            <Self as #root::core::Model>::SCHEMA,
4342                            #pk_column_lit,
4343                            ::core::convert::Into::<#root::core::SqlValue>::into(
4344                                ::core::clone::Clone::clone(&self.#pk_ident)
4345                            ),
4346                        );
4347                        #root::sql::delete_pool(pool, &_query).await
4348                    }
4349                }
4350            }
4351        } else {
4352            quote!()
4353        }
4354    };
4355
4356    // Bulk writers on audited models write one audit row per affected
4357    // row, in the write's transaction (#1747).
4358    let (bulk_delete_run, destroy_run, bulk_update_run, truncate_run) = if audited_fields.is_some()
4359    {
4360        (
4361            quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4362                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4363            })
4364            .await),
4365            quote!(#root::audit::delete_each_with_audit::<Self>(pool, &_queries, |_r: &Self| {
4366                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4367            })
4368            .await),
4369            quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4370                _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4371            })
4372            .await),
4373            quote! {
4374                let _table = <Self as #root::core::Model>::SCHEMA.table;
4375                let _sql = #root::sql::truncate_sql(pool.dialect(), _table);
4376                #root::audit::truncate_with_audit(pool, _table, &_sql).await
4377            },
4378        )
4379    } else {
4380        (
4381            quote!(#root::sql::delete_pool(pool, &_query).await),
4382            quote!(#root::sql::__rustango_delete_all_or_none(pool, _queries).await),
4383            quote!(#root::sql::update_pool(pool, &_query).await),
4384            quote!(#root::sql::truncate(pool, <Self as #root::core::Model>::SCHEMA).await),
4385        )
4386    };
4387
4388    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4389    // matching this instance's PK and overwrite the in-memory state
4390    // with the freshly-fetched columns.
4391    // Issue #825. Only emitted when the model declares a PK; non-PK
4392    // models can't address a specific row.
4393    //
4394    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4395    // every field from `self`; resets the PK to `Auto::Unset` when
4396    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4397    // a fresh autoincrement value. Non-Auto PKs preserve the source
4398    // PK — the caller must overwrite before insert. Pure-Rust, no
4399    // I/O, no dialect surface.
4400    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4401        let other_field_clones: Vec<TokenStream2> = fields
4402            .column_entries
4403            .iter()
4404            .filter(|c| &c.ident != pk_ident)
4405            .map(|c| {
4406                let ident = &c.ident;
4407                quote! {
4408                    #ident: ::core::clone::Clone::clone(&self.#ident)
4409                }
4410            })
4411            .collect();
4412        let pk_clone_token = if fields.pk_is_auto {
4413            quote! { #pk_ident: #root::sql::Auto::Unset }
4414        } else {
4415            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4416        };
4417        let replicate_doc = if fields.pk_is_auto {
4418            quote! {
4419                /// Eloquent-style `replicate()` — return a clone of this
4420                /// row with the primary key reset to [`Auto::Unset`] so
4421                /// the copy is ready for `insert_pool` / `save_pool` to
4422                /// allocate a fresh autoincrement value. Every other
4423                /// field is `Clone`d verbatim. Issue #825.
4424                ///
4425                /// `auto_now_add` / `auto_now` timestamp fields are
4426                /// **not** reset (Eloquent's `replicate` doesn't reset
4427                /// them either) — pass them through the normal insert
4428                /// path if you want fresh values, or assign them
4429                /// explicitly after the call.
4430            }
4431        } else {
4432            quote! {
4433                /// Eloquent-style `replicate()` — clone this row
4434                /// verbatim. Because the primary key is **not** an
4435                /// `Auto<T>`, the clone keeps the source PK; the
4436                /// caller must overwrite `copy.<pk>` before inserting
4437                /// to avoid a unique-key violation. Issue #825.
4438            }
4439        };
4440        // 2026-06-07 — field-name / shortcut collision guard.
4441        //
4442        // The macro emits both `pub const <field>: <field>_col = ...`
4443        // (per-field typed-column const, used by the typed-builder
4444        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4445        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4446        // model has a field named e.g. `count`, both items would land
4447        // in the same inherent impl with the same name, and the
4448        // compiler rejects the derive with "duplicate definitions".
4449        //
4450        // Drop the conflicting shortcut for that model. Callers can
4451        // still reach the same behavior via
4452        // `QuerySet::<Model>::default().count(&pool)`.
4453        let column_names: ::std::collections::HashSet<String> = fields
4454            .column_entries
4455            .iter()
4456            .map(|c| ident_name(&c.ident))
4457            .collect();
4458        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4459            if column_names.contains(name) {
4460                quote! {}
4461            } else {
4462                tokens
4463            }
4464        };
4465        let count_method = emit_if_no_field_collision(
4466            "count",
4467            quote! {
4468                /// Count rows of this model — `SELECT COUNT(*) FROM
4469                /// <table>`. Eloquent `Model::count()` parity.
4470                ///
4471                /// Skipped on models that already declare a field named
4472                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4473                /// in that case.
4474                ///
4475                /// # Errors
4476                /// As [`CounterPool::count`].
4477                ///
4478                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4479                pub async fn count(
4480                    pool: &#root::sql::Pool,
4481                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4482                    use #root::sql::CounterPool as _;
4483                    #root::query::QuerySet::<Self>::default()
4484                        .count(pool)
4485                        .await
4486                }
4487            },
4488        );
4489        let value_method = emit_if_no_field_collision(
4490            "value",
4491            quote! {
4492                /// Pluck a single scalar from the first row.
4493                /// Eloquent `Model::query()->value($col)` parity.
4494                ///
4495                /// Skipped on models that already declare a field named
4496                /// `value`. Drop into
4497                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4498                ///
4499                /// # Errors
4500                /// As `ValuesFlatQuerySet::first`.
4501                pub async fn value<U>(
4502                    col: &str,
4503                    pool: &#root::sql::Pool,
4504                ) -> ::core::result::Result<
4505                    ::core::option::Option<U>,
4506                    #root::sql::ExecError,
4507                >
4508                where
4509                    U: #root::sql::FlatScalar,
4510                {
4511                    let _col_static: &'static str = Self::__resolve_col(col)?;
4512                    #root::query::QuerySet::<Self>::default()
4513                        .values_list_flat(_col_static)
4514                        .first::<U>(pool)
4515                        .await
4516                }
4517            },
4518        );
4519        let sum_method = emit_if_no_field_collision(
4520            "sum",
4521            quote! {
4522                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4523                /// Skipped on models that already declare a field named
4524                /// `sum`.
4525                ///
4526                /// # Errors
4527                /// As [`#root::sql::fetch_aggregate_pool`].
4528                pub async fn sum<U>(
4529                    col: &str,
4530                    pool: &#root::sql::Pool,
4531                ) -> ::core::result::Result<
4532                    ::core::option::Option<U>,
4533                    #root::sql::ExecError,
4534                >
4535                where
4536                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4537                        + #root::sql::MaybeMyFromRow
4538                        + #root::sql::MaybeSqliteFromRow
4539                        + ::core::marker::Send
4540                        + ::core::marker::Unpin,
4541                {
4542                    #root::query::QuerySet::<Self>::default()
4543                        .sum::<U>(col, pool)
4544                        .await
4545                }
4546            },
4547        );
4548        let avg_method = emit_if_no_field_collision(
4549            "avg",
4550            quote! {
4551                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4552                /// models that already declare a field named `avg`.
4553                ///
4554                /// # Errors
4555                /// As [`#root::sql::fetch_aggregate_pool`].
4556                pub async fn avg<U>(
4557                    col: &str,
4558                    pool: &#root::sql::Pool,
4559                ) -> ::core::result::Result<
4560                    ::core::option::Option<U>,
4561                    #root::sql::ExecError,
4562                >
4563                where
4564                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4565                        + #root::sql::MaybeMyFromRow
4566                        + #root::sql::MaybeSqliteFromRow
4567                        + ::core::marker::Send
4568                        + ::core::marker::Unpin,
4569                {
4570                    #root::query::QuerySet::<Self>::default()
4571                        .avg::<U>(col, pool)
4572                        .await
4573                }
4574            },
4575        );
4576        let min_method = emit_if_no_field_collision(
4577            "min",
4578            quote! {
4579                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4580                /// models that already declare a field named `min`.
4581                ///
4582                /// # Errors
4583                /// As [`#root::sql::fetch_aggregate_pool`].
4584                pub async fn min<U>(
4585                    col: &str,
4586                    pool: &#root::sql::Pool,
4587                ) -> ::core::result::Result<
4588                    ::core::option::Option<U>,
4589                    #root::sql::ExecError,
4590                >
4591                where
4592                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4593                        + #root::sql::MaybeMyFromRow
4594                        + #root::sql::MaybeSqliteFromRow
4595                        + ::core::marker::Send
4596                        + ::core::marker::Unpin,
4597                {
4598                    #root::query::QuerySet::<Self>::default()
4599                        .min::<U>(col, pool)
4600                        .await
4601                }
4602            },
4603        );
4604        let max_method = emit_if_no_field_collision(
4605            "max",
4606            quote! {
4607                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4608                /// models that already declare a field named `max`.
4609                ///
4610                /// # Errors
4611                /// As [`#root::sql::fetch_aggregate_pool`].
4612                pub async fn max<U>(
4613                    col: &str,
4614                    pool: &#root::sql::Pool,
4615                ) -> ::core::result::Result<
4616                    ::core::option::Option<U>,
4617                    #root::sql::ExecError,
4618                >
4619                where
4620                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4621                        + #root::sql::MaybeMyFromRow
4622                        + #root::sql::MaybeSqliteFromRow
4623                        + ::core::marker::Send
4624                        + ::core::marker::Unpin,
4625                {
4626                    #root::query::QuerySet::<Self>::default()
4627                        .max::<U>(col, pool)
4628                        .await
4629                }
4630            },
4631        );
4632        let first_method = emit_if_no_field_collision(
4633            "first",
4634            quote! {
4635                /// First row of this model. Eloquent `Model::first()`.
4636                /// Skipped on models that already declare a field named
4637                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4638                ///
4639                /// # Errors
4640                /// As `QuerySet::first`.
4641                pub async fn first(
4642                    pool: &#root::sql::Pool,
4643                ) -> ::core::result::Result<
4644                    ::core::option::Option<Self>,
4645                    #root::sql::ExecError,
4646                > {
4647                    #root::query::QuerySet::<Self>::default()
4648                        .first(pool)
4649                        .await
4650                }
4651            },
4652        );
4653        let last_method = emit_if_no_field_collision(
4654            "last",
4655            quote! {
4656                /// Last row of this model by primary-key DESC.
4657                /// Eloquent `Model::query()->latest('id')->first()`
4658                /// parity — fetches the highest-PK row without
4659                /// requiring the caller to spell the PK column.
4660                /// Returns `None` on an empty table.
4661                ///
4662                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4663                /// Skipped on models that already declare a field
4664                /// named `last`.
4665                ///
4666                /// # Errors
4667                /// As `QuerySet::last`.
4668                pub async fn last(
4669                    pool: &#root::sql::Pool,
4670                ) -> ::core::result::Result<
4671                    ::core::option::Option<Self>,
4672                    #root::sql::ExecError,
4673                > {
4674                    #root::query::QuerySet::<Self>::default()
4675                        .last(pool)
4676                        .await
4677                }
4678            },
4679        );
4680        quote! {
4681            /// Re-SELECT this row by its primary key and overwrite
4682            /// every in-memory field with the freshly-fetched value.
4683            /// Issue #825.
4684            ///
4685            /// Use this when the row may have been modified by another
4686            /// process / connection / job since you read it — e.g. after
4687            /// a queued task callback, or to re-sync stale UI state
4688            /// before re-saving.
4689            ///
4690            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4691            /// when the primary key no longer matches any row (e.g.
4692            /// the row was deleted concurrently).
4693            ///
4694            /// # Errors
4695            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4696            /// the PK no longer exists.
4697            ///
4698            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4699            pub async fn refresh_from_db(
4700                &mut self,
4701                pool: &#root::sql::Pool,
4702            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4703                use #root::sql::FetcherPool as _;
4704                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4705                    ::core::clone::Clone::clone(&self.#pk_ident),
4706                );
4707                let mut _rows: ::std::vec::Vec<Self> =
4708                    #root::query::QuerySet::<Self>::default()
4709                        .filter(::core::stringify!(#pk_ident), _pk_val)
4710                        .limit(1)
4711                        .fetch(pool)
4712                        .await?;
4713                match _rows.into_iter().next() {
4714                    ::core::option::Option::Some(_fresh) => {
4715                        *self = _fresh;
4716                        ::core::result::Result::Ok(())
4717                    }
4718                    ::core::option::Option::None => ::core::result::Result::Err(
4719                        #root::sql::ExecError::Driver(
4720                            #root::sql::sqlx::Error::RowNotFound,
4721                        ),
4722                    ),
4723                }
4724            }
4725
4726            /// Atomically increment the integer column `col` by
4727            /// `by` for this row. Equivalent to
4728            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4729            ///
4730            /// **Doesn't mutate `self`** — the in-memory copy is now
4731            /// stale; call [`Self::refresh_from_db`] /
4732            /// [`Self::fresh`] to re-sync. Returns the rows-
4733            /// affected count (0 when the PK doesn't match any row,
4734            /// 1 on success).
4735            ///
4736            /// `col` is the Rust field name as a string; unknown
4737            /// fields surface as `UnknownField` at runtime. Negative
4738            /// `by` values atomically decrement (see also
4739            /// [`Self::decrement`]).
4740            ///
4741            /// # Errors
4742            /// As [`UpdaterPool::execute_pool`].
4743            ///
4744            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4745            pub async fn increment(
4746                &self,
4747                col: &str,
4748                by: i64,
4749                pool: &#root::sql::Pool,
4750            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4751                Self::__increment_one(self, col, by, pool).await
4752            }
4753
4754            /// Sibling of [`Self::increment`] — atomically
4755            /// decrement this row's `col` by `by`. Eloquent
4756            /// `$model->decrement($col, $by)` parity. Equivalent to
4757            /// `self.increment(col, -by, &pool)`; the separate name
4758            /// keeps call sites readable.
4759            ///
4760            /// # Errors
4761            /// As [`Self::increment`].
4762            pub async fn decrement(
4763                &self,
4764                col: &str,
4765                by: i64,
4766                pool: &#root::sql::Pool,
4767            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4768                Self::__increment_one(self, col, -by, pool).await
4769            }
4770
4771            /// Bulk-increment: add `by` to `col` on every row of the
4772            /// table. Eloquent `Model::query()->increment($col, $by)`
4773            /// parity. Use for counters, score adjustments, view
4774            /// rollups.
4775            ///
4776            /// # Errors
4777            /// As [`UpdaterPool::execute_pool`].
4778            ///
4779            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4780            pub async fn increment_each(
4781                col: &str,
4782                by: i64,
4783                pool: &#root::sql::Pool,
4784            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4785                Self::__increment_all(col, by, pool).await
4786            }
4787
4788            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4789            ///
4790            /// # Errors
4791            /// As [`Self::increment_each`].
4792            pub async fn decrement_each(
4793                col: &str,
4794                by: i64,
4795                pool: &#root::sql::Pool,
4796            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4797                Self::__increment_all(col, -by, pool).await
4798            }
4799
4800            /// Internal: forward to
4801            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4802            /// One-line wrapper kept as a per-Model method so the
4803            /// macro's emitted `increment` / `decrement` instance
4804            /// calls don't have to thread `Self` through manually.
4805            #[doc(hidden)]
4806            pub async fn __increment_one(
4807                this: &Self,
4808                col: &str,
4809                by: i64,
4810                pool: &#root::sql::Pool,
4811            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4812                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4813                    ::core::clone::Clone::clone(&this.#pk_ident),
4814                );
4815                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4816                    ::core::stringify!(#pk_ident),
4817                    _pk_val,
4818                    col,
4819                    by,
4820                    pool,
4821                )
4822                .await
4823            }
4824
4825            /// Internal: run
4826            /// [`#root::sql::model_shortcuts::increment_all_query`].
4827            #[doc(hidden)]
4828            pub async fn __increment_all(
4829                col: &str,
4830                by: i64,
4831                pool: &#root::sql::Pool,
4832            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4833                let _query =
4834                    #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4835                #bulk_update_run
4836            }
4837
4838            /// Internal: forward to
4839            /// [`#root::sql::model_shortcuts::resolve_col`].
4840            #[doc(hidden)]
4841            pub fn __resolve_col(
4842                col: &str,
4843            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4844                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4845            }
4846
4847            /// Internal: forward to
4848            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4849            #[doc(hidden)]
4850            #[must_use]
4851            pub fn __add_signed_expr(
4852                col_static: &'static str,
4853                signed_by: i64,
4854            ) -> #root::core::Expr {
4855                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4856            }
4857
4858            /// Re-SELECT this row by its primary key and return a
4859            /// **new** instance with the freshly-fetched fields.
4860            /// Eloquent `Model::fresh()` parity — non-mutating
4861            /// counterpart of [`Self::refresh_from_db`].
4862            ///
4863            /// Returns `Ok(None)` when the row was deleted
4864            /// concurrently — vs [`Self::refresh_from_db`]
4865            /// which surfaces that as `RowNotFound` because
4866            /// in-place mutation has nothing to write to.
4867            ///
4868            /// Useful when you want to compare the in-memory
4869            /// instance against the persisted state (audit-style
4870            /// diffs, conflict detection) without mutating the
4871            /// reference you already hold.
4872            ///
4873            /// # Errors
4874            /// As [`FetcherPool::fetch`].
4875            ///
4876            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4877            pub async fn fresh(
4878                &self,
4879                pool: &#root::sql::Pool,
4880            ) -> ::core::result::Result<
4881                ::core::option::Option<Self>,
4882                #root::sql::ExecError,
4883            > {
4884                use #root::sql::FetcherPool as _;
4885                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4886                    ::core::clone::Clone::clone(&self.#pk_ident),
4887                );
4888                let _rows: ::std::vec::Vec<Self> =
4889                    #root::query::QuerySet::<Self>::default()
4890                        .filter(::core::stringify!(#pk_ident), _pk_val)
4891                        .limit(1)
4892                        .fetch(pool)
4893                        .await?;
4894                ::core::result::Result::Ok(_rows.into_iter().next())
4895            }
4896
4897            #replicate_doc
4898            #[must_use]
4899            pub fn replicate(&self) -> Self {
4900                Self {
4901                    #pk_clone_token,
4902                    #( #other_field_clones, )*
4903                }
4904            }
4905
4906            #first_method
4907
4908            #last_method
4909
4910            /// Throwing counterpart of [`Self::first`] —
4911            /// errors with `RowNotFound` when the table is empty.
4912            /// Eloquent `Model::firstOrFail()` parity.
4913            ///
4914            /// # Errors
4915            /// As [`Self::first`]; additionally
4916            /// [`sqlx::Error::RowNotFound`] on empty tables.
4917            ///
4918            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4919            pub async fn first_or_fail(
4920                pool: &#root::sql::Pool,
4921            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4922                // Route through the queryset rather than `Self::first`,
4923                // which is suppressed on models with a field named
4924                // `first` (field/shortcut collision guard).
4925                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4926                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4927                    ::core::option::Option::None => ::core::result::Result::Err(
4928                        #root::sql::ExecError::Driver(
4929                            #root::sql::sqlx::Error::RowNotFound,
4930                        ),
4931                    ),
4932                }
4933            }
4934
4935            /// Single-column projection — `SELECT <col> FROM
4936            /// <table>`. Returns `Vec<U>` where each element is the
4937            /// decoded value of the column. Eloquent
4938            /// `Model::pluck($column)` parity.
4939            ///
4940            /// Thin wrapper over `QuerySet::<Self>::default()
4941            /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4942            /// `FlatScalar`; use `Option<_>` for a nullable column.
4943            ///
4944            /// # Errors
4945            /// As `ValuesFlatQuerySet::fetch`.
4946            pub async fn pluck<U>(
4947                col: &'static str,
4948                pool: &#root::sql::Pool,
4949            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4950            where
4951                U: #root::sql::FlatScalar,
4952            {
4953                #root::query::QuerySet::<Self>::default()
4954                    .values_list_flat(col)
4955                    .fetch::<U>(pool)
4956                    .await
4957            }
4958
4959            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4960            /// stream every row of this model in batches of `n`,
4961            /// invoking the callback once per batch. Stable PK-ASC
4962            /// ordering so the LIMIT/OFFSET pagination is
4963            /// deterministic across drivers.
4964            ///
4965            /// The callback is async — it can do further DB work
4966            /// (writes, related-row lookups, queue dispatch) per
4967            /// batch. Return `Err(...)` from the callback to abort
4968            /// the iteration early; the error bubbles up.
4969            ///
4970            /// **When to use**: bulk processing flows that can't
4971            /// fit the whole table in memory — sending newsletters,
4972            /// running data migrations, computing summary
4973            /// statistics. For small / known-bounded tables, plain
4974            /// `Self::all(&pool)` is simpler.
4975            ///
4976            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4977            /// large tables. For multi-million-row scans prefer
4978            /// keyset-by-PK (the standard "WHERE id > last_seen"
4979            /// shape) over `chunk(...)`.
4980            ///
4981            /// Skipped on models without a primary key — chunking
4982            /// needs a stable order to avoid skipping / repeating
4983            /// rows across batches.
4984            pub async fn chunk<F, Fut>(
4985                n: i64,
4986                pool: &#root::sql::Pool,
4987                mut cb: F,
4988            ) -> ::core::result::Result<(), #root::sql::ExecError>
4989            where
4990                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4991                Fut: ::core::future::Future<
4992                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4993                >,
4994            {
4995                use #root::sql::FetcherPool as _;
4996                // The field name: `filter` / `order_by` take fields, not columns.
4997                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4998                    ::core::option::Option::Some(f) => f.name,
4999                    ::core::option::Option::None => {
5000                        return ::core::result::Result::Ok(());
5001                    }
5002                };
5003                let mut offset: i64 = 0;
5004                loop {
5005                    let rows: ::std::vec::Vec<Self> =
5006                        #root::query::QuerySet::<Self>::default()
5007                            .order_by(&[(pk_col, false)])
5008                            .limit(n)
5009                            .offset(offset)
5010                            .fetch(pool)
5011                            .await?;
5012                    if rows.is_empty() {
5013                        return ::core::result::Result::Ok(());
5014                    }
5015                    let len = rows.len() as i64;
5016                    cb(rows).await?;
5017                    if len < n {
5018                        return ::core::result::Result::Ok(());
5019                    }
5020                    offset += n;
5021                }
5022            }
5023
5024            /// Eloquent `Model::chunkById($n, fn (...))` — same
5025            /// per-batch callback shape as [`Self::chunk`], but uses
5026            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
5027            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
5028            /// — the right choice for multi-million-row sweeps.
5029            ///
5030            /// The keyset compares the primary key in its own type,
5031            /// so any orderable PK works (`i32`, `i64`, UUID, string).
5032            ///
5033            /// Callback errors abort iteration; the error bubbles up
5034            /// unchanged. Empty table → callback invoked zero times.
5035            pub async fn chunk_by_id<F, Fut>(
5036                n: i64,
5037                pool: &#root::sql::Pool,
5038                mut cb: F,
5039            ) -> ::core::result::Result<(), #root::sql::ExecError>
5040            where
5041                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
5042                Fut: ::core::future::Future<
5043                    Output = ::core::result::Result<(), #root::sql::ExecError>,
5044                >,
5045            {
5046                use #root::sql::FetcherPool as _;
5047                // The field name: `filter` / `order_by` take fields, not columns.
5048                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
5049                    ::core::option::Option::Some(f) => f.name,
5050                    ::core::option::Option::None => {
5051                        return ::core::result::Result::Ok(());
5052                    }
5053                };
5054                // The largest PK seen, in the PK's own type; none before
5055                // the first batch.
5056                let key = ::std::format!("{}__gt", pk_col);
5057                let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
5058                    ::core::option::Option::None;
5059                loop {
5060                    let mut qs = #root::query::QuerySet::<Self>::default();
5061                    if let ::core::option::Option::Some(v) = last_seen.take() {
5062                        qs = qs.filter(key.as_str(), v);
5063                    }
5064                    let rows: ::std::vec::Vec<Self> = qs
5065                        .order_by(&[(pk_col, false)])
5066                        .limit(n)
5067                        .fetch(pool)
5068                        .await?;
5069                    let len = rows.len() as i64;
5070                    // Capture the last row's PK BEFORE moving rows
5071                    // into the callback.
5072                    let ::core::option::Option::Some(max_pk) =
5073                        rows.last().map(|r| r.__rustango_pk_value())
5074                    else {
5075                        return ::core::result::Result::Ok(());
5076                    };
5077                    cb(rows).await?;
5078                    if len < n {
5079                        return ::core::result::Result::Ok(());
5080                    }
5081                    last_seen = ::core::option::Option::Some(max_pk);
5082                }
5083            }
5084
5085            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
5086            /// per-row callback companion to [`Self::chunk`].
5087            /// Streams every row in keyset-paginated batches of
5088            /// `batch` size, calling `cb` once per row.
5089            ///
5090            /// Inherits the keyset-paginated scan of
5091            /// [`Self::chunk_by_id`] (O(N) total).
5092            ///
5093            /// ```ignore
5094            /// Post::each(500, &pool, |p| async move {
5095            ///     reindex(p).await?;
5096            ///     Ok(())
5097            /// }).await?;
5098            /// ```
5099            ///
5100            /// Return `Err(...)` from the callback to abort
5101            /// iteration; the error bubbles up unchanged.
5102            pub async fn each<F, Fut>(
5103                batch: i64,
5104                pool: &#root::sql::Pool,
5105                mut cb: F,
5106            ) -> ::core::result::Result<(), #root::sql::ExecError>
5107            where
5108                F: ::core::ops::FnMut(Self) -> Fut,
5109                Fut: ::core::future::Future<
5110                    Output = ::core::result::Result<(), #root::sql::ExecError>,
5111                >,
5112            {
5113                use #root::sql::FetcherPool as _;
5114                // The field name: `filter` / `order_by` take fields, not columns.
5115                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
5116                    ::core::option::Option::Some(f) => f.name,
5117                    ::core::option::Option::None => {
5118                        return ::core::result::Result::Ok(());
5119                    }
5120                };
5121                let key = ::std::format!("{}__gt", pk_col);
5122                let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
5123                    ::core::option::Option::None;
5124                loop {
5125                    let mut qs = #root::query::QuerySet::<Self>::default();
5126                    if let ::core::option::Option::Some(v) = last_seen.take() {
5127                        qs = qs.filter(key.as_str(), v);
5128                    }
5129                    let rows: ::std::vec::Vec<Self> = qs
5130                        .order_by(&[(pk_col, false)])
5131                        .limit(batch)
5132                        .fetch(pool)
5133                        .await?;
5134                    let len = rows.len() as i64;
5135                    let ::core::option::Option::Some(max_pk) =
5136                        rows.last().map(|r| r.__rustango_pk_value())
5137                    else {
5138                        return ::core::result::Result::Ok(());
5139                    };
5140                    for row in rows {
5141                        cb(row).await?;
5142                    }
5143                    if len < batch {
5144                        return ::core::result::Result::Ok(());
5145                    }
5146                    last_seen = ::core::option::Option::Some(max_pk);
5147                }
5148            }
5149
5150            /// Delete every row of this model — `TRUNCATE TABLE
5151            /// <table> RESTART IDENTITY CASCADE` on Postgres,
5152            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
5153            /// support `TRUNCATE` inside foreign-key constraints
5154            /// or — for SQLite — at all). Eloquent `Model::truncate()`
5155            /// parity.
5156            ///
5157            /// **Use only in tests / fixture-reset flows.** Production
5158            /// writes through this would silently bypass the
5159            /// `pre_delete` / `post_delete` signals (no per-row hooks
5160            /// fire on a TRUNCATE / bulk DELETE FROM). An audited model
5161            /// gets one bulk audit entry, not one per row. Global scopes
5162            /// do not apply: every row goes, generic M2M links too.
5163            ///
5164            /// # Errors
5165            /// As [`truncate`].
5166            ///
5167            /// [`truncate`]: rustango::sql::truncate
5168            pub async fn truncate(
5169                pool: &#root::sql::Pool,
5170            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5171                #truncate_run
5172            }
5173
5174            /// Bulk-delete every row whose primary key is in
5175            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
5176            /// Returns the affected row count.
5177            ///
5178            /// Eloquent `Model::destroy([1, 2, 3])` parity.
5179            /// Empty `pks` is a no-op (returns 0). All or none: past
5180            /// the bind cap the batches share one transaction.
5181            ///
5182            /// Accepts any iterable whose elements are
5183            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
5184            /// `[i64; N]`, etc.
5185            ///
5186            /// # Errors
5187            /// As `delete_pool`.
5188            pub async fn destroy<V>(
5189                pks: impl ::core::iter::IntoIterator<Item = V>,
5190                pool: &#root::sql::Pool,
5191            ) -> ::core::result::Result<u64, #root::sql::ExecError>
5192            where
5193                V: ::core::convert::Into<#root::core::SqlValue>,
5194            {
5195                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5196                    pks.into_iter().map(::core::convert::Into::into).collect();
5197                if _values.is_empty() {
5198                    return ::core::result::Result::Ok(0);
5199                }
5200                let _pk = <Self as #root::core::Model>::SCHEMA
5201                    .primary_key()
5202                    .ok_or_else(|| {
5203                        #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
5204                    })?;
5205                // One statement per `IN` list the dialect can bind, all or none.
5206                let mut _queries = ::std::vec::Vec::new();
5207                for _keys in #root::sql::__rustango_in_batches::<Self>(pool, _values)? {
5208                    _queries.push(
5209                        #root::query::QuerySet::<Self>::default()
5210                            .filter_op(
5211                                _pk.name,
5212                                #root::core::Op::In,
5213                                #root::core::SqlValue::List(_keys),
5214                            )
5215                            .compile_delete()?,
5216                    );
5217                }
5218                #destroy_run
5219            }
5220
5221            /// Fetch every row where `<col> = <val>`. Eloquent
5222            /// `Model::where($col, $val)->get()` parity.
5223            ///
5224            /// Thin wrapper over `QuerySet::<Self>::default()
5225            /// .filter(col, val).fetch(pool)`. For one row,
5226            /// use [`Self::first_where`]; for a chain that
5227            /// needs further `.filter()` / `.order_by()` /
5228            /// `.limit()`, drop down to `Self::query().filter(...)`
5229            /// directly.
5230            ///
5231            /// `val` accepts any value `Into<SqlValue>` so plain
5232            /// strings, ints, UUIDs, etc. all work.
5233            ///
5234            /// # Errors
5235            /// As [`FetcherPool::fetch`].
5236            ///
5237            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5238            pub async fn where_(
5239                col: &str,
5240                val: impl ::core::convert::Into<#root::core::SqlValue>,
5241                pool: &#root::sql::Pool,
5242            ) -> ::core::result::Result<
5243                ::std::vec::Vec<Self>,
5244                #root::sql::ExecError,
5245            > {
5246                use #root::sql::FetcherPool as _;
5247                #root::query::QuerySet::<Self>::default()
5248                    .filter(col, val)
5249                    .fetch(pool)
5250                    .await
5251            }
5252
5253            /// Fetch every row where `<col> IN (vals)`. Eloquent
5254            /// `Model::whereIn($col, $vals)->get()` parity. Empty
5255            /// `vals` returns no rows (matches SQL's empty-IN
5256            /// semantics).
5257            ///
5258            /// `vals` accepts any iterable whose items are
5259            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5260            /// etc.
5261            ///
5262            /// # Errors
5263            /// As [`FetcherPool::fetch`].
5264            ///
5265            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5266            pub async fn where_in<V>(
5267                col: &str,
5268                vals: impl ::core::iter::IntoIterator<Item = V>,
5269                pool: &#root::sql::Pool,
5270            ) -> ::core::result::Result<
5271                ::std::vec::Vec<Self>,
5272                #root::sql::ExecError,
5273            >
5274            where
5275                V: ::core::convert::Into<#root::core::SqlValue>,
5276            {
5277                use #root::sql::FetcherPool as _;
5278                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5279                    vals.into_iter().map(::core::convert::Into::into).collect();
5280                if _values.is_empty() {
5281                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5282                }
5283                let _key = ::std::format!("{}__in", col);
5284                let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
5285                let _split = _batches.len() > 1;
5286                let mut _rows = ::std::vec::Vec::new();
5287                for _keys in _batches {
5288                    _rows.extend(
5289                        #root::query::QuerySet::<Self>::default()
5290                            .filter(&_key, #root::core::SqlValue::List(_keys))
5291                            .fetch(pool)
5292                            .await?,
5293                    );
5294                }
5295                if _split {
5296                    _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
5297                }
5298                ::core::result::Result::Ok(_rows)
5299            }
5300
5301            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5302            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5303            /// `vals` returns every row (matches SQL's empty-NOT-IN
5304            /// semantics — vacuously true for every row).
5305            ///
5306            /// # Errors
5307            /// As [`FetcherPool::fetch`]; `ExecError::InListUnsplittable`
5308            /// when `vals` pass the backend's bind cap.
5309            ///
5310            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5311            pub async fn where_not_in<V>(
5312                col: &str,
5313                vals: impl ::core::iter::IntoIterator<Item = V>,
5314                pool: &#root::sql::Pool,
5315            ) -> ::core::result::Result<
5316                ::std::vec::Vec<Self>,
5317                #root::sql::ExecError,
5318            >
5319            where
5320                V: ::core::convert::Into<#root::core::SqlValue>,
5321            {
5322                use #root::sql::FetcherPool as _;
5323                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5324                    vals.into_iter().map(::core::convert::Into::into).collect();
5325                if _values.is_empty() {
5326                    return #root::query::QuerySet::<Self>::default()
5327                        .fetch(pool)
5328                        .await;
5329                }
5330                let _key = ::std::format!("{}__not_in", col);
5331                // A `NOT IN` cannot be split: past the bind cap it is refused.
5332                let _values = #root::sql::__rustango_one_in_list::<Self>(pool, _values)?;
5333                #root::query::QuerySet::<Self>::default()
5334                    .filter(&_key, #root::core::SqlValue::List(_values))
5335                    .fetch(pool)
5336                    .await
5337            }
5338
5339            /// Fetch every row where `<col> IS NULL`. Eloquent
5340            /// `Model::whereNull($col)->get()` parity.
5341            ///
5342            /// # Errors
5343            /// As [`FetcherPool::fetch`].
5344            ///
5345            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5346            pub async fn where_null(
5347                col: &str,
5348                pool: &#root::sql::Pool,
5349            ) -> ::core::result::Result<
5350                ::std::vec::Vec<Self>,
5351                #root::sql::ExecError,
5352            > {
5353                use #root::sql::FetcherPool as _;
5354                let _key = ::std::format!("{}__isnull", col);
5355                #root::query::QuerySet::<Self>::default()
5356                    .filter(&_key, #root::core::SqlValue::Bool(true))
5357                    .fetch(pool)
5358                    .await
5359            }
5360
5361            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5362            /// `Model::whereNotNull($col)->get()` parity.
5363            ///
5364            /// # Errors
5365            /// As [`FetcherPool::fetch`].
5366            ///
5367            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5368            pub async fn where_not_null(
5369                col: &str,
5370                pool: &#root::sql::Pool,
5371            ) -> ::core::result::Result<
5372                ::std::vec::Vec<Self>,
5373                #root::sql::ExecError,
5374            > {
5375                use #root::sql::FetcherPool as _;
5376                let _key = ::std::format!("{}__isnull", col);
5377                #root::query::QuerySet::<Self>::default()
5378                    .filter(&_key, #root::core::SqlValue::Bool(false))
5379                    .fetch(pool)
5380                    .await
5381            }
5382
5383            /// Fetch up to `n` rows in random order. Eloquent
5384            /// `Model::inRandomOrder()->limit($n)->get()` /
5385            /// `Model::query()->inRandomOrder()->get()->take($n)`
5386            /// parity. **Performance caveat**: random ordering
5387            /// forces a full table scan + per-row random key sort;
5388            /// the optimizer cannot use an index. Prefer a
5389            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5390            ///
5391            /// # Errors
5392            /// As [`FetcherPool::fetch`].
5393            ///
5394            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5395            pub async fn random_n(
5396                n: i64,
5397                pool: &#root::sql::Pool,
5398            ) -> ::core::result::Result<
5399                ::std::vec::Vec<Self>,
5400                #root::sql::ExecError,
5401            > {
5402                use #root::sql::FetcherPool as _;
5403                #root::query::QuerySet::<Self>::default()
5404                    .order_random()
5405                    .limit(n)
5406                    .fetch(pool)
5407                    .await
5408            }
5409
5410            /// Fetch one row in random order. Eloquent
5411            /// `Model::inRandomOrder()->first()` parity. Same
5412            /// performance caveat as [`Self::random_n`].
5413            ///
5414            /// # Errors
5415            /// As [`FetcherPool::fetch`].
5416            ///
5417            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5418            pub async fn random(
5419                pool: &#root::sql::Pool,
5420            ) -> ::core::result::Result<
5421                ::core::option::Option<Self>,
5422                #root::sql::ExecError,
5423            > {
5424                ::core::result::Result::Ok(
5425                    Self::random_n(1, pool).await?.into_iter().next(),
5426                )
5427            }
5428
5429            /// Fetch every row ordered ASC by `field`. Eloquent
5430            /// `Model::oldest($field)->get()` parity — the multi-row
5431            /// counterpart of [`Self::earliest`].
5432            ///
5433            /// # Errors
5434            /// As [`FetcherPool::fetch`].
5435            ///
5436            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5437            pub async fn oldest(
5438                field: &str,
5439                pool: &#root::sql::Pool,
5440            ) -> ::core::result::Result<
5441                ::std::vec::Vec<Self>,
5442                #root::sql::ExecError,
5443            > {
5444                use #root::sql::FetcherPool as _;
5445                #root::query::QuerySet::<Self>::default()
5446                    .order_by(&[(field, false)])
5447                    .fetch(pool)
5448                    .await
5449            }
5450
5451            /// Fetch every row ordered DESC by `field`. Eloquent
5452            /// `Model::latest($field)->get()` parity — the multi-row
5453            /// counterpart of [`Self::latest`].
5454            ///
5455            /// # Errors
5456            /// As [`FetcherPool::fetch`].
5457            ///
5458            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5459            pub async fn newest(
5460                field: &str,
5461                pool: &#root::sql::Pool,
5462            ) -> ::core::result::Result<
5463                ::std::vec::Vec<Self>,
5464                #root::sql::ExecError,
5465            > {
5466                use #root::sql::FetcherPool as _;
5467                #root::query::QuerySet::<Self>::default()
5468                    .order_by(&[(field, true)])
5469                    .fetch(pool)
5470                    .await
5471            }
5472
5473            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5474            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5475            /// Routes through the existing `__year` lookup suffix
5476            /// (issue #829).
5477            ///
5478            /// # Errors
5479            /// As [`FetcherPool::fetch`].
5480            ///
5481            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5482            pub async fn where_year(
5483                col: &str,
5484                year: i64,
5485                pool: &#root::sql::Pool,
5486            ) -> ::core::result::Result<
5487                ::std::vec::Vec<Self>,
5488                #root::sql::ExecError,
5489            > {
5490                use #root::sql::FetcherPool as _;
5491                let _key = ::std::format!("{}__year", col);
5492                #root::query::QuerySet::<Self>::default()
5493                    .filter(&_key, #root::core::SqlValue::I64(year))
5494                    .fetch(pool)
5495                    .await
5496            }
5497
5498            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5499            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5500            /// `month` is 1–12.
5501            ///
5502            /// # Errors
5503            /// As [`FetcherPool::fetch`].
5504            ///
5505            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5506            pub async fn where_month(
5507                col: &str,
5508                month: i64,
5509                pool: &#root::sql::Pool,
5510            ) -> ::core::result::Result<
5511                ::std::vec::Vec<Self>,
5512                #root::sql::ExecError,
5513            > {
5514                use #root::sql::FetcherPool as _;
5515                let _key = ::std::format!("{}__month", col);
5516                #root::query::QuerySet::<Self>::default()
5517                    .filter(&_key, #root::core::SqlValue::I64(month))
5518                    .fetch(pool)
5519                    .await
5520            }
5521
5522            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5523            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5524            /// `day` is 1–31.
5525            ///
5526            /// # Errors
5527            /// As [`FetcherPool::fetch`].
5528            ///
5529            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5530            pub async fn where_day(
5531                col: &str,
5532                day: i64,
5533                pool: &#root::sql::Pool,
5534            ) -> ::core::result::Result<
5535                ::std::vec::Vec<Self>,
5536                #root::sql::ExecError,
5537            > {
5538                use #root::sql::FetcherPool as _;
5539                let _key = ::std::format!("{}__day", col);
5540                #root::query::QuerySet::<Self>::default()
5541                    .filter(&_key, #root::core::SqlValue::I64(day))
5542                    .fetch(pool)
5543                    .await
5544            }
5545
5546            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5547            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5548            /// `hour` is 0–23.
5549            ///
5550            /// # Errors
5551            /// As [`FetcherPool::fetch`].
5552            ///
5553            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5554            pub async fn where_hour(
5555                col: &str,
5556                hour: i64,
5557                pool: &#root::sql::Pool,
5558            ) -> ::core::result::Result<
5559                ::std::vec::Vec<Self>,
5560                #root::sql::ExecError,
5561            > {
5562                use #root::sql::FetcherPool as _;
5563                let _key = ::std::format!("{}__hour", col);
5564                #root::query::QuerySet::<Self>::default()
5565                    .filter(&_key, #root::core::SqlValue::I64(hour))
5566                    .fetch(pool)
5567                    .await
5568            }
5569
5570            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5571            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5572            /// `minute` is 0–59.
5573            ///
5574            /// # Errors
5575            /// As [`FetcherPool::fetch`].
5576            ///
5577            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5578            pub async fn where_minute(
5579                col: &str,
5580                minute: i64,
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!("{}__minute", col);
5588                #root::query::QuerySet::<Self>::default()
5589                    .filter(&_key, #root::core::SqlValue::I64(minute))
5590                    .fetch(pool)
5591                    .await
5592            }
5593
5594            /// Fetch every row where `<col> LIKE <pattern>` —
5595            /// caller-supplied pattern (must include `%` / `_`
5596            /// wildcards manually). Eloquent `Model::whereLike`
5597            /// parity.
5598            ///
5599            /// # Errors
5600            /// As [`FetcherPool::fetch`].
5601            ///
5602            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5603            pub async fn where_like(
5604                col: &str,
5605                pattern: impl ::core::convert::Into<::std::string::String>,
5606                pool: &#root::sql::Pool,
5607            ) -> ::core::result::Result<
5608                ::std::vec::Vec<Self>,
5609                #root::sql::ExecError,
5610            > {
5611                use #root::sql::FetcherPool as _;
5612                let _key = ::std::format!("{}__like", col);
5613                #root::query::QuerySet::<Self>::default()
5614                    .filter(
5615                        &_key,
5616                        #root::core::SqlValue::String(pattern.into()),
5617                    )
5618                    .fetch(pool)
5619                    .await
5620            }
5621
5622            /// Fetch every row where `<col> ILIKE <pattern>` —
5623            /// case-insensitive LIKE (PG native, MySQL/SQLite
5624            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5625            ///
5626            /// # Errors
5627            /// As [`FetcherPool::fetch`].
5628            ///
5629            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5630            pub async fn where_ilike(
5631                col: &str,
5632                pattern: impl ::core::convert::Into<::std::string::String>,
5633                pool: &#root::sql::Pool,
5634            ) -> ::core::result::Result<
5635                ::std::vec::Vec<Self>,
5636                #root::sql::ExecError,
5637            > {
5638                use #root::sql::FetcherPool as _;
5639                let _key = ::std::format!("{}__ilike", col);
5640                #root::query::QuerySet::<Self>::default()
5641                    .filter(
5642                        &_key,
5643                        #root::core::SqlValue::String(pattern.into()),
5644                    )
5645                    .fetch(pool)
5646                    .await
5647            }
5648
5649            /// Fetch every row where `<col>` starts with `prefix`
5650            /// (auto-appends `%`). Eloquent
5651            /// `whereLike("col", "$prefix%")` parity.
5652            ///
5653            /// # Errors
5654            /// As [`FetcherPool::fetch`].
5655            ///
5656            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5657            pub async fn where_starts_with(
5658                col: &str,
5659                prefix: impl ::core::convert::Into<::std::string::String>,
5660                pool: &#root::sql::Pool,
5661            ) -> ::core::result::Result<
5662                ::std::vec::Vec<Self>,
5663                #root::sql::ExecError,
5664            > {
5665                use #root::sql::FetcherPool as _;
5666                let _key = ::std::format!("{}__startswith", col);
5667                #root::query::QuerySet::<Self>::default()
5668                    .filter(
5669                        &_key,
5670                        #root::core::SqlValue::String(prefix.into()),
5671                    )
5672                    .fetch(pool)
5673                    .await
5674            }
5675
5676            /// Fetch every row where `<col>` ends with `suffix`
5677            /// (auto-prepends `%`). Eloquent
5678            /// `whereLike("col", "%$suffix")` parity.
5679            ///
5680            /// # Errors
5681            /// As [`FetcherPool::fetch`].
5682            ///
5683            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5684            pub async fn where_ends_with(
5685                col: &str,
5686                suffix: impl ::core::convert::Into<::std::string::String>,
5687                pool: &#root::sql::Pool,
5688            ) -> ::core::result::Result<
5689                ::std::vec::Vec<Self>,
5690                #root::sql::ExecError,
5691            > {
5692                use #root::sql::FetcherPool as _;
5693                let _key = ::std::format!("{}__endswith", col);
5694                #root::query::QuerySet::<Self>::default()
5695                    .filter(
5696                        &_key,
5697                        #root::core::SqlValue::String(suffix.into()),
5698                    )
5699                    .fetch(pool)
5700                    .await
5701            }
5702
5703            /// Fetch every row where `<col>` contains `substr`
5704            /// (auto-wraps with `%`).
5705            /// Eloquent `whereLike("col", "%$substr%")` parity.
5706            ///
5707            /// # Errors
5708            /// As [`FetcherPool::fetch`].
5709            ///
5710            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5711            pub async fn where_contains(
5712                col: &str,
5713                substr: impl ::core::convert::Into<::std::string::String>,
5714                pool: &#root::sql::Pool,
5715            ) -> ::core::result::Result<
5716                ::std::vec::Vec<Self>,
5717                #root::sql::ExecError,
5718            > {
5719                use #root::sql::FetcherPool as _;
5720                let _key = ::std::format!("{}__contains", col);
5721                #root::query::QuerySet::<Self>::default()
5722                    .filter(
5723                        &_key,
5724                        #root::core::SqlValue::String(substr.into()),
5725                    )
5726                    .fetch(pool)
5727                    .await
5728            }
5729
5730            /// Fetch every row where `<col> > val`. Eloquent
5731            /// `Model::where($col, ">", $val)->get()` parity.
5732            ///
5733            /// # Errors
5734            /// As [`FetcherPool::fetch`].
5735            ///
5736            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5737            pub async fn where_gt(
5738                col: &str,
5739                val: impl ::core::convert::Into<#root::core::SqlValue>,
5740                pool: &#root::sql::Pool,
5741            ) -> ::core::result::Result<
5742                ::std::vec::Vec<Self>,
5743                #root::sql::ExecError,
5744            > {
5745                use #root::sql::FetcherPool as _;
5746                let _key = ::std::format!("{}__gt", col);
5747                #root::query::QuerySet::<Self>::default()
5748                    .filter(&_key, val)
5749                    .fetch(pool)
5750                    .await
5751            }
5752
5753            /// Fetch every row where `<col> >= val`. Eloquent
5754            /// `Model::where($col, ">=", $val)->get()` parity.
5755            ///
5756            /// # Errors
5757            /// As [`FetcherPool::fetch`].
5758            ///
5759            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5760            pub async fn where_gte(
5761                col: &str,
5762                val: impl ::core::convert::Into<#root::core::SqlValue>,
5763                pool: &#root::sql::Pool,
5764            ) -> ::core::result::Result<
5765                ::std::vec::Vec<Self>,
5766                #root::sql::ExecError,
5767            > {
5768                use #root::sql::FetcherPool as _;
5769                let _key = ::std::format!("{}__gte", col);
5770                #root::query::QuerySet::<Self>::default()
5771                    .filter(&_key, val)
5772                    .fetch(pool)
5773                    .await
5774            }
5775
5776            /// Fetch every row where `<col> < val`. Eloquent
5777            /// `Model::where($col, "<", $val)->get()` parity.
5778            ///
5779            /// # Errors
5780            /// As [`FetcherPool::fetch`].
5781            ///
5782            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5783            pub async fn where_lt(
5784                col: &str,
5785                val: impl ::core::convert::Into<#root::core::SqlValue>,
5786                pool: &#root::sql::Pool,
5787            ) -> ::core::result::Result<
5788                ::std::vec::Vec<Self>,
5789                #root::sql::ExecError,
5790            > {
5791                use #root::sql::FetcherPool as _;
5792                let _key = ::std::format!("{}__lt", col);
5793                #root::query::QuerySet::<Self>::default()
5794                    .filter(&_key, val)
5795                    .fetch(pool)
5796                    .await
5797            }
5798
5799            /// Fetch every row where `<col> <= val`. Eloquent
5800            /// `Model::where($col, "<=", $val)->get()` parity.
5801            ///
5802            /// # Errors
5803            /// As [`FetcherPool::fetch`].
5804            ///
5805            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5806            pub async fn where_lte(
5807                col: &str,
5808                val: impl ::core::convert::Into<#root::core::SqlValue>,
5809                pool: &#root::sql::Pool,
5810            ) -> ::core::result::Result<
5811                ::std::vec::Vec<Self>,
5812                #root::sql::ExecError,
5813            > {
5814                use #root::sql::FetcherPool as _;
5815                let _key = ::std::format!("{}__lte", col);
5816                #root::query::QuerySet::<Self>::default()
5817                    .filter(&_key, val)
5818                    .fetch(pool)
5819                    .await
5820            }
5821
5822            /// Fetch every row where `<col> <> val`. Eloquent
5823            /// `Model::where($col, "!=", $val)->get()` parity.
5824            ///
5825            /// # Errors
5826            /// As [`FetcherPool::fetch`].
5827            ///
5828            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5829            pub async fn where_ne(
5830                col: &str,
5831                val: impl ::core::convert::Into<#root::core::SqlValue>,
5832                pool: &#root::sql::Pool,
5833            ) -> ::core::result::Result<
5834                ::std::vec::Vec<Self>,
5835                #root::sql::ExecError,
5836            > {
5837                use #root::sql::FetcherPool as _;
5838                let _key = ::std::format!("{}__ne", col);
5839                #root::query::QuerySet::<Self>::default()
5840                    .filter(&_key, val)
5841                    .fetch(pool)
5842                    .await
5843            }
5844
5845            /// Fetch every row matching `<col> = val` for ANY of the
5846            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5847            /// parity. Empty `cols` returns no rows.
5848            ///
5849            /// Resolves each `&str` column to its SCHEMA-registered
5850            /// `&'static str` once and builds a single OR-composed
5851            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5852            ///
5853            /// # Errors
5854            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5855            /// when any column is not declared on the model.
5856            ///
5857            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5858            pub async fn where_any(
5859                cols: &[&str],
5860                val: impl ::core::convert::Into<#root::core::SqlValue>,
5861                pool: &#root::sql::Pool,
5862            ) -> ::core::result::Result<
5863                ::std::vec::Vec<Self>,
5864                #root::sql::ExecError,
5865            > {
5866                Self::__where_multi(cols, val, false, pool).await
5867            }
5868
5869            /// Fetch every row matching `<col> = val` for ALL listed
5870            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5871            /// Empty `cols` returns every row (vacuous AND).
5872            ///
5873            /// # Errors
5874            /// As [`Self::where_any`].
5875            pub async fn where_all(
5876                cols: &[&str],
5877                val: impl ::core::convert::Into<#root::core::SqlValue>,
5878                pool: &#root::sql::Pool,
5879            ) -> ::core::result::Result<
5880                ::std::vec::Vec<Self>,
5881                #root::sql::ExecError,
5882            > {
5883                Self::__where_multi(cols, val, true, pool).await
5884            }
5885
5886            /// Internal: build a Q expression composing `cols` via
5887            /// AND (`all`) or OR (`!all`), then fetch. Backs
5888            /// `where_any` / `where_all`.
5889            #[doc(hidden)]
5890            pub async fn __where_multi(
5891                cols: &[&str],
5892                val: impl ::core::convert::Into<#root::core::SqlValue>,
5893                all: bool,
5894                pool: &#root::sql::Pool,
5895            ) -> ::core::result::Result<
5896                ::std::vec::Vec<Self>,
5897                #root::sql::ExecError,
5898            > {
5899                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5900                    .await
5901            }
5902
5903            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5904            /// parity. PK-ordered
5905            /// is NOT guaranteed without an explicit `order_by` —
5906            /// drop into `Self::query()` for that.
5907            ///
5908            /// # Errors
5909            /// As [`FetcherPool::fetch`].
5910            ///
5911            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5912            pub async fn take(
5913                n: i64,
5914                pool: &#root::sql::Pool,
5915            ) -> ::core::result::Result<
5916                ::std::vec::Vec<Self>,
5917                #root::sql::ExecError,
5918            > {
5919                use #root::sql::FetcherPool as _;
5920                #root::query::QuerySet::<Self>::default()
5921                    .limit(n)
5922                    .fetch(pool)
5923                    .await
5924            }
5925
5926            /// Fetch the page-th window of `per_page` rows
5927            /// (1-indexed). Eloquent
5928            /// `Model::query()->forPage($page, $perPage)->get()`
5929            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5930            /// LIMIT per_page`; for large offsets this is O(N) —
5931            /// prefer keyset pagination via PK on hot paths.
5932            ///
5933            /// # Errors
5934            /// As [`FetcherPool::fetch`].
5935            ///
5936            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5937            pub async fn for_page(
5938                page: i64,
5939                per_page: i64,
5940                pool: &#root::sql::Pool,
5941            ) -> ::core::result::Result<
5942                ::std::vec::Vec<Self>,
5943                #root::sql::ExecError,
5944            > {
5945                use #root::sql::FetcherPool as _;
5946                let _offset = #root::list_params::page_offset(page, per_page);
5947                #root::query::QuerySet::<Self>::default()
5948                    .limit(per_page)
5949                    .offset(_offset)
5950                    .fetch(pool)
5951                    .await
5952            }
5953
5954            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5955            /// one page of rows AND the total row count in one
5956            /// call. Returns `(rows, total)`. Two queries: a
5957            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5958            /// for the total.
5959            ///
5960            /// Useful for paginated UIs that need both the visible
5961            /// rows AND a "Page X of Y" / total-count widget. For
5962            /// large tables prefer keyset pagination + cached count;
5963            /// every call to `paginate` re-counts the full table.
5964            ///
5965            /// Same 1-indexed `page` convention as [`Self::for_page`].
5966            ///
5967            /// # Errors
5968            /// As [`Self::for_page`] and [`Self::count`].
5969            pub async fn paginate(
5970                page: i64,
5971                per_page: i64,
5972                pool: &#root::sql::Pool,
5973            ) -> ::core::result::Result<
5974                (::std::vec::Vec<Self>, i64),
5975                #root::sql::ExecError,
5976            > {
5977                // Route through the queryset rather than `Self::count`:
5978                // the `count` inherent method is suppressed on models
5979                // that declare a field named `count` (field/shortcut
5980                // collision guard), and `paginate` must still compile
5981                // for those models.
5982                let total = {
5983                    use #root::sql::CounterPool as _;
5984                    #root::query::QuerySet::<Self>::default()
5985                        .count(pool)
5986                        .await?
5987                };
5988                let rows = Self::for_page(page, per_page, pool).await?;
5989                ::core::result::Result::Ok((rows, total))
5990            }
5991
5992            /// Bulk-update — set `set_col = set_val` on every row
5993            /// matching `where_col = where_val`. Returns affected row
5994            /// count. Eloquent
5995            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5996            /// parity.
5997            ///
5998            /// For multi-column updates drop into the queryset
5999            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
6000            ///
6001            /// # Errors
6002            /// As [`UpdaterPool::execute_pool`].
6003            ///
6004            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
6005            pub async fn update_where(
6006                where_col: &str,
6007                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
6008                set_col: &str,
6009                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
6010                pool: &#root::sql::Pool,
6011            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6012                let _query = #root::query::QuerySet::<Self>::default()
6013                    .filter(where_col, where_val)
6014                    .update()
6015                    .set(set_col, set_val)
6016                    .compile()?;
6017                #bulk_update_run
6018            }
6019
6020            /// Bulk-delete — remove every row matching
6021            /// `where_col = where_val`. Returns affected row count.
6022            /// Eloquent
6023            /// `Model::where($where_col, $where_val)->delete()` parity.
6024            ///
6025            /// # Errors
6026            /// As [`#root::sql::delete_pool`].
6027            pub async fn delete_where(
6028                where_col: &str,
6029                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
6030                pool: &#root::sql::Pool,
6031            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6032                let _query = #root::query::QuerySet::<Self>::default()
6033                    .filter_op(where_col, #root::core::Op::Eq, where_val)
6034                    .compile_delete()?;
6035                #bulk_delete_run
6036            }
6037
6038            /// Bulk-update — set `set_col = set_val` on EVERY row of
6039            /// this model's table. **No WHERE clause** — use with
6040            /// care. Eloquent `Model::query()->update([$col => $val])`
6041            /// parity.
6042            ///
6043            /// Use for backfills, one-shot reset flows, etc. The
6044            /// returned count is rows affected.
6045            ///
6046            /// # Errors
6047            /// As [`UpdaterPool::execute_pool`].
6048            ///
6049            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
6050            pub async fn update_all(
6051                set_col: &str,
6052                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
6053                pool: &#root::sql::Pool,
6054            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6055                let _query = #root::query::QuerySet::<Self>::default()
6056                    .update()
6057                    .set(set_col, set_val)
6058                    .compile()?;
6059                #bulk_update_run
6060            }
6061
6062            /// Fetch every row where `<col> NOT LIKE <pattern>`.
6063            /// Eloquent `Model::whereNotLike` parity. Pattern is
6064            /// passed verbatim — caller controls `%` / `_`.
6065            ///
6066            /// # Errors
6067            /// As [`FetcherPool::fetch`].
6068            ///
6069            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6070            pub async fn where_not_like(
6071                col: &str,
6072                pattern: impl ::core::convert::Into<::std::string::String>,
6073                pool: &#root::sql::Pool,
6074            ) -> ::core::result::Result<
6075                ::std::vec::Vec<Self>,
6076                #root::sql::ExecError,
6077            > {
6078                use #root::sql::FetcherPool as _;
6079                let _key = ::std::format!("{}__not_like", col);
6080                #root::query::QuerySet::<Self>::default()
6081                    .filter(
6082                        &_key,
6083                        #root::core::SqlValue::String(pattern.into()),
6084                    )
6085                    .fetch(pool)
6086                    .await
6087            }
6088
6089            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
6090            /// case-insensitive `NOT LIKE` (PG native, MySQL /
6091            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
6092            ///
6093            /// # Errors
6094            /// As [`FetcherPool::fetch`].
6095            ///
6096            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6097            pub async fn where_not_ilike(
6098                col: &str,
6099                pattern: impl ::core::convert::Into<::std::string::String>,
6100                pool: &#root::sql::Pool,
6101            ) -> ::core::result::Result<
6102                ::std::vec::Vec<Self>,
6103                #root::sql::ExecError,
6104            > {
6105                use #root::sql::FetcherPool as _;
6106                let _key = ::std::format!("{}__not_ilike", col);
6107                #root::query::QuerySet::<Self>::default()
6108                    .filter(
6109                        &_key,
6110                        #root::core::SqlValue::String(pattern.into()),
6111                    )
6112                    .fetch(pool)
6113                    .await
6114            }
6115
6116            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
6117            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
6118            /// parity.
6119            ///
6120            /// # Errors
6121            /// As [`FetcherPool::fetch`].
6122            ///
6123            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6124            pub async fn where_not_between(
6125                col: &str,
6126                lo: impl ::core::convert::Into<#root::core::SqlValue>,
6127                hi: impl ::core::convert::Into<#root::core::SqlValue>,
6128                pool: &#root::sql::Pool,
6129            ) -> ::core::result::Result<
6130                ::std::vec::Vec<Self>,
6131                #root::sql::ExecError,
6132            > {
6133                use #root::sql::FetcherPool as _;
6134                let _key = ::std::format!("{}__not_between", col);
6135                let _vals = #root::core::SqlValue::List(::std::vec![
6136                    ::core::convert::Into::into(lo),
6137                    ::core::convert::Into::into(hi),
6138                ]);
6139                #root::query::QuerySet::<Self>::default()
6140                    .filter(&_key, _vals)
6141                    .fetch(pool)
6142                    .await
6143            }
6144
6145            /// Returns the SQL table name for this model. Eloquent
6146            /// `$model->getTable()` parity.
6147            #[must_use]
6148            pub fn table_name() -> &'static str {
6149                <Self as #root::core::Model>::SCHEMA.table
6150            }
6151
6152            /// Returns the SQL column name of this model's primary
6153            /// key, or `None` when the model has no
6154            /// `#[rustango(primary_key)]`. Eloquent
6155            /// `$model->getKeyName()` parity.
6156            #[must_use]
6157            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
6158                <Self as #root::core::Model>::SCHEMA
6159                    .primary_key()
6160                    .map(|f| f.column)
6161            }
6162
6163            /// Fetch every row where `<col> BETWEEN lo AND hi`
6164            /// (inclusive on both ends — same as SQL). Eloquent
6165            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
6166            ///
6167            /// # Errors
6168            /// As [`FetcherPool::fetch`].
6169            ///
6170            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6171            pub async fn where_between(
6172                col: &str,
6173                lo: impl ::core::convert::Into<#root::core::SqlValue>,
6174                hi: impl ::core::convert::Into<#root::core::SqlValue>,
6175                pool: &#root::sql::Pool,
6176            ) -> ::core::result::Result<
6177                ::std::vec::Vec<Self>,
6178                #root::sql::ExecError,
6179            > {
6180                use #root::sql::FetcherPool as _;
6181                let _key = ::std::format!("{}__between", col);
6182                let _vals = #root::core::SqlValue::List(::std::vec![
6183                    ::core::convert::Into::into(lo),
6184                    ::core::convert::Into::into(hi),
6185                ]);
6186                #root::query::QuerySet::<Self>::default()
6187                    .filter(&_key, _vals)
6188                    .fetch(pool)
6189                    .await
6190            }
6191
6192            /// Fetch the first row where `<col> = <val>`. Returns
6193            /// `Ok(None)` when no row matches. Eloquent
6194            /// `Model::firstWhere($col, $val)` parity.
6195            ///
6196            /// Thin wrapper over `QuerySet::<Self>::default()
6197            /// .filter(col, val).first(pool)`. Use this when you
6198            /// want one row identified by a non-PK column (e.g.
6199            /// `User::first_where_pool("email", "x@y.com", &pool)`).
6200            ///
6201            /// `val` accepts any value `Into<SqlValue>` so plain
6202            /// strings, ints, UUIDs, etc. all work.
6203            ///
6204            /// # Errors
6205            /// As `QuerySet::first`.
6206            pub async fn first_where(
6207                col: &str,
6208                val: impl ::core::convert::Into<#root::core::SqlValue>,
6209                pool: &#root::sql::Pool,
6210            ) -> ::core::result::Result<
6211                ::core::option::Option<Self>,
6212                #root::sql::ExecError,
6213            > {
6214                #root::query::QuerySet::<Self>::default()
6215                    .filter(col, val)
6216                    .first(pool)
6217                    .await
6218            }
6219
6220            #value_method
6221
6222            /// Fetch the row with the largest non-NULL `field` value.
6223            /// Returns `Ok(None)` when the table is empty or every
6224            /// `field` is NULL. Thin wrapper over
6225            /// `QuerySet::<Self>::default().latest(field, pool)`.
6226            ///
6227            /// **Field name** is the Rust field ident as a string
6228            /// (not the SQL column). It is a runtime `&str`, so an
6229            /// unknown one compiles and surfaces as
6230            /// `ExecError::Query(QueryError::UnknownField)` when the
6231            /// query runs.
6232            ///
6233            /// # Errors
6234            /// As `QuerySet::latest`.
6235            pub async fn latest(
6236                field: &str,
6237                pool: &#root::sql::Pool,
6238            ) -> ::core::result::Result<
6239                ::core::option::Option<Self>,
6240                #root::sql::ExecError,
6241            > {
6242                #root::query::QuerySet::<Self>::default()
6243                    .latest(field, pool)
6244                    .await
6245            }
6246
6247            /// Sibling of [`Self::latest`]: fetches the row with the
6248            /// smallest non-NULL `field` value.
6249            ///
6250            /// # Errors
6251            /// As [`Self::latest`].
6252            pub async fn earliest(
6253                field: &str,
6254                pool: &#root::sql::Pool,
6255            ) -> ::core::result::Result<
6256                ::core::option::Option<Self>,
6257                #root::sql::ExecError,
6258            > {
6259                #root::query::QuerySet::<Self>::default()
6260                    .earliest(field, pool)
6261                    .await
6262            }
6263
6264            #count_method
6265
6266            /// `true` when the table contains at least one row.
6267            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6268            /// over `QuerySet::<Self>::default().exists(pool)`.
6269            ///
6270            /// # Errors
6271            /// As [`ExistsPool::exists`].
6272            ///
6273            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6274            pub async fn exists(
6275                pool: &#root::sql::Pool,
6276            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6277                use #root::sql::ExistsPool as _;
6278                #root::query::QuerySet::<Self>::default()
6279                    .exists(pool)
6280                    .await
6281            }
6282
6283            /// Inverse of [`Self::exists`] — returns `true` when
6284            /// the table has zero rows. Eloquent
6285            /// `Model::doesntExist()` parity.
6286            ///
6287            /// # Errors
6288            /// As [`Self::exists`].
6289            pub async fn doesnt_exist(
6290                pool: &#root::sql::Pool,
6291            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6292                Self::exists(pool).await.map(|e| !e)
6293            }
6294
6295            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6296            /// `true` when a row with primary key `pk` exists in
6297            /// the table. Sugar over
6298            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6299            ///
6300            /// Differs from [`Self::exists`] (which checks "any row
6301            /// in the table") by checking a specific PK existence.
6302            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6303            /// because the row is never materialized — the SQL is
6304            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6305            ///
6306            /// # Errors
6307            /// As [`#root::sql::ExistsPool::contains_pk`].
6308            pub async fn contains_pk(
6309                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6310                pool: &#root::sql::Pool,
6311            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6312                use #root::sql::ExistsPool as _;
6313                #root::query::QuerySet::<Self>::default()
6314                    .contains_pk(pool, pk)
6315                    .await
6316            }
6317
6318            #sum_method
6319            #avg_method
6320            #min_method
6321            #max_method
6322
6323            /// Fetch every row of this model from `pool`. Eloquent
6324            /// `Model::all()` parity — a thin wrapper over
6325            /// `QuerySet::<Self>::default().fetch(pool)`.
6326            ///
6327            /// **Use with care on large tables** — there's no
6328            /// pagination or limit; the entire table is materialized
6329            /// into memory. For anything beyond fixture / lookup
6330            /// tables, page through `QuerySet::<Self>::default()
6331            /// .order_by(...).limit(N).offset(M).fetch(pool)`
6332            /// or stream via `.iterator(chunk_size)`.
6333            ///
6334            /// # Errors
6335            /// As [`FetcherPool::fetch`].
6336            ///
6337            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6338            pub async fn all(
6339                pool: &#root::sql::Pool,
6340            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6341            {
6342                use #root::sql::FetcherPool as _;
6343                #root::query::QuerySet::<Self>::default()
6344                    .fetch(pool)
6345                    .await
6346            }
6347
6348            /// Look up every row whose primary key is in `pks`.
6349            /// Returns the matching rows in **inventory** order — NOT
6350            /// the order of `pks`. Empty `pks` returns an empty
6351            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6352            /// with a list) parity.
6353            ///
6354            /// Thin wrapper over `QuerySet::<Self>::default()
6355            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6356            /// Caller-supplied PKs that don't match a row are
6357            /// silently skipped (the returned vec is shorter than
6358            /// the input list). For an order-preserving / "fail
6359            /// when any missing" variant, build the queryset
6360            /// explicitly with `in_bulk` instead.
6361            ///
6362            /// Accepts any iterable whose elements are
6363            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6364            /// `Vec<Uuid>`, etc.
6365            ///
6366            /// # Errors
6367            /// As [`FetcherPool::fetch`].
6368            ///
6369            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6370            pub async fn find_many<V>(
6371                pks: impl ::core::iter::IntoIterator<Item = V>,
6372                pool: &#root::sql::Pool,
6373            ) -> ::core::result::Result<
6374                ::std::vec::Vec<Self>,
6375                #root::sql::ExecError,
6376            >
6377            where
6378                V: ::core::convert::Into<#root::core::SqlValue>,
6379            {
6380                use #root::sql::FetcherPool as _;
6381                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6382                    pks.into_iter().map(::core::convert::Into::into).collect();
6383                if _values.is_empty() {
6384                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6385                }
6386                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6387                let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
6388                let _split = _batches.len() > 1;
6389                let mut _rows = ::std::vec::Vec::new();
6390                for _keys in _batches {
6391                    _rows.extend(
6392                        #root::query::QuerySet::<Self>::default()
6393                            .filter(&_key, #root::core::SqlValue::List(_keys))
6394                            .fetch(pool)
6395                            .await?,
6396                    );
6397                }
6398                if _split {
6399                    _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
6400                }
6401                ::core::result::Result::Ok(_rows)
6402            }
6403
6404            /// Look up the row whose primary key equals `pk`. Returns
6405            /// `Ok(None)` when no row matches, rather than erroring.
6406            /// Eloquent `Model::find`
6407            /// shape — accepts any value `Into<SqlValue>`.
6408            ///
6409            /// One-liner shortcut for the common
6410            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6411            /// .limit(1).fetch(pool).await?.into_iter().next()`
6412            /// dance.
6413            ///
6414            /// # Errors
6415            /// As [`FetcherPool::fetch`].
6416            ///
6417            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6418            pub async fn find(
6419                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6420                pool: &#root::sql::Pool,
6421            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6422            {
6423                use #root::sql::FetcherPool as _;
6424                let _pk_val: #root::core::SqlValue = pk.into();
6425                let mut _rows: ::std::vec::Vec<Self> =
6426                    #root::query::QuerySet::<Self>::default()
6427                        .filter(::core::stringify!(#pk_ident), _pk_val)
6428                        .limit(1)
6429                        .fetch(pool)
6430                        .await?;
6431                ::core::result::Result::Ok(_rows.into_iter().next())
6432            }
6433
6434            /// Look up the row whose primary key equals `pk`. Errors
6435            /// when no row matches — the throwing counterpart of
6436            /// [`Self::find`]. Eloquent `Model::findOrFail`
6437            /// parity.
6438            ///
6439            /// Translates the miss into
6440            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6441            /// so callers can `?`-bubble straight through the typical
6442            /// `ExecError` error chain.
6443            ///
6444            /// # Errors
6445            /// As [`Self::find`]; additionally
6446            /// [`sqlx::Error::RowNotFound`] when no row matches.
6447            ///
6448            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6449            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6450            pub async fn find_or_fail(
6451                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6452                pool: &#root::sql::Pool,
6453            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6454                match Self::find(pk, pool).await? {
6455                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6456                    ::core::option::Option::None => ::core::result::Result::Err(
6457                        #root::sql::ExecError::Driver(
6458                            #root::sql::sqlx::Error::RowNotFound,
6459                        ),
6460                    ),
6461                }
6462            }
6463
6464            /// Look up multiple rows by primary key, **failing** if
6465            /// any requested PK is missing. Eloquent
6466            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6467            /// inventory order (NOT request order — same as
6468            /// [`Self::find_many`]).
6469            ///
6470            /// Empty `pks` returns an empty `Vec` (no rows requested
6471            /// → nothing to fail on).
6472            ///
6473            /// Differs from [`Self::find_many`] in that this method
6474            /// requires every PK to resolve. If even one is missing,
6475            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6476            /// in `ExecError::Driver`). A key counts as found when the
6477            /// database matches it to a row, so repeats and keys its
6478            /// collation folds together (`abc` / `ABC` on MySQL) pass.
6479            ///
6480            /// # Errors
6481            /// As [`Self::find_many`], plus `RowNotFound` when a
6482            /// requested PK matches no row.
6483            pub async fn find_many_or_fail<V>(
6484                pks: impl ::core::iter::IntoIterator<Item = V>,
6485                pool: &#root::sql::Pool,
6486            ) -> ::core::result::Result<
6487                ::std::vec::Vec<Self>,
6488                #root::sql::ExecError,
6489            >
6490            where
6491                V: ::core::convert::Into<#root::core::SqlValue>,
6492            {
6493                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6494                    pks.into_iter().map(::core::convert::Into::into).collect();
6495                if _values.is_empty() {
6496                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6497                }
6498                use #root::sql::ExistsPool as _;
6499                let mut _seen: ::std::collections::HashSet<
6500                    ::std::string::String,
6501                > = ::std::collections::HashSet::new();
6502                let mut _keys: ::std::vec::Vec<#root::core::SqlValue> =
6503                    ::std::vec::Vec::new();
6504                for v in &_values {
6505                    if _seen.insert(v.to_display_string()) {
6506                        _keys.push(::core::clone::Clone::clone(v));
6507                    }
6508                }
6509                let _rows = Self::find_many(_values, pool).await?;
6510                let _found: ::std::collections::HashSet<::std::string::String> = _rows
6511                    .iter()
6512                    .map(|r| r.__rustango_pk_value().to_display_string())
6513                    .collect();
6514                // A key with no exact twin among the rows may still match one
6515                // under the column's collation: the database decides.
6516                for _key in _keys {
6517                    if _found.contains(&_key.to_display_string()) {
6518                        continue;
6519                    }
6520                    let _hit = #root::query::QuerySet::<Self>::default()
6521                        .filter(::core::stringify!(#pk_ident), _key)
6522                        .exists(pool)
6523                        .await?;
6524                    if !_hit {
6525                        return ::core::result::Result::Err(
6526                            #root::sql::ExecError::Driver(
6527                                #root::sql::sqlx::Error::RowNotFound,
6528                            ),
6529                        );
6530                    }
6531                }
6532                ::core::result::Result::Ok(_rows)
6533            }
6534
6535            /// Find by primary key or run `fallback` to produce a
6536            /// default row to return. Eloquent
6537            /// `Model::findOr($pk, fn() => …)` parity.
6538            ///
6539            /// Unlike [`Self::find_or_fail`] (which raises on
6540            /// miss), this is the "give me something sensible"
6541            /// branch: typical use is "fetch the user's row, else
6542            /// fall back to an anonymous/guest stub".
6543            ///
6544            /// `fallback` runs only when no row matches — the DB
6545            /// round-trip happens unconditionally.
6546            ///
6547            /// # Errors
6548            /// As [`Self::find`].
6549            pub async fn find_or<F>(
6550                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6551                pool: &#root::sql::Pool,
6552                fallback: F,
6553            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6554            where
6555                F: ::core::ops::FnOnce() -> Self,
6556            {
6557                ::core::result::Result::Ok(
6558                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6559                )
6560            }
6561
6562            /// Same as [`Self::find_or`] but also returns a `bool`
6563            /// indicating whether the row was found in the DB
6564            /// (`true`) or freshly built from `fallback` (`false`).
6565            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6566            /// in PHP the returned model also exposes `->exists`;
6567            /// here that's surfaced as the second tuple element.
6568            ///
6569            /// Useful for edit-or-create form handlers where the
6570            /// caller needs to know whether to PATCH or POST when
6571            /// the user submits the form.
6572            ///
6573            /// # Errors
6574            /// As [`Self::find`].
6575            pub async fn find_or_new<F>(
6576                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6577                pool: &#root::sql::Pool,
6578                fallback: F,
6579            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6580            where
6581                F: ::core::ops::FnOnce() -> Self,
6582            {
6583                match Self::find(pk, pool).await? {
6584                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6585                    ::core::option::Option::None => {
6586                        ::core::result::Result::Ok((fallback(), false))
6587                    }
6588                }
6589            }
6590
6591            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6592            /// [`Self::find_or_new`] but **persists** the new row
6593            /// when the PK isn't found. Returns `(row, exists: bool)`
6594            /// — `exists=true` when the PK was found,
6595            /// `false` when a fresh row was inserted.
6596            ///
6597            /// ```ignore
6598            /// let (post, found) = Post::find_or_insert(
6599            ///     pk,
6600            ///     &pool,
6601            ///     || Post { id: Auto::default(), title: "new".into() },
6602            /// ).await?;
6603            /// // `found` true → returned existing row;
6604            /// // `found` false → fallback was inserted, `post.id` populated.
6605            /// ```
6606            ///
6607            /// **Caveat**: two concurrent calls that both miss the
6608            /// find can both INSERT, violating uniqueness. Wrap in a
6609            /// transaction or rely on a UNIQUE constraint + handle
6610            /// the conflict error if you need race-free semantics.
6611            ///
6612            /// # Errors
6613            /// As [`Self::find`] and [`Self::save_pool`].
6614            pub async fn find_or_insert<F>(
6615                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6616                pool: &#root::sql::Pool,
6617                fallback: F,
6618            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6619            where
6620                F: ::core::ops::FnOnce() -> Self,
6621            {
6622                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6623                    return ::core::result::Result::Ok((_row, true));
6624                }
6625                let mut _new = fallback();
6626                _new.save_pool(pool).await?;
6627                ::core::result::Result::Ok((_new, false))
6628            }
6629
6630            /// Fetch the first row of the table, or run `fallback`
6631            /// when the table is empty. Eloquent
6632            /// `Model::firstOr(fn() => …)` parity.
6633            ///
6634            /// # Errors
6635            /// As [`Self::first`].
6636            pub async fn first_or<F>(
6637                pool: &#root::sql::Pool,
6638                fallback: F,
6639            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6640            where
6641                F: ::core::ops::FnOnce() -> Self,
6642            {
6643                // Route through the queryset rather than `Self::first`,
6644                // which is suppressed on models with a field named
6645                // `first` (field/shortcut collision guard).
6646                ::core::result::Result::Ok(
6647                    #root::query::QuerySet::<Self>::default()
6648                        .first(pool)
6649                        .await?
6650                        .unwrap_or_else(fallback),
6651                )
6652            }
6653
6654            /// Fetch exactly one row matching `<col> = <val>`. Errors
6655            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6656            /// or more than one matches
6657            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6658            /// Eloquent `Model::sole($col, $val)` parity.
6659            ///
6660            /// # Errors
6661            /// As [`Self::where_`] plus the explicit
6662            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6663            pub async fn sole(
6664                col: &str,
6665                val: impl ::core::convert::Into<#root::core::SqlValue>,
6666                pool: &#root::sql::Pool,
6667            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6668                let mut _rows = Self::where_(col, val, pool).await?;
6669                match _rows.len() {
6670                    0 => ::core::result::Result::Err(
6671                        #root::sql::ExecError::Driver(
6672                            #root::sql::sqlx::Error::RowNotFound,
6673                        ),
6674                    ),
6675                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6676                    n => ::core::result::Result::Err(
6677                        #root::sql::ExecError::MultipleRowsReturned {
6678                            op: "sole",
6679                            table: <Self as #root::core::Model>::SCHEMA.name,
6680                            count: n,
6681                        },
6682                    ),
6683                }
6684            }
6685        }
6686    } else {
6687        quote!()
6688    };
6689
6690    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6691    // the non-audited `_pool` methods but execute against an open
6692    // `PoolTx` so the writes participate in the caller's transaction.
6693    // Auditing inside TX is deferred; these always use the plain
6694    // executor primitives regardless of whether the model is audited.
6695    let tx_insert_method = if fields.has_auto {
6696        let pushes = &fields.insert_pushes;
6697        let returning_cols = &fields.returning_cols;
6698        quote! {
6699            /// Insert this row inside an open transaction, populating
6700            /// any `Auto<T>` PK from the auto-assigned value. Works
6701            /// against any backend that `tx` wraps.
6702            ///
6703            /// # Errors
6704            /// As [`Self::insert_pool`].
6705            pub async fn insert_tx(
6706                &mut self,
6707                tx: &mut #root::sql::PoolTx<'_>,
6708            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6709                let mut _columns: ::std::vec::Vec<&'static str> =
6710                    ::std::vec::Vec::new();
6711                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6712                    ::std::vec::Vec::new();
6713                #( #pushes )*
6714                let _query = #root::core::InsertQuery::new(
6715                    <Self as #root::core::Model>::SCHEMA,
6716                    _columns,
6717                    _values,
6718                )
6719                .returning(::std::vec![ #( #returning_cols ),* ]);
6720                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6721                #root::sql::apply_auto_pk(_result, self)
6722            }
6723        }
6724    } else {
6725        let insert_columns = &fields.insert_columns;
6726        let insert_values = &fields.insert_values;
6727        quote! {
6728            /// Insert this row inside an open transaction.
6729            ///
6730            /// # Errors
6731            /// As [`Self::insert_pool`].
6732            pub async fn insert_tx(
6733                &self,
6734                tx: &mut #root::sql::PoolTx<'_>,
6735            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6736                let _query = #root::core::InsertQuery::new(
6737                    <Self as #root::core::Model>::SCHEMA,
6738                    ::std::vec![ #( #insert_columns ),* ],
6739                    ::std::vec![ #( #insert_values ),* ],
6740                );
6741                #root::sql::insert_tx(tx, &_query).await
6742            }
6743        }
6744    };
6745
6746    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6747        let pk_column_lit = pk_col.as_str();
6748        let assignments = &fields.update_assignments;
6749        let dispatch_unset = if fields.pk_is_auto {
6750            quote! {
6751                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6752                    return self.insert_tx(tx).await.map(|()| 1u64);
6753                }
6754            }
6755        } else {
6756            quote!()
6757        };
6758        let write = restore_auto_now_unless(
6759            &fields.auto_now_idents,
6760            &save_ret,
6761            &save_keep,
6762            &quote! {
6763                let _query = #root::core::UpdateQuery::new(
6764                    <Self as #root::core::Model>::SCHEMA,
6765                    ::std::vec![ #( #assignments ),* ],
6766                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6767                        #pk_column_lit,
6768                        #root::core::Op::Eq,
6769                        ::core::convert::Into::<#root::core::SqlValue>::into(
6770                            ::core::clone::Clone::clone(&self.#pk_ident)
6771                        ),
6772                    )),
6773                );
6774                let _affected = #root::sql::update_tx(tx, &_query).await?;
6775                ::core::result::Result::Ok(_affected)
6776            },
6777        );
6778        quote! {
6779            /// Save this row inside an open transaction. `INSERT` when
6780            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6781            /// PK. Works against any backend that `tx` wraps.
6782            ///
6783            /// # Errors
6784            /// As [`Self::save_pool`].
6785            pub async fn save_tx(
6786                &mut self,
6787                tx: &mut #root::sql::PoolTx<'_>,
6788            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6789                #dispatch_unset
6790                #write
6791            }
6792        }
6793    } else {
6794        quote!()
6795    };
6796
6797    let tx_delete_method = {
6798        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6799        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6800        if let Some(pk_ident) = pk_ident_for_tx {
6801            quote! {
6802                /// Delete the row identified by this instance's PK
6803                /// inside an open transaction. Works against any backend
6804                /// that `tx` wraps.
6805                ///
6806                /// # Errors
6807                /// As [`Self::delete_pool`].
6808                pub async fn delete_tx(
6809                    &self,
6810                    tx: &mut #root::sql::PoolTx<'_>,
6811                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6812                    let _query = #root::core::DeleteQuery::by_pk(
6813                        <Self as #root::core::Model>::SCHEMA,
6814                        #pk_column_lit,
6815                        ::core::convert::Into::<#root::core::SqlValue>::into(
6816                            ::core::clone::Clone::clone(&self.#pk_ident)
6817                        ),
6818                    );
6819                    #root::sql::delete_tx(tx, &_query).await
6820                }
6821            }
6822        } else {
6823            quote!()
6824        }
6825    };
6826
6827    // Update emission captures both BEFORE and AFTER state — runs an
6828    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6829    // each tracked field's prior value, then after the UPDATE diffs
6830    // against the in-memory `&self`. `diff_changes` drops unchanged
6831    // columns so the JSON only contains the actual delta.
6832    //
6833    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6834    // and binds `_audit_before_pairs`; `audit_update_post` runs
6835    // after the UPDATE and emits the PendingEntry.
6836    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6837        audited_fields
6838    {
6839        if tracked.is_empty() {
6840            (quote!(), quote!())
6841        } else {
6842            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6843            let pk_column_lit_for_select =
6844                primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6845            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6846                if fields.pk_is_auto {
6847                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6848                } else {
6849                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6850                }
6851            } else {
6852                quote!(0_i64)
6853            };
6854            let before_pairs = tracked.iter().map(|c| {
6855                let column_lit = c.column.as_str();
6856                let value_ty = &c.value_ty;
6857                quote! {
6858                    (
6859                        #column_lit,
6860                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6861                            &_audit_before_row, #column_lit,
6862                        ) {
6863                            ::core::result::Result::Ok(v) => #root::core::__json_value(&v),
6864                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6865                        },
6866                    )
6867                }
6868            });
6869            let pre = quote! {
6870                let _audit_before_query = #root::audit::before_image_query(
6871                    <Self as #root::core::Model>::SCHEMA,
6872                    #pk_column_lit_for_select,
6873                    ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6874                    &[ #( #tracked_cols ),* ],
6875                );
6876                // A failed pre-read must not let the UPDATE run unaudited (#1907).
6877                let _audit_before_pairs:
6878                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6879                    #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6880                        .await?
6881                        .into_iter()
6882                        .next()
6883                        .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6884            };
6885            let post = quote! {
6886                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6887                    let _audit_after = self.__rustango_audit_pairs();
6888                    if let ::core::option::Option::Some(_audit_entry) =
6889                        #root::audit::PendingEntry::update_diff(
6890                            <Self as #root::core::Model>::SCHEMA.table,
6891                            self.__rustango_audit_pk(),
6892                            &_audit_before,
6893                            &_audit_after,
6894                        )
6895                    {
6896                        #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6897                    }
6898                }
6899            };
6900            (pre, post)
6901        }
6902    } else {
6903        (quote!(), quote!())
6904    };
6905
6906    // Bulk-insert audit: one `Create` entry per row once RETURNING has
6907    // set its PK, written in one batched INSERT.
6908    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6909        quote! {
6910            let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6911                .iter()
6912                .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6913                .collect();
6914            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6915        }
6916    } else {
6917        quote!()
6918    };
6919
6920    let save_method = if fields.pk_is_auto {
6921        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6922        let pk_column_lit = pk_column.as_str();
6923        let assignments = &fields.update_assignments;
6924        let upsert_cols = &fields.upsert_update_columns;
6925        let upsert_pushes = &fields.insert_pushes;
6926        // Read before the pushes, which fill each unset id.
6927        let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6928            fields.uuid_v7_fields.iter().cloned().unzip();
6929        let upsert_returning = &fields.returning_cols;
6930        let upsert_auto_assigns = &fields.auto_assigns;
6931        let auto_now_idents = &fields.auto_now_idents;
6932        // Conflict target: prefer the first declared `unique_together`
6933        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6934        // `BIGSERIAL` and never collide on insert, so a PK-only target
6935        // would silently turn `upsert()` into "always-insert" for
6936        // surrogate-PK models with composite UNIQUE constraints — see
6937        // `RolePermission` / `UserRole` / `UserPermission` in the
6938        // tenancy permission engine. When no `unique_together` is
6939        // declared we keep the PK target (the original behaviour).
6940        let upsert_target_columns: Vec<String> =
6941            upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6942        let upsert_target_lits = upsert_target_columns
6943            .iter()
6944            .map(String::as_str)
6945            .collect::<Vec<_>>();
6946        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6947            quote!(#root::core::ConflictClause::DoNothing)
6948        } else {
6949            quote!(#root::core::ConflictClause::DoUpdate {
6950                target: ::std::vec![ #( #upsert_target_lits ),* ],
6951                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6952            })
6953        };
6954        // Audited: the upsert reports whether it inserted or updated (#1795).
6955        let (upsert_run, audit_upsert_emit) = if audited_fields.is_some() {
6956            (
6957                quote! {
6958                    let (_returning_row_v, _audit_op) =
6959                        #root::audit::upsert_returning_on(&mut *_executor, &query).await?;
6960                },
6961                quote! {
6962                    let _audit_entry = self.__rustango_audit_entry(_audit_op);
6963                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6964                },
6965            )
6966        } else {
6967            (
6968                quote! {
6969                    let _returning_row_v =
6970                        #root::sql::__macro_internals::insert_returning_on(_executor, &query)
6971                            .await?;
6972                },
6973                quote!(),
6974            )
6975        };
6976        let save_on_write = restore_auto_now_unless(
6977            auto_now_idents,
6978            &save_ret,
6979            &save_keep,
6980            &quote! {
6981                #audit_update_pre
6982                let _query = #root::core::UpdateQuery::new(
6983                    <Self as #root::core::Model>::SCHEMA,
6984                    ::std::vec![ #( #assignments ),* ],
6985                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6986                        #pk_column_lit,
6987                        #root::core::Op::Eq,
6988                        ::core::convert::Into::<#root::core::SqlValue>::into(
6989                            ::core::clone::Clone::clone(&self.#pk_ident)
6990                        ),
6991                    )),
6992                );
6993                let _affected = #root::sql::__macro_internals::update_on(
6994                    #executor_passes_to_data_write,
6995                    &_query,
6996                ).await?;
6997                #audit_update_post
6998                ::core::result::Result::Ok(_affected)
6999            },
7000        );
7001        let upsert_write = restore_auto_now_unless(
7002            auto_now_idents,
7003            &quote!(::core::result::Result<(), #root::sql::ExecError>),
7004            &quote!(_saved.is_ok()),
7005            &quote! {
7006                // The UPDATE branch writes `auto_now` too, so stamp it like `save`.
7007                #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
7008                #( #upsert_pushes )*
7009                let query = #root::core::InsertQuery::new(
7010                    <Self as #root::core::Model>::SCHEMA,
7011                    _columns,
7012                    _values,
7013                )
7014                .returning(::std::vec![ #( #upsert_returning ),* ])
7015                .on_conflict(#conflict_clause)
7016                .generated(_generated);
7017                #upsert_run
7018                let _returning_row = &_returning_row_v;
7019                #( #upsert_auto_assigns )*
7020                #audit_upsert_emit
7021                ::core::result::Result::Ok(())
7022            },
7023        );
7024        Some(quote! {
7025            /// Insert this row if its `Auto<T>` primary key is
7026            /// `Unset`, otherwise update the existing row matching the
7027            /// PK. The caller doesn't need to pick `insert` vs the
7028            /// bulk-update path manually.
7029            ///
7030            /// On the insert branch, populates the PK from `RETURNING`
7031            /// (same behavior as `insert`). On the update branch,
7032            /// writes every non-PK column back; if no row matches the
7033            /// PK, returns `Ok(())` silently.
7034            ///
7035            /// Only generated when the primary key is declared as
7036            /// `Auto<T>`. Models with a manually-managed PK must use
7037            /// `insert` or the QuerySet update builder.
7038            ///
7039            /// **Postgres only — prefer [`Self::save_pool`].** This takes a
7040            /// driver-specific `PgPool`, so it does not exist at all on a
7041            /// build without the `postgres` feature: selecting `sqlite`
7042            /// makes the method vanish rather than fail with anything that
7043            /// names the cause.
7044            ///
7045            /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
7046            /// all three backends. The naming is inverted — the short name
7047            /// is the narrow one — and that is tracked in #1293.
7048            ///
7049            /// # Errors
7050            /// Returns [`#root::sql::ExecError`] for SQL-writing
7051            /// or driver failures.
7052            #[cfg(feature = "postgres")]
7053            pub async fn save(
7054                &mut self,
7055                pool: &#root::sql::sqlx::PgPool,
7056            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7057                #pool_to_save_on
7058            }
7059
7060            /// Like [`Self::save`] but accepts any sqlx executor —
7061            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
7062            /// escape hatch for tenant-scoped writes: schema-mode
7063            /// tenants share the registry pool but rely on a per-
7064            /// checkout `SET search_path`, so passing `&PgPool` would
7065            /// silently hit the wrong schema. Acquire a connection
7066            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
7067            ///
7068            /// # Errors
7069            /// As [`Self::save`].
7070            #[cfg(feature = "postgres")]
7071            pub async fn save_on #executor_generics (
7072                &mut self,
7073                #executor_param,
7074            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7075            #executor_where
7076            {
7077                // #1029 — INSERT writes exactly one row → 1; UPDATE returns
7078                // the rows-affected count (0 when the PK no longer exists).
7079                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
7080                    return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
7081                }
7082                #save_on_write
7083            }
7084
7085            /// Per-call override for the audit source. Runs
7086            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
7087            /// scope so the resulting audit entry records `source`
7088            /// instead of the task-local default. Useful for seed
7089            /// scripts and one-off CLI tools that don't sit inside an
7090            /// admin handler. The override applies only to this call;
7091            /// no global state changes.
7092            ///
7093            /// # Errors
7094            /// As [`Self::save_on`].
7095            #[cfg(feature = "postgres")]
7096            pub async fn save_on_with #executor_generics (
7097                &mut self,
7098                #executor_param,
7099                source: #root::audit::AuditSource,
7100            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7101            #executor_where
7102            {
7103                #root::audit::with_source(source, self.save_on(_executor)).await
7104            }
7105
7106            /// Insert this row or update it in-place if the primary key already
7107            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
7108            ///
7109            /// With `Auto::Unset` PK the server assigns a new key and no conflict
7110            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
7111            /// inserted if absent or all non-PK columns are overwritten if present.
7112            ///
7113            /// # Errors
7114            /// As [`Self::insert_on`].
7115            #[cfg(feature = "postgres")]
7116            pub async fn upsert(
7117                &mut self,
7118                pool: &#root::sql::sqlx::PgPool,
7119            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7120                #pool_to_upsert_on
7121            }
7122
7123            /// Like [`Self::upsert`] but accepts any sqlx executor.
7124            /// See [`Self::save_on`] for tenancy-scoped rationale.
7125            ///
7126            /// # Errors
7127            /// As [`Self::upsert`].
7128            #[cfg(feature = "postgres")]
7129            pub async fn upsert_on #executor_generics (
7130                &mut self,
7131                #executor_param,
7132            ) -> ::core::result::Result<(), #root::sql::ExecError>
7133            #executor_where
7134            {
7135                let mut _columns: ::std::vec::Vec<&'static str> =
7136                    ::std::vec::Vec::new();
7137                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7138                    ::std::vec::Vec::new();
7139                #[allow(unused_mut)]
7140                let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7141                #(
7142                    if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
7143                        _generated.push(#gen_cols);
7144                    }
7145                )*
7146                #upsert_write
7147            }
7148        })
7149    } else {
7150        None
7151    };
7152
7153    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
7154        let pk_column_lit = pk_column.as_str();
7155        // Optional `soft_delete_on` / `restore_on` companions when the
7156        // model has a `#[rustango(soft_delete)]` column. They land
7157        // alongside the regular `delete_on` so callers have both
7158        // options — a hard delete (audit-tracked as a real DELETE) and
7159        // a logical delete (audit-tracked as an UPDATE setting the
7160        // deleted_at column to NOW()).
7161        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
7162            let col_lit = col;
7163            let sd_field_ident = fields
7164                .soft_delete_field_ident
7165                .clone()
7166                .expect("soft_delete_column without ident");
7167            quote! {
7168                /// Soft-delete this row by setting its
7169                /// `#[rustango(soft_delete)]` column to `NOW()`.
7170                /// The row stays in the table; query helpers can
7171                /// filter it out by checking the column for `IS NOT
7172                /// NULL`.
7173                ///
7174                /// # Errors
7175                /// As [`Self::delete`].
7176                #[cfg(feature = "postgres")]
7177                pub async fn soft_delete_on #executor_generics (
7178                    &self,
7179                    #executor_param,
7180                ) -> ::core::result::Result<u64, #root::sql::ExecError>
7181                #executor_where
7182                {
7183                    let _now = #root::__chrono::Utc::now();
7184                    let _query = #root::soft_delete::__mark_query(
7185                        <Self as #root::core::Model>::SCHEMA,
7186                        #col_lit,
7187                        #pk_column_lit,
7188                        ::core::convert::Into::<#root::core::SqlValue>::into(
7189                            ::core::clone::Clone::clone(&self.#pk_ident)
7190                        ),
7191                        ::core::option::Option::Some(_now),
7192                    );
7193                    let _affected = #root::sql::__macro_internals::update_on(
7194                        #executor_passes_to_data_write,
7195                        &_query,
7196                    ).await?;
7197                    #audit_softdelete_emit
7198                    ::core::result::Result::Ok(_affected)
7199                }
7200
7201                /// Inverse of [`Self::soft_delete_on`] — clears the
7202                /// soft-delete column back to NULL so the row is
7203                /// considered live again.
7204                ///
7205                /// # Errors
7206                /// As [`Self::delete`].
7207                #[cfg(feature = "postgres")]
7208                pub async fn restore_on #executor_generics (
7209                    &self,
7210                    #executor_param,
7211                ) -> ::core::result::Result<u64, #root::sql::ExecError>
7212                #executor_where
7213                {
7214                    let _query = #root::soft_delete::__mark_query(
7215                        <Self as #root::core::Model>::SCHEMA,
7216                        #col_lit,
7217                        #pk_column_lit,
7218                        ::core::convert::Into::<#root::core::SqlValue>::into(
7219                            ::core::clone::Clone::clone(&self.#pk_ident)
7220                        ),
7221                        ::core::option::Option::None,
7222                    );
7223                    let _affected = #root::sql::__macro_internals::update_on(
7224                        #executor_passes_to_data_write,
7225                        &_query,
7226                    ).await?;
7227                    #audit_restore_emit
7228                    ::core::result::Result::Ok(_affected)
7229                }
7230
7231                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
7232                /// — takes [`#root::sql::Pool`] and dispatches per
7233                /// backend. Eloquent `Model::delete()` semantics on
7234                /// soft-delete-enabled models (closes #821 partial).
7235                ///
7236                /// Sets the `#[rustango(soft_delete)]` column to
7237                /// `NOW()` on every backend. Query helpers
7238                /// (`QuerySet::active()` / `only_trashed()`,
7239                /// `soft_delete::active_filter` /
7240                /// `compose_with_active`) filter trashed rows out by
7241                /// reading `IS NULL` on the same column. Keyed on the PK;
7242                /// global scopes do not apply.
7243                ///
7244                /// # Errors
7245                /// As [`#root::sql::update_pool`].
7246                pub async fn soft_delete(
7247                    &self,
7248                    pool: &#root::sql::Pool,
7249                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7250                    let _now = #root::__chrono::Utc::now();
7251                    let _query = #root::soft_delete::__mark_query(
7252                        <Self as #root::core::Model>::SCHEMA,
7253                        #col_lit,
7254                        #pk_column_lit,
7255                        ::core::convert::Into::<#root::core::SqlValue>::into(
7256                            ::core::clone::Clone::clone(&self.#pk_ident)
7257                        ),
7258                        ::core::option::Option::Some(_now),
7259                    );
7260                    #pool_softdelete_run
7261                }
7262
7263                /// Tri-dialect counterpart of [`Self::restore_on`].
7264                /// Clears the `#[rustango(soft_delete)]` column back
7265                /// to `NULL`, marking the row live again. Eloquent
7266                /// `Model::restore()` parity. Keyed on the PK; global
7267                /// scopes do not apply.
7268                ///
7269                /// # Errors
7270                /// As [`#root::sql::update_pool`].
7271                pub async fn restore(
7272                    &self,
7273                    pool: &#root::sql::Pool,
7274                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7275                    let _query = #root::soft_delete::__mark_query(
7276                        <Self as #root::core::Model>::SCHEMA,
7277                        #col_lit,
7278                        #pk_column_lit,
7279                        ::core::convert::Into::<#root::core::SqlValue>::into(
7280                            ::core::clone::Clone::clone(&self.#pk_ident)
7281                        ),
7282                        ::core::option::Option::None,
7283                    );
7284                    #pool_restore_run
7285                }
7286
7287                /// Hard-delete this row, ignoring the soft-delete
7288                /// column. Eloquent `Model::forceDelete()` parity —
7289                /// the escape hatch when you need to actually purge
7290                /// data (GDPR, fixture cleanup, etc.).
7291                ///
7292                /// Equivalent to [`Self::delete_pool`] (the framework's
7293                /// non-soft delete) — exposed under the Eloquent name
7294                /// for muscle-memory + so soft-delete-enabled models
7295                /// have all three operations (soft / restore / force)
7296                /// in one place.
7297                ///
7298                /// # Errors
7299                /// As [`Self::delete_pool`].
7300                pub async fn force_delete(
7301                    &self,
7302                    pool: &#root::sql::Pool,
7303                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7304                    Self::delete_pool(self, pool).await
7305                }
7306
7307                /// Returns `true` when this row is soft-deleted (its
7308                /// `#[rustango(soft_delete)]` column is currently
7309                /// set — Eloquent `$model->trashed()` parity).
7310                ///
7311                /// Pure in-memory predicate over `&self`; does not
7312                /// hit the database. Useful in admin/template code
7313                /// like `{% if post.trashed() %}…{% endif %}` and in
7314                /// guard clauses on restore/force-delete flows.
7315                pub fn trashed(&self) -> bool {
7316                    ::core::option::Option::is_some(&self.#sd_field_ident)
7317                }
7318
7319                /// Fetch every row whose `#[rustango(soft_delete)]`
7320                /// column is `NULL` (a.k.a. the live, non-trashed
7321                /// rows). Eloquent's default `Model::all()` behavior on
7322                /// a soft-delete model (Eloquent auto-scopes trashed
7323                /// rows out; rustango doesn't have auto-scoping yet —
7324                /// see issue #820 — so this is the explicit shortcut).
7325                ///
7326                /// One-liner over `QuerySet::<Self>::default()
7327                /// .active().fetch(pool)`. Closes #821 partial.
7328                ///
7329                /// # Errors
7330                /// As [`#root::sql::FetcherPool::fetch`].
7331                pub async fn active(
7332                    pool: &#root::sql::Pool,
7333                ) -> ::core::result::Result<
7334                    ::std::vec::Vec<Self>,
7335                    #root::sql::ExecError,
7336                > {
7337                    use #root::sql::FetcherPool as _;
7338                    #root::query::QuerySet::<Self>::default()
7339                        .active()
7340                        .fetch(pool)
7341                        .await
7342                }
7343
7344                /// Fetch ONLY soft-deleted rows. Eloquent
7345                /// `Model::onlyTrashed()->get()` parity — drives the
7346                /// admin "Trash" page, restore flows, GDPR purge
7347                /// scans, etc.
7348                ///
7349                /// One-liner over `QuerySet::<Self>::default()
7350                /// .only_trashed().fetch(pool)`. Closes #821
7351                /// partial.
7352                ///
7353                /// # Errors
7354                /// As [`#root::sql::FetcherPool::fetch`].
7355                pub async fn only_trashed(
7356                    pool: &#root::sql::Pool,
7357                ) -> ::core::result::Result<
7358                    ::std::vec::Vec<Self>,
7359                    #root::sql::ExecError,
7360                > {
7361                    use #root::sql::FetcherPool as _;
7362                    #root::query::QuerySet::<Self>::default()
7363                        .only_trashed()
7364                        .fetch(pool)
7365                        .await
7366                }
7367
7368                /// Fetch every row, both live and soft-deleted.
7369                /// Eloquent `Model::withTrashed()->get()` parity.
7370                ///
7371                /// Today every queryset already includes trashed rows
7372                /// (rustango has no global-scope tracking yet — issue
7373                /// #820), so this is functionally equivalent to
7374                /// [`Self::all`]. Exposed as a named shortcut so
7375                /// soft-delete-aware code reads `Model::with_trashed_pool`
7376                /// rather than `Model::all_pool` — keeps intent visible
7377                /// in callers and stays correct when auto-scoping lands.
7378                ///
7379                /// Closes #821 partial.
7380                ///
7381                /// # Errors
7382                /// As [`#root::sql::FetcherPool::fetch`].
7383                pub async fn with_trashed(
7384                    pool: &#root::sql::Pool,
7385                ) -> ::core::result::Result<
7386                    ::std::vec::Vec<Self>,
7387                    #root::sql::ExecError,
7388                > {
7389                    use #root::sql::FetcherPool as _;
7390                    #root::query::QuerySet::<Self>::default()
7391                        .with_trashed()
7392                        .fetch(pool)
7393                        .await
7394                }
7395
7396            }
7397        } else {
7398            quote!()
7399        };
7400        quote! {
7401            /// Delete the row identified by this instance's primary key.
7402            ///
7403            /// Returns the number of rows affected (0 or 1).
7404            ///
7405            /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7406            /// a driver-specific `PgPool`, so it does not exist on a build
7407            /// without the `postgres` feature. [`Self::delete_pool`] takes
7408            /// `rustango::sql::Pool` and works on all three. The naming is
7409            /// inverted; tracked in #1293.
7410            ///
7411            /// # Errors
7412            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7413            /// driver failures.
7414            #[cfg(feature = "postgres")]
7415            pub async fn delete(
7416                &self,
7417                pool: &#root::sql::sqlx::PgPool,
7418            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7419                #pool_to_delete_on
7420            }
7421
7422            /// Like [`Self::delete`] but accepts any sqlx executor —
7423            /// for tenant-scoped deletes against an explicitly-acquired
7424            /// connection. See [`Self::save_on`] for the rationale.
7425            ///
7426            /// # Errors
7427            /// As [`Self::delete`].
7428            #[cfg(feature = "postgres")]
7429            pub async fn delete_on #executor_generics (
7430                &self,
7431                #executor_param,
7432            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7433            #executor_where
7434            {
7435                let query = #root::core::DeleteQuery::by_pk(
7436                    <Self as #root::core::Model>::SCHEMA,
7437                    #pk_column_lit,
7438                    ::core::convert::Into::<#root::core::SqlValue>::into(
7439                        ::core::clone::Clone::clone(&self.#pk_ident)
7440                    ),
7441                );
7442                let _affected = #root::sql::__macro_internals::delete_on(
7443                    #executor_passes_to_data_write,
7444                    &query,
7445                ).await?;
7446                #audit_delete_emit
7447                ::core::result::Result::Ok(_affected)
7448            }
7449
7450            /// Per-call audit-source override for [`Self::delete_on`].
7451            /// See [`Self::save_on_with`] for shape rationale.
7452            ///
7453            /// # Errors
7454            /// As [`Self::delete_on`].
7455            #[cfg(feature = "postgres")]
7456            pub async fn delete_on_with #executor_generics (
7457                &self,
7458                #executor_param,
7459                source: #root::audit::AuditSource,
7460            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7461            #executor_where
7462            {
7463                #root::audit::with_source(source, self.delete_on(_executor)).await
7464            }
7465            #pool_delete_method
7466            #pool_insert_method
7467            #audit_entry_method
7468            #pool_save_method
7469            #refresh_replicate_methods
7470            #tx_delete_method
7471            #tx_insert_method
7472            #tx_save_method
7473            #soft_delete_methods
7474
7475            /// Returns `true` when `other` represents the same DB
7476            /// row as `self` — i.e. their primary keys compare
7477            /// equal. Eloquent `$model->is($other)` parity.
7478            ///
7479            /// Because both arguments are typed `&Self`, the
7480            /// model/table check is automatic — `Post::is` cannot
7481            /// be invoked against a `Comment` at compile time. Only
7482            /// the PK has to be compared at runtime.
7483            pub fn is(&self, other: &Self) -> bool {
7484                self.#pk_ident == other.#pk_ident
7485            }
7486
7487            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7488            /// parity.
7489            pub fn is_not(&self, other: &Self) -> bool {
7490                self.#pk_ident != other.#pk_ident
7491            }
7492
7493            /// Returns this row's primary-key value as an
7494            /// [`#root::core::SqlValue`]. Eloquent
7495            /// `$model->getKey()` parity.
7496            ///
7497            /// Useful when you need to thread the PK through a
7498            /// generic `Into<SqlValue>`-bound API without knowing the
7499            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7500            #[must_use]
7501            pub fn get_key(&self) -> #root::core::SqlValue {
7502                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7503            }
7504
7505        }
7506    });
7507
7508    let insert_method = if fields.has_auto {
7509        let pushes = &fields.insert_pushes;
7510        let returning_cols = &fields.returning_cols;
7511        let auto_assigns = &fields.auto_assigns;
7512        quote! {
7513            /// Insert this row into its table. Skips columns whose
7514            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7515            /// sequence fills them in, then reads each `Auto` column
7516            /// back via `RETURNING` and stores it on `self`.
7517            ///
7518            /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7519            /// a driver-specific `PgPool`, so it does not exist on a build
7520            /// without the `postgres` feature. [`Self::insert_pool`] takes
7521            /// `rustango::sql::Pool` and works on all three. The naming is
7522            /// inverted; tracked in #1293.
7523            ///
7524            /// # Errors
7525            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7526            /// driver failures.
7527            #[cfg(feature = "postgres")]
7528            pub async fn insert(
7529                &mut self,
7530                pool: &#root::sql::sqlx::PgPool,
7531            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7532                #pool_to_insert_on
7533            }
7534
7535            /// Like [`Self::insert`] but accepts any sqlx executor.
7536            /// See [`Self::save_on`] for tenancy-scoped rationale.
7537            ///
7538            /// # Errors
7539            /// As [`Self::insert`].
7540            #[cfg(feature = "postgres")]
7541            pub async fn insert_on #executor_generics (
7542                &mut self,
7543                #executor_param,
7544            ) -> ::core::result::Result<(), #root::sql::ExecError>
7545            #executor_where
7546            {
7547                let mut _columns: ::std::vec::Vec<&'static str> =
7548                    ::std::vec::Vec::new();
7549                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7550                    ::std::vec::Vec::new();
7551                #( #pushes )*
7552                let query = #root::core::InsertQuery::new(
7553                    <Self as #root::core::Model>::SCHEMA,
7554                    _columns,
7555                    _values,
7556                )
7557                .returning(::std::vec![ #( #returning_cols ),* ]);
7558                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7559                    #executor_passes_to_data_write,
7560                    &query,
7561                ).await?;
7562                let _returning_row = &_returning_row_v;
7563                #( #auto_assigns )*
7564                #audit_insert_emit
7565                ::core::result::Result::Ok(())
7566            }
7567
7568            /// Per-call audit-source override for [`Self::insert_on`].
7569            /// See [`Self::save_on_with`] for shape rationale.
7570            ///
7571            /// # Errors
7572            /// As [`Self::insert_on`].
7573            #[cfg(feature = "postgres")]
7574            pub async fn insert_on_with #executor_generics (
7575                &mut self,
7576                #executor_param,
7577                source: #root::audit::AuditSource,
7578            ) -> ::core::result::Result<(), #root::sql::ExecError>
7579            #executor_where
7580            {
7581                #root::audit::with_source(source, self.insert_on(_executor)).await
7582            }
7583        }
7584    } else {
7585        let insert_columns = &fields.insert_columns;
7586        let insert_values = &fields.insert_values;
7587        quote! {
7588            /// Insert this row into its table.
7589            ///
7590            /// # Errors
7591            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7592            /// driver failures.
7593            #[cfg(feature = "postgres")]
7594            pub async fn insert(
7595                &self,
7596                pool: &#root::sql::sqlx::PgPool,
7597            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7598                self.insert_on(pool).await
7599            }
7600
7601            /// Like [`Self::insert`] but accepts any sqlx executor.
7602            /// See [`Self::save_on`] for tenancy-scoped rationale.
7603            ///
7604            /// # Errors
7605            /// As [`Self::insert`].
7606            #[cfg(feature = "postgres")]
7607            pub async fn insert_on<'_c, _E>(
7608                &self,
7609                _executor: _E,
7610            ) -> ::core::result::Result<(), #root::sql::ExecError>
7611            where
7612                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7613            {
7614                let query = #root::core::InsertQuery::new(
7615                    <Self as #root::core::Model>::SCHEMA,
7616                    ::std::vec![ #( #insert_columns ),* ],
7617                    ::std::vec![ #( #insert_values ),* ],
7618                );
7619                #root::sql::__macro_internals::insert_on(_executor, &query).await
7620            }
7621        }
7622    };
7623
7624    let bulk_insert_method = if fields.has_auto {
7625        let cols_no_auto = &fields.bulk_columns_no_auto;
7626        let cols_all = &fields.bulk_columns_all;
7627        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7628        let pushes_all = &fields.bulk_pushes_all;
7629        let returning_cols = &fields.returning_cols;
7630        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7631        let uniformity = &fields.bulk_auto_uniformity;
7632        let fill_in_place = &fields.bulk_fill_in_place;
7633        // Nothing to read back when every Auto field is filled Rust-side (#1934).
7634        let read_back = if fields.returning_cols.is_empty() {
7635            quote!()
7636        } else {
7637            quote! {
7638                if _returned.len() != rows.len() {
7639                    return ::core::result::Result::Err(
7640                        #root::sql::ExecError::Sql(
7641                            #root::sql::SqlError::BulkInsertReturningMismatch {
7642                                expected: rows.len(),
7643                                actual: _returned.len(),
7644                            }
7645                        )
7646                    );
7647                }
7648                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7649                    #auto_assigns_for_row
7650                }
7651            }
7652        };
7653        // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7654        let first_auto_ident = fields
7655            .first_db_auto
7656            .as_ref()
7657            .map(|(ident, _)| ident)
7658            .or(fields.first_auto_ident.as_ref())
7659            .expect("has_auto implies first_auto_ident is Some");
7660        quote! {
7661            /// Bulk-insert `rows`: one statement, or one per chunk in a
7662            /// transaction past the bind cap. Every row's
7663            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7664            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7665            /// (caller-supplied values). Mixed Set/Unset is rejected
7666            /// — call `insert` per row for that case.
7667            ///
7668            /// Empty slice is a no-op. Each row's `Auto` fields are
7669            /// populated from the `RETURNING` clause in input order
7670            /// before this returns.
7671            ///
7672            /// # Errors
7673            /// Returns [`#root::sql::ExecError`] for validation,
7674            /// SQL-writing, mixed-Auto rejection, or driver failures.
7675            #[cfg(feature = "postgres")]
7676            pub async fn bulk_insert(
7677                rows: &mut [Self],
7678                pool: &#root::sql::sqlx::PgPool,
7679            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7680                #pool_to_bulk_insert_mut
7681            }
7682
7683            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7684            /// See [`Self::save_on`] for tenancy-scoped rationale.
7685            ///
7686            /// # Errors
7687            /// As [`Self::bulk_insert`].
7688            #[cfg(feature = "postgres")]
7689            pub async fn bulk_insert_on #executor_generics (
7690                rows: &mut [Self],
7691                #executor_param,
7692            ) -> ::core::result::Result<(), #root::sql::ExecError>
7693            #executor_where
7694            {
7695                if rows.is_empty() {
7696                    return ::core::result::Result::Ok(());
7697                }
7698                let _first_unset = matches!(
7699                    rows[0].#first_auto_ident,
7700                    #root::sql::Auto::Unset
7701                );
7702                #( #uniformity )*
7703                for _row in rows.iter_mut() {
7704                    #( #fill_in_place )*
7705                }
7706
7707                let mut _all_rows: ::std::vec::Vec<
7708                    ::std::vec::Vec<#root::core::SqlValue>,
7709                > = ::std::vec::Vec::with_capacity(rows.len());
7710                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7711                    for _row in rows.iter() {
7712                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7713                            ::std::vec::Vec::new();
7714                        #( #pushes_no_auto )*
7715                        _all_rows.push(_row_vals);
7716                    }
7717                    ::std::vec![ #( #cols_no_auto ),* ]
7718                } else {
7719                    for _row in rows.iter() {
7720                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7721                            ::std::vec::Vec::new();
7722                        #( #pushes_all )*
7723                        _all_rows.push(_row_vals);
7724                    }
7725                    ::std::vec![ #( #cols_all ),* ]
7726                };
7727
7728                let _query = #root::core::BulkInsertQuery::new(
7729                    <Self as #root::core::Model>::SCHEMA,
7730                    _columns,
7731                    _all_rows,
7732                )
7733                .returning(::std::vec![ #( #returning_cols ),* ]);
7734                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7735                    #executor_passes_to_data_write,
7736                    &_query,
7737                ).await?;
7738                #read_back
7739                #audit_bulk_insert_emit
7740                ::core::result::Result::Ok(())
7741            }
7742        }
7743    } else {
7744        let cols_all = &fields.bulk_columns_all;
7745        let pushes_all = &fields.bulk_pushes_all;
7746        quote! {
7747            /// Bulk-insert `rows`: one statement, or one per chunk in a
7748            /// transaction past the bind cap. Every row's
7749            /// fields are written verbatim — there are no `Auto<T>`
7750            /// fields on this model.
7751            ///
7752            /// Empty slice is a no-op.
7753            ///
7754            /// # Errors
7755            /// Returns [`#root::sql::ExecError`] for validation,
7756            /// SQL-writing, or driver failures.
7757            #[cfg(feature = "postgres")]
7758            pub async fn bulk_insert(
7759                rows: &[Self],
7760                pool: &#root::sql::sqlx::PgPool,
7761            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7762                #pool_to_bulk_insert_on
7763            }
7764
7765            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7766            /// See [`Self::save_on`] for tenancy-scoped rationale.
7767            ///
7768            /// # Errors
7769            /// As [`Self::bulk_insert`].
7770            #[cfg(feature = "postgres")]
7771            pub async fn bulk_insert_on #executor_generics (
7772                rows: &[Self],
7773                #executor_param,
7774            ) -> ::core::result::Result<(), #root::sql::ExecError>
7775            #executor_where
7776            {
7777                if rows.is_empty() {
7778                    return ::core::result::Result::Ok(());
7779                }
7780                let mut _all_rows: ::std::vec::Vec<
7781                    ::std::vec::Vec<#root::core::SqlValue>,
7782                > = ::std::vec::Vec::with_capacity(rows.len());
7783                for _row in rows.iter() {
7784                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7785                        ::std::vec::Vec::new();
7786                    #( #pushes_all )*
7787                    _all_rows.push(_row_vals);
7788                }
7789                let _query = #root::core::BulkInsertQuery::new(
7790                    <Self as #root::core::Model>::SCHEMA,
7791                    ::std::vec![ #( #cols_all ),* ],
7792                    _all_rows,
7793                );
7794                let _ = #root::sql::__macro_internals::bulk_insert_on(
7795                    #executor_passes_to_data_write,
7796                    &_query,
7797                ).await?;
7798                #audit_bulk_insert_emit
7799                ::core::result::Result::Ok(())
7800            }
7801        }
7802    };
7803
7804    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7805    // (no postgres-feature gate); routes through the existing
7806    // `bulk_insert_pool` + per-dialect conflict writer.
7807    //
7808    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7809    // sequence picks the PK for fresh inserts; the UPDATE branch never
7810    // touches the Auto column.
7811    // Audited models audit each written row; MySQL, with no RETURNING,
7812    // is refused there (#1747, #1795).
7813    let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7814        (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7815        (true, true) => quote! {
7816            #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7817                _r.__rustango_audit_entry(_op)
7818            })
7819            .await
7820        },
7821        (true, false) => quote! {
7822            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7823                table: <Self as #root::core::Model>::SCHEMA.table,
7824            })
7825        },
7826    };
7827    let bulk_upsert_pool_method = {
7828        let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7829            fields.uuid_v7_fields.iter().cloned().unzip();
7830        // Pick the "no Auto" columns when the model has Auto fields,
7831        // else every column.
7832        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7833            (
7834                fields.bulk_columns_no_auto.clone(),
7835                fields.bulk_pushes_no_auto.clone(),
7836            )
7837        } else {
7838            (
7839                fields.bulk_columns_all.clone(),
7840                fields.bulk_pushes_all.clone(),
7841            )
7842        };
7843        quote! {
7844            /// Tri-dialect bulk insert that updates on conflict — the
7845            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7846            ///
7847            /// Per-row values are extracted and lowered into a
7848            /// [`#root::core::BulkInsertQuery`] with
7849            /// `on_conflict = DoUpdate { target, update_columns }`. The
7850            /// writer dispatches per-dialect:
7851            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7852            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7853            ///   `target` is not the only unique key, since MySQL matches every UNIQUE index
7854            ///
7855            /// `target` names the column(s) whose unique constraint
7856            /// defines the conflict (typically a `unique` or
7857            /// `unique_together` natural-key column, NOT the `Auto<T>`
7858            /// PK). `update_cols` names the columns to overwrite on
7859            /// conflict — every other column is left untouched on the
7860            /// existing row.
7861            ///
7862            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7863            /// picks the PK on insert; the update path never touches
7864            /// the Auto column). Auto-set rows trigger a hard error.
7865            /// Rows repeating a `target` key: the last one wins.
7866            /// Empty slice is a no-op. On an audited model each written
7867            /// row gets a `create` or `update` audit row; MySQL refuses it.
7868            ///
7869            /// # Errors
7870            /// Returns [`#root::sql::ExecError`] for validation,
7871            /// SQL-writing, or driver failures.
7872            pub async fn bulk_upsert_pool(
7873                rows: &[Self],
7874                target: &[&'static str],
7875                update_cols: &[&'static str],
7876                pool: &#root::sql::Pool,
7877            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7878                if rows.is_empty() {
7879                    return ::core::result::Result::Ok(());
7880                }
7881                let mut _all_rows: ::std::vec::Vec<
7882                    ::std::vec::Vec<#root::core::SqlValue>,
7883                > = ::std::vec::Vec::with_capacity(rows.len());
7884                for _row in rows.iter() {
7885                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7886                        ::std::vec::Vec::new();
7887                    #( #upsert_pushes )*
7888                    _all_rows.push(_row_vals);
7889                }
7890                #[allow(unused_mut)]
7891                let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7892                #(
7893                    if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7894                        _generated.push(#gen_cols);
7895                    }
7896                )*
7897                let _query = #root::core::BulkInsertQuery::new(
7898                    <Self as #root::core::Model>::SCHEMA,
7899                    ::std::vec![ #( #upsert_cols ),* ],
7900                    _all_rows,
7901                )
7902                .on_conflict_do_update(target, update_cols)
7903                .generated(_generated);
7904                #run_conflict_bulk_insert
7905            }
7906
7907            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7908            /// skip rows that would violate a unique constraint. Issue
7909            /// #267 / T1.5. Same per-dialect dispatch as
7910            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7911            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7912            /// <pivot> = <pivot>` (MySQL no-op write).
7913            ///
7914            /// # Errors
7915            /// As [`Self::bulk_upsert_pool`].
7916            pub async fn bulk_insert_or_ignore_pool(
7917                rows: &[Self],
7918                pool: &#root::sql::Pool,
7919            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7920                if rows.is_empty() {
7921                    return ::core::result::Result::Ok(());
7922                }
7923                let mut _all_rows: ::std::vec::Vec<
7924                    ::std::vec::Vec<#root::core::SqlValue>,
7925                > = ::std::vec::Vec::with_capacity(rows.len());
7926                for _row in rows.iter() {
7927                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7928                        ::std::vec::Vec::new();
7929                    #( #upsert_pushes )*
7930                    _all_rows.push(_row_vals);
7931                }
7932                let _query = #root::core::BulkInsertQuery::new(
7933                    <Self as #root::core::Model>::SCHEMA,
7934                    ::std::vec![ #( #upsert_cols ),* ],
7935                    _all_rows,
7936                )
7937                .on_conflict_do_nothing();
7938                #run_conflict_bulk_insert
7939            }
7940        }
7941    };
7942
7943    // Ergonomic `Model::bulk_update(objs, fields)`.
7944    // The SQL/IR/executor stack
7945    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7946    // `write_bulk_update_*` writers) already existed; what was missing
7947    // was the per-model constructor that maps `&[Self]` + a runtime
7948    // column list into rows of `[pk, col_vals…]` so callers don't
7949    // hand-build the IR. Emitted only when the model has a primary key
7950    // (the PK is the join key and can't itself be updated).
7951    let bulk_update_rows_run = if audited_fields.is_some() {
7952        quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7953            _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7954        })
7955        .await)
7956    } else {
7957        quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7958    };
7959    let bulk_update_method = match &fields.primary_key {
7960        None => quote! {},
7961        Some((pk_ident, pk_col)) => {
7962            // One pair of match arms per non-PK column: a validation arm
7963            // resolving the runtime name to its `&'static str` column,
7964            // and a value arm pushing that field off the row.
7965            let mut col_arms: Vec<TokenStream2> = Vec::new();
7966            let mut val_arms: Vec<TokenStream2> = Vec::new();
7967            for entry in &fields.column_entries {
7968                if &entry.column == pk_col {
7969                    continue;
7970                }
7971                let col = &entry.column;
7972                let ident = &entry.ident;
7973                col_arms.push(quote! { #col => #col, });
7974                val_arms.push(quote! {
7975                    #col => _row_vals.push(
7976                        ::core::convert::Into::<#root::core::SqlValue>::into(
7977                            ::core::clone::Clone::clone(&_o.#ident)
7978                        )
7979                    ),
7980                });
7981            }
7982            quote! {
7983                /// Write per-row-different values for the named `fields`
7984                /// across every object in `objs`, matched by primary key.
7985                /// Past the backend's bind cap the rows go in batches inside
7986                /// one transaction, so the update is all or none.
7987                ///
7988                /// `fields` names the **columns** to update. The primary
7989                /// key identifies each row and cannot itself be updated
7990                /// (pass it and you get
7991                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7992                /// Empty `objs` or `fields` is a no-op returning `0`.
7993                /// Objects whose PK matches no row are simply not updated.
7994                /// Returns the number of rows affected.
7995                ///
7996                /// Tri-dialect: lowers to a
7997                /// [`#root::core::BulkUpdateQuery`] and dispatches
7998                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7999                /// a CTE + correlated subquery on SQLite, an inner
8000                /// `JOIN (VALUES …)` on MySQL.
8001                ///
8002                /// # Errors
8003                /// [`#root::core::QueryError::UnknownField`] for a name
8004                /// that isn't a column on this model,
8005                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
8006                /// `fields` names the PK, or [`#root::sql::ExecError`] for
8007                /// SQL-writing / driver failures.
8008                pub async fn bulk_update(
8009                    objs: &[Self],
8010                    fields: &[&str],
8011                    pool: &#root::sql::Pool,
8012                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
8013                    if objs.is_empty() || fields.is_empty() {
8014                        return ::core::result::Result::Ok(0);
8015                    }
8016                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
8017                    let mut _update_columns: ::std::vec::Vec<&'static str> =
8018                        ::std::vec::Vec::with_capacity(fields.len());
8019                    for &_f in fields {
8020                        let _col: &'static str = match _f {
8021                            #pk_col => {
8022                                return ::core::result::Result::Err(
8023                                    ::core::convert::Into::into(
8024                                        #root::core::QueryError::BulkUpdatePrimaryKey {
8025                                            model: _model_name,
8026                                            field: ::std::string::ToString::to_string(_f),
8027                                        }
8028                                    )
8029                                );
8030                            }
8031                            #( #col_arms )*
8032                            _ => {
8033                                return ::core::result::Result::Err(
8034                                    ::core::convert::Into::into(
8035                                        #root::core::QueryError::UnknownField {
8036                                            model: _model_name,
8037                                            field: ::std::string::ToString::to_string(_f),
8038                                        }
8039                                    )
8040                                );
8041                            }
8042                        };
8043                        // A model whose only field is the primary key emits no
8044                        // non-PK arms, so every arm above diverges and this push
8045                        // is genuinely unreachable — correct, since such a model
8046                        // has nothing to bulk-update. Scoped to the statement
8047                        // rather than the fn so a real unreachability elsewhere
8048                        // in this body still warns. Without it every PK-only
8049                        // model warns in the *user's* crate, which they cannot
8050                        // act on (#1290).
8051                        #[allow(unreachable_code)]
8052                        _update_columns.push(_col);
8053                    }
8054                    let mut _rows: ::std::vec::Vec<
8055                        ::std::vec::Vec<#root::core::SqlValue>,
8056                    > = ::std::vec::Vec::with_capacity(objs.len());
8057                    for _o in objs.iter() {
8058                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
8059                            ::std::vec::Vec::with_capacity(fields.len() + 1);
8060                        // PK first — the writers expect `[pk, …update cols]`.
8061                        _row_vals.push(
8062                            ::core::convert::Into::<#root::core::SqlValue>::into(
8063                                ::core::clone::Clone::clone(&_o.#pk_ident)
8064                            )
8065                        );
8066                        for &_f in fields {
8067                            match _f {
8068                                #( #val_arms )*
8069                                // Unreachable: every name was validated
8070                                // against the same arm set above.
8071                                _ => {}
8072                            }
8073                        }
8074                        _rows.push(_row_vals);
8075                    }
8076                    let _query = #root::core::BulkUpdateQuery::new(
8077                        <Self as #root::core::Model>::SCHEMA,
8078                        _update_columns,
8079                        _rows,
8080                    );
8081                    #bulk_update_rows_run
8082                }
8083            }
8084        }
8085    };
8086
8087    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
8088        quote! {
8089            /// Hidden runtime accessor for the primary-key value as a
8090            /// [`SqlValue`]. Used by reverse-relation helpers
8091            /// (`<parent>::<child>_set`) emitted from sibling models'
8092            /// FK fields. Not part of the public API.
8093            #[doc(hidden)]
8094            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
8095                ::core::convert::Into::<#root::core::SqlValue>::into(
8096                    ::core::clone::Clone::clone(&self.#pk_ident)
8097                )
8098            }
8099        }
8100    });
8101
8102    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
8103        let column_arms = fields.column_entries.iter().map(|entry| {
8104            let col = &entry.column;
8105            let ident = &entry.ident;
8106            quote! {
8107                #col => ::core::option::Option::Some(
8108                    ::core::convert::Into::<#root::core::SqlValue>::into(
8109                        ::core::clone::Clone::clone(&self.#ident)
8110                    )
8111                ),
8112            }
8113        });
8114        quote! {
8115            impl #root::sql::HasPkValue for #struct_name {
8116                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
8117                    ::core::convert::Into::<#root::core::SqlValue>::into(
8118                        ::core::clone::Clone::clone(&self.#pk_ident)
8119                    )
8120                }
8121                fn __rustango_column_value(
8122                    &self,
8123                    column: &str,
8124                ) -> ::core::option::Option<#root::core::SqlValue> {
8125                    match column {
8126                        #( #column_arms )*
8127                        _ => ::core::option::Option::None,
8128                    }
8129                }
8130            }
8131        }
8132    });
8133
8134    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
8135
8136    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
8137    // dispatch to the right per-backend body without the macro emitting
8138    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
8139    // so audited models with non-Auto PKs (which still go through
8140    // `insert_one_with_audit` → `apply_auto_pk`) link.
8141    let assign_auto_pk_pool_impl = {
8142        let auto_assigns = &fields.auto_assigns;
8143        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
8144        // so the body is structurally identical to `auto_assigns` —
8145        // only the helper name swaps from `try_get_returning` to
8146        // `try_get_returning_sqlite` so the closure typechecks against
8147        // a `SqliteRow` instead of a `PgRow`.
8148        let auto_assigns_sqlite: Vec<TokenStream2> = fields
8149            .auto_field_idents
8150            .iter()
8151            .map(|(ident, column, uuid)| {
8152                // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
8153                if *uuid {
8154                    return quote! {
8155                        self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
8156                            _returning_row, #column
8157                        )?);
8158                    };
8159                }
8160                quote! {
8161                    self.#ident = #root::sql::try_get_returning_sqlite(
8162                        _returning_row, #column
8163                    )?;
8164                }
8165            })
8166            .collect();
8167        // #1028 — decode each `generated_as` column from the same
8168        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
8169        // own type drives the decode (no `Auto<T>` wrapper).
8170        let generated_assigns: Vec<TokenStream2> = fields
8171            .generated_field_idents
8172            .iter()
8173            .map(|(ident, column)| {
8174                quote! {
8175                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
8176                }
8177            })
8178            .collect();
8179        let generated_assigns_sqlite: Vec<TokenStream2> = fields
8180            .generated_field_idents
8181            .iter()
8182            .map(|(ident, column)| {
8183                quote! {
8184                    self.#ident = #root::sql::try_get_returning_sqlite(
8185                        _returning_row, #column
8186                    )?;
8187                }
8188            })
8189            .collect();
8190        let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
8191            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
8192            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
8193            // Auto<Uuid>/etc. fail to link against MySQL (intended —
8194            // those models can't use AUTO_INCREMENT). The trait is only
8195            // touched on the MySQL arm at runtime, so PG-only consumers
8196            // never see the bound failure.
8197            //
8198            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
8199            // Auto<i64> PK + auto_now_add timestamp) errored hard at
8200            // runtime with "multi-column RETURNING". MySQL has no
8201            // multi-column RETURNING semantic and a follow-up SELECT
8202            // would need cross-trait plumbing. Pragmatic shape: succeed
8203            // with the FIRST Auto field populated from LAST_INSERT_ID();
8204            // any other Auto fields stay `Auto::Unset`. Callers that
8205            // need the DB-defaulted timestamp / UUID can re-fetch the
8206            // row by PK after `save_pool`. Fixes the cookbook chapter
8207            // 12 dialect divergence.
8208            quote! {
8209                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
8210                    ::rustango_from_mysql_auto_id(_id)?;
8211                self.#first = #root::sql::Auto::Set(_converted);
8212                ::core::result::Result::Ok(())
8213            }
8214        } else {
8215            quote! {
8216                let _ = _id;
8217                ::core::result::Result::Ok(())
8218            }
8219        };
8220        quote! {
8221            impl #root::sql::AssignAutoPkPool for #struct_name {
8222                fn __rustango_assign_from_pg_row(
8223                    &mut self,
8224                    _returning_row: &#root::sql::PgReturningRow,
8225                ) -> ::core::result::Result<(), #root::sql::ExecError> {
8226                    #( #auto_assigns )*
8227                    #( #generated_assigns )*
8228                    ::core::result::Result::Ok(())
8229                }
8230                fn __rustango_assign_from_mysql_id(
8231                    &mut self,
8232                    _id: i64,
8233                ) -> ::core::result::Result<(), #root::sql::ExecError> {
8234                    #mysql_body
8235                }
8236                fn __rustango_assign_from_sqlite_row(
8237                    &mut self,
8238                    _returning_row: &#root::sql::SqliteReturningRow,
8239                ) -> ::core::result::Result<(), #root::sql::ExecError> {
8240                    #( #auto_assigns_sqlite )*
8241                    #( #generated_assigns_sqlite )*
8242                    ::core::result::Result::Ok(())
8243                }
8244            }
8245        }
8246    };
8247
8248    let from_aliased_row_inits = &fields.from_aliased_row_inits;
8249    let aliased_row_helper = quote! {
8250        /// Decode a row's aliased target columns (produced by
8251        /// `select_related`'s LEFT JOIN) into a fresh instance of
8252        /// this model. Reads each column via
8253        /// `format!("{prefix}__{col}")`, matching the alias the
8254        /// SELECT writer emitted. Slice 9.0d.
8255        #[doc(hidden)]
8256        #[cfg(feature = "postgres")]
8257        pub fn __rustango_from_aliased_row(
8258            row: &#root::sql::sqlx::postgres::PgRow,
8259            prefix: &str,
8260        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8261            ::core::result::Result::Ok(Self {
8262                #( #from_aliased_row_inits ),*
8263            })
8264        }
8265    };
8266    // v0.23.0-batch8 — MySQL counterpart, gated through the
8267    // cfg-aware macro_rules so PG-only builds expand to nothing.
8268    let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
8269    let aliased_row_helper_my = quote! {
8270        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
8271            #( #from_aliased_row_inits_my ),*
8272        });
8273    };
8274
8275    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
8276    // pattern + cfg gate on the `sqlite` feature.
8277    let aliased_row_helper_sqlite = quote! {
8278        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
8279            #( #from_aliased_row_inits ),*
8280        });
8281    };
8282
8283    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
8284    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
8285    let load_related_impl_sqlite =
8286        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
8287
8288    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
8289    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
8290    // default `Self::objects()`. Each accessor returns a fresh
8291    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
8292    // methods the user defined.
8293    let extra_manager_fns: Vec<TokenStream2> = manager_fns
8294        .iter()
8295        .map(|fn_ident| {
8296            let model_name_str = struct_name.to_string();
8297            let fn_name_str = fn_ident.to_string();
8298            let doc = format!(
8299                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
8300                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
8301                 equivalent to `Self::objects()`. Chains with any \
8302                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
8303                 extension methods."
8304            );
8305            quote! {
8306                #[doc = #doc]
8307                #[must_use]
8308                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
8309                    #root::query::QuerySet::new()
8310                }
8311            }
8312        })
8313        .collect();
8314
8315    quote! {
8316        impl #struct_name {
8317            /// Start a new `QuerySet` over this model.
8318            #[must_use]
8319            pub fn objects() -> #root::query::QuerySet<#struct_name> {
8320                #root::query::QuerySet::new()
8321            }
8322
8323            /// Eloquent-shape alias of [`Self::objects`]. Returns
8324            /// a fresh `QuerySet<Self>` ready for `.filter()` /
8325            /// `.where_()` / etc. Matches Laravel muscle-memory:
8326            ///
8327            /// ```ignore
8328            /// // Eloquent:    Post::query()->where('published', true)
8329            /// // rustango:    Post::query().filter("published", true)
8330            /// //         or:  Post::objects().filter("published", true)
8331            /// ```
8332            ///
8333            /// Both names point at the same underlying constructor;
8334            /// neither is preferred.
8335            #[must_use]
8336            pub fn query() -> #root::query::QuerySet<#struct_name> {
8337                #root::query::QuerySet::new()
8338            }
8339
8340            #( #extra_manager_fns )*
8341
8342            #insert_method
8343
8344            #bulk_insert_method
8345
8346            #bulk_upsert_pool_method
8347
8348            #bulk_update_method
8349
8350            #save_method
8351
8352            #pk_methods
8353
8354            #pk_value_helper
8355
8356            #aliased_row_helper
8357
8358            #column_consts
8359        }
8360
8361        #aliased_row_helper_my
8362
8363        #aliased_row_helper_sqlite
8364
8365        #load_related_impl
8366
8367        #load_related_impl_my
8368
8369        #load_related_impl_sqlite
8370
8371        #has_pk_value_impl
8372
8373        #fk_pk_access_impl
8374
8375        #assign_auto_pk_pool_impl
8376    }
8377}
8378
8379/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8380/// `auto_assigns` but reading from `_returning_row` and writing to
8381/// `_row_mut` instead of `self`.
8382fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8383    let root = rustango_root();
8384    let lines = fields.auto_field_idents.iter().map(|(ident, column, _)| {
8385        let col_lit = column.as_str();
8386        quote! {
8387            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8388                _returning_row,
8389                #col_lit,
8390            )?;
8391        }
8392    });
8393    quote! { #( #lines )* }
8394}
8395
8396/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8397fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8398    let lines = entries.iter().map(|e| {
8399        let ident = &e.ident;
8400        let col_ty = column_type_ident(ident);
8401        quote! {
8402            #[allow(non_upper_case_globals)]
8403            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8404        }
8405    });
8406    quote! { #(#lines)* }
8407}
8408
8409/// Emit a hidden per-model module carrying one zero-sized type per field,
8410/// each with a `Column` impl pointing back at the model.
8411fn column_module_tokens(
8412    module_ident: &syn::Ident,
8413    struct_name: &syn::Ident,
8414    entries: &[ColumnEntry],
8415) -> TokenStream2 {
8416    let root = rustango_root();
8417    let items = entries.iter().map(|e| {
8418        let col_ty = column_type_ident(&e.ident);
8419        let value_ty = &e.value_ty;
8420        let name = &e.name;
8421        let column = &e.column;
8422        let field_type_tokens = &e.field_type_tokens;
8423        quote! {
8424            #[derive(::core::clone::Clone, ::core::marker::Copy)]
8425            pub struct #col_ty;
8426
8427            impl #root::core::Column for #col_ty {
8428                type Model = super::#struct_name;
8429                type Value = #value_ty;
8430                const NAME: &'static str = #name;
8431                const COLUMN: &'static str = #column;
8432                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8433            }
8434        }
8435    });
8436    quote! {
8437        #[doc(hidden)]
8438        #[allow(non_camel_case_types, non_snake_case)]
8439        pub mod #module_ident {
8440            // Re-import the parent scope so field types referencing
8441            // sibling models (e.g. `ForeignKey<Author>`) resolve
8442            // inside this submodule. Without this we'd hit
8443            // `proc_macro_derive_resolution_fallback` warnings.
8444            #[allow(unused_imports)]
8445            use super::*;
8446            #(#items)*
8447        }
8448    }
8449}
8450
8451fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8452    let name = ident_name(field_ident);
8453    syn::Ident::new(&format!("{name}_col"), field_ident.span())
8454}
8455
8456/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8457/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8458fn restore_auto_now_unless(
8459    idents: &[syn::Ident],
8460    ret: &TokenStream2,
8461    keep: &TokenStream2,
8462    body: &TokenStream2,
8463) -> TokenStream2 {
8464    if idents.is_empty() {
8465        return body.clone();
8466    }
8467    let prev: Vec<syn::Ident> = (0..idents.len())
8468        .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8469        .collect();
8470    quote! {
8471        #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8472        let _saved: #ret = async { #body }.await;
8473        if !(#keep) {
8474            #( self.#idents = #prev; )*
8475        }
8476        _saved
8477    }
8478}
8479
8480/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8481fn ident_name(ident: &syn::Ident) -> String {
8482    syn::ext::IdentExt::unraw(ident).to_string()
8483}
8484
8485/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8486fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8487    match syn::parse_str::<syn::Ident>(name) {
8488        Ok(mut ident) => {
8489            ident.set_span(span);
8490            ident
8491        }
8492        Err(_) => syn::Ident::new_raw(name, span),
8493    }
8494}
8495
8496fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8497    syn::Ident::new(
8498        &format!("__rustango_cols_{struct_name}"),
8499        struct_name.span(),
8500    )
8501}
8502
8503fn from_row_impl_tokens(
8504    struct_name: &syn::Ident,
8505    from_row_inits: &[TokenStream2],
8506    from_row_inits_my: &[TokenStream2],
8507) -> TokenStream2 {
8508    let root = rustango_root();
8509    // The Postgres impl is always emitted — every rustango build pulls in
8510    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8511    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8512    // macro_rules whose body collapses to nothing when rustango is built
8513    // without the `mysql` feature. No user-facing feature shim required.
8514    //
8515    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8516    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8517    // back into a comma-separated list inside square brackets. Since each
8518    // entry is already in `field: expr` shape, the existing tokens slot in.
8519    quote! {
8520        #[cfg(feature = "postgres")]
8521        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8522            for #struct_name
8523        {
8524            fn from_row(
8525                row: &'r #root::sql::sqlx::postgres::PgRow,
8526            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8527                ::core::result::Result::Ok(Self {
8528                    #( #from_row_inits ),*
8529                })
8530            }
8531        }
8532
8533        #root::__impl_my_from_row!(#struct_name, |row| {
8534            #( #from_row_inits_my ),*
8535        });
8536
8537        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8538            #( #from_row_inits ),*
8539        });
8540    }
8541}
8542
8543struct ContainerAttrs {
8544    table: Option<String>,
8545    display: Option<(String, proc_macro2::Span)>,
8546    /// Explicit app label from `#[rustango(app = "blog")]`.
8547    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8548    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8549    /// at runtime — this attribute is the override for cases where
8550    /// the inference is wrong (e.g. a model that conceptually belongs
8551    /// to one app but is physically in another module).
8552    app: Option<String>,
8553    /// Per-model admin knobs from
8554    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8555    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8556    /// admin code falls back to `AdminConfig::DEFAULT`.
8557    admin: Option<AdminAttrs>,
8558    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8559    /// `None` when the model isn't audited — write paths emit no
8560    /// audit entries. When present, single-row writes capture
8561    /// before/after for the listed fields and bulk writes batch
8562    /// snapshots into one INSERT into `rustango_audit_log`.
8563    audit: Option<AuditAttrs>,
8564    /// `true` when `#[rustango(permissions)]` is present. Signals that
8565    /// `auto_create_permissions` should seed the four CRUD codenames for
8566    /// this model.
8567    permissions: bool,
8568    /// Many-to-many relations declared via
8569    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8570    ///                 src = "post_id", dst = "tag_id"))]`.
8571    m2m: Vec<M2MAttr>,
8572    /// Polymorphic M2M relations declared via
8573    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8574    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8575    ///   related_column = "tag_id"))]` (issue #818).
8576    generic_m2m: Vec<GenericM2MAttr>,
8577    /// Composite indexes declared via
8578    /// `#[rustango(index("col1, col2"))]` or
8579    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8580    /// Single-column indexes from `#[rustango(index)]` on fields are
8581    /// accumulated here during field collection.
8582    indexes: Vec<IndexAttr>,
8583    /// Table-level CHECK constraints declared via
8584    /// `#[rustango(check(name = "…", expr = "…"))]`.
8585    checks: Vec<CheckAttr>,
8586    /// Table-level PG `EXCLUDE` constraints declared via
8587    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8588    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8589    /// migration writer renders them on Postgres and skips with a
8590    /// warning on MySQL/SQLite. Issue #319.
8591    excludes: Vec<ExcludeAttr>,
8592    /// Composite (multi-column) FKs declared via
8593    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8594    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8595    composite_fks: Vec<CompositeFkAttr>,
8596    /// Generic ("any model") FKs declared via
8597    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8598    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8599    generic_fks: Vec<GenericFkAttr>,
8600    /// Where this model lives in a tenancy deployment, declared via
8601    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8602    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8603    /// to partition diff output between registry-scoped and
8604    /// tenant-scoped migration files.
8605    scope: Option<String>,
8606    /// Extra QuerySet accessor names from
8607    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8608    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8609    /// the default `Self::objects()`. Multiple attributes allowed.
8610    manager_fns: Vec<syn::Ident>,
8611    /// Default ordering declared via `#[rustango(default_order =
8612    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8613    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8614    /// `-` prefix means descending; the `+` prefix or no prefix means
8615    /// ascending.
8616    default_order: Vec<(String, bool, proc_macro2::Span)>,
8617    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8618    /// Routes the emitted schema's `is_view = true` so the migration
8619    /// snapshot skips this model (its underlying SQL view is operator-
8620    /// managed, not rustango-managed).
8621    is_view: bool,
8622    /// `#[rustango(managed = false)]` — who owns the table.
8623    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8624    /// the migration snapshot skips this model so `makemigrations` /
8625    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8626    /// TABLE` against it (operator-managed schema).
8627    managed: bool,
8628    /// Name of the model's base manager, from
8629    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8630    /// `ModelSchema::base_manager_name`. Declarative-only today.
8631    base_manager_name: Option<String>,
8632    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8633    /// field this model's instances are ordered relative to.
8634    /// Declarative-only today; threaded onto
8635    /// `ModelSchema::order_with_respect_to`.
8636    order_with_respect_to: Option<String>,
8637    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8638    /// Marks the model as a proxy that
8639    /// shares its DB table with another struct. Threaded into
8640    /// `ModelSchema::proxy` so future codegen can skip table-owning
8641    /// behavior for proxies.
8642    proxy: bool,
8643    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8644    /// Each comma-separated capability token surfaces on
8645    /// `ModelSchema::required_db_features` so `manage check --deploy`
8646    /// can warn when the active dialect lacks one.
8647    required_db_features: Vec<String>,
8648    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8649    /// Normalized to the dialect name `manage check --deploy`
8650    /// compares against `Settings.database.backend`. Aliases
8651    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8652    /// stored under the canonical name.
8653    required_db_vendor: Option<String>,
8654    /// `#[rustango(default_related_name = "...")]`. Threaded into
8655    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8656    /// name to use when an FK / M2M field doesn't override it.
8657    /// Today rustango doesn't auto-emit reverse managers; the
8658    /// metadata is the foundation for that work.
8659    default_related_name: Option<String>,
8660    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8661    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8662    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8663    /// inline `COMMENT='...'`, SQLite: no-op).
8664    db_table_comment: Option<String>,
8665    /// `#[rustango(get_latest_by = "created_at")]` /
8666    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8667    /// `(column, descending)` where `descending = true` when the
8668    /// attribute value starts with `-`. Threaded into
8669    /// `ModelSchema::get_latest_by`.
8670    get_latest_by: Option<(String, bool, proc_macro2::Span)>,
8671    /// Extra `(codename, label)` permissions
8672    /// from `#[rustango(extra_permissions = "approve:Can approve,
8673    /// archive:Can archive")]`. Comma-separated `codename:label`
8674    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8675    extra_permissions: Vec<(String, String)>,
8676    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8677    /// `"view"`) the framework
8678    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8679    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8680    /// Validated at parse time; unknown actions fail with a span-pointing
8681    /// error.
8682    default_permissions: Vec<String>,
8683    /// `#[rustango(verbose_name = "blog post")]` — the
8684    /// human-readable singular label for the model. Threaded into
8685    /// `ModelSchema::verbose_name` so admin section headers /
8686    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8687    /// over the Rust struct identifier.
8688    verbose_name: Option<String>,
8689    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8690    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8691    /// When unset, `display_label_plural()` auto-suffixes `s`.
8692    verbose_name_plural: Option<String>,
8693    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8694    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8695    /// a name (used by `QuerySet::without_global_scope`) with a
8696    /// `fn() -> WhereExpr` path that the macro emits into
8697    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8698    global_scopes: Vec<GlobalScopeAttr>,
8699    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8700    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8701    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8702    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8703    /// entry emits an inherent `<name>_through(&self)` accessor that
8704    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8705    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8706    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8707    through_relations: Vec<ThroughAttr>,
8708    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8709    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8710    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8711    /// Each entry emits two associated functions on the parent —
8712    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8713    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8714    /// over a correlated subquery against the child table. Users
8715    /// drop the result into `QuerySet::where_raw(...)`.
8716    reverse_has_relations: Vec<ReverseHasAttr>,
8717    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8718    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8719    /// entry so the relation-existence family resolves polymorphic
8720    /// children by name.
8721    generic_has_relations: Vec<GenericHasAttr>,
8722}
8723
8724/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8725/// declaration. Each entry becomes one `core::GlobalScope` in the
8726/// emitted schema literal; `apply` resolves at macro-expand time
8727/// against the consumer's scope so the function must be in scope at
8728/// the use site. Issue #820.
8729struct GlobalScopeAttr {
8730    name: String,
8731    apply: syn::Path,
8732}
8733
8734/// Parsed `#[rustango(through(...))]` declaration. Issue
8735/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8736/// `hasManyThrough` / `hasOneThrough` parity.
8737///
8738/// `Country hasManyThrough Post via User` declares as:
8739///
8740/// ```ignore
8741/// #[rustango(through(
8742///     name                   = "posts",
8743///     far                    = "Post",
8744///     far_fk_column          = "author_id",
8745///     intermediate           = "User",
8746///     intermediate_fk_column = "country_id",
8747/// ))]
8748/// struct Country { ... }
8749/// ```
8750///
8751/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8752/// which returns a queryset filtered via a correlated subquery —
8753/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8754/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8755/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8756/// because the subquery lives inside a `WhereExpr::InSubquery` node
8757/// and the outer queryset's pending list stays empty.
8758///
8759/// All four required arguments use **SQL column / table names**
8760/// (not Rust field names) to sidestep the multi-hop-filter substrate
8761/// gap. Once that substrate lands, a higher-level Rust-field-name
8762/// shorthand can be added without breaking this surface.
8763struct ThroughAttr {
8764    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8765    name: String,
8766    /// Far model type identifier. `far = "Post"` → returns
8767    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8768    /// the derive expands.
8769    far: syn::Ident,
8770    /// SQL column on the far model's table that references the
8771    /// intermediate model's primary key. For `Post`'s
8772    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8773    /// default `<field>_id` convention) or whatever the user
8774    /// declared via `#[rustango(db_column = "...")]`.
8775    far_fk_column: String,
8776    /// Intermediate model type identifier. Needed to look up its
8777    /// `SCHEMA` so the subquery's `FROM` clause points at the
8778    /// intermediate table. `intermediate = "User"`.
8779    intermediate: syn::Ident,
8780    /// SQL column on the intermediate's table that references the
8781    /// source (this) model's primary key. For `User`'s
8782    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8783    intermediate_fk_column: String,
8784    /// SQL primary-key column on the intermediate's table — the
8785    /// column the subquery projects. Optional; defaults to `"id"`
8786    /// (rustango's default PK column name). Override when the
8787    /// intermediate declares a custom PK column.
8788    intermediate_pk_column: String,
8789}
8790
8791/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8792/// child_fk_column = "..."))]` declaration. Issue
8793/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8794/// `whereHas` / `whereDoesntHave` parity.
8795///
8796/// `Post hasMany Comment` declares as:
8797///
8798/// ```ignore
8799/// #[rustango(reverse_has(
8800///     name             = "comments",
8801///     child            = "Comment",
8802///     child_fk_column  = "post_id",
8803/// ))]
8804/// struct Post { ... }
8805/// ```
8806///
8807/// The macro emits two associated functions on `Post`:
8808///
8809/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8810///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8811///   <outer>.<self_pk_column>)`.
8812/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8813///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8814///
8815/// User code:
8816///
8817/// ```ignore
8818/// // Posts with at least one comment:
8819/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8820/// // Posts with no comments:
8821/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8822/// ```
8823///
8824/// As with #817, all column / table identifiers are **SQL names**
8825/// (not Rust field names) so the substrate is independent of the
8826/// outstanding multi-hop filter resolver gap. The emitted
8827/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8828/// SQL-emit time via the writer's scope stack.
8829struct ReverseHasAttr {
8830    /// Accessor name. `name = "comments"` → emits
8831    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8832    name: String,
8833    /// Child model type identifier. `child = "Comment"` — needed to
8834    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8835    /// points at the child table.
8836    child: syn::Ident,
8837    /// SQL column on the child's table that references this model's
8838    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8839    /// column is `"post_id"`.
8840    child_fk_column: String,
8841    /// Column on **this** model's table the child FK references — the
8842    /// `OuterRef` target. Defaults to the PK column (`"id"` without one).
8843    self_pk_column: Option<String>,
8844}
8845
8846impl ReverseHasAttr {
8847    fn self_pk_column(&self) -> &str {
8848        self.self_pk_column.as_deref().unwrap_or("id")
8849    }
8850}
8851
8852/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8853/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8854/// relation-existence family (issue #830). The child is a polymorphic,
8855/// content-type-discriminated model; emits a `Model::
8856/// generic_reverse_relations()` entry the queryset resolves by name.
8857struct GenericHasAttr {
8858    /// Accessor name, e.g. `name = "tags"`.
8859    name: String,
8860    /// Child model type identifier (`child = "Tag"`) — looked up for its
8861    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8862    child: syn::Ident,
8863    /// Column on the child table holding the parent's content-type id.
8864    /// Optional; defaults to `"content_type_id"`.
8865    ct_column: String,
8866    /// Column on the child table holding the parent's PK value.
8867    /// Optional; defaults to `"object_pk"`.
8868    pk_column: String,
8869    /// SQL primary-key column on **this** (parent) model's table.
8870    /// Defaults to the PK column (`"id"` without one).
8871    self_pk_column: Option<String>,
8872}
8873
8874impl GenericHasAttr {
8875    fn self_pk_column(&self) -> &str {
8876        self.self_pk_column.as_deref().unwrap_or("id")
8877    }
8878}
8879
8880/// Parsed form of one index declaration (field-level or container-level).
8881struct IndexAttr {
8882    /// The declaring attribute, for diagnostics.
8883    span: proc_macro2::Span,
8884    /// Index name; auto-derived when `None` at parse time.
8885    name: Option<String>,
8886    /// Column names in the index.
8887    columns: Vec<String>,
8888    /// `true` for `CREATE UNIQUE INDEX`.
8889    unique: bool,
8890    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8891    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8892    /// `"btree"` when the attribute is absent — the DDL writer omits
8893    /// the `USING` clause and the backend uses its own default
8894    /// (btree on every supported dialect).
8895    method: String,
8896    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8897    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8898    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8899    where_clause: Option<String>,
8900    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8901    /// Empty `Vec` (the default) means "no covering columns".
8902    include: Vec<String>,
8903}
8904
8905/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8906struct CheckAttr {
8907    name: String,
8908    expr: String,
8909}
8910
8911/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8912/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8913/// PG-only — surfaced on every backend in the macro emit; the migration
8914/// writer skips the op on MySQL/SQLite. Issue #319.
8915struct ExcludeAttr {
8916    /// Constraint name (free-form Rust identifier).
8917    name: String,
8918    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8919    using: String,
8920    /// Comma-separated `(column, operator)` pairs, in declaration
8921    /// order. Parsed from `"col WITH op, col WITH op"`.
8922    elements: Vec<(String, String)>,
8923    /// Optional `WHERE` predicate (raw SQL).
8924    where_clause: Option<String>,
8925}
8926
8927/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8928/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8929/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8930/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8931/// the model, not the field.
8932struct CompositeFkAttr {
8933    /// Logical relation name (free-form Rust identifier).
8934    name: String,
8935    /// SQL table name of the target.
8936    to: String,
8937    /// Source-side column names, in declaration order.
8938    from: Vec<String>,
8939    /// Target-side column names, same length / order as `from`.
8940    on: Vec<String>,
8941}
8942
8943/// Parsed form of one `#[rustango(generic_fk(name = "target",
8944/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8945/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8946/// generic ("any model") FKs live on the model, not the field.
8947struct GenericFkAttr {
8948    /// Logical relation name (free-form Rust identifier).
8949    name: String,
8950    /// Source-side column carrying the `content_type_id` value.
8951    ct_column: String,
8952    /// Source-side column carrying the target row's primary key.
8953    pk_column: String,
8954}
8955
8956/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8957struct M2MAttr {
8958    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8959    name: String,
8960    /// Target table (e.g. `"app_tags"`).
8961    to: String,
8962    /// Junction table (e.g. `"post_tags"`).
8963    through: String,
8964    /// Source FK column in the junction table (e.g. `"post_id"`).
8965    src: String,
8966    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8967    dst: String,
8968    /// Whether the migration writer should auto-create the junction
8969    /// table. Default `true`. Set `auto_create = false` (#324) when
8970    /// the operator declares the through table as its own
8971    /// `#[derive(Model)]` struct with extra columns.
8972    auto_create: bool,
8973}
8974
8975/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8976/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8977/// junction carries a ContentType discriminator so unrelated models
8978/// share one pivot + related set.
8979struct GenericM2MAttr {
8980    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8981    name: String,
8982    /// Polymorphic junction table (e.g. `"taggables"`).
8983    through: String,
8984    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8985    pk_column: String,
8986    /// Junction column holding the owning model's `content_type_id`
8987    /// discriminator (e.g. `"taggable_type"`).
8988    ct_column: String,
8989    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8990    related_column: String,
8991}
8992
8993/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8994/// "user"))]`. `track` is a comma-separated list of field names whose
8995/// before/after values land in the JSONB `changes` column. `source`
8996/// is informational only — it pins a default source when the model
8997/// is written outside any `audit::with_source(...)` scope (rare).
8998#[derive(Default)]
8999struct AuditAttrs {
9000    /// Field names to capture in the `changes` JSONB. Validated
9001    /// against declared scalar fields at compile time. Empty means
9002    /// "track every scalar field".
9003    track: Option<(Vec<String>, proc_macro2::Span)>,
9004}
9005
9006/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
9007/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
9008/// comma-separated strings; we validate each ident against the model's
9009/// declared fields at compile time.
9010#[derive(Default)]
9011struct AdminAttrs {
9012    list_display: Option<(Vec<String>, proc_macro2::Span)>,
9013    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
9014    list_per_page: Option<usize>,
9015    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
9016    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
9017    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
9018    /// Bulk action names. No field-validation against model fields —
9019    /// these are action handlers, not column references.
9020    actions: Option<(Vec<String>, proc_macro2::Span)>,
9021    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
9022    /// sections, comma-separated fields per section, optional
9023    /// `Title:` prefix. Empty title omits the `<legend>`.
9024    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
9025    /// `admin(list_display_links = "title")`. Names
9026    /// from `list_display` whose cells should link to detail/edit.
9027    /// Issue #350.
9028    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
9029    /// `admin(search_help_text = "...")`. Short
9030    /// caption rendered beside the admin list view's search box.
9031    /// Issue #353.
9032    search_help_text: Option<String>,
9033    /// `admin(actions_on_top = false)`. Hides the
9034    /// action-bar above the table. Default `true`. Issue #354.
9035    actions_on_top: Option<bool>,
9036    /// `admin(actions_on_bottom = true)`. Renders an
9037    /// additional action-bar below the table. Default `false`.
9038    /// Issue #354.
9039    actions_on_bottom: Option<bool>,
9040    /// `admin(date_hierarchy = "created_at")`. Name of
9041    /// a date / datetime field whose values render as a clickable
9042    /// year / month / day drill-down strip above the list table.
9043    /// Empty / unset disables the strip. Issue #355.
9044    date_hierarchy: Option<String>,
9045    /// `admin(prepopulated_fields = "slug:title")`.
9046    /// Each entry is `target:source[+source2]`; multiple entries are
9047    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
9048    /// The admin change-form emits JS that slugifies the source values
9049    /// into the target field on every keystroke. Issue #356.
9050    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
9051    /// `admin(raw_id_fields = "parent, owner")`. Names
9052    /// of FK fields whose change-form widget renders a lookup link
9053    /// next to the input. Issue #357.
9054    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
9055    /// `admin(autocomplete_fields = "author_id")`.
9056    /// Names of FK fields whose change-form widget renders an
9057    /// Ajax-driven typeahead populated from a `__autocomplete`
9058    /// endpoint on the target model. Issue #358.
9059    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
9060    /// `admin(list_select_related = "all" | "none" | "author, …")`
9061    /// — tunes the admin list view's FK auto-JOIN
9062    /// policy. Default `"all"` matches rustango's join-everything
9063    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
9064    list_select_related: Option<String>,
9065    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
9066    /// Each entry is `field_name:widget_name`; multiple
9067    /// entries comma-separated. Empty / unset → no overrides. The
9068    /// list of widget names supported is documented on
9069    /// `AdminConfig::formfield_overrides`. Issue #359.
9070    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
9071}
9072
9073/// The names `#[derive(Model)]` reads its attributes under: `rgo` is a
9074/// short alias of `rustango` with the same meaning.
9075#[derive(Clone, Copy, PartialEq, Eq)]
9076enum ModelAttrName {
9077    Rustango,
9078    Rgo,
9079}
9080
9081impl ModelAttrName {
9082    fn of(attr: &syn::Attribute) -> Option<Self> {
9083        let path = attr.path();
9084        if path.is_ident("rustango") {
9085            Some(Self::Rustango)
9086        } else if path.is_ident("rgo") {
9087            Some(Self::Rgo)
9088        } else {
9089            None
9090        }
9091    }
9092
9093    fn as_str(self) -> &'static str {
9094        match self {
9095            Self::Rustango => "rustango",
9096            Self::Rgo => "rgo",
9097        }
9098    }
9099}
9100
9101/// The model attributes in `attrs`, under either name. One item or field
9102/// uses one name, so a mix reads as a mistake rather than a merge.
9103fn model_attrs(attrs: &[syn::Attribute]) -> syn::Result<Vec<&syn::Attribute>> {
9104    let mut first: Option<ModelAttrName> = None;
9105    let mut out = Vec::new();
9106    for attr in attrs {
9107        let Some(name) = ModelAttrName::of(attr) else {
9108            continue;
9109        };
9110        match first {
9111            Some(seen) if seen != name => {
9112                return Err(syn::Error::new_spanned(
9113                    attr.path(),
9114                    format!(
9115                        "`#[{}(...)]` and `#[{}(...)]` are the same attribute; \
9116                         use one name on an item or field",
9117                        seen.as_str(),
9118                        name.as_str(),
9119                    ),
9120                ));
9121            }
9122            _ => first = Some(name),
9123        }
9124        out.push(attr);
9125    }
9126    Ok(out)
9127}
9128
9129fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
9130    let mut out = ContainerAttrs {
9131        table: None,
9132        display: None,
9133        app: None,
9134        admin: None,
9135        audit: None,
9136        // Default `permissions = true` so every `#[derive(Model)]`
9137        // gets the four CRUD codenames seeded by `auto_create_permissions`
9138        // and is visible to non-superusers in the tenant admin without
9139        // manual per-model annotation. Models that intentionally don't
9140        // want permission rows (registry-internal types, framework
9141        // tables operators shouldn't manage directly) opt out via
9142        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
9143        // out-of-the-box admin invisibility regression (#62).
9144        permissions: true,
9145        m2m: Vec::new(),
9146        generic_m2m: Vec::new(),
9147        indexes: Vec::new(),
9148        checks: Vec::new(),
9149        excludes: Vec::new(),
9150        composite_fks: Vec::new(),
9151        generic_fks: Vec::new(),
9152        scope: None,
9153        manager_fns: Vec::new(),
9154        default_order: Vec::new(),
9155        is_view: false,
9156        managed: true,
9157        verbose_name: None,
9158        verbose_name_plural: None,
9159        base_manager_name: None,
9160        order_with_respect_to: None,
9161        proxy: false,
9162        required_db_features: Vec::new(),
9163        required_db_vendor: None,
9164        default_related_name: None,
9165        db_table_comment: None,
9166        get_latest_by: None,
9167        extra_permissions: Vec::new(),
9168        default_permissions: Vec::new(),
9169        global_scopes: Vec::new(),
9170        through_relations: Vec::new(),
9171        reverse_has_relations: Vec::new(),
9172        generic_has_relations: Vec::new(),
9173    };
9174    for attr in model_attrs(&input.attrs)? {
9175        // The name the user wrote, for hints that quote the attribute.
9176        let written = ModelAttrName::of(attr).map_or("rustango", ModelAttrName::as_str);
9177        attr.parse_nested_meta(|meta| {
9178            if meta.path.is_ident("table") {
9179                let s: LitStr = meta.value()?.parse()?;
9180                let name = s.value();
9181                // v0.27.3 (#65) — macro-time guard against table names
9182                // that compile but break SQL downstream. Hyphens are
9183                // the common footgun: PostgreSQL accepts a quoted
9184                // `"intermediate-region"` in CREATE TABLE, but the
9185                // FK / index name derivation in `migrate::ddl`
9186                // emits `intermediate-region_field_fkey` unquoted,
9187                // which then fails the SQL parser. Same shape rule
9188                // as Postgres regular identifiers so the safe path
9189                // is the only path.
9190                validate_table_name(&name, s.span())?;
9191                out.table = Some(name);
9192                return Ok(());
9193            }
9194            if meta.path.is_ident("display") {
9195                let s: LitStr = meta.value()?.parse()?;
9196                out.display = Some((s.value(), s.span()));
9197                return Ok(());
9198            }
9199            if meta.path.is_ident("app") {
9200                let s: LitStr = meta.value()?.parse()?;
9201                out.app = Some(s.value());
9202                return Ok(());
9203            }
9204            if meta.path.is_ident("scope") {
9205                let s: LitStr = meta.value()?.parse()?;
9206                let val = s.value();
9207                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
9208                    return Err(meta.error(format!(
9209                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
9210                    )));
9211                }
9212                out.scope = Some(val);
9213                return Ok(());
9214            }
9215            if meta.path.is_ident("admin") {
9216                let mut admin = AdminAttrs::default();
9217                meta.parse_nested_meta(|inner| {
9218                    if inner.path.is_ident("list_display") {
9219                        let s: LitStr = inner.value()?.parse()?;
9220                        admin.list_display =
9221                            Some((split_field_list(&s.value()), s.span()));
9222                        return Ok(());
9223                    }
9224                    if inner.path.is_ident("search_fields") {
9225                        let s: LitStr = inner.value()?.parse()?;
9226                        admin.search_fields =
9227                            Some((split_field_list(&s.value()), s.span()));
9228                        return Ok(());
9229                    }
9230                    if inner.path.is_ident("readonly_fields") {
9231                        let s: LitStr = inner.value()?.parse()?;
9232                        admin.readonly_fields =
9233                            Some((split_field_list(&s.value()), s.span()));
9234                        return Ok(());
9235                    }
9236                    if inner.path.is_ident("list_per_page") {
9237                        let lit: syn::LitInt = inner.value()?.parse()?;
9238                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
9239                        return Ok(());
9240                    }
9241                    if inner.path.is_ident("ordering") {
9242                        let s: LitStr = inner.value()?.parse()?;
9243                        admin.ordering = Some((
9244                            parse_ordering_list(&s.value()),
9245                            s.span(),
9246                        ));
9247                        return Ok(());
9248                    }
9249                    if inner.path.is_ident("list_filter") {
9250                        let s: LitStr = inner.value()?.parse()?;
9251                        admin.list_filter =
9252                            Some((split_field_list(&s.value()), s.span()));
9253                        return Ok(());
9254                    }
9255                    if inner.path.is_ident("actions") {
9256                        let s: LitStr = inner.value()?.parse()?;
9257                        admin.actions =
9258                            Some((split_field_list(&s.value()), s.span()));
9259                        return Ok(());
9260                    }
9261                    if inner.path.is_ident("fieldsets") {
9262                        let s: LitStr = inner.value()?.parse()?;
9263                        admin.fieldsets =
9264                            Some((parse_fieldset_list(&s.value()), s.span()));
9265                        return Ok(());
9266                    }
9267                    if inner.path.is_ident("list_display_links") {
9268                        let s: LitStr = inner.value()?.parse()?;
9269                        admin.list_display_links =
9270                            Some((split_field_list(&s.value()), s.span()));
9271                        return Ok(());
9272                    }
9273                    if inner.path.is_ident("search_help_text") {
9274                        let s: LitStr = inner.value()?.parse()?;
9275                        admin.search_help_text = Some(s.value());
9276                        return Ok(());
9277                    }
9278                    if inner.path.is_ident("actions_on_top") {
9279                        let lit: syn::LitBool = inner.value()?.parse()?;
9280                        admin.actions_on_top = Some(lit.value);
9281                        return Ok(());
9282                    }
9283                    if inner.path.is_ident("actions_on_bottom") {
9284                        let lit: syn::LitBool = inner.value()?.parse()?;
9285                        admin.actions_on_bottom = Some(lit.value);
9286                        return Ok(());
9287                    }
9288                    if inner.path.is_ident("date_hierarchy") {
9289                        let s: LitStr = inner.value()?.parse()?;
9290                        admin.date_hierarchy = Some(s.value());
9291                        return Ok(());
9292                    }
9293                    if inner.path.is_ident("prepopulated_fields") {
9294                        let s: LitStr = inner.value()?.parse()?;
9295                        admin.prepopulated_fields =
9296                            Some((parse_prepopulated_list(&s.value()), s.span()));
9297                        return Ok(());
9298                    }
9299                    if inner.path.is_ident("raw_id_fields") {
9300                        let s: LitStr = inner.value()?.parse()?;
9301                        admin.raw_id_fields =
9302                            Some((split_field_list(&s.value()), s.span()));
9303                        return Ok(());
9304                    }
9305                    if inner.path.is_ident("autocomplete_fields") {
9306                        let s: LitStr = inner.value()?.parse()?;
9307                        admin.autocomplete_fields =
9308                            Some((split_field_list(&s.value()), s.span()));
9309                        return Ok(());
9310                    }
9311                    if inner.path.is_ident("list_select_related") {
9312                        let s: LitStr = inner.value()?.parse()?;
9313                        admin.list_select_related = Some(s.value());
9314                        return Ok(());
9315                    }
9316                    if inner.path.is_ident("formfield_overrides") {
9317                        let s: LitStr = inner.value()?.parse()?;
9318                        admin.formfield_overrides =
9319                            Some((parse_formfield_overrides(&s.value()), s.span()));
9320                        return Ok(());
9321                    }
9322                    Err(inner.error(
9323                        "unknown admin attribute (supported: \
9324                         `list_display`, `list_display_links`, \
9325                         `search_fields`, `search_help_text`, \
9326                         `readonly_fields`, \
9327                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
9328                         `actions_on_top`, `actions_on_bottom`, \
9329                         `date_hierarchy`, \
9330                         `prepopulated_fields`, \
9331                         `raw_id_fields`, \
9332                         `autocomplete_fields`, \
9333                         `list_select_related`, \
9334                         `formfield_overrides`, \
9335                         `fieldsets`)",
9336                    ))
9337                })?;
9338                out.admin = Some(admin);
9339                return Ok(());
9340            }
9341            if meta.path.is_ident("manager") {
9342                // Its empty trait could not take methods (#2132).
9343                return Err(meta.error(
9344                    "`manager(ext = ...)` was removed (#2132): declare your own \
9345                     `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
9346                ));
9347            }
9348            if meta.path.is_ident("manager_fn") {
9349                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
9350                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
9351                // next to the default `Self::objects()`. Multiple
9352                // attributes accumulate.
9353                let s: LitStr = meta.value()?.parse()?;
9354                let name = s.value();
9355                if name.is_empty() {
9356                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
9357                }
9358                if name == "objects" {
9359                    return Err(meta.error(
9360                        "`manager_fn = \"objects\"` collides with the default \
9361                         accessor — pick a different name",
9362                    ));
9363                }
9364                let ident = syn::Ident::new(&name, s.span());
9365                if out.manager_fns.iter().any(|prev| *prev == ident) {
9366                    return Err(meta.error(format!(
9367                        "duplicate `manager_fn = \"{name}\"`"
9368                    )));
9369                }
9370                out.manager_fns.push(ident);
9371                return Ok(());
9372            }
9373            if meta.path.is_ident("default_order") {
9374                // `#[rustango(default_order = "-created_at, status")]`
9375                // — issue #291 / T2.5. Comma-separated list; `-prefix`
9376                // means descending, `+prefix` or bare name means ascending.
9377                // Per-query opt-in via `QuerySet::with_default_order()`.
9378                let s: LitStr = meta.value()?.parse()?;
9379                let raw = s.value();
9380                let span = s.span();
9381                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9382                    Vec::new();
9383                for entry in raw.split(',') {
9384                    let trimmed = entry.trim();
9385                    if trimmed.is_empty() {
9386                        return Err(syn::Error::new(
9387                            span,
9388                            "`default_order = \"...\"` has an empty entry — \
9389                             check for a stray comma",
9390                        ));
9391                    }
9392                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9393                        (true, rest.trim().to_owned())
9394                    } else if let Some(rest) = trimmed.strip_prefix('+') {
9395                        (false, rest.trim().to_owned())
9396                    } else {
9397                        (false, trimmed.to_owned())
9398                    };
9399                    if name.is_empty() {
9400                        return Err(syn::Error::new(
9401                            span,
9402                            "`default_order` entry has no column name after the prefix",
9403                        ));
9404                    }
9405                    if parsed.iter().any(|(n, _, _)| *n == name) {
9406                        return Err(syn::Error::new(
9407                            span,
9408                            format!("duplicate column `{name}` in `default_order`"),
9409                        ));
9410                    }
9411                    parsed.push((name, desc, span));
9412                }
9413                if parsed.is_empty() {
9414                    return Err(syn::Error::new(
9415                        span,
9416                        "`default_order = \"...\"` cannot be empty",
9417                    ));
9418                }
9419                out.default_order = parsed;
9420                return Ok(());
9421            }
9422            if meta.path.is_ident("global_scope") {
9423                // `#[rustango(global_scope(name = "active", apply =
9424                //  path::to::fn))]` — issue #820. The apply function
9425                // path resolves at macro-expand time in the consumer's
9426                // scope; the macro re-emits it verbatim into the
9427                // `ModelSchema::global_scopes` slice literal.
9428                let span = meta.path.span();
9429                let mut scope_name: Option<String> = None;
9430                let mut apply_path: Option<syn::Path> = None;
9431                meta.parse_nested_meta(|inner| {
9432                    if inner.path.is_ident("name") {
9433                        let s: LitStr = inner.value()?.parse()?;
9434                        let raw = s.value();
9435                        if raw.trim().is_empty() {
9436                            return Err(syn::Error::new(
9437                                s.span(),
9438                                "`global_scope(name = \"...\")` must not be empty",
9439                            ));
9440                        }
9441                        scope_name = Some(raw);
9442                        return Ok(());
9443                    }
9444                    if inner.path.is_ident("apply") {
9445                        let p: syn::Path = inner.value()?.parse()?;
9446                        apply_path = Some(p);
9447                        return Ok(());
9448                    }
9449                    Err(inner.error(
9450                        "unknown `global_scope` attribute (supported: \
9451                         `name`, `apply`)",
9452                    ))
9453                })?;
9454                let Some(name) = scope_name else {
9455                    return Err(syn::Error::new(
9456                        span,
9457                        "`global_scope` requires `name = \"...\"`",
9458                    ));
9459                };
9460                let Some(apply) = apply_path else {
9461                    return Err(syn::Error::new(
9462                        span,
9463                        "`global_scope` requires `apply = fn_path`",
9464                    ));
9465                };
9466                if out.global_scopes.iter().any(|s| s.name == name) {
9467                    return Err(syn::Error::new(
9468                        span,
9469                        format!(
9470                            "duplicate global scope name `{name}` — \
9471                             pick a unique identifier so \
9472                             `QuerySet::without_global_scope(\"{name}\")` \
9473                             is unambiguous"
9474                        ),
9475                    ));
9476                }
9477                out.global_scopes.push(GlobalScopeAttr { name, apply });
9478                return Ok(());
9479            }
9480            if meta.path.is_ident("through") {
9481                // `#[rustango(through(name = "posts", far = "Post",
9482                //  far_fk_column = "author_id", intermediate = "User",
9483                //  intermediate_fk_column = "country_id"
9484                //  [, intermediate_pk_column = "id"]))]` — issue #817.
9485                let span = meta.path.span();
9486                let mut accessor_name: Option<String> = None;
9487                let mut far_ident: Option<syn::Ident> = None;
9488                let mut far_fk_column: Option<String> = None;
9489                let mut intermediate_ident: Option<syn::Ident> = None;
9490                let mut intermediate_fk_column: Option<String> = None;
9491                let mut intermediate_pk_column: Option<String> = None;
9492                fn parse_nonempty_string(
9493                    inner: &syn::meta::ParseNestedMeta<'_>,
9494                    field: &str,
9495                ) -> syn::Result<String> {
9496                    let s: LitStr = inner.value()?.parse()?;
9497                    let raw = s.value();
9498                    let trimmed = raw.trim();
9499                    if trimmed.is_empty() {
9500                        return Err(syn::Error::new(
9501                            s.span(),
9502                            format!("`through({field} = \"...\")` must not be empty"),
9503                        ));
9504                    }
9505                    Ok(trimmed.to_owned())
9506                }
9507                meta.parse_nested_meta(|inner| {
9508                    if inner.path.is_ident("name") {
9509                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9510                        return Ok(());
9511                    }
9512                    if inner.path.is_ident("far") {
9513                        // Far model name accepted as a string literal
9514                        // so the attribute fits inside the existing
9515                        // `parse_nested_meta` shape; parsed back into a
9516                        // `syn::Ident` so the emitted accessor can
9517                        // reference the type directly.
9518                        let s: LitStr = inner.value()?.parse()?;
9519                        let raw = s.value();
9520                        let trimmed = raw.trim();
9521                        if trimmed.is_empty() {
9522                            return Err(syn::Error::new(
9523                                s.span(),
9524                                "`through(far = \"...\")` must not be empty",
9525                            ));
9526                        }
9527                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9528                        return Ok(());
9529                    }
9530                    if inner.path.is_ident("far_fk_column") {
9531                        far_fk_column =
9532                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9533                        return Ok(());
9534                    }
9535                    if inner.path.is_ident("intermediate") {
9536                        let s: LitStr = inner.value()?.parse()?;
9537                        let raw = s.value();
9538                        let trimmed = raw.trim();
9539                        if trimmed.is_empty() {
9540                            return Err(syn::Error::new(
9541                                s.span(),
9542                                "`through(intermediate = \"...\")` must not be empty",
9543                            ));
9544                        }
9545                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9546                        return Ok(());
9547                    }
9548                    if inner.path.is_ident("intermediate_fk_column") {
9549                        intermediate_fk_column =
9550                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9551                        return Ok(());
9552                    }
9553                    if inner.path.is_ident("intermediate_pk_column") {
9554                        intermediate_pk_column =
9555                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9556                        return Ok(());
9557                    }
9558                    Err(inner.error(
9559                        "unknown `through` attribute (supported: \
9560                         `name`, `far`, `far_fk_column`, \
9561                         `intermediate`, `intermediate_fk_column`, \
9562                         `intermediate_pk_column`)",
9563                    ))
9564                })?;
9565                let Some(name) = accessor_name else {
9566                    return Err(syn::Error::new(
9567                        span,
9568                        "`through` requires `name = \"...\"`",
9569                    ));
9570                };
9571                let Some(far) = far_ident else {
9572                    return Err(syn::Error::new(
9573                        span,
9574                        "`through` requires `far = \"FarModelType\"`",
9575                    ));
9576                };
9577                let Some(far_fk_column) = far_fk_column else {
9578                    return Err(syn::Error::new(
9579                        span,
9580                        "`through` requires `far_fk_column = \"<column>\"`",
9581                    ));
9582                };
9583                let Some(intermediate) = intermediate_ident else {
9584                    return Err(syn::Error::new(
9585                        span,
9586                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9587                    ));
9588                };
9589                let Some(intermediate_fk_column) = intermediate_fk_column else {
9590                    return Err(syn::Error::new(
9591                        span,
9592                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9593                    ));
9594                };
9595                let intermediate_pk_column =
9596                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9597                if out.through_relations.iter().any(|t| t.name == name) {
9598                    return Err(syn::Error::new(
9599                        span,
9600                        format!(
9601                            "duplicate `through(name = \"{name}\")` — \
9602                             pick a unique accessor name"
9603                        ),
9604                    ));
9605                }
9606                out.through_relations.push(ThroughAttr {
9607                    name,
9608                    far,
9609                    far_fk_column,
9610                    intermediate,
9611                    intermediate_fk_column,
9612                    intermediate_pk_column,
9613                });
9614                return Ok(());
9615            }
9616            if meta.path.is_ident("reverse_has") {
9617                // `#[rustango(reverse_has(name = "comments",
9618                //  child = "Comment", child_fk_column = "post_id"
9619                //  [, self_pk_column = "id"]))]` — issue #830.
9620                let span = meta.path.span();
9621                let mut accessor_name: Option<String> = None;
9622                let mut child_ident: Option<syn::Ident> = None;
9623                let mut child_fk_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                                "`reverse_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 raw = s.value();
9641                        let trimmed = raw.trim();
9642                        if trimmed.is_empty() {
9643                            return Err(syn::Error::new(
9644                                s.span(),
9645                                "`reverse_has(child = \"...\")` must not be empty",
9646                            ));
9647                        }
9648                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9649                        return Ok(());
9650                    }
9651                    if inner.path.is_ident("child_fk_column") {
9652                        let s: LitStr = inner.value()?.parse()?;
9653                        let raw = s.value();
9654                        let trimmed = raw.trim();
9655                        if trimmed.is_empty() {
9656                            return Err(syn::Error::new(
9657                                s.span(),
9658                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9659                            ));
9660                        }
9661                        child_fk_column = Some(trimmed.to_owned());
9662                        return Ok(());
9663                    }
9664                    if inner.path.is_ident("self_pk_column") {
9665                        let s: LitStr = inner.value()?.parse()?;
9666                        let raw = s.value();
9667                        let trimmed = raw.trim();
9668                        if trimmed.is_empty() {
9669                            return Err(syn::Error::new(
9670                                s.span(),
9671                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9672                            ));
9673                        }
9674                        self_pk_column = Some(trimmed.to_owned());
9675                        return Ok(());
9676                    }
9677                    Err(inner.error(
9678                        "unknown `reverse_has` attribute (supported: \
9679                         `name`, `child`, `child_fk_column`, \
9680                         `self_pk_column`)",
9681                    ))
9682                })?;
9683                let Some(name) = accessor_name else {
9684                    return Err(syn::Error::new(
9685                        span,
9686                        "`reverse_has` requires `name = \"...\"`",
9687                    ));
9688                };
9689                let Some(child) = child_ident else {
9690                    return Err(syn::Error::new(
9691                        span,
9692                        "`reverse_has` requires `child = \"ChildModelType\"`",
9693                    ));
9694                };
9695                let Some(child_fk_column) = child_fk_column else {
9696                    return Err(syn::Error::new(
9697                        span,
9698                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9699                    ));
9700                };
9701                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9702                    return Err(syn::Error::new(
9703                        span,
9704                        format!(
9705                            "duplicate `reverse_has(name = \"{name}\")` — \
9706                             pick a unique accessor name"
9707                        ),
9708                    ));
9709                }
9710                out.reverse_has_relations.push(ReverseHasAttr {
9711                    name,
9712                    child,
9713                    child_fk_column,
9714                    self_pk_column,
9715                });
9716                return Ok(());
9717            }
9718            if meta.path.is_ident("generic_has") {
9719                // `#[rustango(generic_has(name = "tags",
9720                //  child = "Tag", ct_column = "content_type_id",
9721                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9722                //  — issue #830, the reverse generic-FK (GFK) arm of the
9723                //  relation-existence family.
9724                let span = meta.path.span();
9725                let mut accessor_name: Option<String> = None;
9726                let mut child_ident: Option<syn::Ident> = None;
9727                let mut ct_column: Option<String> = None;
9728                let mut pk_column: Option<String> = None;
9729                let mut self_pk_column: Option<String> = None;
9730                meta.parse_nested_meta(|inner| {
9731                    if inner.path.is_ident("name") {
9732                        let s: LitStr = inner.value()?.parse()?;
9733                        let raw = s.value();
9734                        if raw.trim().is_empty() {
9735                            return Err(syn::Error::new(
9736                                s.span(),
9737                                "`generic_has(name = \"...\")` must not be empty",
9738                            ));
9739                        }
9740                        accessor_name = Some(raw);
9741                        return Ok(());
9742                    }
9743                    if inner.path.is_ident("child") {
9744                        let s: LitStr = inner.value()?.parse()?;
9745                        let trimmed = s.value().trim().to_owned();
9746                        if trimmed.is_empty() {
9747                            return Err(syn::Error::new(
9748                                s.span(),
9749                                "`generic_has(child = \"...\")` must not be empty",
9750                            ));
9751                        }
9752                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9753                        return Ok(());
9754                    }
9755                    if inner.path.is_ident("ct_column") {
9756                        let s: LitStr = inner.value()?.parse()?;
9757                        let trimmed = s.value().trim().to_owned();
9758                        if trimmed.is_empty() {
9759                            return Err(syn::Error::new(
9760                                s.span(),
9761                                "`generic_has(ct_column = \"...\")` must not be empty",
9762                            ));
9763                        }
9764                        ct_column = Some(trimmed);
9765                        return Ok(());
9766                    }
9767                    if inner.path.is_ident("pk_column") {
9768                        let s: LitStr = inner.value()?.parse()?;
9769                        let trimmed = s.value().trim().to_owned();
9770                        if trimmed.is_empty() {
9771                            return Err(syn::Error::new(
9772                                s.span(),
9773                                "`generic_has(pk_column = \"...\")` must not be empty",
9774                            ));
9775                        }
9776                        pk_column = Some(trimmed);
9777                        return Ok(());
9778                    }
9779                    if inner.path.is_ident("self_pk_column") {
9780                        let s: LitStr = inner.value()?.parse()?;
9781                        let trimmed = s.value().trim().to_owned();
9782                        if trimmed.is_empty() {
9783                            return Err(syn::Error::new(
9784                                s.span(),
9785                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9786                            ));
9787                        }
9788                        self_pk_column = Some(trimmed);
9789                        return Ok(());
9790                    }
9791                    Err(inner.error(
9792                        "unknown `generic_has` attribute (supported: \
9793                         `name`, `child`, `ct_column`, `pk_column`, \
9794                         `self_pk_column`)",
9795                    ))
9796                })?;
9797                let Some(name) = accessor_name else {
9798                    return Err(syn::Error::new(
9799                        span,
9800                        "`generic_has` requires `name = \"...\"`",
9801                    ));
9802                };
9803                let Some(child) = child_ident else {
9804                    return Err(syn::Error::new(
9805                        span,
9806                        "`generic_has` requires `child = \"ChildModelType\"`",
9807                    ));
9808                };
9809                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9810                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9811                if out.generic_has_relations.iter().any(|r| r.name == name) {
9812                    return Err(syn::Error::new(
9813                        span,
9814                        format!(
9815                            "duplicate `generic_has(name = \"{name}\")` — \
9816                             pick a unique accessor name"
9817                        ),
9818                    ));
9819                }
9820                out.generic_has_relations.push(GenericHasAttr {
9821                    name,
9822                    child,
9823                    ct_column,
9824                    pk_column,
9825                    self_pk_column,
9826                });
9827                return Ok(());
9828            }
9829            if meta.path.is_ident("audit") {
9830                let mut audit = AuditAttrs::default();
9831                meta.parse_nested_meta(|inner| {
9832                    if inner.path.is_ident("track") {
9833                        let s: LitStr = inner.value()?.parse()?;
9834                        audit.track =
9835                            Some((split_field_list(&s.value()), s.span()));
9836                        return Ok(());
9837                    }
9838                    Err(inner.error(
9839                        "unknown audit attribute (supported: `track`)",
9840                    ))
9841                })?;
9842                out.audit = Some(audit);
9843                return Ok(());
9844            }
9845            if meta.path.is_ident("permissions") {
9846                // Two forms accepted:
9847                //   #[rustango(permissions)]          — flag form, true
9848                //   #[rustango(permissions = false)]  — explicit opt-out
9849                //   #[rustango(permissions = true)]   — explicit opt-in
9850                if let Ok(v) = meta.value() {
9851                    let lit: syn::LitBool = v.parse()?;
9852                    out.permissions = lit.value;
9853                } else {
9854                    out.permissions = true;
9855                }
9856                return Ok(());
9857            }
9858            if meta.path.is_ident("view") {
9859                // Issue #293 / T2.10. Two forms accepted, matching
9860                // the `permissions` flag pattern:
9861                //   #[rustango(view)]          — flag form, true
9862                //   #[rustango(view = false)]  — explicit opt-out
9863                //   #[rustango(view = true)]   — explicit opt-in
9864                if let Ok(v) = meta.value() {
9865                    let lit: syn::LitBool = v.parse()?;
9866                    out.is_view = lit.value;
9867                } else {
9868                    out.is_view = true;
9869                }
9870                return Ok(());
9871            }
9872            if meta.path.is_ident("managed") {
9873                // Who owns the table. Issue #321.
9874                //   #[rustango(managed = false)]  — operator-managed table
9875                //   #[rustango(managed = true)]   — rustango-managed (the default)
9876                // Bare-flag form is intentionally not accepted: writing
9877                // `#[rustango(managed)]` reads as "yes please manage it"
9878                // which is already the default. The opt-out is the only
9879                // useful state, so it must be explicit.
9880                let v = meta.value()?;
9881                let lit: syn::LitBool = v.parse()?;
9882                out.managed = lit.value;
9883                return Ok(());
9884            }
9885            if meta.path.is_ident("verbose_name") {
9886                let s: LitStr = meta.value()?.parse()?;
9887                out.verbose_name = Some(s.value());
9888                return Ok(());
9889            }
9890            if meta.path.is_ident("verbose_name_plural") {
9891                let s: LitStr = meta.value()?.parse()?;
9892                out.verbose_name_plural = Some(s.value());
9893                return Ok(());
9894            }
9895            if meta.path.is_ident("db_table_comment") {
9896                // Free-form table-level comment attached to the DB
9897                // catalog.
9898                let s: LitStr = meta.value()?.parse()?;
9899                out.db_table_comment = Some(s.value());
9900                return Ok(());
9901            }
9902            if meta.path.is_ident("proxy") {
9903                // Declarative flag
9904                // marking the struct as a proxy of another model that
9905                // shares its DB table. Stored on `ModelSchema::proxy`
9906                // so future codegen can skip `CreateTable` emission
9907                // for proxies (parent owns the table) and route
9908                // per-instance method resolution to the proxy class.
9909                //
9910                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9911                let value = if meta.input.peek(syn::Token![=]) {
9912                    meta.value()?.parse::<syn::LitBool>()?.value
9913                } else {
9914                    true
9915                };
9916                out.proxy = value;
9917                return Ok(());
9918            }
9919            if meta.path.is_ident("order_with_respect_to") {
9920                // `order_with_respect_to = "parent_fk"` — the model's
9921                // instances are intrinsically ordered relative to
9922                // their parent FK.
9923                //
9924                // rustango stores the FK field name on
9925                // `ModelSchema::order_with_respect_to`. Declarative-only
9926                // today: the migration writer + admin surfaces still
9927                // treat every model identically. Future codegen will
9928                // key off the metadata to auto-emit the `_order`
9929                // column and reorder helpers.
9930                //
9931                // Validated as a Rust-shape identifier so the macro
9932                // can reject typos at derive time.
9933                let s: LitStr = meta.value()?.parse()?;
9934                let raw = s.value();
9935                if raw.is_empty() {
9936                    return Err(syn::Error::new(
9937                        s.span(),
9938                        "`order_with_respect_to` must be a non-empty FK field name",
9939                    ));
9940                }
9941                let valid = raw
9942                    .chars()
9943                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9944                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9945                if !valid {
9946                    return Err(syn::Error::new(
9947                        s.span(),
9948                        format!(
9949                            "`order_with_respect_to` must be a valid Rust \
9950                             identifier (letters / digits / underscores, \
9951                             not starting with a digit); got `{raw}`"
9952                        ),
9953                    ));
9954                }
9955                out.order_with_respect_to = Some(raw);
9956                return Ok(());
9957            }
9958            if meta.path.is_ident("required_db_features") {
9959                // Capability tokens the model needs (e.g.
9960                // `"json_extract"`, `"window_functions"`,
9961                // `"row_security"`). Comma-separated.
9962                // `manage check --deploy` walks every model and warns
9963                // when the active backend doesn't advertise the
9964                // capability.
9965                //
9966                // rustango ships a small registry of capability tokens
9967                // each dialect supports — see `Dialect::supports`.
9968                // Unknown tokens still parse (they end up on the
9969                // schema and show up in the warning) so projects can
9970                // declare aspirational capabilities and the check
9971                // verb will keep nagging until the dialect implements
9972                // them.
9973                let s: LitStr = meta.value()?.parse()?;
9974                let raw = s.value();
9975                let features: Vec<String> = raw
9976                    .split(',')
9977                    .map(str::trim)
9978                    .filter(|s| !s.is_empty())
9979                    .map(str::to_owned)
9980                    .collect();
9981                if features.is_empty() {
9982                    return Err(syn::Error::new(
9983                        s.span(),
9984                        "`required_db_features` must list at least one \
9985                         comma-separated capability token",
9986                    ));
9987                }
9988                out.required_db_features = features;
9989                return Ok(());
9990            }
9991            if meta.path.is_ident("required_db_vendor") {
9992                // The model is only meant to run against the named DB
9993                // backend. `manage check --deploy` flags a mismatch so
9994                // ops catches "I forgot to switch DATABASE_URL" at
9995                // deploy time rather than runtime.
9996                //
9997                // Restricted to the three backends rustango ships
9998                // dialects for so the check verb can compare reliably.
9999                let s: LitStr = meta.value()?.parse()?;
10000                let raw = s.value().to_ascii_lowercase();
10001                match raw.as_str() {
10002                    "postgresql" | "postgres" | "pg" => {
10003                        out.required_db_vendor = Some("postgres".to_owned());
10004                    }
10005                    "mysql" | "mariadb" => {
10006                        out.required_db_vendor = Some("mysql".to_owned());
10007                    }
10008                    "sqlite" | "sqlite3" => {
10009                        out.required_db_vendor = Some("sqlite".to_owned());
10010                    }
10011                    _ => {
10012                        return Err(syn::Error::new(
10013                            s.span(),
10014                            format!(
10015                                "unknown required_db_vendor `{raw}` — \
10016                                 expected `postgres` (aliases: `postgresql`, `pg`), \
10017                                 `mysql` (alias: `mariadb`), or `sqlite` \
10018                                 (alias: `sqlite3`)"
10019                            ),
10020                        ));
10021                    }
10022                }
10023                return Ok(());
10024            }
10025            if meta.path.is_ident("base_manager_name") {
10026                // Name of the manager that `<instance>.<relation>_set`
10027                // uses when resolving reverse-relation managers.
10028                // Distinct from `default_manager_name` (what
10029                // `Self::objects()` returns). Stored on
10030                // `ModelSchema::base_manager_name`.
10031                //
10032                // Validated as a Rust identifier so it stays safe to
10033                // re-emit as code in future reverse-manager codegen.
10034                let s: LitStr = meta.value()?.parse()?;
10035                let raw = s.value();
10036                if raw.is_empty() {
10037                    return Err(syn::Error::new(
10038                        s.span(),
10039                        "`base_manager_name` must be a non-empty string",
10040                    ));
10041                }
10042                let valid = raw
10043                    .chars()
10044                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
10045                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
10046                if !valid {
10047                    return Err(syn::Error::new(
10048                        s.span(),
10049                        format!(
10050                            "`base_manager_name` must be a valid Rust \
10051                             identifier (letters / digits / underscores, \
10052                             not starting with a digit); got `{raw}`"
10053                        ),
10054                    ));
10055                }
10056                out.base_manager_name = Some(raw);
10057                return Ok(());
10058            }
10059            if meta.path.is_ident("default_related_name") {
10060                // The name reverse-relation accessors use when callers
10061                // don't override `related_name = "..."` on the FK / M2M
10062                // field. Stored on `ModelSchema::default_related_name`
10063                // so future reverse-manager codegen / schema emit /
10064                // admin templates can pick the right accessor name
10065                // (today rustango doesn't auto-emit reverse managers;
10066                // the metadata is the foundation for that work).
10067                //
10068                // Enforced non-empty + ASCII identifier-shape so the
10069                // string is safe to use as a Rust ident later.
10070                let s: LitStr = meta.value()?.parse()?;
10071                let raw = s.value();
10072                if raw.is_empty() {
10073                    return Err(syn::Error::new(
10074                        s.span(),
10075                        "`default_related_name` must be a non-empty string",
10076                    ));
10077                }
10078                let valid = raw
10079                    .chars()
10080                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
10081                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
10082                if !valid {
10083                    return Err(syn::Error::new(
10084                        s.span(),
10085                        format!(
10086                            "`default_related_name` must be snake_case ASCII \
10087                             (lowercase letters / digits / underscores, not \
10088                             starting with a digit); got `{raw}`"
10089                        ),
10090                    ));
10091                }
10092                out.default_related_name = Some(raw);
10093                return Ok(());
10094            }
10095            if meta.path.is_ident("extra_permissions") {
10096                // Comma-separated `codename:label` pairs.
10097                let s: LitStr = meta.value()?.parse()?;
10098                let raw = s.value();
10099                let mut pairs = Vec::new();
10100                for entry in raw.split(',') {
10101                    let entry = entry.trim();
10102                    if entry.is_empty() {
10103                        continue;
10104                    }
10105                    let (codename, label) = match entry.split_once(':') {
10106                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
10107                        None => (entry.to_owned(), entry.to_owned()),
10108                    };
10109                    if codename.is_empty() {
10110                        return Err(meta.error(
10111                            "`extra_permissions` entries must be `codename:label` pairs",
10112                        ));
10113                    }
10114                    pairs.push((codename, label));
10115                }
10116                if pairs.is_empty() {
10117                    return Err(meta
10118                        .error("`extra_permissions = \"…\"` must list at least one pair"));
10119                }
10120                out.extra_permissions = pairs;
10121                return Ok(());
10122            }
10123            if meta.path.is_ident("default_permissions") {
10124                // Comma-separated subset of the CRUD action set.
10125                // Empty means all four (the framework default).
10126                let s: LitStr = meta.value()?.parse()?;
10127                let raw = s.value();
10128                let mut actions: Vec<String> = Vec::new();
10129                for entry in raw.split(',') {
10130                    let action = entry.trim().to_ascii_lowercase();
10131                    if action.is_empty() {
10132                        continue;
10133                    }
10134                    match action.as_str() {
10135                        "add" | "change" | "delete" | "view" => {}
10136                        other => {
10137                            return Err(syn::Error::new(
10138                                s.span(),
10139                                format!(
10140                                    "unknown default_permissions action `{other}` — \
10141                                     expected one of `add`, `change`, `delete`, `view`"
10142                                ),
10143                            ));
10144                        }
10145                    }
10146                    if !actions.contains(&action) {
10147                        actions.push(action);
10148                    }
10149                }
10150                if actions.is_empty() {
10151                    return Err(syn::Error::new(
10152                        s.span(),
10153                        "`default_permissions = \"…\"` must list at least one action; \
10154                         use `permissions = false` on the container if you want NO \
10155                         permissions seeded for this model.",
10156                    ));
10157                }
10158                out.default_permissions = actions;
10159                return Ok(());
10160            }
10161            if meta.path.is_ident("get_latest_by") {
10162                // The `-` prefix selects descending order.
10163                let s: LitStr = meta.value()?.parse()?;
10164                let raw = s.value();
10165                let trimmed = raw.trim();
10166                if trimmed.is_empty() {
10167                    return Err(meta.error("`get_latest_by` must name a column"));
10168                }
10169                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
10170                    (stripped.to_owned(), true)
10171                } else if let Some(stripped) = trimmed.strip_prefix('+') {
10172                    (stripped.to_owned(), false)
10173                } else {
10174                    (trimmed.to_owned(), false)
10175                };
10176                if col.is_empty() {
10177                    return Err(meta.error("`get_latest_by` must name a column"));
10178                }
10179                out.get_latest_by = Some((col, desc, s.span()));
10180                return Ok(());
10181            }
10182            if meta.path.is_ident("unique_together") {
10183                // Composite UNIQUE index. Two syntaxes:
10184                //
10185                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
10186                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
10187                //
10188                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
10189                // (col1, col2)`, where <name> defaults to
10190                // `<table>_<col1>_<col2>_uq` when not supplied.
10191                let (columns, name) = parse_together_attr(&meta, "unique_together", written)?;
10192                out.indexes.push(IndexAttr {
10193                    span: meta.path.span(),
10194                    name,
10195                    columns,
10196                    unique: true,
10197                    method: "btree".to_owned(),
10198                    where_clause: None,
10199                include: Vec::new(),
10200                });
10201                return Ok(());
10202            }
10203            if meta.path.is_ident("index_together") {
10204                // Composite (non-unique) index. Two syntaxes
10205                // mirroring `unique_together`.
10206                //
10207                //   #[rustango(index_together = "created_at, status")]
10208                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
10209                let (columns, name) = parse_together_attr(&meta, "index_together", written)?;
10210                out.indexes.push(IndexAttr {
10211                    span: meta.path.span(),
10212                    name,
10213                    columns,
10214                    unique: false,
10215                    method: "btree".to_owned(),
10216                    where_clause: None,
10217                include: Vec::new(),
10218                });
10219                return Ok(());
10220            }
10221            if meta.path.is_ident("unique_when") {
10222                // Partial unique index. Issue #265 / T1.3.
10223                //
10224                //   #[rustango(unique_when(
10225                //       columns   = "email",
10226                //       condition = "deleted_at IS NULL",
10227                //       name      = "unique_active_email"
10228                //   ))]
10229                //
10230                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
10231                // on PG / SQLite (both ship partial indexes natively).
10232                // MySQL falls back to a plain UNIQUE index — the
10233                // condition is lost; document the limitation in the
10234                // generated migration.
10235                let mut columns: Option<Vec<String>> = None;
10236                let mut condition: Option<String> = None;
10237                let mut name: Option<String> = None;
10238                let mut include: Vec<String> = Vec::new();
10239                meta.parse_nested_meta(|inner| {
10240                    if inner.path.is_ident("columns") {
10241                        let s: LitStr = inner.value()?.parse()?;
10242                        columns = Some(split_field_list(&s.value()));
10243                        return Ok(());
10244                    }
10245                    if inner.path.is_ident("condition") {
10246                        let s: LitStr = inner.value()?.parse()?;
10247                        condition = Some(s.value());
10248                        return Ok(());
10249                    }
10250                    if inner.path.is_ident("name") {
10251                        let s: LitStr = inner.value()?.parse()?;
10252                        name = Some(s.value());
10253                        return Ok(());
10254                    }
10255                    if inner.path.is_ident("include") {
10256                        // PG 11+ covering-index columns. Non-key columns
10257                        // travel with the index leaf for index-only
10258                        // scans. Dropped on MySQL/SQLite by the writer.
10259                        let s: LitStr = inner.value()?.parse()?;
10260                        include = split_field_list(&s.value());
10261                        return Ok(());
10262                    }
10263                    Err(inner.error(
10264                        "unknown unique_when attribute (supported: \
10265                         `columns = \"...\"`, `condition = \"...\"`, \
10266                         `name = \"...\"`, `include = \"...\"`)",
10267                    ))
10268                })?;
10269                let columns = columns.ok_or_else(|| {
10270                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
10271                })?;
10272                let condition = condition.ok_or_else(|| {
10273                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
10274                })?;
10275                if columns.is_empty() {
10276                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
10277                }
10278                out.indexes.push(IndexAttr {
10279                    span: meta.path.span(),
10280                    name,
10281                    columns,
10282                    unique: true,
10283                    method: "btree".to_owned(),
10284                    where_clause: Some(condition),
10285                    include,
10286                });
10287                return Ok(());
10288            }
10289            if meta.path.is_ident("index_when") {
10290                // Non-unique partial index. Sibling of `unique_when`
10291                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
10292                //
10293                //   #[rustango(index_when(
10294                //       columns   = "status, created_at",
10295                //       condition = "deleted_at IS NULL",
10296                //       name      = "active_status_created_idx"
10297                //   ))]
10298                //
10299                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
10300                // on PG / SQLite (both ship partial indexes natively).
10301                // MySQL has no native partial-index support — the writer
10302                // emits a plain CREATE INDEX and the condition is lost;
10303                // operators wanting that selectivity on MySQL should
10304                // declare a covering index plus an application-level
10305                // filter.
10306                let mut columns: Option<Vec<String>> = None;
10307                let mut condition: Option<String> = None;
10308                let mut name: Option<String> = None;
10309                let mut method: String = "btree".to_owned();
10310                let mut include: Vec<String> = Vec::new();
10311                meta.parse_nested_meta(|inner| {
10312                    if inner.path.is_ident("columns") {
10313                        let s: LitStr = inner.value()?.parse()?;
10314                        columns = Some(split_field_list(&s.value()));
10315                        return Ok(());
10316                    }
10317                    if inner.path.is_ident("condition") {
10318                        let s: LitStr = inner.value()?.parse()?;
10319                        condition = Some(s.value());
10320                        return Ok(());
10321                    }
10322                    if inner.path.is_ident("name") {
10323                        let s: LitStr = inner.value()?.parse()?;
10324                        name = Some(s.value());
10325                        return Ok(());
10326                    }
10327                    if inner.path.is_ident("method") {
10328                        let s: LitStr = inner.value()?.parse()?;
10329                        method = s.value();
10330                        return Ok(());
10331                    }
10332                    if inner.path.is_ident("include") {
10333                        // PG 11+ covering-index columns; non-key
10334                        // columns travel with the index leaf. Dropped
10335                        // on MySQL/SQLite.
10336                        let s: LitStr = inner.value()?.parse()?;
10337                        include = split_field_list(&s.value());
10338                        return Ok(());
10339                    }
10340                    Err(inner.error(
10341                        "unknown index_when attribute (supported: \
10342                         `columns = \"...\"`, `condition = \"...\"`, \
10343                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
10344                         `include = \"...\"`)",
10345                    ))
10346                })?;
10347                let columns = columns
10348                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
10349                let condition = condition.ok_or_else(|| {
10350                    meta.error("`index_when(...)` requires `condition = \"...\"`")
10351                })?;
10352                if columns.is_empty() {
10353                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
10354                }
10355                out.indexes.push(IndexAttr {
10356                    span: meta.path.span(),
10357                    name,
10358                    columns,
10359                    unique: false,
10360                    method,
10361                    where_clause: Some(condition),
10362                    include,
10363                });
10364                return Ok(());
10365            }
10366            if meta.path.is_ident("index") {
10367                // Container-level composite index. Two syntaxes:
10368                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
10369                //   #[rustango(index("col1, col2"))]    — call form (same result)
10370                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
10371                // The call form takes the column list as a leading string
10372                // literal, then optional `unique` / `name = "..."` /
10373                // `method = "..."` flags (a leading literal can't compose
10374                // under `parse_nested_meta`, so the paren body is parsed by
10375                // hand). `unique_together` / `index_together` remain
10376                // aliases for the same feature.
10377                let cols_lit: LitStr;
10378                let mut unique = false;
10379                let mut name: Option<String> = None;
10380                let mut method = "btree".to_owned();
10381                if meta.input.peek(syn::token::Paren) {
10382                    let content;
10383                    syn::parenthesized!(content in meta.input);
10384                    cols_lit = content.parse()?;
10385                    while content.peek(syn::Token![,]) {
10386                        content.parse::<syn::Token![,]>()?;
10387                        if content.is_empty() {
10388                            break;
10389                        }
10390                        let flag: syn::Ident = content.parse()?;
10391                        if flag == "unique" {
10392                            unique = true;
10393                        } else if flag == "name" {
10394                            content.parse::<syn::Token![=]>()?;
10395                            let s: LitStr = content.parse()?;
10396                            name = Some(s.value());
10397                        } else if flag == "method" {
10398                            content.parse::<syn::Token![=]>()?;
10399                            let s: LitStr = content.parse()?;
10400                            let v = s.value();
10401                            match v.as_str() {
10402                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10403                                | "bloom" => method = v,
10404                                other => {
10405                                    return Err(syn::Error::new(
10406                                        s.span(),
10407                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10408                                    ));
10409                                }
10410                            }
10411                        } else {
10412                            return Err(syn::Error::new(
10413                                flag.span(),
10414                                "unknown index flag (supported: `unique`, `name`, `method`)",
10415                            ));
10416                        }
10417                    }
10418                } else {
10419                    cols_lit = meta.value()?.parse()?;
10420                }
10421                let columns = split_field_list(&cols_lit.value());
10422                out.indexes.push(IndexAttr {
10423                    span: meta.path.span(),
10424                    name,
10425                    columns,
10426                    unique,
10427                    method,
10428                    where_clause: None,
10429                    include: Vec::new(),
10430                });
10431                return Ok(());
10432            }
10433            if meta.path.is_ident("check") {
10434                // #[rustango(check(name = "…", expr = "…"))]
10435                let mut name: Option<String> = None;
10436                let mut expr: Option<String> = None;
10437                meta.parse_nested_meta(|inner| {
10438                    if inner.path.is_ident("name") {
10439                        let s: LitStr = inner.value()?.parse()?;
10440                        name = Some(s.value());
10441                        return Ok(());
10442                    }
10443                    if inner.path.is_ident("expr") {
10444                        let s: LitStr = inner.value()?.parse()?;
10445                        expr = Some(s.value());
10446                        return Ok(());
10447                    }
10448                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10449                })?;
10450                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10451                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10452                out.checks.push(CheckAttr { name, expr });
10453                return Ok(());
10454            }
10455            if meta.path.is_ident("exclude") {
10456                // #[rustango(exclude(name = "…", using = "gist",
10457                //                    elements = "col WITH op, col WITH op",
10458                //                    where = "…"))]
10459                let mut name: Option<String> = None;
10460                let mut using: Option<String> = None;
10461                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10462                let mut where_clause: Option<String> = None;
10463                meta.parse_nested_meta(|inner| {
10464                    if inner.path.is_ident("name") {
10465                        let s: LitStr = inner.value()?.parse()?;
10466                        name = Some(s.value());
10467                        return Ok(());
10468                    }
10469                    if inner.path.is_ident("using") {
10470                        let s: LitStr = inner.value()?.parse()?;
10471                        using = Some(s.value());
10472                        return Ok(());
10473                    }
10474                    if inner.path.is_ident("elements") {
10475                        let s: LitStr = inner.value()?.parse()?;
10476                        elements_raw = Some((s.value(), s.span()));
10477                        return Ok(());
10478                    }
10479                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10480                        let s: LitStr = inner.value()?.parse()?;
10481                        where_clause = Some(s.value());
10482                        return Ok(());
10483                    }
10484                    Err(inner.error(
10485                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10486                    ))
10487                })?;
10488                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10489                let using = using.unwrap_or_else(|| "gist".to_owned());
10490                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10491                    meta.error(
10492                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
10493                    )
10494                })?;
10495                // Parse `col WITH op` pairs separated by commas.
10496                let mut elements: Vec<(String, String)> = Vec::new();
10497                for pair in elements_str.split(',') {
10498                    let pair = pair.trim();
10499                    if pair.is_empty() {
10500                        continue;
10501                    }
10502                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10503                    let col = split.next().unwrap_or("").trim();
10504                    let rest = split.next().unwrap_or("").trim();
10505                    // `WITH op` — case-insensitive on `WITH`, then op.
10506                    let rest_lc = rest.to_ascii_lowercase();
10507                    let op = rest_lc
10508                        .strip_prefix("with")
10509                        .map(|r| r.trim_start())
10510                        .filter(|r| !r.is_empty())
10511                        .map(|_| {
10512                            // Pull the original-case op from `rest` after the
10513                            // `WITH ` token (5 chars).
10514                            rest[4..].trim_start().to_owned()
10515                        });
10516                    let Some(op) = op else {
10517                        return Err(syn::Error::new(
10518                            elements_span,
10519                            format!(
10520                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10521                                 (e.g. `room_id WITH =` or `during WITH &&`)"
10522                            ),
10523                        ));
10524                    };
10525                    if col.is_empty() || op.is_empty() {
10526                        return Err(syn::Error::new(
10527                            elements_span,
10528                            format!(
10529                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10530                                 (both sides non-empty)"
10531                            ),
10532                        ));
10533                    }
10534                    elements.push((col.to_owned(), op));
10535                }
10536                if elements.is_empty() {
10537                    return Err(syn::Error::new(
10538                        elements_span,
10539                        "exclude requires at least one `col WITH op` element",
10540                    ));
10541                }
10542                out.excludes.push(ExcludeAttr {
10543                    name,
10544                    using,
10545                    elements,
10546                    where_clause,
10547                });
10548                return Ok(());
10549            }
10550            if meta.path.is_ident("generic_fk") {
10551                let mut gfk = GenericFkAttr {
10552                    name: String::new(),
10553                    ct_column: String::new(),
10554                    pk_column: String::new(),
10555                };
10556                meta.parse_nested_meta(|inner| {
10557                    if inner.path.is_ident("name") {
10558                        let s: LitStr = inner.value()?.parse()?;
10559                        gfk.name = s.value();
10560                        return Ok(());
10561                    }
10562                    if inner.path.is_ident("ct_column") {
10563                        let s: LitStr = inner.value()?.parse()?;
10564                        gfk.ct_column = s.value();
10565                        return Ok(());
10566                    }
10567                    if inner.path.is_ident("pk_column") {
10568                        let s: LitStr = inner.value()?.parse()?;
10569                        gfk.pk_column = s.value();
10570                        return Ok(());
10571                    }
10572                    Err(inner.error(
10573                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10574                    ))
10575                })?;
10576                if gfk.name.is_empty() {
10577                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10578                }
10579                if gfk.ct_column.is_empty() {
10580                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10581                }
10582                if gfk.pk_column.is_empty() {
10583                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10584                }
10585                out.generic_fks.push(gfk);
10586                return Ok(());
10587            }
10588            if meta.path.is_ident("fk_composite") {
10589                let mut fk = CompositeFkAttr {
10590                    name: String::new(),
10591                    to: String::new(),
10592                    from: Vec::new(),
10593                    on: Vec::new(),
10594                };
10595                meta.parse_nested_meta(|inner| {
10596                    if inner.path.is_ident("name") {
10597                        let s: LitStr = inner.value()?.parse()?;
10598                        fk.name = s.value();
10599                        return Ok(());
10600                    }
10601                    if inner.path.is_ident("to") {
10602                        let s: LitStr = inner.value()?.parse()?;
10603                        fk.to = s.value();
10604                        return Ok(());
10605                    }
10606                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10607                    // comma-list of string literals.
10608                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10609                        let value = inner.value()?;
10610                        let content;
10611                        syn::parenthesized!(content in value);
10612                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10613                            content.parse_terminated(
10614                                |p| p.parse::<syn::LitStr>(),
10615                                syn::Token![,],
10616                            )?;
10617                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10618                        if inner.path.is_ident("on") {
10619                            fk.on = cols;
10620                        } else {
10621                            fk.from = cols;
10622                        }
10623                        return Ok(());
10624                    }
10625                    Err(inner.error(
10626                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10627                    ))
10628                })?;
10629                if fk.name.is_empty() {
10630                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10631                }
10632                if fk.to.is_empty() {
10633                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10634                }
10635                if fk.from.is_empty() || fk.on.is_empty() {
10636                    return Err(meta.error(
10637                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10638                    ));
10639                }
10640                if fk.from.len() != fk.on.len() {
10641                    return Err(meta.error(format!(
10642                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10643                        fk.from.len(),
10644                        fk.on.len(),
10645                    )));
10646                }
10647                out.composite_fks.push(fk);
10648                return Ok(());
10649            }
10650            if meta.path.is_ident("m2m") {
10651                let mut m2m = M2MAttr {
10652                    name: String::new(),
10653                    to: String::new(),
10654                    through: String::new(),
10655                    src: String::new(),
10656                    dst: String::new(),
10657                    auto_create: true,
10658                };
10659                meta.parse_nested_meta(|inner| {
10660                    if inner.path.is_ident("name") {
10661                        let s: LitStr = inner.value()?.parse()?;
10662                        m2m.name = s.value();
10663                        return Ok(());
10664                    }
10665                    if inner.path.is_ident("to") {
10666                        let s: LitStr = inner.value()?.parse()?;
10667                        m2m.to = s.value();
10668                        return Ok(());
10669                    }
10670                    if inner.path.is_ident("through") {
10671                        let s: LitStr = inner.value()?.parse()?;
10672                        m2m.through = s.value();
10673                        return Ok(());
10674                    }
10675                    if inner.path.is_ident("src") {
10676                        let s: LitStr = inner.value()?.parse()?;
10677                        m2m.src = s.value();
10678                        return Ok(());
10679                    }
10680                    if inner.path.is_ident("dst") {
10681                        let s: LitStr = inner.value()?.parse()?;
10682                        m2m.dst = s.value();
10683                        return Ok(());
10684                    }
10685                    if inner.path.is_ident("auto_create") {
10686                        let lit: syn::LitBool = inner.value()?.parse()?;
10687                        m2m.auto_create = lit.value;
10688                        return Ok(());
10689                    }
10690                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10691                })?;
10692                if m2m.name.is_empty() {
10693                    return Err(meta.error("m2m requires `name = \"...\"`"));
10694                }
10695                if m2m.to.is_empty() {
10696                    return Err(meta.error("m2m requires `to = \"...\"`"));
10697                }
10698                if m2m.through.is_empty() {
10699                    return Err(meta.error("m2m requires `through = \"...\"`"));
10700                }
10701                if m2m.src.is_empty() {
10702                    return Err(meta.error("m2m requires `src = \"...\"`"));
10703                }
10704                if m2m.dst.is_empty() {
10705                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10706                }
10707                out.m2m.push(m2m);
10708                return Ok(());
10709            }
10710            if meta.path.is_ident("generic_m2m") {
10711                let mut gm = GenericM2MAttr {
10712                    name: String::new(),
10713                    through: String::new(),
10714                    pk_column: String::new(),
10715                    ct_column: String::new(),
10716                    related_column: String::new(),
10717                };
10718                meta.parse_nested_meta(|inner| {
10719                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10720                        let s: LitStr = inner.value()?.parse()?;
10721                        Ok(s.value())
10722                    };
10723                    if inner.path.is_ident("name") {
10724                        gm.name = field(&inner)?;
10725                        return Ok(());
10726                    }
10727                    if inner.path.is_ident("through") {
10728                        gm.through = field(&inner)?;
10729                        return Ok(());
10730                    }
10731                    if inner.path.is_ident("pk_column") {
10732                        gm.pk_column = field(&inner)?;
10733                        return Ok(());
10734                    }
10735                    if inner.path.is_ident("ct_column") {
10736                        gm.ct_column = field(&inner)?;
10737                        return Ok(());
10738                    }
10739                    if inner.path.is_ident("related_column") {
10740                        gm.related_column = field(&inner)?;
10741                        return Ok(());
10742                    }
10743                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10744                })?;
10745                for (val, label) in [
10746                    (&gm.name, "name"),
10747                    (&gm.through, "through"),
10748                    (&gm.pk_column, "pk_column"),
10749                    (&gm.ct_column, "ct_column"),
10750                    (&gm.related_column, "related_column"),
10751                ] {
10752                    if val.is_empty() {
10753                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10754                    }
10755                }
10756                out.generic_m2m.push(gm);
10757                return Ok(());
10758            }
10759            Err(unknown_key_error(&meta, "container", CONTAINER_ATTR_KEYS))
10760        })?;
10761    }
10762    Ok(out)
10763}
10764
10765/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10766/// owned field names, trimming whitespace and skipping empty entries.
10767/// Field-name validation against the model is done by the caller.
10768fn split_field_list(raw: &str) -> Vec<String> {
10769    raw.split(',')
10770        .map(str::trim)
10771        .filter(|s| !s.is_empty())
10772        .map(str::to_owned)
10773        .collect()
10774}
10775
10776/// Shared parser for `unique_together` and `index_together` container
10777/// attrs. Accepts both shapes:
10778///
10779///   * `attr = "col1, col2"`              — auto-derived index name.
10780///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10781///
10782/// Returns `(columns, name)`.
10783fn parse_together_attr(
10784    meta: &syn::meta::ParseNestedMeta<'_>,
10785    attr: &str,
10786    written: &str,
10787) -> syn::Result<(Vec<String>, Option<String>)> {
10788    // Disambiguate by whether the next token is `=` (key-value) or
10789    // `(` (parenthesized).
10790    if meta.input.peek(syn::Token![=]) {
10791        let cols_lit: LitStr = meta.value()?.parse()?;
10792        let columns = split_field_list(&cols_lit.value());
10793        check_together_columns(meta, attr, written, &columns)?;
10794        return Ok((columns, None));
10795    }
10796    let mut columns: Option<Vec<String>> = None;
10797    let mut name: Option<String> = None;
10798    meta.parse_nested_meta(|inner| {
10799        if inner.path.is_ident("columns") {
10800            let s: LitStr = inner.value()?.parse()?;
10801            columns = Some(split_field_list(&s.value()));
10802            return Ok(());
10803        }
10804        if inner.path.is_ident("name") {
10805            let s: LitStr = inner.value()?.parse()?;
10806            name = Some(s.value());
10807            return Ok(());
10808        }
10809        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10810    })?;
10811    let columns = columns.ok_or_else(|| {
10812        meta.error(format!(
10813            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10814        ))
10815    })?;
10816    check_together_columns(meta, attr, written, &columns)?;
10817    Ok((columns, name))
10818}
10819
10820fn check_together_columns(
10821    meta: &syn::meta::ParseNestedMeta<'_>,
10822    attr: &str,
10823    written: &str,
10824    columns: &[String],
10825) -> syn::Result<()> {
10826    if columns.len() < 2 {
10827        let flag = if attr == "unique_together" {
10828            "unique"
10829        } else {
10830            "index"
10831        };
10832        let single = format!("#[{written}({flag})] on the field");
10833        return Err(meta.error(format!(
10834            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10835        )));
10836    }
10837    Ok(())
10838}
10839
10840/// Parse the fieldsets DSL: pipe-separated sections, optional
10841/// `"Title:"` prefix on each, comma-separated field names after.
10842/// Examples:
10843/// * `"name, office"` → one untitled section with two fields
10844/// * `"Identity: name, office | Metadata: created_at"` → two titled
10845///   sections
10846///
10847/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10848fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10849    raw.split('|')
10850        .map(str::trim)
10851        .filter(|s| !s.is_empty())
10852        .map(|section| {
10853            // Split off an optional `Title:` prefix (first colon).
10854            let (title, rest) = match section.split_once(':') {
10855                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10856                _ => (String::new(), section),
10857            };
10858            let fields = split_field_list(rest);
10859            (title, fields)
10860        })
10861        .collect()
10862}
10863
10864/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10865/// comma-separated entry maps a target field to one or more source
10866/// fields joined with `+`. Whitespace around tokens is trimmed.
10867/// Entries missing `:` or with empty target/source lists are dropped.
10868fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10869    raw.split(',')
10870        .map(str::trim)
10871        .filter(|s| !s.is_empty())
10872        .filter_map(|entry| {
10873            let (target, sources_raw) = entry.split_once(':')?;
10874            let target = target.trim().to_owned();
10875            if target.is_empty() {
10876                return None;
10877            }
10878            let sources: Vec<String> = sources_raw
10879                .split('+')
10880                .map(|s| s.trim().to_owned())
10881                .filter(|s| !s.is_empty())
10882                .collect();
10883            if sources.is_empty() {
10884                return None;
10885            }
10886            Some((target, sources))
10887        })
10888        .collect()
10889}
10890
10891/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10892/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10893/// and empty halves drop silently — the macro layer only enforces shape,
10894/// not field-name vs. widget-name validity (those checks happen at
10895/// `AdminConfig` consumption time). Issue #359.
10896fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10897    raw.split(',')
10898        .map(str::trim)
10899        .filter(|s| !s.is_empty())
10900        .filter_map(|entry| {
10901            let (field, widget) = entry.split_once(':')?;
10902            let field = field.trim().to_owned();
10903            let widget = widget.trim().to_owned();
10904            if field.is_empty() || widget.is_empty() {
10905                return None;
10906            }
10907            Some((field, widget))
10908        })
10909        .collect()
10910}
10911
10912/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10913/// Returns `(field_name, desc)` pairs in the same order as the input.
10914fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10915    raw.split(',')
10916        .map(str::trim)
10917        .filter(|s| !s.is_empty())
10918        .map(|spec| {
10919            spec.strip_prefix('-')
10920                .map_or((spec.to_owned(), false), |rest| {
10921                    (rest.trim().to_owned(), true)
10922                })
10923        })
10924        .collect()
10925}
10926
10927struct FieldAttrs {
10928    column: Option<String>,
10929    primary_key: bool,
10930    fk: Option<String>,
10931    o2o: Option<String>,
10932    on: Option<String>,
10933    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10934    /// "set_default" | "no_action")]` — what happens to this row when
10935    /// the row it points at is deleted.
10936    /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10937    /// set; the macro errors at compile time if applied to a
10938    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10939    /// DDL writer renders `ON DELETE <action>` after the constraint
10940    /// clause when this is `Some`; `None` falls back to the database
10941    /// default (NO ACTION on every backend rustango supports).
10942    on_delete: Option<String>,
10943    /// `#[rustango(related_name = "...")]` — per-FK
10944    /// reverse-accessor override. When set, the derive emits
10945    /// `Parent::<related_name>[_pool]` instead of the container-level
10946    /// `default_related_name` or the `<child_snake>_set[_pool]`
10947    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10948    /// silently ignored on non-FK fields. Follow-up to #816.
10949    /// `"+"` emits none, for a parent defined in another crate.
10950    related_name: Option<String>,
10951    max_length: Option<u32>,
10952    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10953    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10954    /// unconstrained `vector` column.
10955    vector_dims: Option<u32>,
10956    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10957    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10958    /// unconstrained `geometry(Point)` column (SRID 0).
10959    geometry_srid: Option<u32>,
10960    min: Option<i64>,
10961    max: Option<i64>,
10962    default: Option<String>,
10963    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10964    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10965    /// "gen_random_uuid()"`. The Rust field type must be
10966    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10967    /// INSERTs so the DB DEFAULT fires.
10968    auto_uuid: bool,
10969    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10970    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10971    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10972    /// the field is `Auto::Unset`, then bound as a normal parameter
10973    /// rather than relying on a per-dialect DB function. Issue #823
10974    /// (Eloquent `HasUuids`).
10975    ///
10976    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10977    /// Composes with every backend (PG / MySQL / SQLite) — no
10978    /// `gen_random_uuid()` requirement on the database.
10979    default_uuid_v7: bool,
10980    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10981    /// Server-set on insert, immutable from app code afterwards.
10982    /// Implies `auto + default = "now()"`. Field type must be
10983    /// `DateTime<Utc>`.
10984    auto_now_add: bool,
10985    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10986    /// every insert AND every update. Implies `auto + default =
10987    /// "now()"`; the macro additionally rewrites `update_on` /
10988    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10989    /// field value.
10990    auto_now: bool,
10991    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10992    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10993    /// `soft_delete_on(executor)` and `restore_on(executor)`
10994    /// methods on the model.
10995    soft_delete: bool,
10996    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10997    /// the column in the generated DDL.
10998    unique: bool,
10999    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
11000    /// generates a `CREATE INDEX` for this column. `unique` here means
11001    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
11002    index: bool,
11003    index_unique: bool,
11004    index_name: Option<String>,
11005    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
11006    /// `"btree"`. Issue #34.
11007    index_method: String,
11008    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
11009    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
11010    /// the macro skips this column from every INSERT and UPDATE
11011    /// path, so the database always recomputes the value from
11012    /// `EXPR`. Backlog item #35.
11013    generated_as: Option<String>,
11014    /// `#[rustango(help_text = "…")]` — help text
11015    /// rendered below the admin form's input. Threaded into
11016    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
11017    /// layers can read it.
11018    help_text: Option<String>,
11019    /// `#[rustango(choices = "value:Label, value:Label")]` — the
11020    /// enumerated allowed values. Threaded into `FieldSchema::choices`
11021    /// as a `&'static [(&'static str, &'static str)]` slice. When
11022    /// present, the admin form renders a `<select>` instead of `<input>`
11023    /// and the validator rejects values not in the list. Only meaningful
11024    /// for `FieldType::String`; the macro errors at compile time if
11025    /// applied to a non-string field.
11026    choices: Option<Vec<(String, String)>>,
11027    /// `#[rustango(db_comment = "…")]` — DB-side column
11028    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
11029    /// the comment in CREATE TABLE; Postgres emits a separate
11030    /// `COMMENT ON COLUMN` statement after the table is created;
11031    /// SQLite silently drops the value (no native column comments).
11032    db_comment: Option<String>,
11033    /// `#[rustango(verbose_name = "…")]` — human-readable
11034    /// label for the field. Threaded into `FieldSchema::verbose_name`
11035    /// so admin column headers, form labels, and other display
11036    /// surfaces can prefer the friendly caption over the Rust
11037    /// identifier. `None` means renderers fall back to the field name.
11038    verbose_name: Option<String>,
11039    /// `#[rustango(editable = false)]` — opt out of
11040    /// auto-generated form rendering. Defaults to `true` so existing
11041    /// fields keep their current admin / form behavior; setting
11042    /// `false` removes the field from the admin change-form entirely
11043    /// (the value is still visible on detail / list views, just not
11044    /// editable).
11045    editable: bool,
11046    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
11047    /// "form may submit empty even when DB is NOT NULL". Threaded into
11048    /// `FieldSchema::blank`. Defaults to `false`.
11049    blank: bool,
11050    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
11051    /// case-insensitive text column. Threaded into
11052    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
11053    /// fields; the macro errors at derive time if applied elsewhere.
11054    case_insensitive: bool,
11055    /// `#[rustango(validators = "email,url")]` — the
11056    /// model-level validator chain. Comma-separated names that
11057    /// dispatch to the `validators::*` family in `validate_value`.
11058    /// Empty by default; fires on every typed INSERT/UPDATE. Each is
11059    /// spanned on the attribute string, so an unknown name errors there.
11060    validators: Vec<syn::Ident>,
11061}
11062
11063/// Keys `parse_field_attrs` accepts, listed in its unknown-key error.
11064const FIELD_ATTR_KEYS: &[&str] = &[
11065    "column",
11066    "primary_key",
11067    "fk",
11068    "o2o",
11069    "on",
11070    "on_delete",
11071    "related_name",
11072    "max_length",
11073    "vector",
11074    "geometry",
11075    "min",
11076    "max",
11077    "default",
11078    "generated_as",
11079    "help_text",
11080    "choices",
11081    "db_comment",
11082    "verbose_name",
11083    "editable",
11084    "blank",
11085    "citext",
11086    "validators",
11087    "auto_uuid",
11088    "default_uuid_v7",
11089    "auto_now_add",
11090    "auto_now",
11091    "soft_delete",
11092    "unique",
11093    "index",
11094];
11095
11096/// Keys `parse_container_attrs` accepts, listed in its unknown-key error.
11097const CONTAINER_ATTR_KEYS: &[&str] = &[
11098    "table",
11099    "display",
11100    "app",
11101    "scope",
11102    "admin",
11103    "manager",
11104    "manager_fn",
11105    "default_order",
11106    "global_scope",
11107    "through",
11108    "reverse_has",
11109    "generic_has",
11110    "audit",
11111    "permissions",
11112    "view",
11113    "managed",
11114    "verbose_name",
11115    "verbose_name_plural",
11116    "db_table_comment",
11117    "proxy",
11118    "order_with_respect_to",
11119    "required_db_features",
11120    "required_db_vendor",
11121    "base_manager_name",
11122    "default_related_name",
11123    "extra_permissions",
11124    "default_permissions",
11125    "get_latest_by",
11126    "unique_together",
11127    "index_together",
11128    "unique_when",
11129    "index_when",
11130    "index",
11131    "check",
11132    "exclude",
11133    "generic_fk",
11134    "fk_composite",
11135    "m2m",
11136    "generic_m2m",
11137];
11138
11139/// A flag attribute: `#[rustango(key)]` or `#[rustango(key = true)]`.
11140/// `= false` is refused: off is the default, so the attribute is a no-op.
11141fn parse_flag(meta: &syn::meta::ParseNestedMeta<'_>, key: &str) -> syn::Result<()> {
11142    if meta.input.peek(syn::Token![=]) {
11143        let lit: syn::LitBool = meta.value()?.parse()?;
11144        if !lit.value {
11145            return Err(syn::Error::new(
11146                lit.span(),
11147                format!("`{key} = false` is the default; remove the attribute"),
11148            ));
11149        }
11150    }
11151    Ok(())
11152}
11153
11154/// "unknown rustango <what> attribute `key`", with the nearest key and the full list.
11155fn unknown_key_error(
11156    meta: &syn::meta::ParseNestedMeta<'_>,
11157    what: &str,
11158    keys: &[&str],
11159) -> syn::Error {
11160    let path = &meta.path;
11161    let got = path
11162        .get_ident()
11163        .map_or_else(|| quote!(#path).to_string(), ToString::to_string);
11164    let mut msg = format!("unknown rustango {what} attribute `{got}`");
11165    if let Some(near) = keys
11166        .iter()
11167        .map(|k| (edit_distance(&got, k), *k))
11168        .filter(|(d, k)| *d <= 2.max(k.len() / 4))
11169        .min_by_key(|(d, _)| *d)
11170        .map(|(_, k)| k)
11171    {
11172        msg.push_str(&format!("; did you mean `{near}`?"));
11173    }
11174    msg.push_str(&format!(" Supported: {}", keys.join(", ")));
11175    meta.error(msg)
11176}
11177
11178/// Levenshtein distance, for "did you mean" hints.
11179fn edit_distance(a: &str, b: &str) -> usize {
11180    let b: Vec<char> = b.chars().collect();
11181    let mut prev: Vec<usize> = (0..=b.len()).collect();
11182    for (i, ca) in a.chars().enumerate() {
11183        let mut cur = vec![i + 1];
11184        for (j, cb) in b.iter().enumerate() {
11185            let sub = prev[j] + usize::from(ca != *cb);
11186            cur.push(sub.min(prev[j + 1] + 1).min(cur[j] + 1));
11187        }
11188        prev = cur;
11189    }
11190    prev[b.len()]
11191}
11192
11193fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
11194    let mut out = FieldAttrs {
11195        column: None,
11196        primary_key: false,
11197        fk: None,
11198        o2o: None,
11199        on: None,
11200        on_delete: None,
11201        related_name: None,
11202        max_length: None,
11203        vector_dims: None,
11204        geometry_srid: None,
11205        min: None,
11206        max: None,
11207        default: None,
11208        auto_uuid: false,
11209        default_uuid_v7: false,
11210        auto_now_add: false,
11211        auto_now: false,
11212        soft_delete: false,
11213        unique: false,
11214        index: false,
11215        index_unique: false,
11216        index_name: None,
11217        index_method: "btree".to_owned(),
11218        generated_as: None,
11219        help_text: None,
11220        choices: None,
11221        db_comment: None,
11222        verbose_name: None,
11223        editable: true,
11224        blank: false,
11225        case_insensitive: false,
11226        validators: Vec::new(),
11227    };
11228    for attr in model_attrs(&field.attrs)? {
11229        attr.parse_nested_meta(|meta| {
11230            if meta.path.is_ident("column") {
11231                let s: LitStr = meta.value()?.parse()?;
11232                let name = s.value();
11233                validate_sql_identifier(&name, "column", s.span())?;
11234                out.column = Some(name);
11235                return Ok(());
11236            }
11237            if meta.path.is_ident("primary_key") {
11238                parse_flag(&meta, "primary_key")?;
11239                out.primary_key = true;
11240                return Ok(());
11241            }
11242            if meta.path.is_ident("fk") {
11243                let s: LitStr = meta.value()?.parse()?;
11244                out.fk = Some(s.value());
11245                return Ok(());
11246            }
11247            if meta.path.is_ident("o2o") {
11248                let s: LitStr = meta.value()?.parse()?;
11249                out.o2o = Some(s.value());
11250                return Ok(());
11251            }
11252            if meta.path.is_ident("on") {
11253                let s: LitStr = meta.value()?.parse()?;
11254                out.on = Some(s.value());
11255                return Ok(());
11256            }
11257            if meta.path.is_ident("on_delete") {
11258                let s: LitStr = meta.value()?.parse()?;
11259                let raw = s.value();
11260                let normalized = raw.trim().to_ascii_lowercase();
11261                // Validate at parse time so the user gets a clear span
11262                // rather than a downstream compile error in the emit.
11263                match normalized.as_str() {
11264                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
11265                    _ => {
11266                        return Err(syn::Error::new(
11267                            s.span(),
11268                            format!(
11269                                "unknown on_delete action `{raw}`; expected one of \
11270                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
11271                            ),
11272                        ));
11273                    }
11274                }
11275                out.on_delete = Some(normalized);
11276                return Ok(());
11277            }
11278            if meta.path.is_ident("related_name") {
11279                let s: LitStr = meta.value()?.parse()?;
11280                let raw = s.value();
11281                if raw.trim().is_empty() {
11282                    return Err(syn::Error::new(
11283                        s.span(),
11284                        "`related_name` must be a non-empty identifier",
11285                    ));
11286                }
11287                // Validate as a Rust-ident shape — the value becomes
11288                // a method name on the parent type. Same rule as
11289                // `default_related_name` so the two surfaces match.
11290                // `"+"` emits no accessor at all.
11291                let snake = raw
11292                    .chars()
11293                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
11294                    && !raw.starts_with(char::is_numeric);
11295                if !snake && raw != NO_REVERSE_ACCESSOR {
11296                    return Err(syn::Error::new(
11297                        s.span(),
11298                        "`related_name` must be snake_case ASCII (lowercase letters, \
11299                         digits, underscores; no leading digit), or \"+\" for none",
11300                    ));
11301                }
11302                out.related_name = Some(raw);
11303                return Ok(());
11304            }
11305            if meta.path.is_ident("max_length") {
11306                let lit: syn::LitInt = meta.value()?.parse()?;
11307                out.max_length = Some(lit.base10_parse::<u32>()?);
11308                return Ok(());
11309            }
11310            // `#[rustango(vector(dims = N))]` — pgvector column
11311            // dimension (#824). Nested-meta form so it reads like the
11312            // other typed-column attrs.
11313            if meta.path.is_ident("vector") {
11314                meta.parse_nested_meta(|inner| {
11315                    if inner.path.is_ident("dims") {
11316                        let lit: syn::LitInt = inner.value()?.parse()?;
11317                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
11318                        return Ok(());
11319                    }
11320                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
11321                })?;
11322                return Ok(());
11323            }
11324            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
11325            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
11326            if meta.path.is_ident("geometry") {
11327                meta.parse_nested_meta(|inner| {
11328                    if inner.path.is_ident("srid") {
11329                        let lit: syn::LitInt = inner.value()?.parse()?;
11330                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
11331                        return Ok(());
11332                    }
11333                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
11334                })?;
11335                return Ok(());
11336            }
11337            if meta.path.is_ident("min") {
11338                out.min = Some(parse_signed_i64(&meta)?);
11339                return Ok(());
11340            }
11341            if meta.path.is_ident("max") {
11342                out.max = Some(parse_signed_i64(&meta)?);
11343                return Ok(());
11344            }
11345            if meta.path.is_ident("default") {
11346                let s: LitStr = meta.value()?.parse()?;
11347                out.default = Some(s.value());
11348                return Ok(());
11349            }
11350            if meta.path.is_ident("generated_as") {
11351                let s: LitStr = meta.value()?.parse()?;
11352                out.generated_as = Some(s.value());
11353                return Ok(());
11354            }
11355            if meta.path.is_ident("help_text") {
11356                let s: LitStr = meta.value()?.parse()?;
11357                out.help_text = Some(s.value());
11358                return Ok(());
11359            }
11360            if meta.path.is_ident("choices") {
11361                let s: LitStr = meta.value()?.parse()?;
11362                let raw = s.value();
11363                let mut pairs: Vec<(String, String)> = Vec::new();
11364                for chunk in raw.split(',') {
11365                    let chunk = chunk.trim();
11366                    if chunk.is_empty() {
11367                        continue;
11368                    }
11369                    let (value, label) = match chunk.split_once(':') {
11370                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
11371                        None => (chunk.to_owned(), chunk.to_owned()),
11372                    };
11373                    if value.is_empty() {
11374                        return Err(syn::Error::new(
11375                            s.span(),
11376                            "`choices` entry has empty value before `:`",
11377                        ));
11378                    }
11379                    pairs.push((value, label));
11380                }
11381                if pairs.is_empty() {
11382                    return Err(syn::Error::new(
11383                        s.span(),
11384                        "`choices = \"…\"` must contain at least one value",
11385                    ));
11386                }
11387                out.choices = Some(pairs);
11388                return Ok(());
11389            }
11390            if meta.path.is_ident("db_comment") {
11391                let s: LitStr = meta.value()?.parse()?;
11392                out.db_comment = Some(s.value());
11393                return Ok(());
11394            }
11395            if meta.path.is_ident("verbose_name") {
11396                let s: LitStr = meta.value()?.parse()?;
11397                out.verbose_name = Some(s.value());
11398                return Ok(());
11399            }
11400            if meta.path.is_ident("editable") {
11401                // Two forms accepted:
11402                //   #[rustango(editable = false)] / true — explicit
11403                //   #[rustango(editable)] — flag form (= true, the
11404                //   default, so harmless; included for symmetry)
11405                if let Ok(v) = meta.value() {
11406                    let lit: syn::LitBool = v.parse()?;
11407                    out.editable = lit.value;
11408                } else {
11409                    out.editable = true;
11410                }
11411                return Ok(());
11412            }
11413            if meta.path.is_ident("blank") {
11414                // Two forms accepted:
11415                //   #[rustango(blank)] — flag form, true
11416                //   #[rustango(blank = false)] / true — explicit
11417                if let Ok(v) = meta.value() {
11418                    let lit: syn::LitBool = v.parse()?;
11419                    out.blank = lit.value;
11420                } else {
11421                    out.blank = true;
11422                }
11423                return Ok(());
11424            }
11425            if meta.path.is_ident("citext") {
11426                // Case-insensitive text column (#344). Two forms:
11427                //   #[rustango(citext)]          — flag form, true
11428                //   #[rustango(citext = true)]   — explicit
11429                //   #[rustango(citext = false)]  — explicit opt-out
11430                // String-only validation lives in the field-type
11431                // emitter (the FieldType discriminant is computed in
11432                // `detect_type`); the macro records the flag and the
11433                // DDL writer emits dialect-specific COLLATE / CITEXT.
11434                if let Ok(v) = meta.value() {
11435                    let lit: syn::LitBool = v.parse()?;
11436                    out.case_insensitive = lit.value;
11437                } else {
11438                    out.case_insensitive = true;
11439                }
11440                return Ok(());
11441            }
11442            if meta.path.is_ident("validators") {
11443                let s: LitStr = meta.value()?.parse()?;
11444                let raw = s.value();
11445                out.validators = raw
11446                    .split(',')
11447                    .map(str::trim)
11448                    .filter(|s| !s.is_empty())
11449                    .map(|name| {
11450                        let mut ident = syn::parse_str::<syn::Ident>(name).map_err(|_| {
11451                            syn::Error::new(s.span(), format!("unknown validator `{name}`"))
11452                        })?;
11453                        ident.set_span(s.span());
11454                        Ok(ident)
11455                    })
11456                    .collect::<syn::Result<_>>()?;
11457                if out.validators.is_empty() {
11458                    return Err(syn::Error::new(
11459                        s.span(),
11460                        "`validators = \"…\"` must list at least one name",
11461                    ));
11462                }
11463                return Ok(());
11464            }
11465            if meta.path.is_ident("auto_uuid") {
11466                parse_flag(&meta, "auto_uuid")?;
11467                out.auto_uuid = true;
11468                // Implied: PK + auto + DEFAULT gen_random_uuid().
11469                // Each is also explicitly settable; the explicit
11470                // value wins if conflicting.
11471                out.primary_key = true;
11472                if out.default.is_none() {
11473                    out.default = Some("gen_random_uuid()".into());
11474                }
11475                return Ok(());
11476            }
11477            if meta.path.is_ident("default_uuid_v7") {
11478                parse_flag(&meta, "default_uuid_v7")?;
11479                // Backend-neutral counterpart of `auto_uuid` — issue #823.
11480                // No SQL DEFAULT (the macro fills the value Rust-side
11481                // before binding); just mark the field as PK + Auto
11482                // so the insert path is the `Auto::Unset → generate`
11483                // branch.
11484                out.default_uuid_v7 = true;
11485                out.primary_key = true;
11486                return Ok(());
11487            }
11488            if meta.path.is_ident("auto_now_add") {
11489                parse_flag(&meta, "auto_now_add")?;
11490                out.auto_now_add = true;
11491                if out.default.is_none() {
11492                    out.default = Some("now()".into());
11493                }
11494                return Ok(());
11495            }
11496            if meta.path.is_ident("auto_now") {
11497                parse_flag(&meta, "auto_now")?;
11498                out.auto_now = true;
11499                if out.default.is_none() {
11500                    out.default = Some("now()".into());
11501                }
11502                return Ok(());
11503            }
11504            if meta.path.is_ident("soft_delete") {
11505                parse_flag(&meta, "soft_delete")?;
11506                out.soft_delete = true;
11507                return Ok(());
11508            }
11509            if meta.path.is_ident("unique") {
11510                parse_flag(&meta, "unique")?;
11511                out.unique = true;
11512                return Ok(());
11513            }
11514            if meta.path.is_ident("index") {
11515                parse_flag(&meta, "index")?;
11516                out.index = true;
11517                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11518                if meta.input.peek(syn::token::Paren) {
11519                    meta.parse_nested_meta(|inner| {
11520                        if inner.path.is_ident("unique") {
11521                            out.index_unique = true;
11522                            return Ok(());
11523                        }
11524                        if inner.path.is_ident("name") {
11525                            let s: LitStr = inner.value()?.parse()?;
11526                            out.index_name = Some(s.value());
11527                            return Ok(());
11528                        }
11529                        if inner.path.is_ident("method") {
11530                            let s: LitStr = inner.value()?.parse()?;
11531                            let v = s.value();
11532                            match v.as_str() {
11533                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11534                                    out.index_method = v;
11535                                }
11536                                other => {
11537                                    return Err(inner.error(format!(
11538                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11539                                    )));
11540                                }
11541                            }
11542                            return Ok(());
11543                        }
11544                        Err(inner.error(
11545                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11546                        ))
11547                    })?;
11548                }
11549                return Ok(());
11550            }
11551            Err(unknown_key_error(&meta, "field", FIELD_ATTR_KEYS))
11552        })?;
11553    }
11554    Ok(out)
11555}
11556
11557/// Parse a signed integer literal, accepting optional leading `-`.
11558fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11559    let expr: syn::Expr = meta.value()?.parse()?;
11560    match expr {
11561        syn::Expr::Lit(syn::ExprLit {
11562            lit: syn::Lit::Int(lit),
11563            ..
11564        }) => lit.base10_parse::<i64>(),
11565        syn::Expr::Unary(syn::ExprUnary {
11566            op: syn::UnOp::Neg(_),
11567            expr,
11568            ..
11569        }) => {
11570            if let syn::Expr::Lit(syn::ExprLit {
11571                lit: syn::Lit::Int(lit),
11572                ..
11573            }) = *expr
11574            {
11575                let v: i64 = lit.base10_parse()?;
11576                Ok(-v)
11577            } else {
11578                Err(syn::Error::new_spanned(expr, "expected integer literal"))
11579            }
11580        }
11581        other => Err(syn::Error::new_spanned(
11582            other,
11583            "expected integer literal (signed)",
11584        )),
11585    }
11586}
11587
11588struct FieldInfo<'a> {
11589    ident: &'a syn::Ident,
11590    column: String,
11591    primary_key: bool,
11592    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11593    /// skip this column when `Auto::Unset` and emit it under
11594    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11595    auto: bool,
11596    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11597    /// the `Column::Value` associated type for typed-column tokens.
11598    value_ty: &'a Type,
11599    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11600    field_type_tokens: TokenStream2,
11601    schema: TokenStream2,
11602    from_row_init: TokenStream2,
11603    /// Variant of [`Self::from_row_init`] that reads the column via
11604    /// `format!("{prefix}__{col}")` so a model can be decoded out of
11605    /// the aliased columns of a JOINed row. Drives slice 9.0d's
11606    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11607    /// helper that `select_related` calls when stitching loaded FKs.
11608    from_aliased_row_init: TokenStream2,
11609    /// MySQL twins of the two inits above. They differ only for a UUID
11610    /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11611    from_row_init_my: TokenStream2,
11612    from_aliased_row_init_my: TokenStream2,
11613    /// A bare or optional `Uuid` field.
11614    uuid_column: bool,
11615    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11616    /// relation helper emit (`Author::<child>_set`) needs to know `T`
11617    /// to point the generated method at the right child model.
11618    fk_inner: Option<Type>,
11619    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11620    /// `kind` (since ForeignKey detection sets `kind` to K's
11621    /// underlying type) but stored separately for clarity at the
11622    /// `FkRelation` construction site, which only sees the FK's
11623    /// surface fields.
11624    fk_pk_kind: DetectedKind,
11625    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11626    /// the bare `ForeignKey<T, K>`. Routes the load_related and
11627    /// fk_pk_access emitters to wrap assignments / accessors in
11628    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11629    /// FK column compiles end-to-end. The DDL writer reads this off
11630    /// the field schema (`nullable` flag); the macro just needs to
11631    /// keep the Rust-side codegen consistent.
11632    nullable: bool,
11633    /// `true` when this column was marked `#[rustango(auto_now)]` —
11634    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11635    /// column instead of the user-supplied value, so `updated_at`
11636    /// always reflects the latest write without the caller having
11637    /// to remember to set it.
11638    auto_now: bool,
11639    /// `true` when this column was marked `#[rustango(auto_now_add)]`
11640    /// — the column is server-set on INSERT (DB DEFAULT) and
11641    /// **immutable** afterwards. `update_on` / `save_on` skip the
11642    /// column entirely so a stale `created_at` value in memory never
11643    /// rewrites the persisted timestamp.
11644    auto_now_add: bool,
11645    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11646    /// Triggers emission of `soft_delete_on(executor)` and
11647    /// `restore_on(executor)` on the model's inherent impl. There is
11648    /// at most one such column per model — emission asserts this.
11649    soft_delete: bool,
11650    /// `Some` when this column was marked
11651    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11652    /// every INSERT and UPDATE path; the database recomputes the
11653    /// value from `EXPR`. Backlog item #35.
11654    generated_as: Option<String>,
11655    /// `true` when this column was marked
11656    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11657    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11658    /// with `Uuid::now_v7()` before binding, so the PK is generated
11659    /// Rust-side and the column is always present in the INSERT
11660    /// statement (no DB DEFAULT requirement). Issue #823.
11661    default_uuid_v7: bool,
11662    /// `Some` when this FK field carried `#[rustango(related_name =
11663    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11664    /// consumed by [`reverse_helper_tokens`] to override the default
11665    /// `<child_snake>_set` accessor name. Follow-up to #816.
11666    related_name: Option<String>,
11667}
11668
11669/// Reject table names that won't survive SQL identifier
11670/// derivation downstream. Postgres' regular-identifier rule
11671/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11672/// through the framework's unquoted FK / index / constraint name
11673/// emission without surprises. We also disallow leading-digit and
11674/// the empty string for clarity.
11675///
11676/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11677/// here — those produce a runtime error from the SQL parser,
11678/// which is loud enough; statically enumerating reserved words
11679/// across the three supported dialects is more friction than help.
11680///
11681/// Backlog item #65.
11682fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11683    validate_sql_identifier(name, "table", span)
11684}
11685
11686/// Reject SQL identifiers that compile but break downstream SQL
11687/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11688/// `kind` is "table" / "column" — used for the error message so users
11689/// see which attribute caused the failure.
11690fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11691    if name.is_empty() {
11692        return Err(syn::Error::new(
11693            span,
11694            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11695        ));
11696    }
11697    let mut chars = name.chars();
11698    let first = chars.next().unwrap();
11699    if !(first.is_ascii_alphabetic() || first == '_') {
11700        return Err(syn::Error::new(
11701            span,
11702            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11703        ));
11704    }
11705    for c in chars {
11706        if !(c.is_ascii_alphanumeric() || c == '_') {
11707            return Err(syn::Error::new(
11708                span,
11709                format!(
11710                    "{kind} name `{name}` contains invalid character {c:?} — \
11711                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11712                     Hyphens in particular break FK / index name derivation \
11713                     downstream; use underscores instead (e.g. `{}`)",
11714                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11715                ),
11716            ));
11717        }
11718    }
11719    Ok(())
11720}
11721
11722fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11723    let root = rustango_root();
11724    let attrs = parse_field_attrs(field)?;
11725    let ident = field
11726        .ident
11727        .as_ref()
11728        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11729    let name = ident_name(ident);
11730    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11731    let primary_key = attrs.primary_key;
11732    let DetectedType {
11733        kind,
11734        nullable,
11735        auto: detected_auto,
11736        fk_inner,
11737    } = detect_type(&field.ty)?;
11738    check_bound_compatibility(field, &attrs, kind)?;
11739    let auto = detected_auto;
11740    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11741    // INSERT path: the user must wrap the field in `Auto<T>`, which
11742    // marks the column as DB-default-supplied. The mixin attrs then
11743    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11744    // for `auto_now`, force the value on UPDATE too.
11745    if attrs.auto_uuid {
11746        if kind != DetectedKind::Uuid {
11747            return Err(syn::Error::new_spanned(
11748                field,
11749                "`#[rustango(auto_uuid)]` requires the field type to be \
11750                 `Auto<uuid::Uuid>`",
11751            ));
11752        }
11753        if !detected_auto {
11754            return Err(syn::Error::new_spanned(
11755                field,
11756                "`#[rustango(auto_uuid)]` requires the field type to be \
11757                 wrapped in `Auto<...>` so the macro skips the column on \
11758                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11759            ));
11760        }
11761    }
11762    if attrs.default_uuid_v7 {
11763        if kind != DetectedKind::Uuid {
11764            return Err(syn::Error::new_spanned(
11765                field,
11766                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11767                 `Auto<uuid::Uuid>`",
11768            ));
11769        }
11770        if !detected_auto {
11771            return Err(syn::Error::new_spanned(
11772                field,
11773                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11774                 wrapped in `Auto<...>` so the macro can detect the \
11775                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11776            ));
11777        }
11778        if attrs.auto_uuid {
11779            return Err(syn::Error::new_spanned(
11780                field,
11781                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11782                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11783                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11784                 Pick one.",
11785            ));
11786        }
11787    }
11788    if attrs.auto_now_add || attrs.auto_now {
11789        if kind != DetectedKind::DateTime {
11790            return Err(syn::Error::new_spanned(
11791                field,
11792                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11793                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11794            ));
11795        }
11796        if !detected_auto {
11797            return Err(syn::Error::new_spanned(
11798                field,
11799                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11800                 the field type to be wrapped in `Auto<...>` so the macro skips \
11801                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11802            ));
11803        }
11804    }
11805    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11806        return Err(syn::Error::new_spanned(
11807            field,
11808            "`#[rustango(soft_delete)]` requires the field type to be \
11809             `Option<chrono::DateTime<chrono::Utc>>`",
11810        ));
11811    }
11812    let is_mixin_auto =
11813        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11814    if detected_auto && !primary_key && !is_mixin_auto {
11815        return Err(syn::Error::new_spanned(
11816            field,
11817            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11818             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11819             `auto_now`",
11820        ));
11821    }
11822    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11823        return Err(syn::Error::new_spanned(
11824            field,
11825            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11826             SERIAL / BIGSERIAL already supplies a default sequence.",
11827        ));
11828    }
11829    if fk_inner.is_some() && primary_key {
11830        return Err(syn::Error::new_spanned(
11831            field,
11832            "`ForeignKey<T>` is not allowed on a primary-key field — \
11833             a row's PK is its own identity, not a reference to a parent.",
11834        ));
11835    }
11836    if attrs.generated_as.is_some() {
11837        if primary_key {
11838            return Err(syn::Error::new_spanned(
11839                field,
11840                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11841                 primary-key field — a PK must be writable so the row \
11842                 has an identity at INSERT time.",
11843            ));
11844        }
11845        if attrs.default.is_some() {
11846            return Err(syn::Error::new_spanned(
11847                field,
11848                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11849                 `default = \"…\"` — Postgres rejects DEFAULT on \
11850                 generated columns. The expression IS the default.",
11851            ));
11852        }
11853        if detected_auto {
11854            return Err(syn::Error::new_spanned(
11855                field,
11856                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11857                 an `Auto<T>` field — generated columns are computed \
11858                 by the DB, not server-assigned via a sequence. Use a \
11859                 plain Rust type (e.g. `f64`).",
11860            ));
11861        }
11862        if fk_inner.is_some() {
11863            return Err(syn::Error::new_spanned(
11864                field,
11865                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11866                 ForeignKey field.",
11867            ));
11868        }
11869    }
11870    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11871    let column_lit = column.as_str();
11872    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11873    // dimension that the bare `Vector` Rust type can't carry, so emit
11874    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11875    // fallback of `Vector(0)`.
11876    let field_type_tokens = if kind == DetectedKind::Vector {
11877        let root = rustango_root();
11878        let dims = attrs.vector_dims.unwrap_or(0);
11879        quote!(#root::core::FieldType::Vector(#dims))
11880    } else if kind == DetectedKind::Geometry {
11881        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11882        // SRID that the bare `Point` Rust type can't carry, so emit
11883        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11884        let root = rustango_root();
11885        let srid = attrs.geometry_srid.unwrap_or(0);
11886        quote!(#root::core::FieldType::Geometry(#srid))
11887    } else {
11888        kind.variant_tokens()
11889    };
11890    let max_length = optional_u32(attrs.max_length);
11891    let min = optional_i64(attrs.min);
11892    let max = optional_i64(attrs.max);
11893    let default = optional_str(attrs.default.as_deref());
11894
11895    let unique = attrs.unique;
11896    let generated_as = optional_str(attrs.generated_as.as_deref());
11897    let help_text = optional_str(attrs.help_text.as_deref());
11898    let choices = optional_choices(attrs.choices.as_deref());
11899    let db_comment = optional_str(attrs.db_comment.as_deref());
11900    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11901    let editable = attrs.editable;
11902    let blank = attrs.blank;
11903    let case_insensitive = attrs.case_insensitive;
11904    // A path per name: one the runtime does not know fails to resolve.
11905    let validators = &attrs.validators;
11906    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11907    {
11908        return Err(syn::Error::new_spanned(
11909            field,
11910            "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11911             or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11912             has no meaning on a non-FK column.",
11913        ));
11914    }
11915    if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11916        return Err(syn::Error::new_spanned(
11917            field,
11918            "`on_delete = \"set_null\"` needs a nullable column: use \
11919             `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11920             alias for `Option` is not seen).",
11921        ));
11922    }
11923    let fk_on_delete = match attrs.on_delete.as_deref() {
11924        None => quote!(::core::option::Option::None),
11925        Some(action) => {
11926            let variant = match action {
11927                "cascade" => quote!(Cascade),
11928                "restrict" => quote!(Restrict),
11929                "set_null" => quote!(SetNull),
11930                "set_default" => quote!(SetDefault),
11931                "no_action" => quote!(NoAction),
11932                // parse_field_attrs already validated this — guard against future drift.
11933                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11934            };
11935            quote!(::core::option::Option::Some(
11936                #root::core::OnDeleteAction::#variant
11937            ))
11938        }
11939    };
11940    let auto_now_flag = attrs.auto_now;
11941    let cast = match cast_type(&field.ty) {
11942        Some(ty) => quote!(::core::option::Option::Some(<#ty>::FIELD_CAST)),
11943        None => quote!(::core::option::Option::None),
11944    };
11945    let schema = quote! {{
11946        #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11947            name: #name,
11948            column: #column_lit,
11949            ty: #field_type_tokens,
11950            nullable: #nullable,
11951            primary_key: #primary_key,
11952            relation: #relation,
11953            max_length: #max_length,
11954            min: #min,
11955            max: #max,
11956            default: #default,
11957            auto: #auto,
11958            auto_now: #auto_now_flag,
11959            unique: #unique,
11960            generated_as: #generated_as,
11961            help_text: #help_text,
11962            choices: #choices,
11963            db_comment: #db_comment,
11964            verbose_name: #verbose_name,
11965            editable: #editable,
11966            blank: #blank,
11967            case_insensitive: #case_insensitive,
11968            fk_on_delete: #fk_on_delete,
11969            validators: &[ #( #root::core::__validator_names::#validators ),* ],
11970            cast: #cast,
11971        })
11972    }};
11973
11974    // sqlx-sqlite has no `Decimal` decode: it goes through `FlatScalar`'s cell.
11975    let decimal_column = kind == DetectedKind::Decimal && !detected_auto && fk_inner.is_none();
11976    let (from_row_init, from_aliased_row_init) = if decimal_column {
11977        (
11978            quote! { #ident: #root::sql::try_get_flat(row, #column_lit)? },
11979            quote! {
11980                #ident: #root::sql::try_get_flat(
11981                    row,
11982                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11983                )?
11984            },
11985        )
11986    } else {
11987        (
11988            quote! { #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)? },
11989            quote! {
11990                #ident: #root::sql::sqlx::Row::try_get(
11991                    row,
11992                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11993                )?
11994            },
11995        )
11996    };
11997    // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11998    // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11999    let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
12000    let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
12001        (
12002            quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
12003            quote! {
12004                #ident: #root::sql::try_get_flat_my(
12005                    row,
12006                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
12007                )?
12008            },
12009        )
12010    } else {
12011        (from_row_init.clone(), from_aliased_row_init.clone())
12012    };
12013
12014    Ok(FieldInfo {
12015        ident,
12016        column,
12017        primary_key,
12018        auto,
12019        value_ty: &field.ty,
12020        field_type_tokens,
12021        schema,
12022        from_row_init,
12023        from_aliased_row_init,
12024        from_row_init_my,
12025        from_aliased_row_init_my,
12026        uuid_column,
12027        fk_inner: fk_inner.cloned(),
12028        fk_pk_kind: kind,
12029        nullable,
12030        auto_now: attrs.auto_now,
12031        auto_now_add: attrs.auto_now_add,
12032        soft_delete: attrs.soft_delete,
12033        generated_as: attrs.generated_as.clone(),
12034        default_uuid_v7: attrs.default_uuid_v7,
12035        related_name: attrs.related_name.clone(),
12036    })
12037}
12038
12039fn check_bound_compatibility(
12040    field: &syn::Field,
12041    attrs: &FieldAttrs,
12042    kind: DetectedKind,
12043) -> syn::Result<()> {
12044    if attrs.max_length.is_some() && kind != DetectedKind::String {
12045        return Err(syn::Error::new_spanned(
12046            field,
12047            "`max_length` is only valid on `String` fields (or `Option<String>`)",
12048        ));
12049    }
12050    // These were silently dropped on the wrong type (#1937).
12051    if attrs.case_insensitive && kind != DetectedKind::String {
12052        return Err(syn::Error::new_spanned(
12053            field,
12054            "`citext` is only valid on `String` fields (or `Option<String>`)",
12055        ));
12056    }
12057    if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
12058        return Err(syn::Error::new_spanned(
12059            field,
12060            "`vector(dims = …)` is only valid on a pgvector `Vector` field",
12061        ));
12062    }
12063    if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
12064        return Err(syn::Error::new_spanned(
12065            field,
12066            "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
12067        ));
12068    }
12069    if attrs.choices.is_some() && kind != DetectedKind::String {
12070        return Err(syn::Error::new_spanned(
12071            field,
12072            "`choices` is only valid on `String` fields (or `Option<String>`) — \
12073             integer-valued enumerations should be modeled with a Rust enum and \
12074             custom (de)serializer for now",
12075        ));
12076    }
12077    // They run only on string values, so elsewhere they would never fire.
12078    if !attrs.validators.is_empty() && kind != DetectedKind::String {
12079        return Err(syn::Error::new_spanned(
12080            field,
12081            "`validators` is only valid on `String` fields (or `Option<String>`)",
12082        ));
12083    }
12084    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
12085        return Err(syn::Error::new_spanned(
12086            field,
12087            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
12088        ));
12089    }
12090    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
12091        if min > max {
12092            return Err(syn::Error::new_spanned(
12093                field,
12094                format!("`min` ({min}) is greater than `max` ({max})"),
12095            ));
12096        }
12097    }
12098    if let Some(default) = &attrs.default {
12099        check_default_expr(default, kind).map_err(|msg| syn::Error::new_spanned(field, msg))?;
12100    }
12101    Ok(())
12102}
12103
12104/// SQL words a `default` may be on its own; any other bare word is a
12105/// column reference, which Postgres and MySQL refuse in a DEFAULT.
12106const DEFAULT_KEYWORDS: &[&str] = &[
12107    "NULL",
12108    "TRUE",
12109    "FALSE",
12110    "CURRENT_TIMESTAMP",
12111    "CURRENT_DATE",
12112    "CURRENT_TIME",
12113    "LOCALTIMESTAMP",
12114    "LOCALTIME",
12115];
12116
12117/// `default` is raw SQL. Refuse the shapes only SQLite accepts: a bare
12118/// word (read as a column on PG/MySQL) and a non-numeric literal on a
12119/// numeric field.
12120fn check_default_expr(default: &str, kind: DetectedKind) -> Result<(), String> {
12121    let expr = default.trim();
12122    let is_word = expr.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
12123        && expr
12124            .chars()
12125            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ' ');
12126    let upper = expr.to_ascii_uppercase();
12127    if is_word && ["CURRENT_USER", "SESSION_USER", "USER"].contains(&upper.as_str()) {
12128        return Err(format!(
12129            "`default = \"{expr}\"` works on Postgres only: MySQL refuses it as a default \
12130             and SQLite stores the text `{expr}`; set the value in code instead"
12131        ));
12132    }
12133    if is_word && !DEFAULT_KEYWORDS.contains(&upper.as_str()) {
12134        return Err(format!(
12135            "`default = \"{expr}\"` is raw SQL, so `{expr}` is read as a column name; \
12136             for the text '{expr}' write `default = \"'{expr}'\"`"
12137        ));
12138    }
12139    let numeric = kind.is_integer()
12140        || matches!(
12141            kind,
12142            DetectedKind::F32 | DetectedKind::F64 | DetectedKind::Decimal
12143        );
12144    if numeric {
12145        let unquoted = expr
12146            .strip_prefix('\'')
12147            .and_then(|e| e.strip_suffix('\''))
12148            .unwrap_or(expr);
12149        let is_number = if kind.is_integer() {
12150            unquoted.parse::<i64>().is_ok()
12151        } else {
12152            unquoted.parse::<f64>().is_ok_and(f64::is_finite)
12153        };
12154        // Keep expressions (`(1 + 2)`, `abs(-1)`) and NULL; refuse a
12155        // literal that is no number. `""` renders as `''`.
12156        let is_literal = is_word || expr.is_empty() || expr.starts_with('\'');
12157        if is_literal && !is_number && upper != "NULL" {
12158            return Err(format!(
12159                "`default = \"{expr}\"` is not a number, but the field is numeric"
12160            ));
12161        }
12162    }
12163    Ok(())
12164}
12165
12166/// A `&'static [ty]` of `const fn` results. The slice needs its own
12167/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
12168fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
12169    quote!({
12170        const ITEMS: &[#ty] = &[ #(#items),* ];
12171        ITEMS
12172    })
12173}
12174
12175fn optional_u32(value: Option<u32>) -> TokenStream2 {
12176    if let Some(v) = value {
12177        quote!(::core::option::Option::Some(#v))
12178    } else {
12179        quote!(::core::option::Option::None)
12180    }
12181}
12182
12183fn optional_i64(value: Option<i64>) -> TokenStream2 {
12184    if let Some(v) = value {
12185        quote!(::core::option::Option::Some(#v))
12186    } else {
12187        quote!(::core::option::Option::None)
12188    }
12189}
12190
12191fn optional_str(value: Option<&str>) -> TokenStream2 {
12192    if let Some(v) = value {
12193        quote!(::core::option::Option::Some(#v))
12194    } else {
12195        quote!(::core::option::Option::None)
12196    }
12197}
12198
12199fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
12200    let Some(pairs) = pairs else {
12201        return quote!(::core::option::Option::None);
12202    };
12203    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
12204    quote!(::core::option::Option::Some(&[#(#entries),*]))
12205}
12206
12207fn relation_tokens(
12208    field: &syn::Field,
12209    attrs: &FieldAttrs,
12210    fk_inner: Option<&syn::Type>,
12211    table: &str,
12212) -> syn::Result<TokenStream2> {
12213    let root = rustango_root();
12214    if let Some(inner) = fk_inner {
12215        if attrs.fk.is_some() || attrs.o2o.is_some() {
12216            return Err(syn::Error::new_spanned(
12217                field,
12218                "`ForeignKey<T>` already declares the FK target via the type parameter — \
12219                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
12220            ));
12221        }
12222        // No `on`: the target's PK column, which may be renamed.
12223        let on = match attrs.on.as_deref() {
12224            Some(on) => quote!(#on),
12225            None => quote! {
12226                match <#inner as #root::core::Model>::SCHEMA.pk_column() {
12227                    ::core::option::Option::Some(c) => c,
12228                    ::core::option::Option::None => "id",
12229                }
12230            },
12231        };
12232        return Ok(quote! {
12233            ::core::option::Option::Some(#root::core::Relation::fk(
12234                <#inner as #root::core::Model>::SCHEMA.table,
12235                #on,
12236            ))
12237        });
12238    }
12239    match (&attrs.fk, &attrs.o2o) {
12240        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
12241            field,
12242            "`fk` and `o2o` are mutually exclusive",
12243        )),
12244        (Some(to), None) => {
12245            let on = attrs.on.as_deref().unwrap_or("id");
12246            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
12247            // the model's own table. Threaded as a literal string at
12248            // macro-expansion time to sidestep the const-eval cycle
12249            // that `Self::SCHEMA.table` would create when referenced
12250            // inside Self::SCHEMA's own initializer.
12251            let resolved = if to == "self" { table } else { to };
12252            Ok(quote! {
12253                ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
12254            })
12255        }
12256        (None, Some(to)) => {
12257            let on = attrs.on.as_deref().unwrap_or("id");
12258            let resolved = if to == "self" { table } else { to };
12259            Ok(quote! {
12260                ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
12261            })
12262        }
12263        (None, None) => {
12264            if attrs.on.is_some() {
12265                return Err(syn::Error::new_spanned(
12266                    field,
12267                    "`on` requires `fk` or `o2o`",
12268                ));
12269            }
12270            Ok(quote!(::core::option::Option::None))
12271        }
12272    }
12273}
12274
12275/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
12276/// about kinds without depending on `rustango-core` (which would require a
12277/// proc-macro/normal split it doesn't have today).
12278#[derive(Clone, Copy, PartialEq, Eq)]
12279enum DetectedKind {
12280    I16,
12281    I32,
12282    I64,
12283    F32,
12284    F64,
12285    Bool,
12286    String,
12287    DateTime,
12288    Date,
12289    Time,
12290    Uuid,
12291    Json,
12292    Decimal,
12293    Binary,
12294    /// `Array<String>` → PG `text[]` (#341).
12295    ArrayText,
12296    /// `Array<i32>` → PG `integer[]` (#341).
12297    ArrayInt,
12298    /// `Array<i64>` → PG `bigint[]` (#341).
12299    ArrayBigInt,
12300    /// `Range<i32>` → PG `int4range` (#343).
12301    RangeInt,
12302    /// `Range<i64>` → PG `int8range` (#343).
12303    RangeBigInt,
12304    /// `Range<Decimal>` → PG `numrange` (#343).
12305    RangeNumeric,
12306    /// `Range<NaiveDate>` → PG `daterange` (#343).
12307    RangeDate,
12308    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
12309    RangeDateTime,
12310    /// `HStore` → PG `hstore` (#342).
12311    HStore,
12312    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
12313    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
12314    /// in at the `FieldType` emission site (not carried on this enum).
12315    Vector,
12316    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
12317    /// from the `#[rustango(geometry(srid = N))]` field attribute,
12318    /// threaded in at the `FieldType` emission site.
12319    Geometry,
12320}
12321
12322impl DetectedKind {
12323    fn variant_tokens(self) -> TokenStream2 {
12324        let root = rustango_root();
12325        match self {
12326            Self::I16 => quote!(#root::core::FieldType::I16),
12327            Self::I32 => quote!(#root::core::FieldType::I32),
12328            Self::I64 => quote!(#root::core::FieldType::I64),
12329            Self::F32 => quote!(#root::core::FieldType::F32),
12330            Self::F64 => quote!(#root::core::FieldType::F64),
12331            Self::Bool => quote!(#root::core::FieldType::Bool),
12332            Self::String => quote!(#root::core::FieldType::String),
12333            Self::DateTime => quote!(#root::core::FieldType::DateTime),
12334            Self::Date => quote!(#root::core::FieldType::Date),
12335            Self::Time => quote!(#root::core::FieldType::Time),
12336            Self::Uuid => quote!(#root::core::FieldType::Uuid),
12337            Self::Json => quote!(#root::core::FieldType::Json),
12338            Self::Decimal => quote!(#root::core::FieldType::Decimal),
12339            Self::Binary => quote!(#root::core::FieldType::Binary),
12340            Self::ArrayText => {
12341                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
12342            }
12343            Self::ArrayInt => {
12344                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
12345            }
12346            Self::ArrayBigInt => {
12347                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
12348            }
12349            Self::RangeInt => {
12350                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
12351            }
12352            Self::RangeBigInt => {
12353                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
12354            }
12355            Self::RangeNumeric => {
12356                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
12357            }
12358            Self::RangeDate => {
12359                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
12360            }
12361            Self::RangeDateTime => {
12362                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
12363            }
12364            Self::HStore => quote!(#root::core::FieldType::HStore),
12365            // Dimension comes from the `vector(dims = N)` attribute,
12366            // applied at the emission site; `0` here is just a fallback.
12367            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
12368            // SRID comes from the `geometry(srid = N)` attribute, applied
12369            // at the emission site; `0` here is just a fallback.
12370            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
12371        }
12372    }
12373
12374    fn is_integer(self) -> bool {
12375        matches!(self, Self::I16 | Self::I32 | Self::I64)
12376    }
12377}
12378
12379/// Result of walking a field's Rust type. `kind` is the underlying
12380/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
12381/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
12382/// `Some(<T>)` when the field was `ForeignKey<T>` (or
12383/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
12384#[derive(Clone, Copy)]
12385struct DetectedType<'a> {
12386    kind: DetectedKind,
12387    nullable: bool,
12388    auto: bool,
12389    fk_inner: Option<&'a syn::Type>,
12390}
12391
12392/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
12393/// non-`Auto` types — the caller should already have routed Auto-only
12394/// codegen through this helper, so a `None` indicates a macro-internal
12395/// invariant break.
12396fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
12397    let Type::Path(TypePath { path, qself: None }) = ty else {
12398        return None;
12399    };
12400    let last = path.segments.last()?;
12401    if last.ident != "Auto" {
12402        return None;
12403    }
12404    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12405        return None;
12406    };
12407    args.args.iter().find_map(|a| match a {
12408        syn::GenericArgument::Type(t) => Some(t),
12409        _ => None,
12410    })
12411}
12412
12413/// The `Cast<C>` inside `ty` (bare or in an `Option`), for its schema record.
12414fn cast_type(ty: &syn::Type) -> Option<&syn::Type> {
12415    let Type::Path(TypePath { path, qself: None }) = ty else {
12416        return None;
12417    };
12418    let last = path.segments.last()?;
12419    if last.ident == "Cast" {
12420        return Some(ty);
12421    }
12422    if last.ident != "Option" {
12423        return None;
12424    }
12425    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12426        return None;
12427    };
12428    match args.args.first()? {
12429        syn::GenericArgument::Type(inner) => cast_type(inner).filter(|_| args.args.len() == 1),
12430        _ => None,
12431    }
12432}
12433
12434fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
12435    let Type::Path(TypePath { path, qself: None }) = ty else {
12436        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
12437    };
12438    let last = path
12439        .segments
12440        .last()
12441        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
12442
12443    if last.ident == "Option" {
12444        let inner = generic_inner(ty, &last.arguments, "Option")?;
12445        let inner_det = detect_type(inner)?;
12446        if inner_det.nullable {
12447            return Err(syn::Error::new_spanned(
12448                ty,
12449                "nested Option is not supported",
12450            ));
12451        }
12452        if inner_det.auto {
12453            return Err(syn::Error::new_spanned(
12454                ty,
12455                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12456            ));
12457        }
12458        return Ok(DetectedType {
12459            nullable: true,
12460            ..inner_det
12461        });
12462    }
12463
12464    if last.ident == "Auto" {
12465        let inner = generic_inner(ty, &last.arguments, "Auto")?;
12466        let inner_det = detect_type(inner)?;
12467        if inner_det.auto {
12468            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
12469        }
12470        if inner_det.nullable {
12471            return Err(syn::Error::new_spanned(
12472                ty,
12473                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12474            ));
12475        }
12476        if inner_det.fk_inner.is_some() {
12477            return Err(syn::Error::new_spanned(
12478                ty,
12479                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
12480            ));
12481        }
12482        if !matches!(
12483            inner_det.kind,
12484            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
12485        ) {
12486            return Err(syn::Error::new_spanned(
12487                ty,
12488                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
12489                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
12490                 (DEFAULT now())",
12491            ));
12492        }
12493        return Ok(DetectedType {
12494            auto: true,
12495            ..inner_det
12496        });
12497    }
12498
12499    if last.ident == "ForeignKey" {
12500        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
12501        // Resolve the FK column's underlying SQL type from `K`. When the
12502        // user wrote `ForeignKey<T>` without a key parameter, the type
12503        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
12504        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
12505        // recurse into K so the column DDL emits the right SQL type
12506        // (VARCHAR for String, UUID for Uuid, …) and the load_related
12507        // emitter knows which `SqlValue` variant to match.
12508        let kind = match key_ty {
12509            Some(k) => detect_type(k)?.kind,
12510            None => DetectedKind::I64,
12511        };
12512        return Ok(DetectedType {
12513            kind,
12514            nullable: false,
12515            auto: false,
12516            fk_inner: Some(inner),
12517        });
12518    }
12519
12520    let kind = match last.ident.to_string().as_str() {
12521        "i16" => DetectedKind::I16,
12522        "i32" => DetectedKind::I32,
12523        "i64" => DetectedKind::I64,
12524        "f32" => DetectedKind::F32,
12525        "f64" => DetectedKind::F64,
12526        "bool" => DetectedKind::Bool,
12527        "String" => DetectedKind::String,
12528        "DateTime" => DetectedKind::DateTime,
12529        "NaiveDate" => DetectedKind::Date,
12530        "NaiveTime" => DetectedKind::Time,
12531        "Uuid" => DetectedKind::Uuid,
12532        "Value" => DetectedKind::Json,
12533        "Decimal" => DetectedKind::Decimal,
12534        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
12535        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
12536        // — that would emit Binary DDL and decode-fail at runtime.
12537        "Vec" => {
12538            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
12539            if let Type::Path(TypePath { path, qself: None }) = inner {
12540                if let Some(seg) = path.segments.last() {
12541                    if seg.ident == "u8" && seg.arguments.is_empty() {
12542                        return Ok(DetectedType {
12543                            kind: DetectedKind::Binary,
12544                            nullable: false,
12545                            auto: false,
12546                            fk_inner: None,
12547                        });
12548                    }
12549                }
12550            }
12551            return Err(syn::Error::new_spanned(
12552                ty,
12553                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
12554                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
12555            ));
12556        }
12557        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
12558        // `integer[]` / `bigint[]` (#341).
12559        "Array" => {
12560            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
12561            let elem = match inner {
12562                Type::Path(TypePath { path, qself: None }) => {
12563                    path.segments.last().map(|s| s.ident.to_string())
12564                }
12565                _ => None,
12566            };
12567            let kind = match elem.as_deref() {
12568                Some("String") => DetectedKind::ArrayText,
12569                Some("i32") => DetectedKind::ArrayInt,
12570                Some("i64") => DetectedKind::ArrayBigInt,
12571                _ => {
12572                    return Err(syn::Error::new_spanned(
12573                        ty,
12574                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12575                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12576                    ));
12577                }
12578            };
12579            return Ok(DetectedType {
12580                kind,
12581                nullable: false,
12582                auto: false,
12583                fk_inner: None,
12584            });
12585        }
12586        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12587        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12588        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12589        "Range" => {
12590            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12591            let elem = match inner {
12592                Type::Path(TypePath { path, qself: None }) => {
12593                    path.segments.last().map(|s| s.ident.to_string())
12594                }
12595                _ => None,
12596            };
12597            let kind = match elem.as_deref() {
12598                Some("i32") => DetectedKind::RangeInt,
12599                Some("i64") => DetectedKind::RangeBigInt,
12600                Some("Decimal") => DetectedKind::RangeNumeric,
12601                Some("NaiveDate") => DetectedKind::RangeDate,
12602                Some("DateTime") => DetectedKind::RangeDateTime,
12603                _ => {
12604                    return Err(syn::Error::new_spanned(
12605                        ty,
12606                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12607                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12608                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12609                         (→ tstzrange) are supported (#343)",
12610                    ));
12611                }
12612            };
12613            return Ok(DetectedType {
12614                kind,
12615                nullable: false,
12616                auto: false,
12617                fk_inner: None,
12618            });
12619        }
12620        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12621        // (the `CastValue` impl bridges logical↔stored); the field's own
12622        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12623        // schema just needs `FieldType::String`.
12624        "Cast" => {
12625            return Ok(DetectedType {
12626                kind: DetectedKind::String,
12627                nullable: false,
12628                auto: false,
12629                fk_inner: None,
12630            });
12631        }
12632        // `HStore` → PG `hstore` (#342). No generic
12633        // parameter — always a string→string map.
12634        "HStore" => {
12635            return Ok(DetectedType {
12636                kind: DetectedKind::HStore,
12637                nullable: false,
12638                auto: false,
12639                fk_inner: None,
12640            });
12641        }
12642        // `Vector` → pgvector `vector(N)` (#824). The dimension is
12643        // supplied by `#[rustango(vector(dims = N))]`, not the type.
12644        "Vector" => {
12645            return Ok(DetectedType {
12646                kind: DetectedKind::Vector,
12647                nullable: false,
12648                auto: false,
12649                fk_inner: None,
12650            });
12651        }
12652        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12653        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12654        "Point" => {
12655            return Ok(DetectedType {
12656                kind: DetectedKind::Geometry,
12657                nullable: false,
12658                auto: false,
12659                fk_inner: None,
12660            });
12661        }
12662        other => {
12663            return Err(syn::Error::new_spanned(
12664                ty,
12665                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)"),
12666            ));
12667        }
12668    };
12669    Ok(DetectedType {
12670        kind,
12671        nullable: false,
12672        auto: false,
12673        fk_inner: None,
12674    })
12675}
12676
12677fn generic_inner<'a>(
12678    ty: &'a Type,
12679    arguments: &'a PathArguments,
12680    wrapper: &str,
12681) -> syn::Result<&'a Type> {
12682    let PathArguments::AngleBracketed(args) = arguments else {
12683        return Err(syn::Error::new_spanned(
12684            ty,
12685            format!("{wrapper} requires a generic argument"),
12686        ));
12687    };
12688    args.args
12689        .iter()
12690        .find_map(|a| match a {
12691            GenericArgument::Type(t) => Some(t),
12692            _ => None,
12693        })
12694        .ok_or_else(|| {
12695            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12696        })
12697}
12698
12699/// Like [`generic_inner`] but pulls *two* type args — the first is
12700/// required, the second is optional. Used by the `ForeignKey<T, K>`
12701/// detection where K defaults to `i64` when omitted.
12702fn generic_pair<'a>(
12703    ty: &'a Type,
12704    arguments: &'a PathArguments,
12705    wrapper: &str,
12706) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12707    let PathArguments::AngleBracketed(args) = arguments else {
12708        return Err(syn::Error::new_spanned(
12709            ty,
12710            format!("{wrapper} requires a generic argument"),
12711        ));
12712    };
12713    let mut types = args.args.iter().filter_map(|a| match a {
12714        GenericArgument::Type(t) => Some(t),
12715        _ => None,
12716    });
12717    let first = types.next().ok_or_else(|| {
12718        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12719    })?;
12720    let second = types.next();
12721    Ok((first, second))
12722}
12723
12724fn to_snake_case(s: &str) -> String {
12725    let mut out = String::with_capacity(s.len() + 4);
12726    for (i, ch) in s.chars().enumerate() {
12727        if ch.is_ascii_uppercase() {
12728            if i > 0 {
12729                out.push('_');
12730            }
12731            out.push(ch.to_ascii_lowercase());
12732        } else {
12733            out.push(ch);
12734        }
12735    }
12736    out
12737}
12738
12739// ============================================================
12740//  #[derive(Form)]  —  slice 8.4B
12741// ============================================================
12742
12743/// Per-field `#[form(...)]` attributes recognised by the derive.
12744#[derive(Default)]
12745struct FormFieldAttrs {
12746    min: Option<i64>,
12747    max: Option<i64>,
12748    min_length: Option<u32>,
12749    max_length: Option<u32>,
12750    /// `#[form(clean = "fn_name")]` — per-field clean hook.
12751    /// The named static method on the form struct is called after the
12752    /// field's typed parse + length/range checks; it gets the parsed
12753    /// value by reference and returns `Result<<FieldType>, String>`.
12754    /// On Ok, the returned value replaces the parsed one; on Err, the
12755    /// message is attached to the field error list. Issue #372.
12756    clean: Option<syn::Ident>,
12757}
12758
12759/// Container-level `#[form(...)]` attributes. Currently only the
12760/// cross-field `validate` hook (issue #373).
12761#[derive(Default)]
12762struct FormContainerAttrs {
12763    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12764    /// After every per-field parse succeeds, the named method on the
12765    /// form struct is called with `&self` and may return
12766    /// `Result<(), FormErrors>`. Errors merge into the field error
12767    /// list. Issue #373.
12768    validate: Option<syn::Ident>,
12769}
12770
12771/// Detected shape of a form field's Rust type.
12772#[derive(Clone, Copy)]
12773enum FormFieldKind {
12774    String,
12775    I16,
12776    I32,
12777    I64,
12778    F32,
12779    F64,
12780    Bool,
12781}
12782
12783impl FormFieldKind {
12784    fn parse_method(self) -> &'static str {
12785        match self {
12786            Self::I16 => "i16",
12787            Self::I32 => "i32",
12788            Self::I64 => "i64",
12789            Self::F32 => "f32",
12790            Self::F64 => "f64",
12791            // String + Bool don't go through `str::parse`; the codegen
12792            // handles them inline.
12793            Self::String | Self::Bool => "",
12794        }
12795    }
12796}
12797
12798/// A generated `derive(Form)` local; mixed-site so a same-named field can't shadow it.
12799fn form_local(name: &str) -> syn::Ident {
12800    syn::Ident::new(name, proc_macro2::Span::mixed_site())
12801}
12802
12803fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12804    let root = rustango_root();
12805    let (errs, cleaned, data_param) = (
12806        form_local("__errors"),
12807        form_local("__cleaned"),
12808        form_local("data"),
12809    );
12810    let struct_name = &input.ident;
12811
12812    let Data::Struct(data) = &input.data else {
12813        return Err(syn::Error::new_spanned(
12814            struct_name,
12815            "Form can only be derived on structs",
12816        ));
12817    };
12818    let Fields::Named(named) = &data.fields else {
12819        return Err(syn::Error::new_spanned(
12820            struct_name,
12821            "Form requires a struct with named fields",
12822        ));
12823    };
12824
12825    // #373 — container-level `#[form(validate = "fn")]` hook.
12826    let container = parse_form_container_attrs(input)?;
12827    let post_field_clean: Vec<TokenStream2> = Vec::new();
12828    let _ = post_field_clean;
12829
12830    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12831    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12832
12833    for field in &named.named {
12834        let ident = field
12835            .ident
12836            .as_ref()
12837            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12838        let attrs = parse_form_field_attrs(field)?;
12839        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12840        let is_string = matches!(kind, FormFieldKind::String);
12841        let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12842        if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12843            return Err(syn::Error::new(
12844                field.span(),
12845                "`min_length` / `max_length` need a String field",
12846            ));
12847        }
12848        if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12849            return Err(syn::Error::new(
12850                field.span(),
12851                "`min` / `max` need a numeric field",
12852            ));
12853        }
12854
12855        let name_lit = ident_name(ident);
12856        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12857        // #372 — append the per-field `clean_<field>` call right after
12858        // the parse block when the attribute is set. The clean fn
12859        // takes &T and returns Result<T, String>; on Err we attach
12860        // the message to the field error list without aborting, so
12861        // the caller sees every field error at once.
12862        let clean_block = if let Some(clean_fn) = &attrs.clean {
12863            quote! {
12864                if #errs.fields().get(#name_lit).is_none() {
12865                    match Self::#clean_fn(&#ident) {
12866                        ::core::result::Result::Ok(#cleaned) => { #ident = #cleaned; }
12867                        ::core::result::Result::Err(__msg) => {
12868                            #errs.add(#name_lit, __msg);
12869                        }
12870                    }
12871                }
12872            }
12873        } else {
12874            quote! {}
12875        };
12876        field_blocks.push(quote! {
12877            #parse_block
12878            #clean_block
12879        });
12880        field_idents.push(ident);
12881    }
12882
12883    // #373 — after every per-field parse + clean succeeds, call the
12884    // cross-field validator if declared. Errors merge into the
12885    // outgoing FormErrors via the existing `FormErrors::merge` helper
12886    // (same primitive the serializer cross-field hook uses).
12887    let cross_field_call = if let Some(validate_fn) = &container.validate {
12888        quote! {
12889            if #errs.is_empty() {
12890                let __candidate = Self { #( #field_idents ),* };
12891                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12892                    #errs.merge(__other);
12893                }
12894                if !#errs.is_empty() {
12895                    return ::core::result::Result::Err(#errs);
12896                }
12897                return ::core::result::Result::Ok(__candidate);
12898            }
12899        }
12900    } else {
12901        quote! {}
12902    };
12903
12904    Ok(quote! {
12905        impl #root::forms::Form for #struct_name {
12906            fn parse(
12907                #data_param: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12908            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12909                let mut #errs = #root::forms::FormErrors::default();
12910                #( #field_blocks )*
12911                #cross_field_call
12912                if !#errs.is_empty() {
12913                    return ::core::result::Result::Err(#errs);
12914                }
12915                ::core::result::Result::Ok(Self {
12916                    #( #field_idents ),*
12917                })
12918            }
12919        }
12920    })
12921}
12922
12923fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12924    let mut out = FormContainerAttrs::default();
12925    for attr in &input.attrs {
12926        if !attr.path().is_ident("form") {
12927            continue;
12928        }
12929        attr.parse_nested_meta(|meta| {
12930            if meta.path.is_ident("validate") {
12931                let s: LitStr = meta.value()?.parse()?;
12932                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12933                return Ok(());
12934            }
12935            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12936        })?;
12937    }
12938    Ok(out)
12939}
12940
12941fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12942    let mut out = FormFieldAttrs::default();
12943    for attr in &field.attrs {
12944        if !attr.path().is_ident("form") {
12945            continue;
12946        }
12947        attr.parse_nested_meta(|meta| {
12948            if meta.path.is_ident("min") {
12949                let lit: syn::LitInt = meta.value()?.parse()?;
12950                out.min = Some(lit.base10_parse::<i64>()?);
12951                return Ok(());
12952            }
12953            if meta.path.is_ident("max") {
12954                let lit: syn::LitInt = meta.value()?.parse()?;
12955                out.max = Some(lit.base10_parse::<i64>()?);
12956                return Ok(());
12957            }
12958            if meta.path.is_ident("min_length") {
12959                let lit: syn::LitInt = meta.value()?.parse()?;
12960                out.min_length = Some(lit.base10_parse::<u32>()?);
12961                return Ok(());
12962            }
12963            if meta.path.is_ident("max_length") {
12964                let lit: syn::LitInt = meta.value()?.parse()?;
12965                out.max_length = Some(lit.base10_parse::<u32>()?);
12966                return Ok(());
12967            }
12968            if meta.path.is_ident("clean") {
12969                let s: LitStr = meta.value()?.parse()?;
12970                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12971                return Ok(());
12972            }
12973            Err(meta.error(
12974                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12975            ))
12976        })?;
12977    }
12978    Ok(out)
12979}
12980
12981fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12982    let Type::Path(TypePath { path, qself: None }) = ty else {
12983        return Err(syn::Error::new(
12984            span,
12985            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12986        ));
12987    };
12988    let last = path
12989        .segments
12990        .last()
12991        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12992
12993    if last.ident == "Option" {
12994        let inner = generic_inner(ty, &last.arguments, "Option")?;
12995        let (kind, nested) = detect_form_field(inner, span)?;
12996        if nested {
12997            return Err(syn::Error::new(
12998                span,
12999                "nested Option in Form fields is not supported",
13000            ));
13001        }
13002        return Ok((kind, true));
13003    }
13004
13005    let kind = match last.ident.to_string().as_str() {
13006        "String" => FormFieldKind::String,
13007        "i16" => FormFieldKind::I16,
13008        "i32" => FormFieldKind::I32,
13009        "i64" => FormFieldKind::I64,
13010        "f32" => FormFieldKind::F32,
13011        "f64" => FormFieldKind::F64,
13012        "bool" => FormFieldKind::Bool,
13013        other => {
13014            return Err(syn::Error::new(
13015                span,
13016                format!(
13017                    "Form field type `{other}` is not supported in v0.8 — use String / \
13018                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
13019                ),
13020            ));
13021        }
13022    };
13023    Ok((kind, false))
13024}
13025
13026#[allow(clippy::too_many_lines)]
13027fn render_form_field_parse(
13028    ident: &syn::Ident,
13029    name_lit: &str,
13030    kind: FormFieldKind,
13031    nullable: bool,
13032    attrs: &FormFieldAttrs,
13033) -> TokenStream2 {
13034    let (errs, data_param) = (form_local("__errors"), form_local("data"));
13035    // Pull the raw &str from the payload `Form::parse(data: &HashMap<…>)`.
13036    let lookup = quote! {
13037        let __raw: ::core::option::Option<&::std::string::String> = #data_param.get(#name_lit);
13038    };
13039
13040    let root = rustango_root();
13041    // Strict, as ModelForm: an unknown spelling is an error, not `true`.
13042    let checkbox = quote! {
13043        match #root::forms::checkbox(__s) {
13044            ::core::option::Option::Some(__b) => __b,
13045            ::core::option::Option::None => {
13046                #errs.add(#name_lit, "Enter true or false.");
13047                false
13048            }
13049        }
13050    };
13051    let parsed_value = match kind {
13052        // `Option<bool>`: absent or blank is `None`, as for the other types.
13053        FormFieldKind::Bool if nullable => quote! {
13054            let __v: ::core::option::Option<bool> = match __raw {
13055                ::core::option::Option::None => ::core::option::Option::None,
13056                ::core::option::Option::Some(__s) if __s.is_empty() => {
13057                    ::core::option::Option::None
13058                }
13059                ::core::option::Option::Some(__s) => ::core::option::Option::Some(#checkbox),
13060            };
13061        },
13062        FormFieldKind::Bool => quote! {
13063            let __v: bool = match __raw {
13064                ::core::option::Option::None => false,
13065                ::core::option::Option::Some(__s) => #checkbox,
13066            };
13067        },
13068        FormFieldKind::String => {
13069            if nullable {
13070                quote! {
13071                    let __v: ::core::option::Option<::std::string::String> = match __raw {
13072                        ::core::option::Option::None => ::core::option::Option::None,
13073                        ::core::option::Option::Some(__s) if __s.is_empty() => {
13074                            ::core::option::Option::None
13075                        }
13076                        ::core::option::Option::Some(__s) => {
13077                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
13078                        }
13079                    };
13080                }
13081            } else {
13082                quote! {
13083                    let __v: ::std::string::String = match __raw {
13084                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
13085                            ::core::clone::Clone::clone(__s)
13086                        }
13087                        _ => {
13088                            #errs.add(#name_lit, "This field is required.");
13089                            ::std::string::String::new()
13090                        }
13091                    };
13092                }
13093            }
13094        }
13095        FormFieldKind::I16
13096        | FormFieldKind::I32
13097        | FormFieldKind::I64
13098        | FormFieldKind::F32
13099        | FormFieldKind::F64 => {
13100            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
13101            let ty_lit = kind.parse_method();
13102            let default_val = match kind {
13103                FormFieldKind::I16 => quote! { 0i16 },
13104                FormFieldKind::I32 => quote! { 0i32 },
13105                FormFieldKind::I64 => quote! { 0i64 },
13106                FormFieldKind::F32 => quote! { 0f32 },
13107                FormFieldKind::F64 => quote! { 0f64 },
13108                _ => quote! { Default::default() },
13109            };
13110            // NaN and inf parse, then pass every min/max compare.
13111            let finite = if matches!(kind, FormFieldKind::F32 | FormFieldKind::F64) {
13112                quote! {
13113                    .and_then(|__n| if __n.is_finite() {
13114                        ::core::result::Result::Ok(__n)
13115                    } else {
13116                        ::core::result::Result::Err(::std::string::String::from("not a finite number"))
13117                    })
13118                }
13119            } else {
13120                quote! {}
13121            };
13122            if nullable {
13123                quote! {
13124                    let __v: ::core::option::Option<#parse_ty> = match __raw {
13125                        ::core::option::Option::None => ::core::option::Option::None,
13126                        ::core::option::Option::Some(__s) if __s.is_empty() => {
13127                            ::core::option::Option::None
13128                        }
13129                        ::core::option::Option::Some(__s) => {
13130                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
13131                                ::core::result::Result::Ok(__n) => {
13132                                    ::core::option::Option::Some(__n)
13133                                }
13134                                ::core::result::Result::Err(__e) => {
13135                                    #errs.add(
13136                                        #name_lit,
13137                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
13138                                    );
13139                                    ::core::option::Option::None
13140                                }
13141                            }
13142                        }
13143                    };
13144                }
13145            } else {
13146                quote! {
13147                    let __v: #parse_ty = match __raw {
13148                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
13149                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
13150                                ::core::result::Result::Ok(__n) => __n,
13151                                ::core::result::Result::Err(__e) => {
13152                                    #errs.add(
13153                                        #name_lit,
13154                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
13155                                    );
13156                                    #default_val
13157                                }
13158                            }
13159                        }
13160                        _ => {
13161                            #errs.add(#name_lit, "This field is required.");
13162                            #default_val
13163                        }
13164                    };
13165                }
13166            }
13167        }
13168    };
13169
13170    let validators = render_form_validators(name_lit, kind, nullable, attrs);
13171
13172    quote! {
13173        // `mut` so the per-field `clean` hook (#372) can rewrite the
13174        // parsed value in-place when it returns Ok with a normalized
13175        // form (e.g. trim / lowercase).
13176        let mut #ident = {
13177            #lookup
13178            #parsed_value
13179            #validators
13180            __v
13181        };
13182    }
13183}
13184
13185fn render_form_validators(
13186    name_lit: &str,
13187    kind: FormFieldKind,
13188    nullable: bool,
13189    attrs: &FormFieldAttrs,
13190) -> TokenStream2 {
13191    let errs = form_local("__errors");
13192    let mut checks: Vec<TokenStream2> = Vec::new();
13193
13194    let val_ref = if nullable {
13195        quote! { __v.as_ref() }
13196    } else {
13197        quote! { ::core::option::Option::Some(&__v) }
13198    };
13199
13200    let is_string = matches!(kind, FormFieldKind::String);
13201    let is_numeric = matches!(
13202        kind,
13203        FormFieldKind::I16
13204            | FormFieldKind::I32
13205            | FormFieldKind::I64
13206            | FormFieldKind::F32
13207            | FormFieldKind::F64
13208    );
13209
13210    if is_string {
13211        if let Some(min_len) = attrs.min_length {
13212            let min_len_usize = min_len as usize;
13213            checks.push(quote! {
13214                if let ::core::option::Option::Some(__s) = #val_ref {
13215                    if __s.chars().count() < #min_len_usize {
13216                        #errs.add(
13217                            #name_lit,
13218                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
13219                        );
13220                    }
13221                }
13222            });
13223        }
13224        if let Some(max_len) = attrs.max_length {
13225            let max_len_usize = max_len as usize;
13226            checks.push(quote! {
13227                if let ::core::option::Option::Some(__s) = #val_ref {
13228                    if __s.chars().count() > #max_len_usize {
13229                        #errs.add(
13230                            #name_lit,
13231                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
13232                        );
13233                    }
13234                }
13235            });
13236        }
13237    }
13238
13239    if is_numeric {
13240        if let Some(min) = attrs.min {
13241            checks.push(quote! {
13242                if let ::core::option::Option::Some(__n) = #val_ref {
13243                    if (*__n as f64) < (#min as f64) {
13244                        #errs.add(
13245                            #name_lit,
13246                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
13247                        );
13248                    }
13249                }
13250            });
13251        }
13252        if let Some(max) = attrs.max {
13253            checks.push(quote! {
13254                if let ::core::option::Option::Some(__n) = #val_ref {
13255                    if (*__n as f64) > (#max as f64) {
13256                        #errs.add(
13257                            #name_lit,
13258                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
13259                        );
13260                    }
13261                }
13262            });
13263        }
13264    }
13265
13266    quote! { #( #checks )* }
13267}
13268
13269// ============================================================
13270//  #[derive(ViewSet)]
13271// ============================================================
13272
13273struct ViewSetAttrs {
13274    model: syn::Path,
13275    fields: Vec<String>,
13276    filter_fields: Vec<String>,
13277    search_fields: Vec<String>,
13278    /// (field_name, desc)
13279    ordering: Vec<(String, bool)>,
13280    page_size: Option<usize>,
13281    read_only: bool,
13282    allow_anonymous: bool,
13283    perms: ViewSetPermsAttrs,
13284    /// `#[viewset(serializer = SomeSerializer)]` — render list /
13285    /// retrieve / create responses through this `#[derive(Serializer)]`
13286    /// type instead of the default field-level projection (requires the
13287    /// `serializer` feature). Tri-dialect.
13288    serializer: Option<syn::Path>,
13289}
13290
13291#[derive(Default)]
13292struct ViewSetPermsAttrs {
13293    list: Vec<String>,
13294    retrieve: Vec<String>,
13295    create: Vec<String>,
13296    update: Vec<String>,
13297    destroy: Vec<String>,
13298}
13299
13300fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
13301    let root = rustango_root();
13302    let struct_name = &input.ident;
13303
13304    // Must be a unit struct or an empty named struct.
13305    match &input.data {
13306        Data::Struct(s) => match &s.fields {
13307            Fields::Unit | Fields::Named(_) => {}
13308            Fields::Unnamed(_) => {
13309                return Err(syn::Error::new_spanned(
13310                    struct_name,
13311                    "ViewSet can only be derived on a unit struct or an empty named struct",
13312                ));
13313            }
13314        },
13315        _ => {
13316            return Err(syn::Error::new_spanned(
13317                struct_name,
13318                "ViewSet can only be derived on a struct",
13319            ));
13320        }
13321    }
13322
13323    let attrs = parse_viewset_attrs(input)?;
13324    let model_path = &attrs.model;
13325
13326    let field_lits = attrs.fields.iter().map(|f| f.as_str());
13327
13328    let filter_fields_call = if attrs.filter_fields.is_empty() {
13329        quote!()
13330    } else {
13331        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
13332        quote!(.filter_fields(&[ #(#lits),* ]))
13333    };
13334
13335    let search_fields_call = if attrs.search_fields.is_empty() {
13336        quote!()
13337    } else {
13338        let lits = attrs.search_fields.iter().map(|f| f.as_str());
13339        quote!(.search_fields(&[ #(#lits),* ]))
13340    };
13341
13342    let ordering_call = if attrs.ordering.is_empty() {
13343        quote!()
13344    } else {
13345        let pairs = attrs.ordering.iter().map(|(f, desc)| {
13346            let f = f.as_str();
13347            quote!((#f, #desc))
13348        });
13349        quote!(.ordering(&[ #(#pairs),* ]))
13350    };
13351
13352    let page_size_call = if let Some(n) = attrs.page_size {
13353        quote!(.page_size(#n))
13354    } else {
13355        quote!()
13356    };
13357
13358    let read_only_call = if attrs.read_only {
13359        quote!(.read_only())
13360    } else {
13361        quote!()
13362    };
13363    let allow_anonymous_call = if attrs.allow_anonymous {
13364        quote!(.allow_anonymous())
13365    } else {
13366        quote!()
13367    };
13368
13369    // `.serializer::<S>()` — reshape responses through a derived
13370    // serializer. Requires the downstream crate to enable the
13371    // `serializer` feature (the method is gated on it).
13372    let serializer_call = if let Some(ref ser) = attrs.serializer {
13373        quote!(.serializer::<#ser>())
13374    } else {
13375        quote!()
13376    };
13377
13378    let perms = &attrs.perms;
13379    let perms_call = if perms.list.is_empty()
13380        && perms.retrieve.is_empty()
13381        && perms.create.is_empty()
13382        && perms.update.is_empty()
13383        && perms.destroy.is_empty()
13384    {
13385        quote!()
13386    } else {
13387        let list_lits = perms.list.iter().map(|s| s.as_str());
13388        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
13389        let create_lits = perms.create.iter().map(|s| s.as_str());
13390        let update_lits = perms.update.iter().map(|s| s.as_str());
13391        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
13392        quote! {
13393            .permissions(#root::viewset::ViewSetPerms {
13394                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
13395                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
13396                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
13397                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
13398                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
13399            })
13400        }
13401    };
13402
13403    Ok(quote! {
13404        impl #struct_name {
13405            /// Build an `axum::Router` with the six standard REST endpoints
13406            /// for this ViewSet, mounted at `prefix`.
13407            ///
13408            /// Accepts any backend's pool — a `sqlx::PgPool`,
13409            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
13410            /// enum — because they all `Into<Pool>` (#1273). This is why
13411            /// the derive no longer names `PgPool`, which would not
13412            /// exist on a project built without the `postgres` feature.
13413            pub fn router(
13414                prefix: &str,
13415                pool: impl ::core::convert::Into<#root::sql::Pool>,
13416            ) -> #root::__axum::Router {
13417                #root::viewset::ViewSet::for_model(
13418                    <#model_path as #root::core::Model>::SCHEMA,
13419                    &[ #(#field_lits),* ],
13420                )
13421                    #filter_fields_call
13422                    #search_fields_call
13423                    #ordering_call
13424                    #page_size_call
13425                    #perms_call
13426                    #read_only_call
13427                    #allow_anonymous_call
13428                    #serializer_call
13429                    .router_pool(prefix, pool.into())
13430            }
13431        }
13432    })
13433}
13434
13435fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
13436    let mut model: Option<syn::Path> = None;
13437    let mut fields: Option<Vec<String>> = None;
13438    let mut filter_fields: Vec<String> = Vec::new();
13439    let mut search_fields: Vec<String> = Vec::new();
13440    let mut ordering: Vec<(String, bool)> = Vec::new();
13441    let mut page_size: Option<usize> = None;
13442    let mut read_only = false;
13443    let mut allow_anonymous = false;
13444    let mut perms = ViewSetPermsAttrs::default();
13445    let mut serializer: Option<syn::Path> = None;
13446
13447    for attr in &input.attrs {
13448        if !attr.path().is_ident("viewset") {
13449            continue;
13450        }
13451        attr.parse_nested_meta(|meta| {
13452            if meta.path.is_ident("model") {
13453                let path: syn::Path = meta.value()?.parse()?;
13454                model = Some(path);
13455                return Ok(());
13456            }
13457            if meta.path.is_ident("serializer") {
13458                let path: syn::Path = meta.value()?.parse()?;
13459                serializer = Some(path);
13460                return Ok(());
13461            }
13462            if meta.path.is_ident("fields") {
13463                let s: LitStr = meta.value()?.parse()?;
13464                fields = Some(split_field_list(&s.value()));
13465                return Ok(());
13466            }
13467            if meta.path.is_ident("filter_fields") {
13468                let s: LitStr = meta.value()?.parse()?;
13469                filter_fields = split_field_list(&s.value());
13470                return Ok(());
13471            }
13472            if meta.path.is_ident("search_fields") {
13473                let s: LitStr = meta.value()?.parse()?;
13474                search_fields = split_field_list(&s.value());
13475                return Ok(());
13476            }
13477            if meta.path.is_ident("ordering") {
13478                let s: LitStr = meta.value()?.parse()?;
13479                ordering = parse_ordering_list(&s.value());
13480                return Ok(());
13481            }
13482            if meta.path.is_ident("page_size") {
13483                let lit: syn::LitInt = meta.value()?.parse()?;
13484                page_size = Some(lit.base10_parse::<usize>()?);
13485                return Ok(());
13486            }
13487            if meta.path.is_ident("read_only") {
13488                read_only = true;
13489                return Ok(());
13490            }
13491            if meta.path.is_ident("allow_anonymous") {
13492                allow_anonymous = true;
13493                return Ok(());
13494            }
13495            if meta.path.is_ident("permissions") {
13496                meta.parse_nested_meta(|inner| {
13497                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
13498                        let s: LitStr = inner.value()?.parse()?;
13499                        Ok(split_field_list(&s.value()))
13500                    };
13501                    if inner.path.is_ident("list") {
13502                        perms.list = parse_codenames(&inner)?;
13503                    } else if inner.path.is_ident("retrieve") {
13504                        perms.retrieve = parse_codenames(&inner)?;
13505                    } else if inner.path.is_ident("create") {
13506                        perms.create = parse_codenames(&inner)?;
13507                    } else if inner.path.is_ident("update") {
13508                        perms.update = parse_codenames(&inner)?;
13509                    } else if inner.path.is_ident("destroy") {
13510                        perms.destroy = parse_codenames(&inner)?;
13511                    } else {
13512                        return Err(inner.error(
13513                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
13514                        ));
13515                    }
13516                    Ok(())
13517                })?;
13518                return Ok(());
13519            }
13520            Err(meta.error(
13521                "unknown viewset attribute (supported: model, fields, filter_fields, \
13522                 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
13523                 permissions(...))",
13524            ))
13525        })?;
13526    }
13527
13528    let model = model.ok_or_else(|| {
13529        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
13530    })?;
13531    // The field list is the write allow-list, so it is never implied.
13532    let fields = fields.ok_or_else(|| {
13533        syn::Error::new_spanned(
13534            &input.ident,
13535            "`#[viewset(fields = \"id, title\")]` is required: the fields the API renders and writes",
13536        )
13537    })?;
13538
13539    Ok(ViewSetAttrs {
13540        model,
13541        fields,
13542        filter_fields,
13543        search_fields,
13544        ordering,
13545        page_size,
13546        read_only,
13547        allow_anonymous,
13548        perms,
13549        serializer,
13550    })
13551}
13552
13553// ============================================================ #[derive(Serializer)]
13554
13555struct SerializerContainerAttrs {
13556    model: syn::Path,
13557    /// `#[serializer(validate = "fn_name")]` on the struct — the
13558    /// cross-field validation hook (#436). The named inherent method
13559    /// must take `&self` and return
13560    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
13561    /// `validate()` runs every per-field validator first; then calls
13562    /// the cross-field method if declared; aggregates all errors into
13563    /// one `FormErrors`.
13564    cross_validate: Option<syn::Ident>,
13565}
13566
13567#[derive(Default)]
13568struct SerializerFieldAttrs {
13569    read_only: bool,
13570    write_only: bool,
13571    source: Option<String>,
13572    skip: bool,
13573    /// `#[serializer(method = "fn_name")]` — computed field. The macro
13574    /// emits a `from_model` initializer that calls
13575    /// `Self::fn_name(&model)` and stores the return value.
13576    method: Option<String>,
13577    /// `#[serializer(validate = "fn_name")]` — per-field validator
13578    /// callable run by `Self::validate(&self)`. Must return
13579    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
13580    /// the field name.
13581    validate: Option<String>,
13582    /// `#[serializer(nested)]` on a field whose type is another
13583    /// `Serializer` — the macro emits `from_model` initializer that
13584    /// reads the parent via `model.<source>.value()` then calls the
13585    /// child serializer's `from_model(parent)`. When the FK is
13586    /// unloaded the field falls back to `Default::default()` (does
13587    /// NOT panic) so a missing prefetch in prod degrades gracefully.
13588    /// Source field on the model defaults to the field name; override
13589    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
13590    /// panic-on-unloaded behavior for tests.
13591    nested: bool,
13592    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
13593    /// strict behavior: panic when the FK isn't loaded. Useful in
13594    /// test code where forgetting select_related must trip a hard
13595    /// failure rather than render a blank nested object.
13596    nested_strict: bool,
13597    /// `#[serializer(many = TagSerializer)]` — declare the field as
13598    /// a list of nested serializers. Field type must be `Vec<S>`
13599    /// where `S` is the inner serializer. The macro initializes the
13600    /// field to `Vec::new()` in `from_model` and emits a typed
13601    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13602    /// maps each model row through `S::from_model`. Auto-load isn't
13603    /// possible (the M2M / one-to-many accessor is async); callers
13604    /// fetch the children + call the setter post-from_model.
13605    many: Option<syn::Type>,
13606    /// `#[serializer(slug = "name")]` — render a relation as one of
13607    /// its fields.
13608    /// Source field on the model must be a `ForeignKey<T>`; the
13609    /// macro emits `from_model` glue that walks
13610    /// `model.<source>.value()?.<slug>` and clones it. Field type on
13611    /// the serializer is typically `String` (whatever type the slug
13612    /// column has). When the FK is unloaded the field falls back to
13613    /// `Default::default()`, same graceful-degrade contract as
13614    /// `nested`. Source defaults to the field name; override with
13615    /// `source = "..."`. v0.44.
13616    slug: Option<String>,
13617    /// `#[serializer(max_length = N)]` — caps
13618    /// the character count of a string field on write. Overrides the
13619    /// model's `max_length`; when absent the model value is inherited.
13620    max_length: Option<u64>,
13621    /// `#[serializer(min_length = N)]` — minimum character count.
13622    /// Serializer-only (the model has no `min_length` column).
13623    min_length: Option<u64>,
13624    /// `#[serializer(min = N)]` — inclusive
13625    /// integer lower bound; overrides the model's `min` when given.
13626    min: Option<i64>,
13627    /// `#[serializer(max = N)]` — inclusive
13628    /// integer upper bound; overrides the model's `max` when given.
13629    max: Option<i64>,
13630}
13631
13632fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13633    let mut model: Option<syn::Path> = None;
13634    let mut cross_validate: Option<syn::Ident> = None;
13635    for attr in &input.attrs {
13636        if !attr.path().is_ident("serializer") {
13637            continue;
13638        }
13639        attr.parse_nested_meta(|meta| {
13640            if meta.path.is_ident("model") {
13641                let _eq: syn::Token![=] = meta.input.parse()?;
13642                model = Some(meta.input.parse()?);
13643                return Ok(());
13644            }
13645            if meta.path.is_ident("validate") {
13646                // #436 — container-level `validate = "fn_name"` for
13647                // cross-field validation. Field-level
13648                // `#[serializer(validate = "...")]` on a field is
13649                // parsed separately in `parse_serializer_field_attrs`.
13650                let s: LitStr = meta.value()?.parse()?;
13651                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13652                return Ok(());
13653            }
13654            Err(meta.error(
13655                "unknown serializer container attribute \
13656                 (supported: `model`, `validate`)",
13657            ))
13658        })?;
13659    }
13660    let model = model.ok_or_else(|| {
13661        syn::Error::new_spanned(
13662            &input.ident,
13663            "`#[serializer(model = SomeModel)]` is required",
13664        )
13665    })?;
13666    Ok(SerializerContainerAttrs {
13667        model,
13668        cross_validate,
13669    })
13670}
13671
13672/// The `#[serde(...)]` keys the Serializer derive honours. Its own
13673/// `Serialize` impl would otherwise drop them silently.
13674#[derive(Default)]
13675struct SerdeWireAttrs {
13676    rename_ser: Option<String>,
13677    rename_de: Option<String>,
13678    skip_ser: bool,
13679    skip_de: bool,
13680    skip_ser_if: Option<syn::ExprPath>,
13681}
13682
13683/// A serde `rename_all` rule, applied to a `snake_case` field name.
13684#[derive(Clone, Copy)]
13685enum SerdeRenameRule {
13686    Lower,
13687    Upper,
13688    Pascal,
13689    Camel,
13690    ScreamingSnake,
13691    Kebab,
13692    ScreamingKebab,
13693}
13694
13695impl SerdeRenameRule {
13696    fn parse(s: &LitStr) -> syn::Result<Self> {
13697        Ok(match s.value().as_str() {
13698            "lowercase" | "snake_case" => Self::Lower,
13699            "UPPERCASE" => Self::Upper,
13700            "PascalCase" => Self::Pascal,
13701            "camelCase" => Self::Camel,
13702            "SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
13703            "kebab-case" => Self::Kebab,
13704            "SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
13705            _ => {
13706                return Err(syn::Error::new_spanned(
13707                    s,
13708                    "unknown serde `rename_all` rule",
13709                ))
13710            }
13711        })
13712    }
13713
13714    /// Same output as serde's own field renaming.
13715    fn apply(self, field: &str) -> String {
13716        match self {
13717            Self::Lower => field.to_owned(),
13718            Self::Upper | Self::ScreamingSnake => field.to_ascii_uppercase(),
13719            Self::Pascal | Self::Camel => {
13720                let mut out = String::new();
13721                let mut cap = matches!(self, Self::Pascal);
13722                for ch in field.chars() {
13723                    if ch == '_' {
13724                        cap = !out.is_empty() || matches!(self, Self::Pascal);
13725                    } else if cap {
13726                        out.push(ch.to_ascii_uppercase());
13727                        cap = false;
13728                    } else {
13729                        out.push(ch);
13730                    }
13731                }
13732                out
13733            }
13734            Self::Kebab => field.replace('_', "-"),
13735            Self::ScreamingKebab => field.to_ascii_uppercase().replace('_', "-"),
13736        }
13737    }
13738}
13739
13740/// Consume the value of a `serde` key the derive does not act on.
13741fn skip_serde_meta_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
13742    if meta.input.peek(syn::Token![=]) {
13743        let _: syn::Expr = meta.value()?.parse()?;
13744    } else if meta.input.peek(syn::token::Paren) {
13745        let _: proc_macro2::Group = meta.input.parse()?;
13746    }
13747    Ok(())
13748}
13749
13750/// `rename = "x"` or `rename(serialize = "a", deserialize = "b")`.
13751fn parse_serde_rename(
13752    meta: &syn::meta::ParseNestedMeta,
13753    mut set: impl FnMut(bool, LitStr) -> syn::Result<()>,
13754) -> syn::Result<()> {
13755    if meta.input.peek(syn::Token![=]) {
13756        let s: LitStr = meta.value()?.parse()?;
13757        set(true, s.clone())?;
13758        return set(false, s);
13759    }
13760    meta.parse_nested_meta(|inner| {
13761        let is_ser = inner.path.is_ident("serialize");
13762        if !is_ser && !inner.path.is_ident("deserialize") {
13763            return Err(inner.error("expected `serialize` or `deserialize`"));
13764        }
13765        set(is_ser, inner.value()?.parse()?)
13766    })
13767}
13768
13769/// Container `rename_all` rules: (serialize, deserialize).
13770fn parse_serde_container_rules(
13771    input: &DeriveInput,
13772) -> syn::Result<(Option<SerdeRenameRule>, Option<SerdeRenameRule>)> {
13773    let (mut ser, mut de) = (None, None);
13774    for attr in input.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13775        attr.parse_nested_meta(|meta| {
13776            if meta.path.is_ident("rename_all") {
13777                return parse_serde_rename(&meta, |is_ser, s| {
13778                    let rule = Some(SerdeRenameRule::parse(&s)?);
13779                    if is_ser {
13780                        ser = rule;
13781                    } else {
13782                        de = rule;
13783                    }
13784                    Ok(())
13785                });
13786            }
13787            skip_serde_meta_value(&meta)
13788        })?;
13789    }
13790    Ok((ser, de))
13791}
13792
13793fn parse_serde_field_attrs(field: &syn::Field) -> syn::Result<SerdeWireAttrs> {
13794    let mut out = SerdeWireAttrs::default();
13795    for attr in field.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13796        attr.parse_nested_meta(|meta| {
13797            if meta.path.is_ident("rename") {
13798                return parse_serde_rename(&meta, |is_ser, s| {
13799                    if is_ser {
13800                        out.rename_ser = Some(s.value());
13801                    } else {
13802                        out.rename_de = Some(s.value());
13803                    }
13804                    Ok(())
13805                });
13806            }
13807            if meta.path.is_ident("skip") {
13808                out.skip_ser = true;
13809                out.skip_de = true;
13810                return Ok(());
13811            }
13812            if meta.path.is_ident("skip_serializing") {
13813                out.skip_ser = true;
13814                return Ok(());
13815            }
13816            if meta.path.is_ident("skip_deserializing") {
13817                out.skip_de = true;
13818                return Ok(());
13819            }
13820            if meta.path.is_ident("skip_serializing_if") {
13821                let s: LitStr = meta.value()?.parse()?;
13822                out.skip_ser_if = Some(s.parse()?);
13823                return Ok(());
13824            }
13825            skip_serde_meta_value(&meta)
13826        })?;
13827    }
13828    Ok(out)
13829}
13830
13831fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13832    let mut out = SerializerFieldAttrs::default();
13833    for attr in &field.attrs {
13834        if !attr.path().is_ident("serializer") {
13835            continue;
13836        }
13837        attr.parse_nested_meta(|meta| {
13838            if meta.path.is_ident("read_only") {
13839                out.read_only = true;
13840                return Ok(());
13841            }
13842            if meta.path.is_ident("write_only") {
13843                out.write_only = true;
13844                return Ok(());
13845            }
13846            if meta.path.is_ident("skip") {
13847                out.skip = true;
13848                return Ok(());
13849            }
13850            if meta.path.is_ident("source") {
13851                let s: LitStr = meta.value()?.parse()?;
13852                out.source = Some(s.value());
13853                return Ok(());
13854            }
13855            if meta.path.is_ident("method") {
13856                let s: LitStr = meta.value()?.parse()?;
13857                out.method = Some(s.value());
13858                return Ok(());
13859            }
13860            if meta.path.is_ident("validate") {
13861                let s: LitStr = meta.value()?.parse()?;
13862                out.validate = Some(s.value());
13863                return Ok(());
13864            }
13865            if meta.path.is_ident("many") {
13866                let _eq: syn::Token![=] = meta.input.parse()?;
13867                out.many = Some(meta.input.parse()?);
13868                return Ok(());
13869            }
13870            if meta.path.is_ident("nested") {
13871                out.nested = true;
13872                // Optional strict flag inside parentheses:
13873                //   #[serializer(nested(strict))]
13874                if meta.input.peek(syn::token::Paren) {
13875                    meta.parse_nested_meta(|inner| {
13876                        if inner.path.is_ident("strict") {
13877                            out.nested_strict = true;
13878                            return Ok(());
13879                        }
13880                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13881                    })?;
13882                }
13883                return Ok(());
13884            }
13885            if meta.path.is_ident("slug") {
13886                let s: LitStr = meta.value()?.parse()?;
13887                out.slug = Some(s.value());
13888                return Ok(());
13889            }
13890            if meta.path.is_ident("max_length") {
13891                let lit: syn::LitInt = meta.value()?.parse()?;
13892                out.max_length = Some(lit.base10_parse::<u64>()?);
13893                return Ok(());
13894            }
13895            if meta.path.is_ident("min_length") {
13896                let lit: syn::LitInt = meta.value()?.parse()?;
13897                out.min_length = Some(lit.base10_parse::<u64>()?);
13898                return Ok(());
13899            }
13900            if meta.path.is_ident("min") {
13901                let lit: syn::LitInt = meta.value()?.parse()?;
13902                out.min = Some(lit.base10_parse::<i64>()?);
13903                return Ok(());
13904            }
13905            if meta.path.is_ident("max") {
13906                let lit: syn::LitInt = meta.value()?.parse()?;
13907                out.max = Some(lit.base10_parse::<i64>()?);
13908                return Ok(());
13909            }
13910            Err(meta.error(
13911                "unknown serializer field attribute (supported: \
13912                 `read_only`, `write_only`, `source`, `skip`, `method`, \
13913                 `validate`, `nested`, `many`, `slug`, `max_length`, \
13914                 `min_length`, `min`, `max`)",
13915            ))
13916        })?;
13917    }
13918    // Validate: read_only + write_only is nonsensical
13919    if out.read_only && out.write_only {
13920        return Err(syn::Error::new_spanned(
13921            field,
13922            "a field cannot be both `read_only` and `write_only`",
13923        ));
13924    }
13925    if out.method.is_some() && out.source.is_some() {
13926        return Err(syn::Error::new_spanned(
13927            field,
13928            "`method` and `source` are mutually exclusive — `method` computes \
13929             the value from a method, `source` reads it from a different model field",
13930        ));
13931    }
13932    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13933        return Err(syn::Error::new_spanned(
13934            field,
13935            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13936             — pick one strategy for populating the field",
13937        ));
13938    }
13939    Ok(out)
13940}
13941
13942fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13943    let root = rustango_root();
13944    let struct_name = &input.ident;
13945    let struct_name_lit = struct_name.to_string();
13946
13947    let Data::Struct(data) = &input.data else {
13948        return Err(syn::Error::new_spanned(
13949            struct_name,
13950            "Serializer can only be derived on structs",
13951        ));
13952    };
13953    let Fields::Named(named) = &data.fields else {
13954        return Err(syn::Error::new_spanned(
13955            struct_name,
13956            "Serializer requires a struct with named fields",
13957        ));
13958    };
13959
13960    let container = parse_serializer_container_attrs(input)?;
13961    let model_path = &container.model;
13962
13963    // Classify each field. `ty` is only consumed by the
13964    // `#[cfg(feature = "openapi")]` block below, but we always
13965    // capture it to keep the field-info build a single pass.
13966    #[allow(dead_code)]
13967    struct FieldInfo {
13968        ident: syn::Ident,
13969        ty: syn::Type,
13970        attrs: SerializerFieldAttrs,
13971        serde: SerdeWireAttrs,
13972        /// JSON key in the output / in a request body.
13973        ser_name: String,
13974        de_name: String,
13975    }
13976    let (ser_rule, de_rule) = parse_serde_container_rules(input)?;
13977    let mut fields_info: Vec<FieldInfo> = Vec::new();
13978    for field in &named.named {
13979        let ident = field.ident.clone().expect("named field has ident");
13980        let attrs = parse_serializer_field_attrs(field)?;
13981        let serde = parse_serde_field_attrs(field)?;
13982        let name = ident_name(&ident);
13983        let wire = |rename: &Option<String>, rule: Option<SerdeRenameRule>| {
13984            rename
13985                .clone()
13986                .unwrap_or_else(|| rule.map_or_else(|| name.clone(), |r| r.apply(&name)))
13987        };
13988        fields_info.push(FieldInfo {
13989            ser_name: wire(&serde.rename_ser, ser_rule),
13990            de_name: wire(&serde.rename_de, de_rule),
13991            ident,
13992            ty: field.ty.clone(),
13993            attrs,
13994            serde,
13995        });
13996    }
13997
13998    // Generate from_model body: struct literal with each field assigned.
13999    let from_model_fields = fields_info.iter().map(|fi| {
14000        let ident = &fi.ident;
14001        if let Some(_inner) = &fi.attrs.many {
14002            // Many — collection field. Initialize empty; caller
14003            // populates via the macro-emitted set_<field> helper
14004            // after fetching the M2M children.
14005            quote! { #ident: ::std::vec::Vec::new() }
14006        } else if let Some(method) = &fi.attrs.method {
14007            // SerializerMethodField: call Self::<method>(&model) to
14008            // compute the value. Method signature must be
14009            // `fn <method>(model: &T) -> <field type>`.
14010            let method_ident = syn::Ident::new(method, ident.span());
14011            quote! { #ident: Self::#method_ident(model) }
14012        } else if let Some(slug_field) = &fi.attrs.slug {
14013            // v0.44 — SlugRelatedField. Source defaults to the field
14014            // name on this struct; override via `source = "..."`. The
14015            // source field on the model is expected to be a
14016            // `ForeignKey<T>`; the slug field on the parent is named
14017            // by the attribute value. When the FK is unloaded the
14018            // field falls back to `Default::default()` — same
14019            // graceful-degrade contract as `nested`; a NULL
14020            // `Option<ForeignKey>` renders `None`.
14021            let src_name = fi
14022                .attrs
14023                .source
14024                .as_deref()
14025                .unwrap_or(&ident_name(&fi.ident))
14026                .to_owned();
14027            let src_ident = ident_from_name(&src_name, ident.span());
14028            let slug_ident = syn::Ident::new(slug_field, ident.span());
14029            quote! {
14030                #ident: #root::serializer::LoadedRelation::render(
14031                    &model.#src_ident,
14032                    |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
14033                    ::core::default::Default::default,
14034                )
14035            }
14036        } else if fi.attrs.nested {
14037            // Nested serializer. Source defaults to the field name on
14038            // this struct; override via `source = "..."`. The source
14039            // field on the model is a `ForeignKey<T>` or
14040            // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
14041            //
14042            // Behavior matrix (tweakable per-field):
14043            //   * FK loaded   → nested object materializes via
14044            //                   ChildSerializer::from_model(parent).
14045            //   * FK unloaded → fall back to ChildSerializer::default()
14046            //                   (so prod doesn't crash on a missing
14047            //                   prefetch — just renders a blank nested
14048            //                   object). Add `#[serializer(nested(
14049            //                   strict))]` to keep the v0.18.1
14050            //                   panic-on-unloaded behavior for tests
14051            //                   that want hard guardrails.
14052            //   * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
14053            //                   field (JSON null), strict or not.
14054            let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
14055            let src_ident = ident_from_name(&src_name, ident.span());
14056            let unloaded = if fi.attrs.nested_strict {
14057                let panic_msg = format!(
14058                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
14059                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
14060                );
14061                quote! { || ::core::panic!("{}", #panic_msg) }
14062            } else {
14063                quote! { ::core::default::Default::default }
14064            };
14065            quote! {
14066                #ident: #root::serializer::LoadedRelation::render(
14067                    &model.#src_ident,
14068                    |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
14069                    #unloaded,
14070                )
14071            }
14072        } else if fi.attrs.write_only || fi.attrs.skip {
14073            // Not read from model — use default
14074            quote! { #ident: ::core::default::Default::default() }
14075        } else if let Some(src) = &fi.attrs.source {
14076            let src_ident = ident_from_name(src, ident.span());
14077            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
14078        } else {
14079            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
14080        }
14081    });
14082
14083    // is_writable predicate (also used by writable_lits /
14084    // writable_source_lits below).
14085    let is_writable = |fi: &&FieldInfo| {
14086        !fi.attrs.read_only
14087            && !fi.attrs.skip
14088            && !fi.serde.skip_de
14089            && fi.attrs.method.is_none()
14090            && !fi.attrs.nested
14091            && fi.attrs.many.is_none()
14092            && fi.attrs.slug.is_none()
14093    };
14094
14095    // Declarative field constraints: one block
14096    // per writable field, run inside `validate()`. Each resolves its
14097    // bounds as the serializer attr when given (`#[serializer(max_length
14098    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
14099    // `max` / `choices`), then dispatches on the field's JSON value via
14100    // `forms::validators::check_value` (string → length/choices, integer
14101    // → min/max).
14102    let opt_usize = |v: Option<u64>| match v {
14103        Some(n) => {
14104            let n = n as usize;
14105            quote!(::core::option::Option::Some(#n))
14106        }
14107        None => quote!(::core::option::Option::None),
14108    };
14109    #[cfg(feature = "openapi")]
14110    let opt_u64 = |v: Option<u64>| match v {
14111        Some(n) => quote!(::core::option::Option::Some(#n)),
14112        None => quote!(::core::option::Option::None),
14113    };
14114    let opt_i64 = |v: Option<i64>| match v {
14115        Some(n) => quote!(::core::option::Option::Some(#n)),
14116        None => quote!(::core::option::Option::None),
14117    };
14118    let constraint_blocks: Vec<_> = fields_info
14119        .iter()
14120        .filter(is_writable)
14121        .map(|fi| {
14122            let ident = &fi.ident;
14123            let fname = &fi.de_name;
14124            let mname = fi
14125                .attrs
14126                .source
14127                .clone()
14128                .unwrap_or_else(|| ident_name(&fi.ident));
14129            let attr_max_len = opt_usize(fi.attrs.max_length);
14130            let attr_min_len = opt_usize(fi.attrs.min_length);
14131            let attr_min = opt_i64(fi.attrs.min);
14132            let attr_max = opt_i64(fi.attrs.max);
14133            // A bound the field's type cannot take is a compile error, not a no-op.
14134            let ty = &fi.ty;
14135            let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
14136                .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
14137            let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
14138                .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
14139            quote! {
14140                #len_check
14141                #num_check
14142                if __sent(#fname) {
14143                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
14144                    let __max_length: ::core::option::Option<usize> = #attr_max_len
14145                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
14146                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
14147                    let __min: ::core::option::Option<i64> =
14148                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
14149                    let __max: ::core::option::Option<i64> =
14150                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
14151                    let __choices = __sf.and_then(|__f| __f.choices);
14152                    let __v = #root::__serde_json::to_value(&self.#ident)
14153                        .unwrap_or(#root::__serde_json::Value::Null);
14154                    #root::forms::validators::check_value(
14155                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
14156                        &mut __errors,
14157                    );
14158                }
14159            }
14160        })
14161        .collect();
14162    let has_constraints = !constraint_blocks.is_empty();
14163
14164    // Per-field validators. Emit a
14165    // `validate(&self)` method that runs each user-defined validator
14166    // and aggregates errors into `FormErrors`.
14167    let validator_calls: Vec<_> = fields_info
14168        .iter()
14169        .filter_map(|fi| {
14170            let ident = &fi.ident;
14171            let name_lit = &fi.de_name;
14172            let method = fi.attrs.validate.as_ref()?;
14173            let method_ident = syn::Ident::new(method, ident.span());
14174            Some(quote! {
14175                if __sent(#name_lit) {
14176                    if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
14177                        __errors.add(#name_lit.to_owned(), __e);
14178                    }
14179                }
14180            })
14181        })
14182        .collect();
14183    // #436 — cross-field `validate(&self)`. If the
14184    // container declared `#[serializer(validate = "fn_name")]`,
14185    // the macro-generated `validate(&self)` runs every per-field
14186    // validator first, then calls the user's cross-field method,
14187    // merging its `FormErrors` into the per-field errors. Either
14188    // alone is enough to emit the wrapper.
14189    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
14190        quote! {
14191            // Merge cross-field errors into the per-field bucket so
14192            // a single .validate() call surfaces both layers.
14193            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
14194                __errors.merge(__cross);
14195            }
14196        }
14197    });
14198    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
14199    // Anything to run? Declarative constraints (for any writable field) +
14200    // per-field validators + cross-field hook.
14201    let has_run_validations = has_validators || has_constraints;
14202    // Shared body: declarative field constraints first (length / range /
14203    // choices, serializer-attr-or-model), then per-field validators, then
14204    // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
14205    let checks = quote! {
14206        let mut __errors = #root::forms::FormErrors::default();
14207        #( #constraint_blocks )*
14208        #( #validator_calls )*
14209        #cross_validate_call
14210        if __errors.is_empty() {
14211            ::core::result::Result::Ok(())
14212        } else {
14213            ::core::result::Result::Err(__errors)
14214        }
14215    };
14216    let validate_body = quote! {
14217        let __sent = |_: &str| true;
14218        #checks
14219    };
14220    // PATCH: only the keys the body carries.
14221    let validate_patch_body = quote! {
14222        let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
14223        #checks
14224    };
14225    // Inherent `validate(&self)` — kept for back-compat with direct
14226    // `serializer.validate()` calls that don't import `ModelSerializer`.
14227    // Emitted only when the serializer declares per-field/cross-field
14228    // validators (unchanged rule) so it never collides with a
14229    // hand-written inherent `validate`.
14230    let validate_method = if has_validators {
14231        quote! {
14232            impl #struct_name {
14233                /// Run the declarative field constraints, every
14234                /// `#[serializer(validate = "...")]` per-field validator,
14235                /// and (when declared) the container-level cross-field
14236                /// validator. Aggregates errors into `FormErrors` keyed by
14237                /// the field name. Returns `Ok(())` when all pass.
14238                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
14239                    #validate_body
14240                }
14241            }
14242        }
14243    } else {
14244        quote! {}
14245    };
14246    // Trait-method override so the type-erased ViewSet write path can
14247    // dispatch `ModelSerializer::validate` generically. Emitted whenever
14248    // there's anything to run (declarative constraints inherited from the
14249    // model count); otherwise the trait's default no-op is used.
14250    let trait_validate_override = if has_run_validations {
14251        quote! {
14252            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
14253                #validate_body
14254            }
14255        }
14256    } else {
14257        quote! {}
14258    };
14259
14260    // For every `#[serializer(many = S)]` field, emit a
14261    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
14262    // helper that maps the parents through `S::from_model`.
14263    let many_setters: Vec<_> = fields_info
14264        .iter()
14265        .filter_map(|fi| {
14266            let many_ty = fi.attrs.many.as_ref()?;
14267            let ident = &fi.ident;
14268            let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
14269            Some(quote! {
14270                /// Populate this `many` field by mapping each parent model
14271                /// through the inner serializer's `from_model`. Call after
14272                /// fetching the M2M / one-to-many children since
14273                /// `from_model` itself can't await an SQL query.
14274                pub fn #setter(
14275                    &mut self,
14276                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
14277                ) -> &mut Self {
14278                    self.#ident = models.iter()
14279                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
14280                        .collect();
14281                    self
14282                }
14283            })
14284        })
14285        .collect();
14286    let many_setters_impl = if many_setters.is_empty() {
14287        quote! {}
14288    } else {
14289        quote! {
14290            impl #struct_name {
14291                #( #many_setters )*
14292            }
14293        }
14294    };
14295
14296    // Generate custom Serialize: skip write_only and serde-skipped fields
14297    let output_fields: Vec<_> = fields_info
14298        .iter()
14299        .filter(|fi| !fi.attrs.write_only && !fi.serde.skip_ser)
14300        .collect();
14301    // `skip_serializing_if` fields count only when rendered.
14302    let output_field_count = output_fields.iter().map(|fi| {
14303        let ident = &fi.ident;
14304        if let Some(pred) = &fi.serde.skip_ser_if {
14305            quote! { + if #pred(&self.#ident) { 0 } else { 1 } }
14306        } else {
14307            quote! { + 1 }
14308        }
14309    });
14310    let output_field_count = quote! { 0usize #( #output_field_count )* };
14311    let serialize_fields = output_fields.iter().map(|fi| {
14312        let ident = &fi.ident;
14313        let name_lit = &fi.ser_name;
14314        if let Some(pred) = &fi.serde.skip_ser_if {
14315            quote! {
14316                if #pred(&self.#ident) {
14317                    __state.skip_field(#name_lit)?;
14318                } else {
14319                    __state.serialize_field(#name_lit, &self.#ident)?;
14320                }
14321            }
14322        } else {
14323            quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
14324        }
14325    });
14326
14327    // writable_fields: normal + write_only.
14328    // Exclude:
14329    //   - `read_only` — server-computed.
14330    //   - `skip` — caller sets manually post-from_model.
14331    //   - `method` — computed from a Self::fn(&model) call; accepting
14332    //     it on write is meaningless.
14333    //   - `nested` / `many` — populated from related-model data, not
14334    //     from a field on the wire body.
14335    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
14336    // `many` in `writable_fields()`, which made the ViewSet write
14337    // path accept those fields from the JSON body and try to bind
14338    // them to the SQL UPDATE — a silent no-op at best, a type
14339    // mismatch at worst.
14340    // (`is_writable` is defined above, near the constraint codegen.)
14341    let writable_lits: Vec<_> = fields_info
14342        .iter()
14343        .filter(is_writable)
14344        .map(|fi| fi.de_name.clone())
14345        .collect();
14346
14347    // `writable_source_fields`: the MODEL field names of writable
14348    // serializer fields (source-resolved). The ViewSet write path skips
14349    // every model column NOT in this set, so `read_only` / `method` /
14350    // computed fields a client posts are ignored instead of written.
14351    let writable_source_lits: Vec<String> = fields_info
14352        .iter()
14353        .filter(is_writable)
14354        .map(|fi| {
14355            fi.attrs
14356                .source
14357                .clone()
14358                .unwrap_or_else(|| ident_name(&fi.ident))
14359        })
14360        .collect();
14361
14362    // `readable_source_fields`: model columns the JSON output copies
14363    // straight from the model, so `?ordering=` cannot sort on a column
14364    // the API never shows (#1845).
14365    let readable: Vec<&FieldInfo> = fields_info
14366        .iter()
14367        .filter(|fi| {
14368            !fi.attrs.write_only
14369                && !fi.serde.skip_ser
14370                && !fi.attrs.skip
14371                && fi.attrs.method.is_none()
14372                && !fi.attrs.nested
14373                && fi.attrs.many.is_none()
14374                && fi.attrs.slug.is_none()
14375        })
14376        .collect();
14377    let readable_source_lits: Vec<String> = readable
14378        .iter()
14379        .map(|fi| {
14380            fi.attrs
14381                .source
14382                .clone()
14383                .unwrap_or_else(|| ident_name(&fi.ident))
14384        })
14385        .collect();
14386    // Their JSON keys, so the ViewSet finds a renamed cursor column.
14387    let readable_lits: Vec<String> = readable.iter().map(|fi| fi.ser_name.clone()).collect();
14388
14389    // `from_writable_json`: build a partial instance for input
14390    // validation. Writable fields are parsed from the JSON body (keyed
14391    // by serializer field name); every other field defaults. Per-field
14392    // type errors collect into `FormErrors` keyed by the field name.
14393    // Construction is field-by-field (not `Self::default()`) so it works
14394    // for serializers regardless of a struct-level `Default`.
14395    let from_writable_json_inits: Vec<_> = fields_info
14396        .iter()
14397        .map(|fi| {
14398            let ident = &fi.ident;
14399            let fname = &fi.de_name;
14400            let ty = &fi.ty;
14401            if is_writable(&fi) {
14402                quote! {
14403                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
14404                        ::core::option::Option::Some(__v) => {
14405                            match #root::__serde_json::from_value::<#ty>(
14406                                ::core::clone::Clone::clone(__v),
14407                            ) {
14408                                ::core::result::Result::Ok(__x) => __x,
14409                                ::core::result::Result::Err(__e) => {
14410                                    __errors.add(#fname.to_owned(), __e.to_string());
14411                                    ::core::default::Default::default()
14412                                }
14413                            }
14414                        }
14415                        ::core::option::Option::None => ::core::default::Default::default(),
14416                    }
14417                }
14418            } else {
14419                quote! { #ident: ::core::default::Default::default() }
14420            }
14421        })
14422        .collect();
14423    // `validate_patch`: overlay each sent writable field on the stored row.
14424    let patch_overlays: Vec<_> = fields_info
14425        .iter()
14426        .filter(is_writable)
14427        .map(|fi| {
14428            let ident = &fi.ident;
14429            let fname = &fi.de_name;
14430            let ty = &fi.ty;
14431            quote! {
14432                if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
14433                    match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
14434                        ::core::result::Result::Ok(__x) => __merged.#ident = __x,
14435                        ::core::result::Result::Err(__e) => {
14436                            __errors.add(#fname.to_owned(), __e.to_string());
14437                        }
14438                    }
14439                }
14440            }
14441        })
14442        .collect();
14443    let patch_validate_call = if has_run_validations {
14444        quote! { __merged.__rustango_validate_patch(__obj) }
14445    } else {
14446        quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
14447    };
14448    let patch_validate_helper = if has_run_validations {
14449        quote! {
14450            impl #struct_name {
14451                #[doc(hidden)]
14452                fn __rustango_validate_patch(
14453                    &self,
14454                    __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
14455                ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14456                    #validate_patch_body
14457                }
14458            }
14459        }
14460    } else {
14461        quote! {}
14462    };
14463
14464    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
14465    // Only includes fields shown in JSON output (skips write_only). For each
14466    // `Option<T>` field, omit from `required` and add `.nullable()`.
14467    let openapi_impl = {
14468        #[cfg(feature = "openapi")]
14469        {
14470            let property_calls = output_fields.iter().map(|fi| {
14471                let name_lit = &fi.ser_name;
14472                let ty = &fi.ty;
14473                let nullable_call = if is_option(ty) {
14474                    quote! { .nullable() }
14475                } else {
14476                    quote! {}
14477                };
14478                // The bounds `validate()` enforces on a written field.
14479                let limits_call = is_writable(fi).then(|| {
14480                    let mname = fi
14481                        .attrs
14482                        .source
14483                        .clone()
14484                        .unwrap_or_else(|| ident_name(&fi.ident));
14485                    let (a, b, c, d) = (
14486                        opt_u64(fi.attrs.max_length),
14487                        opt_u64(fi.attrs.min_length),
14488                        opt_i64(fi.attrs.min),
14489                        opt_i64(fi.attrs.max),
14490                    );
14491                    quote! {
14492                        .field_limits(
14493                            <#model_path as #root::core::Model>::SCHEMA.field(#mname),
14494                            #a, #b, #c, #d,
14495                        )
14496                    }
14497                });
14498                quote! {
14499                    .property(
14500                        #name_lit,
14501                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
14502                            #limits_call
14503                            #nullable_call,
14504                    )
14505                }
14506            });
14507            let required_lits: Vec<_> = output_fields
14508                .iter()
14509                .filter(|fi| !is_option(&fi.ty) && fi.serde.skip_ser_if.is_none())
14510                .map(|fi| fi.ser_name.clone())
14511                .collect();
14512            quote! {
14513                impl #root::openapi::OpenApiSchema for #struct_name {
14514                    fn openapi_schema() -> #root::openapi::Schema {
14515                        #root::openapi::Schema::object()
14516                            #( #property_calls )*
14517                            .required([ #( #required_lits ),* ])
14518                    }
14519                }
14520            }
14521        }
14522        #[cfg(not(feature = "openapi"))]
14523        {
14524            quote! {}
14525        }
14526    };
14527
14528    Ok(quote! {
14529        impl #root::serializer::ModelSerializer for #struct_name {
14530            type Model = #model_path;
14531
14532            fn from_model(model: &Self::Model) -> Self {
14533                Self {
14534                    #( #from_model_fields ),*
14535                }
14536            }
14537
14538            fn writable_fields() -> &'static [&'static str] {
14539                &[ #( #writable_lits ),* ]
14540            }
14541
14542            fn writable_source_fields() -> &'static [&'static str] {
14543                &[ #( #writable_source_lits ),* ]
14544            }
14545
14546            fn readable_source_fields() -> &'static [&'static str] {
14547                &[ #( #readable_source_lits ),* ]
14548            }
14549
14550            fn readable_fields() -> &'static [&'static str] {
14551                &[ #( #readable_lits ),* ]
14552            }
14553
14554            fn from_writable_json(
14555                __body: &#root::__serde_json::Value,
14556            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
14557                let mut __errors = #root::forms::FormErrors::default();
14558                let __obj = __body.as_object();
14559                let __out = Self {
14560                    #( #from_writable_json_inits ),*
14561                };
14562                if __errors.is_empty() {
14563                    ::core::result::Result::Ok(__out)
14564                } else {
14565                    ::core::result::Result::Err(__errors)
14566                }
14567            }
14568
14569            fn validate_patch(
14570                __stored: &Self::Model,
14571                __body: &#root::__serde_json::Value,
14572            ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14573                let mut __errors = #root::forms::FormErrors::default();
14574                let __obj = __body.as_object();
14575                #[allow(unused_mut)]
14576                let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
14577                #( #patch_overlays )*
14578                if !__errors.is_empty() {
14579                    return ::core::result::Result::Err(__errors);
14580                }
14581                #patch_validate_call
14582            }
14583
14584            #trait_validate_override
14585        }
14586
14587        #patch_validate_helper
14588
14589        impl #root::__serde::Serialize for #struct_name {
14590            fn serialize<S>(&self, serializer: S)
14591                -> ::core::result::Result<S::Ok, S::Error>
14592            where
14593                S: #root::__serde::Serializer,
14594            {
14595                use #root::__serde::ser::SerializeStruct;
14596                let mut __state = serializer.serialize_struct(
14597                    #struct_name_lit,
14598                    #output_field_count,
14599                )?;
14600                #( #serialize_fields )*
14601                __state.end()
14602            }
14603        }
14604
14605        #openapi_impl
14606
14607        #validate_method
14608
14609        #many_setters_impl
14610    })
14611}
14612
14613/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
14614/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
14615/// when the feature is off.
14616#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
14617fn is_option(ty: &syn::Type) -> bool {
14618    if let syn::Type::Path(p) = ty {
14619        if let Some(last) = p.path.segments.last() {
14620            return last.ident == "Option";
14621        }
14622    }
14623    false
14624}
14625
14626#[cfg(test)]
14627mod main_attr_tests {
14628    use super::*;
14629
14630    fn expand(args: &str) -> String {
14631        let body = quote! { async fn main() { run().await } };
14632        expand_main(args.parse().unwrap(), body)
14633            .expect("expansion failed")
14634            .to_string()
14635    }
14636
14637    /// Asserts on the expansion rather than on `parse_logging`,
14638    /// because the defect was a subscriber reaching the output, not
14639    /// an argument being misread (#1465).
14640    #[test]
14641    fn logging_false_emits_no_subscriber() {
14642        let out = expand("logging = false");
14643        assert!(
14644            !out.contains("logging :: Setup"),
14645            "`logging = false` must not install a subscriber:\n{out}",
14646        );
14647        assert!(
14648            out.contains("block_on"),
14649            "the runtime must still be built:\n{out}",
14650        );
14651    }
14652
14653    #[test]
14654    fn default_still_installs_a_subscriber() {
14655        // The control. Without it, an `expand_main` that dropped the
14656        // prologue unconditionally would pass the test above while
14657        // leaving every default project with no logging at all.
14658        let out = expand("");
14659        assert!(
14660            out.contains("logging :: Setup"),
14661            "the default entrypoint must still install one:\n{out}",
14662        );
14663        // `./app serve` or `./app --port 8080` must keep logging to stdout:
14664        // only `Cli`, which knows its verbs, moves logs to stderr.
14665        assert!(
14666            !out.contains("args ()"),
14667            "the entrypoint must not route logs by argv:\n{out}",
14668        );
14669    }
14670
14671    #[test]
14672    fn logging_false_composes_with_flavor() {
14673        let out = expand("flavor = \"current_thread\", logging = false");
14674        assert!(!out.contains("logging :: Setup"), "{out}");
14675        assert!(out.contains("new_current_thread"), "{out}");
14676    }
14677
14678    #[test]
14679    fn logging_true_is_the_default_shape() {
14680        // `logging = true` is accepted and means the default, so the
14681        // match must not fire on the substring `logging`.
14682        assert!(parse_logging(&"logging = true".parse().unwrap()));
14683        assert!(!parse_logging(&"logging=false".parse().unwrap()));
14684    }
14685}
14686
14687#[cfg(test)]
14688mod main_arg_warning_tests {
14689    use super::*;
14690
14691    fn expand(args: &str) -> String {
14692        let body = quote! { async fn main() { run().await } };
14693        expand_main(args.parse().unwrap(), body)
14694            .expect("expansion failed")
14695            .to_string()
14696    }
14697
14698    /// A typo or unknown key used to be dropped without a word.
14699    #[test]
14700    fn unknown_or_misspelt_args_warn() {
14701        for args in ["logging = flase", "multi_thread", "workers = 4"] {
14702            let out = expand(args);
14703            assert!(out.contains("unknown argument"), "{args}:\n{out}");
14704        }
14705    }
14706
14707    #[test]
14708    fn supported_args_do_not_warn() {
14709        for args in [
14710            "",
14711            r#"flavor = "current_thread""#,
14712            r#"flavor = "multi_thread", logging = false"#,
14713            "logging = true",
14714            "worker_threads = 4, start_paused = true",
14715            r#"crate = "tokio", unhandled_panic = "shutdown_runtime""#,
14716            r#"flavor = "curent_thread""#,
14717        ] {
14718            assert!(!expand(args).contains("RUSTANGO_WARNING"), "{args}");
14719        }
14720    }
14721}
14722
14723#[cfg(test)]
14724mod embed_migrations_tests {
14725    use super::*;
14726
14727    fn expand(path: &str) -> String {
14728        expand_embed_migrations(quote! { #path })
14729            .expect("expansion failed")
14730            .to_string()
14731    }
14732
14733    #[test]
14734    fn missing_dir_warns() {
14735        let out = expand("./no_such_migrations_dir");
14736        assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
14737        assert!(out.contains("no_such_migrations_dir"), "{out}");
14738    }
14739
14740    #[test]
14741    fn existing_dir_does_not_warn() {
14742        let out = expand("./src");
14743        assert!(!out.contains("deprecated"), "{out}");
14744    }
14745}
14746
14747#[cfg(test)]
14748mod field_attr_tests {
14749    use super::*;
14750
14751    fn error_for(field: syn::Field) -> Option<String> {
14752        let attrs = parse_field_attrs(&field).unwrap();
14753        let kind = detect_type(&field.ty).unwrap().kind;
14754        check_bound_compatibility(&field, &attrs, kind)
14755            .err()
14756            .map(|e| e.to_string())
14757    }
14758
14759    /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
14760    #[test]
14761    fn type_specific_attrs_reject_other_types() {
14762        let f: syn::FieldsNamed = syn::parse_quote!({
14763            #[rustango(citext)] a: i64,
14764            #[rustango(vector(dims = 3))] b: String,
14765            #[rustango(geometry(srid = 4326))] c: String,
14766            #[rustango(citext)] d: String,
14767        });
14768        let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
14769        assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
14770        assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
14771        assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
14772        assert_eq!(errs[3], None);
14773    }
14774
14775    /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
14776    #[test]
14777    fn manager_ext_is_refused() {
14778        let input: DeriveInput = syn::parse_quote! {
14779            #[rustango(table = "p", manager(ext = "PostManagerExt"))]
14780            struct Post { id: i64 }
14781        };
14782        let err = parse_container_attrs(&input).err().expect("refused");
14783        assert!(err.to_string().contains("#2132"), "{err}");
14784    }
14785
14786    /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
14787    #[test]
14788    fn set_null_needs_an_optional_foreign_key() {
14789        let input: DeriveInput = syn::parse_quote! {
14790            #[rustango(table = "c")]
14791            struct Child {
14792                #[rustango(primary_key)] id: i64,
14793                #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
14794            }
14795        };
14796        let err = expand(&input).err().expect("refused");
14797        assert!(err.to_string().contains("set_null"), "{err}");
14798        let input: DeriveInput = syn::parse_quote! {
14799            #[rustango(table = "c")]
14800            struct Child {
14801                #[rustango(primary_key)] id: i64,
14802                #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
14803            }
14804        };
14805        assert!(expand(&input).is_ok());
14806    }
14807
14808    /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
14809    #[test]
14810    fn set_null_needs_a_nullable_fk_column() {
14811        let input: DeriveInput = syn::parse_quote! {
14812            #[rustango(table = "c")]
14813            struct Child {
14814                #[rustango(primary_key)] id: i64,
14815                #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
14816                #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
14817            }
14818        };
14819        let err = expand(&input).err().expect("refused");
14820        assert!(err.to_string().contains("set_null"), "{err}");
14821        let input: DeriveInput = syn::parse_quote! {
14822            #[rustango(table = "c")]
14823            struct Child {
14824                #[rustango(primary_key)] id: i64,
14825                #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
14826                #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
14827                #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
14828            }
14829        };
14830        assert!(expand(&input).is_ok());
14831    }
14832
14833    /// An index or `get_latest_by` naming no column warns; it does not fail.
14834    #[test]
14835    fn unknown_index_columns_warn() {
14836        let input: DeriveInput = syn::parse_quote! {
14837            #[rustango(
14838                table = "w",
14839                unique_together = "a, nope",
14840                index_together = "a, b_typo",
14841                index("missing"),
14842                get_latest_by = "-when_typo"
14843            )]
14844            struct W { #[rustango(primary_key)] id: i64, a: i64, b: i64 }
14845        };
14846        let out = expand(&input).expect("still compiles").to_string();
14847        for name in ["nope", "b_typo", "missing", "when_typo"] {
14848            assert!(out.contains(&format!("`{name}`")), "{name}:\n{out}");
14849        }
14850    }
14851
14852    /// Real columns, field names for `get_latest_by` and expression
14853    /// indexes emit nothing new.
14854    #[test]
14855    fn known_index_columns_do_not_warn() {
14856        let input: DeriveInput = syn::parse_quote! {
14857            #[rustango(
14858                table = "w",
14859                unique_together = "a, b_col",
14860                index("lower(a)"),
14861                get_latest_by = "-b"
14862            )]
14863            struct W {
14864                #[rustango(primary_key)] id: i64,
14865                a: i64,
14866                #[rustango(column = "b_col")] b: i64,
14867            }
14868        };
14869        let out = expand(&input).expect("compiles").to_string();
14870        assert!(!out.contains("RUSTANGO_WARNING"), "{out}");
14871    }
14872
14873    /// Two FKs to one parent would emit the same reverse accessor twice.
14874    #[test]
14875    fn reverse_accessor_clash_names_related_name() {
14876        let input: DeriveInput = syn::parse_quote! {
14877            #[rustango(table = "m")]
14878            struct Message {
14879                #[rustango(primary_key)] id: i64,
14880                sender: ForeignKey<User>,
14881                recipient: ForeignKey<User>,
14882            }
14883        };
14884        let err = expand(&input).err().expect("refused");
14885        assert!(err.to_string().contains("related_name"), "{err}");
14886    }
14887
14888    /// `related_name = "+"` emits no reverse accessor, so a parent from
14889    /// another crate, which cannot take an inherent impl, still derives.
14890    #[test]
14891    fn related_name_plus_skips_the_reverse_accessor() {
14892        let input: DeriveInput = syn::parse_quote! {
14893            #[rustango(table = "m")]
14894            struct Message {
14895                #[rustango(primary_key)] id: i64,
14896                sender: ForeignKey<User>,
14897                #[rustango(related_name = "+")] recipient: ForeignKey<User>,
14898                #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
14899            }
14900        };
14901        let out = expand(&input).expect("derives").to_string();
14902        assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
14903        assert!(!out.contains("impl other :: Account {"), "{out}");
14904    }
14905
14906    fn field_error(src: &str) -> Option<String> {
14907        let f: syn::FieldsNamed = syn::parse_str(&format!("{{ {src} }}")).unwrap();
14908        let field = f.named.into_iter().next().unwrap();
14909        match parse_field_attrs(&field) {
14910            Err(e) => Some(e.to_string()),
14911            Ok(_) => error_for(field),
14912        }
14913    }
14914
14915    /// Flag attrs take `= true` like `blank`; `= false` is refused by name.
14916    #[test]
14917    fn flag_attrs_accept_eq_true() {
14918        for key in [
14919            "primary_key",
14920            "unique",
14921            "soft_delete",
14922            "index",
14923            "auto_now",
14924            "auto_now_add",
14925        ] {
14926            let f: syn::FieldsNamed =
14927                syn::parse_str(&format!("{{ #[rustango({key} = true)] a: i64 }}")).unwrap();
14928            let field = f.named.into_iter().next().unwrap();
14929            assert!(parse_field_attrs(&field).is_ok(), "{key} = true");
14930            let err = field_error(&format!("#[rustango({key} = false)] a: i64")).unwrap();
14931            assert!(err.contains(&format!("`{key} = false`")), "{err}");
14932        }
14933        let f: syn::Field =
14934            syn::parse_quote!(#[rustango(primary_key = true, unique = true)] a: i64);
14935        let attrs = parse_field_attrs(&f).unwrap();
14936        assert!(attrs.primary_key && attrs.unique);
14937    }
14938
14939    /// An unknown key is named, the nearest key suggested and the list shown.
14940    #[test]
14941    fn unknown_key_is_named_with_a_suggestion() {
14942        let err = field_error("#[rustango(max_lenght = 5)] a: String").unwrap();
14943        assert!(err.contains("`max_lenght`"), "{err}");
14944        assert!(err.contains("did you mean `max_length`"), "{err}");
14945        assert!(err.contains("Supported: column"), "{err}");
14946        let input: DeriveInput = syn::parse_quote! {
14947            #[rustango(tabel = "p")]
14948            struct P { id: i64 }
14949        };
14950        let err = parse_container_attrs(&input).err().unwrap().to_string();
14951        assert!(
14952            err.contains("`tabel`") && err.contains("did you mean `table`"),
14953            "{err}"
14954        );
14955        let err = field_error("#[rustango(zzzzzz)] a: i64").unwrap();
14956        assert!(!err.contains("did you mean"), "{err}");
14957    }
14958
14959    /// The listed keys are the parsed keys; a stale list would mislead.
14960    #[test]
14961    fn listed_keys_are_all_known() {
14962        for key in FIELD_ATTR_KEYS {
14963            let err = field_error(&format!("#[rustango({key})] a: i64")).unwrap_or_default();
14964            assert!(!err.contains("unknown rustango field"), "{key}: {err}");
14965        }
14966        for key in CONTAINER_ATTR_KEYS {
14967            let input: DeriveInput =
14968                syn::parse_str(&format!("#[rustango({key})] struct P {{ id: i64 }}")).unwrap();
14969            let err = parse_container_attrs(&input)
14970                .err()
14971                .map(|e| e.to_string())
14972                .unwrap_or_default();
14973            assert!(!err.contains("unknown rustango container"), "{key}: {err}");
14974        }
14975    }
14976
14977    /// `default` is raw SQL: a bare word or a non-number on a numeric field is refused.
14978    #[test]
14979    fn default_shapes_only_sqlite_takes_are_refused() {
14980        for bad in [
14981            r#"#[rustango(default = "draft")] s: String"#,
14982            r#"#[rustango(default = "draft")] s: Option<String>"#,
14983            r#"#[rustango(default = "abc")] n: i32"#,
14984            r#"#[rustango(default = "'abc'")] n: i64"#,
14985            r#"#[rustango(default = "true")] n: i32"#,
14986            r#"#[rustango(default = "")] n: i32"#,
14987            r#"#[rustango(default = "'x'")] n: f64"#,
14988        ] {
14989            assert!(field_error(bad).is_some(), "{bad} must be refused");
14990        }
14991        let err = field_error(r#"#[rustango(default = "draft")] s: String"#).unwrap();
14992        assert!(err.contains(r#"default = "'draft'""#), "{err}");
14993        // Valid on Postgres alone, so the message says that, not "a column".
14994        let err = field_error(r#"#[rustango(default = "current_user")] s: String"#).unwrap();
14995        assert!(
14996            err.contains("Postgres only") && !err.contains("column"),
14997            "{err}"
14998        );
14999        for good in [
15000            r#"#[rustango(default = "'draft'")] s: String"#,
15001            r#"#[rustango(default = "")] s: String"#,
15002            r#"#[rustango(default = "now()")] s: String"#,
15003            r#"#[rustango(default = "CURRENT_TIMESTAMP")] s: String"#,
15004            r#"#[rustango(default = "NULL")] s: Option<String>"#,
15005            r#"#[rustango(default = "0")] n: i32"#,
15006            r#"#[rustango(default = "-1")] n: i64"#,
15007            r#"#[rustango(default = "'5'")] n: i64"#,
15008            r#"#[rustango(default = "(1 + 2)")] n: i64"#,
15009            r#"#[rustango(default = "1.5")] n: f64"#,
15010            r#"#[rustango(default = "true")] b: bool"#,
15011        ] {
15012            assert_eq!(field_error(good), None, "{good}");
15013        }
15014    }
15015
15016    /// A raw ident derives names without `r#`; this used to panic (#1937).
15017    #[test]
15018    fn raw_idents_derive_plain_names() {
15019        let ident: syn::Ident = syn::parse_quote!(r#type);
15020        assert_eq!(column_type_ident(&ident).to_string(), "type_col");
15021        assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
15022    }
15023}
15024
15025#[cfg(test)]
15026mod model_attr_name_tests {
15027    use super::*;
15028
15029    fn container_error(item: proc_macro2::TokenStream) -> Option<String> {
15030        let input: DeriveInput = syn::parse2(item).unwrap();
15031        parse_container_attrs(&input).err().map(|e| e.to_string())
15032    }
15033
15034    fn field_error(field: proc_macro2::TokenStream) -> Option<String> {
15035        let f: syn::FieldsNamed = syn::parse2(quote!({ #field })).unwrap();
15036        let field = f.named.into_iter().next().unwrap();
15037        parse_field_attrs(&field).err().map(|e| e.to_string())
15038    }
15039
15040    /// `#[rgo(...)]` reads exactly as `#[rustango(...)]`.
15041    #[test]
15042    fn rgo_is_read_like_rustango() {
15043        let long: DeriveInput = syn::parse_quote! {
15044            #[rustango(table = "t", display = "name")] struct S;
15045        };
15046        let short: DeriveInput = syn::parse_quote! {
15047            #[rgo(table = "t", display = "name")] struct S;
15048        };
15049        let (long, short) = (
15050            parse_container_attrs(&long).unwrap(),
15051            parse_container_attrs(&short).unwrap(),
15052        );
15053        assert_eq!(long.table, short.table);
15054        assert_eq!(short.table.as_deref(), Some("t"));
15055        assert_eq!(long.display.map(|d| d.0), short.display.map(|d| d.0));
15056    }
15057
15058    #[test]
15059    fn mixing_names_on_one_item_or_field_is_refused() {
15060        let err = container_error(quote! {
15061            #[rustango(table = "t")] #[rgo(display = "name")] struct S;
15062        })
15063        .expect("a mix on an item must fail");
15064        assert!(
15065            err.contains("`#[rustango(...)]` and `#[rgo(...)]`"),
15066            "{err}"
15067        );
15068        let err = field_error(quote! { #[rgo(primary_key)] #[rustango(column = "c")] a: i64 })
15069            .expect("a mix on a field must fail");
15070        assert!(
15071            err.contains("`#[rgo(...)]` and `#[rustango(...)]`"),
15072            "{err}"
15073        );
15074        // Either name repeated alone is fine.
15075        assert_eq!(
15076            container_error(quote! { #[rgo(table = "t")] #[rgo(display = "name")] struct S; }),
15077            None
15078        );
15079        assert_eq!(
15080            field_error(quote! { #[rustango(primary_key)] #[rustango(column = "c")] a: i64 }),
15081            None
15082        );
15083    }
15084
15085    /// A hint quotes the attribute under the name the user wrote.
15086    #[test]
15087    fn hints_use_the_written_name() {
15088        let err = container_error(quote! { #[rgo(table = "t", unique_together = "a")] struct S; })
15089            .expect("one column must fail");
15090        assert!(err.contains("#[rgo(unique)]"), "{err}");
15091        let err =
15092            container_error(quote! { #[rustango(table = "t", index_together = "a")] struct S; })
15093                .expect("one column must fail");
15094        assert!(err.contains("#[rustango(index)]"), "{err}");
15095    }
15096}