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///     allow_anonymous, // or `read_only` / `permissions(...)`
71/// )]
72/// pub struct PostViewSet;
73///
74/// // Mount into your app:
75/// let app = Router::new()
76///     .merge(PostViewSet::router("/api/posts", pool.clone()));
77/// ```
78///
79/// Attributes:
80/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
81///   `SCHEMA` constant drives the endpoints.
82/// * `fields = "a, b, c"` — *required*. The only scalar fields list/retrieve
83///   JSON renders and create/update accept; other body keys are ignored.
84/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
85/// * `search_fields = "a, b"` — fields searched by `?search=...`.
86/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
87/// * `page_size = N` — default page size (default: 20; requests are capped
88///   at `ViewSet::max_page_size`, default 100).
89/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
90/// * `allow_anonymous` — flag; write actions without codenames are
91///   intended. Without it (or `read_only`) they refuse to mount.
92/// * `serializer = SomeSerializer` — render list / retrieve / create
93///   responses through a `#[derive(Serializer)]` type instead of the
94///   default field-level projection (`read_only` / `source` / `method`
95///   / `write_only` overrides apply). Tri-dialect. Requires the
96///   `serializer` feature.
97/// * `permissions(list = "...", retrieve = "...", create = "...",
98///   update = "...", destroy = "...")` — codenames required per action.
99#[proc_macro_derive(ViewSet, attributes(viewset))]
100pub fn derive_viewset(input: TokenStream) -> TokenStream {
101    let input = parse_macro_input!(input as DeriveInput);
102    expand_viewset(&input)
103        .unwrap_or_else(syn::Error::into_compile_error)
104        .into()
105}
106
107/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
108/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
109/// that walks every named field and:
110///
111/// * Parses the string value into the field's Rust type (`String`,
112///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
113///   nullable case).
114/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
115///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
116///   validators in declaration order, returning `FormError::Parse`
117///   on the first failure.
118///
119/// Example:
120///
121/// ```ignore
122/// #[derive(Form)]
123/// pub struct CreateItemForm {
124///     #[form(min_length = 1, max_length = 64)]
125///     pub name: String,
126///     #[form(min = 0, max = 150)]
127///     pub age: i32,
128///     pub active: bool,
129///     pub email: Option<String>,
130/// }
131///
132/// let parsed = CreateItemForm::parse(&form_map)?;
133/// ```
134#[proc_macro_derive(Form, attributes(form))]
135pub fn derive_form(input: TokenStream) -> TokenStream {
136    let input = parse_macro_input!(input as DeriveInput);
137    expand_form(&input)
138        .unwrap_or_else(syn::Error::into_compile_error)
139        .into()
140}
141
142/// Derive `rustango::serializer::ModelSerializer` for a struct.
143/// (intra-doc link disabled — the macro crate doesn't depend on
144/// `rustango` itself, so rustdoc can't resolve the path.)
145///
146/// # Container attribute (required)
147/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
148///
149/// # Field attributes
150/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
151/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
152/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
153/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
154/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
155/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
156/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
157/// - `#[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)
158/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
159/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
160///   write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
161///   no attr is given, override it when given (`min_length` is serializer-only)
162///
163/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
164/// It honours `#[serde(rename, rename_all, skip, skip_serializing, skip_serializing_if,
165/// skip_deserializing)]`; other `serde` keys are ignored.
166#[proc_macro_derive(Serializer, attributes(serializer, serde))]
167pub fn derive_serializer(input: TokenStream) -> TokenStream {
168    let input = parse_macro_input!(input as DeriveInput);
169    expand_serializer(&input)
170        .unwrap_or_else(syn::Error::into_compile_error)
171        .into()
172}
173
174/// Bake every `*.json` migration file in a directory into the binary
175/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
176/// of `(name, json_content)` pairs, lex-sorted by file stem.
177///
178/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
179/// behaviour as `migrate(pool, dir)` but with no filesystem access.
180/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
181/// (i.e. the crate that invokes the macro). Default is
182/// `"./migrations"` if no argument is supplied.
183///
184/// ```ignore
185/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
186/// // or:
187/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
188///
189/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
190/// ```
191///
192/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
193/// file's `name` field must equal its file stem, every `prev`
194/// reference must point to another migration in the same directory,
195/// and the JSON must parse. A broken chain — orphan `prev`, missing
196/// predecessor, malformed file — fails at macro-expansion time with
197/// a clear `compile_error!`. *No other Rust web framework
198/// validates migration chains at compile time*: Cot's migrations are
199/// imperative Rust code (no static chain), Loco's are SeaORM
200/// up/down (same), Rwf's are raw SQL (no chain at all).
201///
202/// Each migration is included via `include_str!` so cargo's rebuild
203/// detection picks up file *content* changes. Cargo does not watch
204/// directory listings, so a new migration file is missed until the
205/// crate rebuilds. Add a `build.rs` that tracks the directory:
206///
207/// ```ignore
208/// fn main() { println!("cargo:rerun-if-changed=migrations"); }
209/// ```
210///
211/// A missing directory embeds nothing and emits a deprecation warning.
212#[proc_macro]
213pub fn embed_migrations(input: TokenStream) -> TokenStream {
214    expand_embed_migrations(input.into())
215        .unwrap_or_else(syn::Error::into_compile_error)
216        .into()
217}
218
219/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
220/// compile time against typed columns. Issue #269 / T1.7.
221///
222/// Each invocation lowers to the equivalent typed-column method call:
223///
224/// ```ignore
225/// // These expand identically:
226/// Q!(User.email__icontains = "alice")
227/// User::email.ilike("%alice%")
228/// ```
229///
230/// Field-name typos fail the build (the macro emits `User::no_such_field`
231/// which doesn't exist) — the headline ergonomic win of this slice over
232/// filters keyed by a plain string.
233///
234/// # Supported lookup suffixes
235///
236/// * bare `=` / `__exact` → `.eq(value)`
237/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
238/// * `__ne` → `.ne(value)`
239/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
240/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
241/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
242/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
243/// * `__in` → `.is_in(iterable)`
244/// * `__not_in` → `.not_in(iterable)`
245/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
246/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
247/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
248///
249/// Unknown suffixes fail the build with a `compile_error!` pointing at
250/// the lookup token.
251///
252/// # Combine
253///
254/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
255/// `.and()` / `.or()` / `.not()` methods:
256///
257/// ```ignore
258/// User::objects()
259///     .where_(
260///         Q!(User.active = true)
261///             .and(Q!(User.email__icontains = "alice"))
262///     )
263///     .fetch(&pool).await?;
264/// ```
265///
266/// All emitted code routes through existing per-dialect writers — no new
267/// SQL emission machinery. Tri-dialect support is inherent.
268#[allow(non_snake_case)]
269#[proc_macro]
270pub fn Q(input: TokenStream) -> TokenStream {
271    expand_q(input.into())
272        .unwrap_or_else(syn::Error::into_compile_error)
273        .into()
274}
275
276/// `#[rustango::main]` — the runserver entrypoint. Wraps
277/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
278/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
279/// functions are zero-boilerplate. `RUST_LOG` and
280/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
281/// environment, else from `./.env` (only those keys are read):
282///
283/// ```ignore
284/// #[rustango::main]
285/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
286///     rustango::server::Builder::from_env().await?
287///         .migrate("migrations").await?
288///         .api(my_app::urls::api())
289///         .seed_with(my_app::seed::run).await?
290///         .serve("0.0.0.0:8080").await
291/// }
292/// ```
293///
294/// Optional `flavor = "current_thread"` passes through to
295/// `#[tokio::main]`; default is the multi-threaded runtime.
296///
297/// The body runs on a thread named `main` with an 8 MiB stack, since
298/// Windows gives the real main thread 1 MiB; `stack_size = <bytes>`
299/// overrides it. A panic there resumes on the real main thread.
300///
301/// `logging = false` suppresses the default subscriber, for apps
302/// that install their own. Needed whenever the body reaches
303/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
304/// macro installs gets there first, and `tracing` keeps the first
305/// one, so the `[logging]` section would otherwise be read and
306/// discarded (#1465).
307///
308/// ```ignore
309/// #[rustango::main(logging = false)]
310/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
311///     rustango::manage::Cli::new()
312///         .with_settings_from_env()
313///         .with_logging()          // now the first installer
314///         .run().await
315/// }
316/// ```
317///
318/// Pulls `tracing-subscriber` into the rustango crate behind the
319/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
320/// out get plain `#[tokio::main]` ergonomics without the dependency.
321#[proc_macro_attribute]
322pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
323    expand_main(args.into(), item.into())
324        .unwrap_or_else(syn::Error::into_compile_error)
325        .into()
326}
327
328fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
329    let mut input: syn::ItemFn = syn::parse2(item)?;
330    if input.sig.asyncness.is_none() {
331        return Err(syn::Error::new(
332            input.sig.ident.span(),
333            "`#[rustango::main]` must wrap an `async fn`",
334        ));
335    }
336
337    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
338    // to `#[tokio::main]`. Tokio's proc-macro internally emits
339    // `::tokio::*` paths that resolve against the user crate's deps,
340    // so calling it through the rustango re-export still requires the
341    // user to add tokio to their own Cargo.toml. Building the
342    // runtime ourselves keeps the dep transitive through the
343    // `runtime` feature on rustango.
344    //
345    // `flavor` and `logging`; anything else is a compile error.
346    let root = rustango_root();
347    let MainArgs {
348        flavor,
349        logging,
350        stack_size,
351    } = parse_main_args(&args)?;
352    let stack_size = match stack_size {
353        Some(n) => quote!(#n),
354        None => quote!(#root::__private_runtime::MAIN_STACK_SIZE),
355    };
356    let builder_call = match flavor {
357        Flavor::CurrentThread => quote! {
358            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
359        },
360        // Worker threads run handlers and `tokio::spawn` tasks: same stack.
361        Flavor::MultiThread => quote! {
362            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
363                .thread_stack_size(#stack_size)
364        },
365    };
366
367    // The user's fn becomes an inner `async fn` with its own return type;
368    // the outer sync fn runs it on a big-stack thread and reports its exit code.
369    let mut inner = input.clone();
370    inner.sig.ident = syn::Ident::new("__rustango_main_body", input.sig.ident.span());
371    // Lint attributes still apply to the body; the rest stay on the outer fn.
372    inner.attrs.retain(|a| {
373        ["allow", "expect", "deny", "warn", "forbid"]
374            .iter()
375            .any(|l| a.path().is_ident(l))
376    });
377    inner.vis = syn::Visibility::Inherited;
378    input.sig.asyncness = None;
379    input.sig.output = syn::parse2(quote!(-> ::std::process::ExitCode))?;
380
381    // `logging = false` drops the block entirely, so a later
382    // `Cli::with_logging()` is the first installer and its
383    // `[logging]` settings actually take effect (#1465).
384    let logging_prologue = if logging {
385        quote! {
386            // `Setup`'s defaults, not a copy of them: filter, format and
387            // the `Color::Auto` rule live in one place (#1493).
388            // `logging` and this macro share the `runtime` gate.
389            // The real `RUST_LOG` still wins over `./.env`'s (#2204).
390            let _ = #root::logging::Setup::new()
391                .with_default_env_filter(#root::__private_runtime::main_default_filter())
392                .with_format(#root::__private_runtime::main_format())
393                .install();
394        }
395    } else {
396        quote! {}
397    };
398
399    input.block = syn::parse2(quote! {{
400        { #logging_prologue }
401        #inner
402        #root::__private_runtime::run_on_main_thread(#stack_size, || {
403            let __rt = #builder_call
404                .enable_all()
405                .build()
406                .expect("failed to build tokio runtime");
407            ::std::process::Termination::report(__rt.block_on(__rustango_main_body()))
408        })
409    }})?;
410
411    Ok(quote! {
412        #input
413    })
414}
415
416enum Flavor {
417    MultiThread,
418    CurrentThread,
419}
420
421/// What `#[rustango::main]` was asked for.
422struct MainArgs {
423    flavor: Flavor,
424    logging: bool,
425    /// Bytes of stack for the main thread; `None` keeps the default.
426    stack_size: Option<usize>,
427}
428
429/// The smallest `stack_size` `#[rustango::main]` accepts: 64 KiB.
430const MIN_STACK_SIZE: usize = 64 * 1024;
431
432const MAIN_ARGS_HELP: &str =
433    "supported: `flavor = \"current_thread\" | \"multi_thread\"`, `logging = true | false`, `stack_size = <bytes>`";
434
435/// Parse `#[rustango::main(...)]`; an unknown or misspelt argument is an error.
436fn parse_main_args(args: &TokenStream2) -> syn::Result<MainArgs> {
437    use syn::parse::Parser as _;
438    let metas = syn::punctuated::Punctuated::<syn::MetaNameValue, syn::Token![,]>::parse_terminated
439        .parse2(args.clone())
440        .map_err(|e| {
441            syn::Error::new(
442                e.span(),
443                format!("#[rustango::main]: {e}; {MAIN_ARGS_HELP}"),
444            )
445        })?;
446    let mut out = MainArgs {
447        flavor: Flavor::MultiThread,
448        logging: true,
449        stack_size: None,
450    };
451    for meta in &metas {
452        let lit = match &meta.value {
453            syn::Expr::Lit(l) => Some(&l.lit),
454            _ => None,
455        };
456        let name = meta.path.get_ident().map(ToString::to_string);
457        match (name.as_deref(), lit) {
458            (Some("flavor"), Some(syn::Lit::Str(s))) if s.value() == "current_thread" => {
459                out.flavor = Flavor::CurrentThread;
460            }
461            (Some("flavor"), Some(syn::Lit::Str(s))) if s.value() == "multi_thread" => {
462                out.flavor = Flavor::MultiThread;
463            }
464            (Some("logging"), Some(syn::Lit::Bool(b))) => out.logging = b.value,
465            (Some("stack_size"), Some(syn::Lit::Int(n))) => {
466                let bytes = n.base10_parse::<usize>()?;
467                if bytes < MIN_STACK_SIZE {
468                    return Err(syn::Error::new(
469                        n.span(),
470                        format!("#[rustango::main]: `stack_size` must be at least {MIN_STACK_SIZE} bytes"),
471                    ));
472                }
473                out.stack_size = Some(bytes);
474            }
475            _ => {
476                return Err(syn::Error::new(
477                    meta.span(),
478                    format!("#[rustango::main]: unsupported argument; {MAIN_ARGS_HELP}"),
479                ))
480            }
481        }
482    }
483    Ok(out)
484}
485
486/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
487struct QInput {
488    base_path: syn::Path,
489    field: syn::Ident,
490    value: syn::Expr,
491}
492
493impl syn::parse::Parse for QInput {
494    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
495        let base_path: syn::Path = input.parse()?;
496        input.parse::<syn::Token![.]>()?;
497        let field: syn::Ident = input.parse()?;
498        input.parse::<syn::Token![=]>()?;
499        let value: syn::Expr = input.parse()?;
500        Ok(QInput {
501            base_path,
502            field,
503            value,
504        })
505    }
506}
507
508fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
509    let q: QInput = syn::parse2(input)?;
510    let root = rustango_root();
511    let field_str = ident_name(&q.field);
512    let field_span = q.field.span();
513    let (base, suffix) = match field_str.find("__") {
514        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
515        None => (field_str.as_str(), ""),
516    };
517    if base.is_empty() {
518        return Err(syn::Error::new(
519            field_span,
520            "Q!(): field name is empty before `__` suffix",
521        ));
522    }
523    let base_ident = ident_from_name(base, field_span);
524    let value = &q.value;
525    let path = &q.base_path;
526
527    // Most suffixes map directly to a Column method with the value
528    // forwarded unchanged. Some need value-shape massaging (wildcards
529    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
530    // ISNULL). Unknown suffixes fail the build.
531    let expanded = match suffix {
532        "" | "exact" => quote! {
533            #root::core::Column::eq(#path::#base_ident, #value)
534        },
535        "ne" => quote! {
536            #root::core::Column::ne(#path::#base_ident, #value)
537        },
538        "gt" => quote! {
539            #root::core::Column::gt(#path::#base_ident, #value)
540        },
541        "gte" => quote! {
542            #root::core::Column::gte(#path::#base_ident, #value)
543        },
544        "lt" => quote! {
545            #root::core::Column::lt(#path::#base_ident, #value)
546        },
547        "lte" => quote! {
548            #root::core::Column::lte(#path::#base_ident, #value)
549        },
550        // The escaped-LIKE lookups delegate to the typed Column
551        // methods, which escape the value and pair it with the
552        // *Escaped ops so `%` / `_` match literally on every dialect
553        // (#1257) — the macro no longer owns any pattern wrapping.
554        "iexact" => quote! {
555            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
556        },
557        "contains" => quote! {
558            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
559        },
560        "icontains" => quote! {
561            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
562        },
563        "startswith" => quote! {
564            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
565        },
566        "istartswith" => quote! {
567            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
568        },
569        "endswith" => quote! {
570            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
571        },
572        "iendswith" => quote! {
573            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
574        },
575        "in" => quote! {
576            #root::core::Column::is_in(#path::#base_ident, #value)
577        },
578        "not_in" => quote! {
579            #root::core::Column::not_in(#path::#base_ident, #value)
580        },
581        "isnull" => {
582            // Must be a bool literal at macro time so we can route to
583            // is_null vs is_not_null without a runtime branch.
584            let b = match value {
585                syn::Expr::Lit(syn::ExprLit {
586                    lit: syn::Lit::Bool(b),
587                    ..
588                }) => b.value(),
589                _ => {
590                    return Err(syn::Error::new_spanned(
591                        value,
592                        "Q!(): `__isnull` requires a `true` or `false` literal",
593                    ));
594                }
595            };
596            if b {
597                quote! { #root::core::Column::is_null(#path::#base_ident) }
598            } else {
599                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
600            }
601        }
602        "between" => {
603            // Accept a tuple literal `(lo, hi)`.
604            let tuple = match value {
605                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
606                _ => {
607                    return Err(syn::Error::new_spanned(
608                        value,
609                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
610                    ));
611                }
612            };
613            let lo = &tuple.elems[0];
614            let hi = &tuple.elems[1];
615            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
616        }
617        "regex" => quote! {
618            #root::core::Column::regex(#path::#base_ident, #value)
619        },
620        "iregex" => quote! {
621            #root::core::Column::iregex(#path::#base_ident, #value)
622        },
623        _ => {
624            return Err(syn::Error::new(
625                field_span,
626                format!(
627                    "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",
628                    suffix
629                ),
630            ));
631        }
632    };
633    Ok(expanded)
634}
635
636fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
637    // Default to "./migrations" if invoked without args.
638    let path_str = if input.is_empty() {
639        "./migrations".to_string()
640    } else {
641        let lit: LitStr = syn::parse2(input)?;
642        lit.value()
643    };
644
645    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
646        syn::Error::new(
647            proc_macro2::Span::call_site(),
648            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
649        )
650    })?;
651    let abs = std::path::Path::new(&manifest).join(&path_str);
652
653    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
654    if abs.is_dir() {
655        let read = std::fs::read_dir(&abs).map_err(|e| {
656            syn::Error::new(
657                proc_macro2::Span::call_site(),
658                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
659            )
660        })?;
661        for entry in read.flatten() {
662            let path = entry.path();
663            if !path.is_file() {
664                continue;
665            }
666            if path.extension().and_then(|s| s.to_str()) != Some("json") {
667                continue;
668            }
669            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
670                continue;
671            };
672            entries.push((stem.to_owned(), path));
673        }
674    }
675    entries.sort_by(|a, b| a.0.cmp(&b.0));
676
677    // Compile-time chain validation: read each migration's JSON,
678    // pull `name` and `prev` (file-stem-keyed for the chain check),
679    // and verify every `prev` points to another migration in the
680    // slice. Mismatches between the file stem and the embedded
681    // `name` field — and broken `prev` chains — fail at MACRO
682    // EXPANSION time so a misshapen migration set never compiles.
683    //
684    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
685    // migrations + a Rust proc-macro that reads them is a combo no
686    // other Rust web framework matches (Cot's are imperative Rust
687    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
688    // a static chain to validate).
689    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
690    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
691    let mut texts: Vec<String> = Vec::with_capacity(entries.len());
692    for (stem, path) in &entries {
693        let raw = std::fs::read(path)
694            .map_err(|e| e.to_string())
695            .and_then(decode_text)
696            .map_err(|e| {
697                syn::Error::new(
698                    proc_macro2::Span::call_site(),
699                    format!(
700                        "embed_migrations!: cannot read {} for chain validation: {e}",
701                        path.display()
702                    ),
703                )
704            })?;
705        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
706            syn::Error::new(
707                proc_macro2::Span::call_site(),
708                format!(
709                    "embed_migrations!: {} is not valid JSON: {e}",
710                    path.display()
711                ),
712            )
713        })?;
714        let name = json
715            .get("name")
716            .and_then(|v| v.as_str())
717            .ok_or_else(|| {
718                syn::Error::new(
719                    proc_macro2::Span::call_site(),
720                    format!(
721                        "embed_migrations!: {} is missing the `name` field",
722                        path.display()
723                    ),
724                )
725            })?
726            .to_owned();
727        if name != *stem {
728            return Err(syn::Error::new(
729                proc_macro2::Span::call_site(),
730                format!(
731                    "embed_migrations!: file stem `{stem}` does not match the migration's \
732                     `name` field `{name}` — rename the file or fix the JSON",
733                ),
734            ));
735        }
736        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
737        chain_names.push(name.clone());
738        prev_refs.push((name, prev));
739        texts.push(raw);
740    }
741
742    let name_set: std::collections::HashSet<&str> =
743        chain_names.iter().map(String::as_str).collect();
744    for (name, prev) in &prev_refs {
745        if let Some(p) = prev {
746            if !name_set.contains(p.as_str()) {
747                return Err(syn::Error::new(
748                    proc_macro2::Span::call_site(),
749                    format!(
750                        "embed_migrations!: broken migration chain — `{name}` declares \
751                         prev=`{p}` but no migration with that name exists in {}",
752                        abs.display()
753                    ),
754                ));
755            }
756        }
757    }
758
759    // The decoded text, so a BOM or UTF-16 file embeds as UTF-8;
760    // `include_bytes!` keeps the file a rebuild input.
761    let pairs: Vec<TokenStream2> = entries
762        .iter()
763        .zip(&texts)
764        .map(|((name, path), text)| {
765            let path_lit = path.display().to_string();
766            quote! { (#name, {
767                const _: &[u8] = ::core::include_bytes!(#path_lit);
768                #text
769            }) }
770        })
771        .collect();
772
773    // A wrong path used to embed nothing silently; a deprecation use is
774    // the only stable way for a proc macro to warn without breaking builds.
775    let missing_warning = if abs.is_dir() {
776        quote! {}
777    } else {
778        let note = format!(
779            "embed_migrations!: directory {} does not exist; no migrations are embedded",
780            abs.display()
781        );
782        quote! {
783            #[deprecated(note = #note)]
784            const __RUSTANGO_MIGRATIONS_DIR_MISSING: () = ();
785            #[allow(clippy::let_unit_value)]
786            let _ = __RUSTANGO_MIGRATIONS_DIR_MISSING;
787        }
788    };
789
790    Ok(quote! {
791        {
792            #missing_warning
793            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
794            __RUSTANGO_EMBEDDED
795        }
796    })
797}
798
799fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
800    let root = rustango_root();
801    let struct_name = &input.ident;
802
803    let Data::Struct(data) = &input.data else {
804        return Err(syn::Error::new_spanned(
805            struct_name,
806            "Model can only be derived on structs",
807        ));
808    };
809    let Fields::Named(named) = &data.fields else {
810        return Err(syn::Error::new_spanned(
811            struct_name,
812            "Model requires a struct with named fields",
813        ));
814    };
815
816    let mut container = parse_container_attrs(input)?;
817    let table = container
818        .table
819        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
820    let model_name = struct_name.to_string();
821
822    let collected = collect_fields(named, &table)?;
823    // An explicit `self_pk_column` must name a column of this model.
824    let explicit_pks = container
825        .reverse_has_relations
826        .iter()
827        .map(|r| (&r.self_pk_column, r.self_pk_span))
828        .chain(
829            container
830                .generic_has_relations
831                .iter()
832                .map(|r| (&r.self_pk_column, r.self_pk_span)),
833        );
834    for (column, span) in explicit_pks {
835        if let (Some(column), Some(span)) = (column, span) {
836            if !collected.column_entries.iter().any(|c| &c.column == column) {
837                return Err(syn::Error::new(
838                    span,
839                    format!("`self_pk_column = \"{column}\"` names no column of `{model_name}`"),
840                ));
841            }
842        }
843    }
844    // `reverse_has` / `generic_has` join on the PK column unless they name another.
845    if let Some((_, pk_column)) = &collected.primary_key {
846        for rel in &mut container.reverse_has_relations {
847            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
848        }
849        for rel in &mut container.generic_has_relations {
850            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
851        }
852    }
853
854    // Validate that #[rustango(display = "…")] names a real field.
855    if let Some((ref display, span)) = container.display {
856        if !collected.field_names.iter().any(|n| n == display) {
857            return Err(syn::Error::new(
858                span,
859                format!("`display = \"{display}\"` does not match any field on this struct"),
860            ));
861        }
862    }
863    let display = container.display.map(|(name, _)| name);
864    let app_label = container.app.clone();
865
866    // Validate admin field-name lists against declared field names.
867    // Note: `list_display` is intentionally NOT validated here. As of
868    // v0.32 it may also reference inventory-registered computed
869    // fields (via `register_admin_computed!`) whose existence the
870    // macro can't see at compile time — they're submitted from any
871    // crate that depends on rustango. The runtime list-view resolves
872    // unknown names against the inventory + silently drops the
873    // truly-bogus ones, which is the cheaper trade-off versus
874    // forcing a per-Model attr to opt out.
875    if let Some(admin) = &container.admin {
876        for (label, list) in [
877            ("search_fields", &admin.search_fields),
878            ("readonly_fields", &admin.readonly_fields),
879            ("list_filter", &admin.list_filter),
880        ] {
881            if let Some((names, span)) = list {
882                for name in names {
883                    if !collected.field_names.iter().any(|n| n == name) {
884                        return Err(syn::Error::new(
885                            *span,
886                            format!(
887                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
888                            ),
889                        ));
890                    }
891                }
892            }
893        }
894        if let Some((pairs, span)) = &admin.ordering {
895            for (name, _) in pairs {
896                if !collected.field_names.iter().any(|n| n == name) {
897                    return Err(syn::Error::new(
898                        *span,
899                        format!(
900                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
901                        ),
902                    ));
903                }
904            }
905        }
906        if let Some((groups, span)) = &admin.fieldsets {
907            for (_, fields) in groups {
908                for name in fields {
909                    if !collected.field_names.iter().any(|n| n == name) {
910                        return Err(syn::Error::new(
911                            *span,
912                            format!(
913                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
914                            ),
915                        ));
916                    }
917                }
918            }
919        }
920    }
921    if let Some(audit) = &container.audit {
922        if let Some((names, span)) = &audit.track {
923            for name in names {
924                if !collected.field_names.iter().any(|n| n == name) {
925                    return Err(syn::Error::new(
926                        *span,
927                        format!(
928                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
929                        ),
930                    ));
931                }
932            }
933        }
934    }
935
936    // Issue #291 / T2.5 — validate each `default_order` column name
937    // against the model's collected fields. Typos fail at macro-expand
938    // time, not at the database.
939    for (col, _desc, span) in &container.default_order {
940        if !collected.field_names.iter().any(|n| n == col) {
941            return Err(syn::Error::new(
942                *span,
943                format!(
944                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
945                ),
946            ));
947        }
948    }
949
950    check_known_columns(&container.indexes, &container.get_latest_by, &collected)?;
951
952    // Build the audit_track list for ModelSchema: None when no audit attr,
953    // Some(empty) when audit present without track, Some(names) when explicit.
954    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
955        audit
956            .track
957            .as_ref()
958            .map(|(names, _)| names.clone())
959            .unwrap_or_default()
960    });
961
962    // The upsert conflict target: a container-level composite unique,
963    // never a field `index(unique)` or a partial index PG can't target.
964    let upsert_unique: Option<Vec<String>> = container
965        .indexes
966        .iter()
967        .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
968        .map(|i| i.columns.clone());
969
970    // Merge field-level indexes into the container's index list.
971    let mut all_indexes: Vec<IndexAttr> = container.indexes;
972    for field in &named.named {
973        let ident = field.ident.as_ref().expect("named");
974        // The same default column as `process_field` (#1937).
975        let col = ident_name(ident);
976        // Re-parse field attrs to check for index flag
977        if let Ok(fa) = parse_field_attrs(field) {
978            if fa.index {
979                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
980                let auto_name = capped_index_name(if fa.index_unique {
981                    format!("{table}_{col_name}_uq_idx")
982                } else {
983                    format!("{table}_{col_name}_idx")
984                });
985                all_indexes.push(IndexAttr {
986                    span: field.span(),
987                    name: fa.index_name.or(Some(auto_name)),
988                    columns: vec![col_name],
989                    unique: fa.index_unique,
990                    method: fa.index_method,
991                    where_clause: None,
992                    include: Vec::new(),
993                });
994            }
995        }
996    }
997    // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
998    for field in &named.named {
999        let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
1000            continue;
1001        };
1002        if det.kind != DetectedKind::String || fa.max_length.is_some() {
1003            continue;
1004        }
1005        let col = fa
1006            .column
1007            .clone()
1008            .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
1009        // MySQL indexes every foreign-key column too.
1010        let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
1011        if fa.primary_key
1012            || fa.unique
1013            || relation
1014            || all_indexes.iter().any(|i| i.columns.contains(&col))
1015        {
1016            return Err(syn::Error::new_spanned(
1017                field,
1018                "a primary-key, unique, indexed or foreign-key `String` needs \
1019                 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
1020            ));
1021        }
1022    }
1023
1024    let model_impl = model_impl_tokens(
1025        struct_name,
1026        &model_name,
1027        &table,
1028        display.as_deref(),
1029        app_label.as_deref(),
1030        container.admin.as_ref(),
1031        &container.default_order,
1032        &collected.field_schemas,
1033        collected.soft_delete_column.as_deref(),
1034        container.permissions,
1035        audit_track_names.as_deref(),
1036        &container.m2m,
1037        &all_indexes,
1038        &container.checks,
1039        &container.excludes,
1040        &container.composite_fks,
1041        &container.generic_fks,
1042        container.scope.as_deref(),
1043        container.access.as_deref(),
1044        container.is_view,
1045        container.verbose_name.as_deref(),
1046        container.verbose_name_plural.as_deref(),
1047        container.managed,
1048        container.base_manager_name.as_deref(),
1049        container.order_with_respect_to.as_deref(),
1050        container.proxy,
1051        &container.required_db_features,
1052        container.required_db_vendor.as_deref(),
1053        container.default_related_name.as_deref(),
1054        container.db_table_comment.as_deref(),
1055        container
1056            .get_latest_by
1057            .as_ref()
1058            .map(|(c, d, _)| (c.as_str(), *d)),
1059        &container.extra_permissions,
1060        &container.default_permissions,
1061        &container.global_scopes,
1062        &container.reverse_has_relations,
1063        &container.generic_has_relations,
1064    );
1065    let module_ident = column_module_ident(struct_name);
1066    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
1067    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
1068        let track_set: Option<std::collections::HashSet<&str>> = audit
1069            .track
1070            .as_ref()
1071            .map(|(names, _)| names.iter().map(String::as_str).collect());
1072        collected
1073            .column_entries
1074            .iter()
1075            .filter(|c| {
1076                track_set
1077                    .as_ref()
1078                    .map_or(true, |s| s.contains(c.name.as_str()))
1079            })
1080            .collect()
1081    });
1082    let inherent_impl = inherent_impl_tokens(
1083        struct_name,
1084        &collected,
1085        collected.primary_key.as_ref(),
1086        &column_consts,
1087        audited_fields.as_deref(),
1088        upsert_unique.as_deref(),
1089        &container.manager_fns,
1090    );
1091    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
1092    let from_row_impl = from_row_impl_tokens(
1093        struct_name,
1094        &collected.from_row_inits,
1095        &collected.from_row_inits_my,
1096    );
1097    let reverse_helpers = reverse_helper_tokens(
1098        struct_name,
1099        &collected.fk_relations,
1100        container.default_related_name.as_deref(),
1101    )?;
1102    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
1103    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
1104    // Issue #817 — `#[rustango(through(...))]` accessors.
1105    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
1106    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
1107    let reverse_has_accessors = reverse_has_accessor_tokens(
1108        struct_name,
1109        &container.reverse_has_relations,
1110        &collected.column_entries,
1111    );
1112    let generic_fk_accessors = generic_fk_accessor_tokens(
1113        struct_name,
1114        &container.generic_fks,
1115        &collected.column_entries,
1116    );
1117
1118    Ok(quote! {
1119        #model_impl
1120        #inherent_impl
1121        #from_row_impl
1122        #column_module
1123        #reverse_helpers
1124        #m2m_accessors
1125        #generic_m2m_accessors
1126        #through_accessors
1127        #reverse_has_accessors
1128        #generic_fk_accessors
1129
1130        #root::core::inventory::submit! {
1131            // `module_path!()` evaluates at the registration site,
1132            // so a Model declared in `crate::blog::models` records
1133            // `"<crate>::blog::models"` and `resolved_app_label()`
1134            // can infer "blog" without an explicit attribute.
1135            #root::core::ModelEntry::new(
1136                <#struct_name as #root::core::Model>::SCHEMA,
1137                ::core::module_path!(),
1138            )
1139            .with_audited(
1140                <#struct_name as #root::core::Model>::__rustango_audited_update,
1141                <#struct_name as #root::core::Model>::__rustango_audited_delete,
1142            )
1143            .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1144            .with_audited_update_record(
1145                <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1146            )
1147            .with_audited_cascade(<#struct_name as #root::core::Model>::__rustango_audited_cascade)
1148        }
1149    })
1150}
1151
1152/// An index or `get_latest_by` name that is no column of the model is a
1153/// compile error; an expression index such as `lower(email)` is not checked.
1154fn check_known_columns(
1155    indexes: &[IndexAttr],
1156    get_latest_by: &Option<(String, bool, proc_macro2::Span)>,
1157    collected: &CollectedFields,
1158) -> syn::Result<()> {
1159    let is_column = |c: &str| collected.column_entries.iter().any(|e| e.column == c);
1160    // Only plain names: an expression index such as `lower(email)` is legal.
1161    let plain = |c: &str| c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
1162    let mut found: Vec<(proc_macro2::Span, String)> = Vec::new();
1163    for idx in indexes {
1164        for col in idx.columns.iter().filter(|c| plain(c) && !is_column(c)) {
1165            found.push((
1166                idx.span,
1167                format!("index column `{col}` is not a column of this model; its DDL will fail"),
1168            ));
1169        }
1170    }
1171    if let Some((col, _, span)) = get_latest_by {
1172        if !is_column(col) && !collected.field_names.iter().any(|n| n == col) {
1173            found.push((
1174                *span,
1175                format!("`get_latest_by`: `{col}` is not a field of this model"),
1176            ));
1177        }
1178    }
1179    let mut errors = found
1180        .into_iter()
1181        .map(|(span, note)| syn::Error::new(span, note));
1182    let Some(mut first) = errors.next() else {
1183        return Ok(());
1184    };
1185    first.extend(errors);
1186    Err(first)
1187}
1188
1189/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1190/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1191fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1192    let root = rustango_root();
1193    let parent_ty = &rel.parent_type;
1194    let field_ident = &rel.field_ident;
1195    let null_fk = if rel.nullable {
1196        quote! {
1197            if #receiver.#field_ident.is_none() {
1198                return ::core::result::Result::Ok(false);
1199            }
1200        }
1201    } else {
1202        quote! {}
1203    };
1204    quote! {
1205        #null_fk
1206        #root::sql::__rustango_require_join(
1207            row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1208        )?;
1209    }
1210}
1211
1212/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1213/// reused, and the deeper chain stitched into it; re-decoding it would
1214/// drop what that chain loaded.
1215fn loaded_reuse_tokens(
1216    rel: &FkRelation,
1217    receiver: &TokenStream2,
1218    load: &TokenStream2,
1219) -> TokenStream2 {
1220    let root = rustango_root();
1221    let field_ident = &rel.field_ident;
1222    let pattern = if rel.nullable {
1223        quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1224    } else {
1225        quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1226    };
1227    quote! {
1228        if let #pattern = &mut #receiver.#field_ident {
1229            if let ::core::option::Option::Some(__r) = __rest {
1230                let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1231            }
1232            return ::core::result::Result::Ok(true);
1233        }
1234    }
1235}
1236
1237/// Store the decoded `_parent` on the FK field, keeping the key the row
1238/// holds: with `on = <unique column>` it is not the parent's PK.
1239fn loaded_assign_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1240    let root = rustango_root();
1241    let field_ident = &rel.field_ident;
1242    if rel.nullable {
1243        quote! {
1244            if let ::core::option::Option::Some(__fk) = &mut #receiver.#field_ident {
1245                let _pk = ::core::clone::Clone::clone(__fk.pk_ref());
1246                *__fk = #root::sql::ForeignKey::loaded(_pk, _parent);
1247            }
1248        }
1249    } else {
1250        quote! {
1251            let _pk = ::core::clone::Clone::clone(#receiver.#field_ident.pk_ref());
1252            #receiver.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1253        }
1254    }
1255}
1256
1257/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1258/// matches `field_name` against the model's FK fields and, for a
1259/// match, decodes the FK target via the parent's macro-generated
1260/// `__rustango_from_aliased_row`, and stores `ForeignKey::Loaded` on
1261/// `self` under the key the row already holds.
1262///
1263/// Always emitted (with empty arms for FK-less models, which
1264/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1265/// trait bound on `fetch_on` is universally satisfied — users
1266/// never have to think about implementing it.
1267fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1268    let root = rustango_root();
1269    let arms = fk_relations.iter().map(|rel| {
1270        let parent_ty = &rel.parent_type;
1271        let field_ident = &rel.field_ident;
1272        let fk_name = ident_name(field_ident);
1273        let fk_col = fk_name.as_str();
1274        let guard = join_guard_tokens(rel, &quote!(self));
1275        let reuse = loaded_reuse_tokens(
1276            rel,
1277            &quote!(self),
1278            &quote!(#root::sql::LoadRelated::__rustango_load_related),
1279        );
1280        let assign = loaded_assign_tokens(rel, &quote!(self));
1281        quote! {
1282            #fk_col => {
1283                #guard
1284                #reuse
1285                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1286                // Audit #451 — multi-hop `select_related("a__b__c")`:
1287                // stitch the deeper relation onto this parent first,
1288                // decoding it at the accumulated `__next_alias`. The
1289                // parent type also impls `LoadRelated`, so this recurses
1290                // the FK chain to arbitrary depth.
1291                if let ::core::option::Option::Some(__r) = __rest {
1292                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1293                        &mut _parent, row, __r, &__next_alias,
1294                    )?;
1295                }
1296                #assign
1297                ::core::result::Result::Ok(true)
1298            }
1299        }
1300    });
1301    quote! {
1302        #[cfg(feature = "postgres")]
1303        impl #root::sql::LoadRelated for #struct_name {
1304            #[allow(unused_variables)]
1305            fn __rustango_load_related(
1306                &mut self,
1307                row: &#root::sql::sqlx::postgres::PgRow,
1308                field_name: &str,
1309                alias: &str,
1310            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1311                // Audit #451 — split the multi-hop path: `base` is the FK
1312                // on THIS model, `__rest` (if any) is the remaining chain
1313                // to stitch onto the loaded parent. `__next_alias` is the
1314                // accumulated join alias the parent's columns live under
1315                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1316                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1317                    match field_name.split_once("__") {
1318                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1319                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1320                    };
1321                let __next_alias: ::std::string::String = match __rest {
1322                    ::core::option::Option::Some(__r) => {
1323                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1324                        ::std::format!("{}__{}", alias, __rb)
1325                    }
1326                    ::core::option::Option::None => ::std::string::String::new(),
1327                };
1328                match __base {
1329                    #( #arms )*
1330                    _ => ::core::result::Result::Ok(false),
1331                }
1332            }
1333        }
1334    }
1335}
1336
1337/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1338/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1339/// which expands to a `LoadRelatedMy` impl when rustango is built with
1340/// the `mysql` feature, and to nothing otherwise. The decoded parent
1341/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1342/// decoder, also batch8) so the dual emission is symmetric across
1343/// backends.
1344fn load_related_impl_my_tokens(
1345    struct_name: &syn::Ident,
1346    fk_relations: &[FkRelation],
1347) -> TokenStream2 {
1348    let root = rustango_root();
1349    let arms = fk_relations.iter().map(|rel| {
1350        let parent_ty = &rel.parent_type;
1351        let field_ident = &rel.field_ident;
1352        let fk_name = ident_name(field_ident);
1353        let fk_col = fk_name.as_str();
1354        let guard = join_guard_tokens(rel, &quote!(__self));
1355        let reuse = loaded_reuse_tokens(
1356            rel,
1357            &quote!(__self),
1358            &quote!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1359        );
1360        let assign = loaded_assign_tokens(rel, &quote!(__self));
1361        // `self` IS hygiene-tracked through macro_rules — emitted from
1362        // a different context than the `&mut self` parameter inside
1363        // the macro_rules-expanded fn. Pass it through as `__self`
1364        // and let the macro_rules rebind it to the receiver.
1365        quote! {
1366            #fk_col => {
1367                #guard
1368                #reuse
1369                let mut _parent: #parent_ty =
1370                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1371                // Audit #451 — multi-hop: stitch the deeper relation onto
1372                // the parent at the accumulated alias (see PG twin).
1373                if let ::core::option::Option::Some(__r) = __rest {
1374                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1375                        &mut _parent, row, __r, &__next_alias,
1376                    )?;
1377                }
1378                #assign
1379                ::core::result::Result::Ok(true)
1380            }
1381        }
1382    });
1383    quote! {
1384        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1385            #( #arms )*
1386        });
1387    }
1388}
1389
1390/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1391/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1392/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1393fn load_related_impl_sqlite_tokens(
1394    struct_name: &syn::Ident,
1395    fk_relations: &[FkRelation],
1396) -> TokenStream2 {
1397    let root = rustango_root();
1398    let arms = fk_relations.iter().map(|rel| {
1399        let parent_ty = &rel.parent_type;
1400        let field_ident = &rel.field_ident;
1401        let fk_name = ident_name(field_ident);
1402        let fk_col = fk_name.as_str();
1403        let guard = join_guard_tokens(rel, &quote!(__self));
1404        let reuse = loaded_reuse_tokens(
1405            rel,
1406            &quote!(__self),
1407            &quote!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1408        );
1409        let assign = loaded_assign_tokens(rel, &quote!(__self));
1410        quote! {
1411            #fk_col => {
1412                #guard
1413                #reuse
1414                let mut _parent: #parent_ty =
1415                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1416                // Audit #451 — multi-hop: stitch the deeper relation onto
1417                // the parent at the accumulated alias (see PG twin).
1418                if let ::core::option::Option::Some(__r) = __rest {
1419                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1420                        &mut _parent, row, __r, &__next_alias,
1421                    )?;
1422                }
1423                #assign
1424                ::core::result::Result::Ok(true)
1425            }
1426        }
1427    });
1428    quote! {
1429        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1430            #( #arms )*
1431        });
1432    }
1433}
1434
1435/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1436/// matches `field_name` against the model's FK fields and returns
1437/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1438/// group children by parent PK.
1439///
1440/// Always emitted (with `_ => None` for FK-less models) so the
1441/// trait bound on `fetch_with_prefetch` is universally satisfied.
1442fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1443    let root = rustango_root();
1444    let arms = fk_relations.iter().map(|rel| {
1445        let field_ident = &rel.field_ident;
1446        let fk_name = ident_name(field_ident);
1447        let fk_col = fk_name.as_str();
1448        if rel.pk_kind == DetectedKind::I64 {
1449            // i64 FK — return the stored PK so prefetch_related can
1450            // group children by it. Nullable variant unwraps via
1451            // `as_ref().map(...)`: an unset (NULL) FK column yields
1452            // `None` and that child sits out of the grouping (correct
1453            // semantics — it has no parent to attach to).
1454            if rel.nullable {
1455                quote! {
1456                    #fk_col => self.#field_ident
1457                        .as_ref()
1458                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1459                }
1460            } else {
1461                quote! {
1462                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1463                }
1464            }
1465        } else {
1466            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1467            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1468            // i64-keyed grouping path — the trait signature is
1469            // `Option<i64>` and a non-i64 PK can't lower into it.
1470            // The FK still works for everything else (CRUD, lazy
1471            // load via `.get()`, select_related JOINs); only the
1472            // bulk prefetch grouper needs the integer key.
1473            quote! {
1474                #fk_col => ::core::option::Option::None,
1475            }
1476        }
1477    });
1478    // PK-type-agnostic version: every FK arm emits an
1479    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1480    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1481    // opt OUT of the legacy i64 method (it returns None) but opt IN
1482    // here.
1483    let value_arms = fk_relations.iter().map(|rel| {
1484        let field_ident = &rel.field_ident;
1485        let fk_name = ident_name(field_ident);
1486        let fk_col = fk_name.as_str();
1487        if rel.nullable {
1488            quote! {
1489                #fk_col => self.#field_ident
1490                    .as_ref()
1491                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1492                        #root::sql::ForeignKey::pk(fk)
1493                    )),
1494            }
1495        } else {
1496            quote! {
1497                #fk_col => ::core::option::Option::Some(
1498                    ::core::convert::Into::<#root::core::SqlValue>::into(
1499                        self.#field_ident.pk()
1500                    )
1501                ),
1502            }
1503        }
1504    });
1505    quote! {
1506        impl #root::sql::FkPkAccess for #struct_name {
1507            #[allow(unused_variables)]
1508            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1509                match field_name {
1510                    #( #arms )*
1511                    _ => ::core::option::Option::None,
1512                }
1513            }
1514            #[allow(unused_variables)]
1515            fn __rustango_fk_pk_value(
1516                &self,
1517                field_name: &str,
1518            ) -> ::core::option::Option<#root::core::SqlValue> {
1519                match field_name {
1520                    #( #value_arms )*
1521                    _ => ::core::option::Option::None,
1522                }
1523            }
1524        }
1525    }
1526}
1527
1528/// `related_name = "+"`: the FK gets no reverse accessor on its parent.
1529const NO_REVERSE_ACCESSOR: &str = "+";
1530
1531/// For every `ForeignKey<Parent>` field on `Child`, emit
1532/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1533/// Reads the column the FK points at (`on`, else the PK) and runs one
1534/// `SELECT … WHERE <fk_column> = $1`: one round trip, no N+1.
1535///
1536/// The accessor takes any `SqlExecutor`; `_set_pool` is its deprecated
1537/// old name.
1538fn reverse_helper_tokens(
1539    child_ident: &syn::Ident,
1540    fk_relations: &[FkRelation],
1541    default_related_name: Option<&str>,
1542) -> syn::Result<TokenStream2> {
1543    let root = rustango_root();
1544    // `related_name = "+"`: no accessor, so a parent from another crate works.
1545    let fk_relations: Vec<&FkRelation> = fk_relations
1546        .iter()
1547        .filter(|rel| rel.related_name.as_deref() != Some(NO_REVERSE_ACCESSOR))
1548        .collect();
1549    if fk_relations.is_empty() {
1550        return Ok(TokenStream2::new());
1551    }
1552    // Method-name resolution per FK (issue #816 + follow-up):
1553    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1554    //      itself — wins over everything else.
1555    //   2. Container-level `default_related_name = "..."` on the
1556    //      child. Applies to every FK on this model that didn't
1557    //      override.
1558    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1559    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1560    //      edge cases.
1561    //
1562    // The PG-on-executor variant keeps the resolved name; the
1563    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1564    // framework's convention for the `&Pool` flavor of every helper).
1565    let default_pg_suffix = default_related_name
1566        .map(str::to_owned)
1567        .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1568    let name_of = |rel: &FkRelation| {
1569        rel.related_name
1570            .clone()
1571            .unwrap_or_else(|| default_pg_suffix.clone())
1572    };
1573    // Two FKs to one parent would define the same method twice (E0592).
1574    for (i, rel) in fk_relations.iter().enumerate() {
1575        let parent_ty = &rel.parent_type;
1576        let parent = quote!(#parent_ty).to_string();
1577        let name = name_of(rel);
1578        if let Some(first) = fk_relations[..i].iter().find(|r| {
1579            let other = &r.parent_type;
1580            quote!(#other).to_string() == parent && name_of(r) == name
1581        }) {
1582            return Err(syn::Error::new_spanned(
1583                &rel.field_ident,
1584                format!(
1585                    "`{}` and `{}` both point at `{parent}` and would both add `{parent}::{name}`; \
1586                     give one `#[rustango(related_name = \"...\")]`, or `related_name = \"+\"` for none",
1587                    first.field_ident, rel.field_ident
1588                ),
1589            ));
1590        }
1591    }
1592    let impls = fk_relations.iter().map(|rel| {
1593        let pg_suffix = name_of(rel);
1594        let pool_suffix = format!("{}_pool", pg_suffix);
1595        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1596        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1597        let parent_ty = &rel.parent_type;
1598        let fk_name = ident_name(&rel.field_ident);
1599        let fk_col = fk_name.as_str();
1600        let doc = format!(
1601            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1602             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1603             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1604             further `{child_ident}::objects()` filters via direct queryset use."
1605        );
1606        quote! {
1607            impl #parent_ty {
1608                #[doc = #doc]
1609                ///
1610                /// Runs on any [`#root::sql::SqlExecutor`]: a `&Pool`, a held
1611                /// `PoolConn`, a `PoolTx` or a Postgres sqlx executor.
1612                ///
1613                /// # Errors
1614                /// Returns [`#root::sql::ExecError`] for SQL-writing
1615                /// or driver failures.
1616                pub async fn #pg_method_ident<'_c, _E: #root::sql::SqlExecutor<'_c>>(
1617                    &self,
1618                    _executor: _E,
1619                ) -> ::core::result::Result<
1620                    ::std::vec::Vec<#child_ident>,
1621                    #root::sql::ExecError,
1622                > {
1623                    let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1624                    #root::query::QuerySet::<#child_ident>::new()
1625                        .filter_op(#fk_col, #root::core::Op::Eq, _key)
1626                        .fetch_on(_executor)
1627                        .await
1628                }
1629
1630                /// Old name of the reverse accessor without `_pool`.
1631                ///
1632                /// # Errors
1633                /// As the accessor.
1634                #[deprecated(since = "0.62.0", note = "drop the `_pool` suffix")]
1635                pub async fn #pool_method_ident(
1636                    &self,
1637                    pool: &#root::sql::Pool,
1638                ) -> ::core::result::Result<
1639                    ::std::vec::Vec<#child_ident>,
1640                    #root::sql::ExecError,
1641                > {
1642                    self.#pg_method_ident(pool).await
1643                }
1644            }
1645        }
1646    });
1647    Ok(quote! { #( #impls )* })
1648}
1649
1650/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1651/// relation declared on the model.
1652/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1653/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1654///
1655/// Closes #239 + #240 — `comment.content_object` /
1656/// `comment.content_object = post` ergonomics on top of the existing
1657/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1658///
1659/// `column_entries` is passed so we can resolve each `ct_column` /
1660/// `pk_column` SQL name back to its Rust field ident — the macro
1661/// only sees the column-side strings in the attribute, but the
1662/// emitted accessor needs to read the actual struct field.
1663fn generic_fk_accessor_tokens(
1664    struct_name: &syn::Ident,
1665    generic_fks: &[GenericFkAttr],
1666    column_entries: &[ColumnEntry],
1667) -> TokenStream2 {
1668    let root = rustango_root();
1669    if generic_fks.is_empty() {
1670        return TokenStream2::new();
1671    }
1672    let methods = generic_fks.iter().filter_map(|gfk| {
1673        // Resolve `ct_column` + `pk_column` to the struct's Rust
1674        // field idents. A typo (column name doesn't match any field)
1675        // emits no method for that registration — the user will see
1676        // the compiler reject the SCHEMA literal anyway, so there's
1677        // a clear error path without us double-reporting.
1678        let ct_ident = column_entries
1679            .iter()
1680            .find(|c| c.column == gfk.ct_column)
1681            .map(|c| c.ident.clone())?;
1682        let pk_ident = column_entries
1683            .iter()
1684            .find(|c| c.column == gfk.pk_column)
1685            .map(|c| c.ident.clone())?;
1686
1687        let accessor_ident =
1688            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1689        let setter_ident =
1690            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1691        let name_literal = gfk.name.as_str();
1692
1693        Some(quote! {
1694            #[doc = concat!(
1695                "Resolve the polymorphic `",
1696                #name_literal,
1697                "` relation. Reads `self.",
1698                stringify!(#ct_ident),
1699                "` + `self.",
1700                stringify!(#pk_ident),
1701                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1702                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1703                #name_literal,
1704                "\", ...))]`."
1705            )]
1706            pub async fn #accessor_ident(
1707                &self,
1708                pool: &#root::sql::Pool,
1709            ) -> ::core::result::Result<
1710                ::core::option::Option<#root::__serde_json::Value>,
1711                #root::sql::ExecError,
1712            > {
1713                let gfk = #root::contenttypes::GenericForeignKey::new(
1714                    self.#ct_ident as i64,
1715                    self.#pk_ident as i64,
1716                );
1717                gfk.get_object(pool).await
1718            }
1719
1720            #[doc = concat!(
1721                "Set the polymorphic `",
1722                #name_literal,
1723                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1724                stringify!(#ct_ident),
1725                "` and `self.",
1726                stringify!(#pk_ident),
1727                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1728                #name_literal,
1729                "\", ...))]`."
1730            )]
1731            pub async fn #setter_ident<T: #root::core::Model>(
1732                &mut self,
1733                pool: &#root::sql::Pool,
1734                target_pk: i64,
1735            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1736                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1737                    pool,
1738                    target_pk,
1739                ).await?;
1740                self.#ct_ident = gfk.content_type_id as _;
1741                self.#pk_ident = gfk.object_pk as _;
1742                ::core::result::Result::Ok(())
1743            }
1744        })
1745    });
1746    quote! {
1747        impl #struct_name {
1748            #( #methods )*
1749        }
1750    }
1751}
1752
1753fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1754    let root = rustango_root();
1755    if m2m_relations.is_empty() {
1756        return TokenStream2::new();
1757    }
1758    let methods = m2m_relations.iter().map(|rel| {
1759        let method_name = format!("{}_m2m", rel.name);
1760        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1761        let through = rel.through.as_str();
1762        let src_col = rel.src.as_str();
1763        let dst_col = rel.dst.as_str();
1764        quote! {
1765            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1766                #root::sql::M2MManager {
1767                    src_pk: self.__rustango_pk_value(),
1768                    through: #through,
1769                    src_col: #src_col,
1770                    dst_col: #dst_col,
1771                }
1772            }
1773        }
1774    });
1775    quote! {
1776        impl #struct_name {
1777            #( #methods )*
1778        }
1779    }
1780}
1781
1782/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1783/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1784fn generic_m2m_accessor_tokens(
1785    struct_name: &syn::Ident,
1786    relations: &[GenericM2MAttr],
1787) -> TokenStream2 {
1788    let root = rustango_root();
1789    if relations.is_empty() {
1790        return TokenStream2::new();
1791    }
1792    let methods = relations.iter().map(|rel| {
1793        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1794        let through = rel.through.as_str();
1795        let pk_col = rel.pk_column.as_str();
1796        let ct_col = rel.ct_column.as_str();
1797        let related_col = rel.related_column.as_str();
1798        quote! {
1799            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1800                #root::sql::GenericM2MManager {
1801                    src_pk: self.__rustango_pk_value(),
1802                    src_schema: <Self as #root::core::Model>::SCHEMA,
1803                    through: #through,
1804                    pk_col: #pk_col,
1805                    ct_col: #ct_col,
1806                    dst_col: #related_col,
1807                }
1808            }
1809        }
1810    });
1811    // Lets an owner delete find and clear its junction rows.
1812    let entries = relations.iter().map(|rel| {
1813        let through = rel.through.as_str();
1814        let pk_col = rel.pk_column.as_str();
1815        let ct_col = rel.ct_column.as_str();
1816        quote! {
1817            #root::core::inventory::submit! {
1818                #root::core::GenericM2MEntry::new(
1819                    <#struct_name as #root::core::Model>::SCHEMA,
1820                    #through,
1821                    #pk_col,
1822                    #ct_col,
1823                )
1824            }
1825        }
1826    });
1827    quote! {
1828        impl #struct_name {
1829            #( #methods )*
1830        }
1831        #( #entries )*
1832    }
1833}
1834
1835/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1836/// associated functions for each `#[rustango(reverse_has(...))]`
1837/// attribute. Issue #830.
1838///
1839/// The two emitted functions return ready-to-use `WhereExpr` nodes
1840/// that downstream callers drop into
1841/// `QuerySet::<Self>::where_raw(...)`:
1842///
1843/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1844///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1845///   `whereHas` parity (without the closure-style sub-predicate
1846///   refinement — that's a follow-up; users can layer additional
1847///   predicates by constructing the SelectQuery themselves and
1848///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1849/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1850///   `whereDoesntHave` parity.
1851///
1852/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1853/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1854/// machinery threads the outer-table reference through automatically
1855/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1856fn reverse_has_accessor_tokens(
1857    struct_name: &syn::Ident,
1858    reverse_has_relations: &[ReverseHasAttr],
1859    columns: &[ColumnEntry],
1860) -> TokenStream2 {
1861    let root = rustango_root();
1862    if reverse_has_relations.is_empty() {
1863        return TokenStream2::new();
1864    }
1865    let methods = reverse_has_relations.iter().map(|rel| {
1866        let exists_name = format!("{}_exists_expr", rel.name);
1867        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1868        let count_name = format!("{}_count", rel.name);
1869        let fetch_name = format!("{}_fetch", rel.name);
1870        let first_name = format!("{}_first", rel.name);
1871        let pluck_name = format!("{}_pluck", rel.name);
1872        let accessor_name = rel.name.as_str();
1873        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1874        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1875        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1876        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1877        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1878        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1879        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1880        let child = &rel.child;
1881        let child_fk_column = rel.child_fk_column.as_str();
1882        let self_pk_column = rel.self_pk_column();
1883        // The instance helpers filter on the value the EXISTS form joins on.
1884        let self_value = columns
1885            .iter()
1886            .find(|c| c.column == self_pk_column)
1887            .map_or_else(
1888                || quote! { self.__rustango_pk_value() },
1889                |c| {
1890                    let ident = &c.ident;
1891                    quote! {
1892                        ::core::convert::Into::<#root::core::SqlValue>::into(
1893                            ::core::clone::Clone::clone(&self.#ident)
1894                        )
1895                    }
1896                },
1897            );
1898        let exists_doc = format!(
1899            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1900             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1901             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1902             filter to {struct_name}s with at least one matching child.",
1903        );
1904        let not_exists_doc = format!(
1905            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1906             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1907             <outer>.{self_pk_column})`. Drop into \
1908             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1909             {struct_name}s with **no** matching child.",
1910        );
1911        let count_doc = format!(
1912            "Eloquent `$model->{name}->count()` analog — returns \
1913             the number of `{child}` rows whose `{child_fk_column}` \
1914             matches this `{struct_name}` instance's `{self_pk_column}`. \
1915             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1916             {child_fk_column} = <self.{self_pk_column}>`.",
1917            name = rel.name,
1918        );
1919        let accessor_doc = format!(
1920            "Eloquent `$model->{name}` accessor — returns a \
1921             `QuerySet<{child}>` filtered to rows whose \
1922             `{child_fk_column}` matches this `{struct_name}` \
1923             instance's `{self_pk_column}`. **Chainable**: compose `.filter()` \
1924             / `.order_by()` / `.limit()` etc. on top, then call \
1925             `.fetch(&pool)` (the QuerySet trait method) when \
1926             done. For the simple \"fetch all\" hot path with no \
1927             further composition, prefer the bare-name \
1928             `{name}_fetch(&pool)` companion.",
1929            name = rel.name,
1930        );
1931        let fetch_doc = format!(
1932            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1933             over `{name}(&self).fetch(&pool)`. Use this when \
1934             you don't need further `.filter()` / `.order_by()` \
1935             composition; falls back to the chainable accessor when \
1936             you do. Avoids the `_pool` suffix on the most common \
1937             call-site shape.",
1938            name = rel.name,
1939        );
1940        quote! {
1941            #[doc = #accessor_doc]
1942            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1943                #root::query::QuerySet::<#child>::new()
1944                    .filter(#child_fk_column, #self_value)
1945            }
1946
1947            #[doc = #fetch_doc]
1948            pub async fn #fetch_ident(
1949                &self,
1950                pool: &#root::sql::Pool,
1951            ) -> ::core::result::Result<
1952                ::std::vec::Vec<#child>,
1953                #root::sql::ExecError,
1954            > {
1955                use #root::sql::FetcherPool as _;
1956                self.#accessor_ident().fetch(pool).await
1957            }
1958
1959            /// Eloquent `$model->relation->first()` / `hasOne`
1960            /// semantics — bare-name shortcut over
1961            /// `self.<name>().first(&pool)`. Returns `None` when no
1962            /// child rows match. Useful when the relation is
1963            /// nominally many-to-one in shape but at most one row is
1964            /// expected (latest comment, primary tag, etc.).
1965            pub async fn #first_ident(
1966                &self,
1967                pool: &#root::sql::Pool,
1968            ) -> ::core::result::Result<
1969                ::core::option::Option<#child>,
1970                #root::sql::ExecError,
1971            > {
1972                self.#accessor_ident().first(pool).await
1973            }
1974
1975            /// Eloquent `$model->relation->pluck($col)` — project a
1976            /// single column from the child rows into a `Vec<U>`.
1977            /// Skips the typed `Child` decode, which is cheaper when
1978            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1979            pub async fn #pluck_ident<U>(
1980                &self,
1981                col: &'static str,
1982                pool: &#root::sql::Pool,
1983            ) -> ::core::result::Result<
1984                ::std::vec::Vec<U>,
1985                #root::sql::ExecError,
1986            >
1987            where
1988                U: #root::sql::FlatScalar,
1989            {
1990                self.#accessor_ident()
1991                    .values_list_flat(col)
1992                    .fetch::<U>(pool)
1993                    .await
1994            }
1995
1996            #[doc = #exists_doc]
1997            pub fn #exists_ident() -> #root::core::WhereExpr {
1998                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1999                let child_schema =
2000                    <#child as #root::core::Model>::SCHEMA;
2001                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
2002                    lhs: Expr::Column(#child_fk_column),
2003                    op: Op::Eq,
2004                    rhs: Expr::OuterRef(#self_pk_column),
2005                });
2006                WhereExpr::Exists(::std::boxed::Box::new(inner))
2007            }
2008
2009            #[doc = #not_exists_doc]
2010            pub fn #not_exists_ident() -> #root::core::WhereExpr {
2011                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
2012                let child_schema =
2013                    <#child as #root::core::Model>::SCHEMA;
2014                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
2015                    lhs: Expr::Column(#child_fk_column),
2016                    op: Op::Eq,
2017                    rhs: Expr::OuterRef(#self_pk_column),
2018                });
2019                WhereExpr::NotExists(::std::boxed::Box::new(inner))
2020            }
2021
2022            #[doc = #count_doc]
2023            pub async fn #count_ident(
2024                &self,
2025                pool: &#root::sql::Pool,
2026            ) -> ::core::result::Result<
2027                i64,
2028                #root::sql::ExecError,
2029            > {
2030                use #root::sql::CounterPool as _;
2031                self.#accessor_ident().count(pool).await
2032            }
2033        }
2034    });
2035    quote! {
2036        impl #struct_name {
2037            #( #methods )*
2038        }
2039    }
2040}
2041
2042/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
2043/// `#[rustango(through(...))]` attribute. Issue #817.
2044///
2045/// Each accessor builds a correlated subquery via
2046/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
2047/// intermediate table, filters on the FK column pointing at this
2048/// model, and projects the intermediate PK. The outer queryset
2049/// filters the far model by its FK-to-intermediate column being in
2050/// that set.
2051///
2052/// The returned `QuerySet<Far>` is **chainable** — the subquery
2053/// lives inside a `where_raw` clause so the user's later
2054/// `.filter()` / `.order_by()` / `.limit()` compositions don't
2055/// disturb it.
2056///
2057/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
2058/// SQLite — no LATERAL or backend-specific syntax involved.
2059fn through_accessor_tokens(
2060    struct_name: &syn::Ident,
2061    through_relations: &[ThroughAttr],
2062) -> TokenStream2 {
2063    let root = rustango_root();
2064    if through_relations.is_empty() {
2065        return TokenStream2::new();
2066    }
2067    let methods = through_relations.iter().map(|rel| {
2068        let method_name = format!("{}_through", rel.name);
2069        let count_name = format!("{}_through_count", rel.name);
2070        let fetch_name = format!("{}_through_fetch", rel.name);
2071        let first_name = format!("{}_through_first", rel.name);
2072        let pluck_name = format!("{}_through_pluck", rel.name);
2073        let method_ident = syn::Ident::new(&method_name, struct_name.span());
2074        let count_ident = syn::Ident::new(&count_name, struct_name.span());
2075        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
2076        let first_ident = syn::Ident::new(&first_name, struct_name.span());
2077        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
2078        let far = &rel.far;
2079        let intermediate = &rel.intermediate;
2080        let far_fk_column = rel.far_fk_column.as_str();
2081        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
2082        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
2083        let doc = format!(
2084            "Eloquent `hasManyThrough` accessor — returns a \
2085             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
2086             instance through the intermediate `{intermediate}` table. \
2087             Generated SQL shape: \
2088             `… WHERE {far_fk_column} IN (SELECT \
2089             {intermediate_pk_column} FROM <{intermediate}> WHERE \
2090             {intermediate_fk_column} = self.pk)`. Chainable like any \
2091             other QuerySet — compose `.filter()` / `.order_by()` / \
2092             `.limit()` etc. on top.",
2093        );
2094        let count_doc = format!(
2095            "Eloquent `$model->{name}->count()` analog for the \
2096             through-relation — returns the number of `{far}` rows \
2097             reachable through `{intermediate}`. Equivalent to \
2098             `self.{name}_through().count(pool)` but spelled \
2099             as a bare instance method for parity with the \
2100             `reverse_has` `<name>_count` shape.",
2101            name = rel.name,
2102        );
2103        quote! {
2104            #[doc = #doc]
2105            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
2106                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
2107                let intermediate_schema =
2108                    <#intermediate as #root::core::Model>::SCHEMA;
2109                let sub = SelectQuery::new(intermediate_schema)
2110                    .where_clause(WhereExpr::Predicate(Filter::new(
2111                        #intermediate_fk_column,
2112                        Op::Eq,
2113                        self.__rustango_pk_value(),
2114                    )))
2115                    .projection(::std::vec![#intermediate_pk_column]);
2116                #root::query::QuerySet::<#far>::new().where_raw(
2117                    WhereExpr::InSubquery {
2118                        column: #far_fk_column,
2119                        negated: false,
2120                        subquery: ::std::boxed::Box::new(sub),
2121                    },
2122                )
2123            }
2124
2125            #[doc = #count_doc]
2126            pub async fn #count_ident(
2127                &self,
2128                pool: &#root::sql::Pool,
2129            ) -> ::core::result::Result<
2130                i64,
2131                #root::sql::ExecError,
2132            > {
2133                use #root::sql::CounterPool as _;
2134                self.#method_ident().count(pool).await
2135            }
2136
2137            /// Eloquent `$model->relation->get()` for the
2138            /// through-relation — bare-name hot-path over
2139            /// `self.<name>_through().fetch(&pool)`. Use this
2140            /// when you don't need further `.filter()` /
2141            /// `.order_by()` composition; falls back to the
2142            /// chainable accessor when you do. Avoids the `_pool`
2143            /// suffix on the most common call-site shape.
2144            pub async fn #fetch_ident(
2145                &self,
2146                pool: &#root::sql::Pool,
2147            ) -> ::core::result::Result<
2148                ::std::vec::Vec<#far>,
2149                #root::sql::ExecError,
2150            > {
2151                use #root::sql::FetcherPool as _;
2152                self.#method_ident().fetch(pool).await
2153            }
2154
2155            /// Eloquent `hasOneThrough` analog — bare-name shortcut
2156            /// over `self.<name>_through().first(&pool)`. Returns
2157            /// `None` when no far rows are reachable through the
2158            /// intermediate. Useful when at most one row is
2159            /// expected (latest comment by country, primary tag,
2160            /// etc.).
2161            pub async fn #first_ident(
2162                &self,
2163                pool: &#root::sql::Pool,
2164            ) -> ::core::result::Result<
2165                ::core::option::Option<#far>,
2166                #root::sql::ExecError,
2167            > {
2168                self.#method_ident().first(pool).await
2169            }
2170
2171            /// Pluck a single column from the far rows into `Vec<U>`
2172            /// — cheaper than the typed `<Far>` decode when only one
2173            /// scalar column is needed.
2174            pub async fn #pluck_ident<U>(
2175                &self,
2176                col: &'static str,
2177                pool: &#root::sql::Pool,
2178            ) -> ::core::result::Result<
2179                ::std::vec::Vec<U>,
2180                #root::sql::ExecError,
2181            >
2182            where
2183                U: #root::sql::FlatScalar,
2184            {
2185                self.#method_ident()
2186                    .values_list_flat(col)
2187                    .fetch::<U>(pool)
2188                    .await
2189            }
2190        }
2191    });
2192    quote! {
2193        impl #struct_name {
2194            #( #methods )*
2195        }
2196    }
2197}
2198
2199struct ColumnEntry {
2200    /// The struct field ident, used both for the inherent const name on
2201    /// the model and for the inner column type's name.
2202    ident: syn::Ident,
2203    /// The struct's field type, used as `Column::Value`.
2204    value_ty: Type,
2205    /// Rust-side field name (e.g. `"id"`).
2206    name: String,
2207    /// SQL-side column name (e.g. `"user_id"`).
2208    column: String,
2209    /// `#root::core::FieldType::I64` etc.
2210    field_type_tokens: TokenStream2,
2211    /// A bare or optional `Uuid` field; MySQL decodes it from text.
2212    uuid_column: bool,
2213}
2214
2215struct CollectedFields {
2216    field_schemas: Vec<TokenStream2>,
2217    from_row_inits: Vec<TokenStream2>,
2218    /// Aliased counterparts of `from_row_inits` — read columns via
2219    /// `format!("{prefix}__{col}")` aliases so a Model can be
2220    /// decoded from a JOINed row's projected target columns.
2221    from_aliased_row_inits: Vec<TokenStream2>,
2222    /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2223    from_row_inits_my: Vec<TokenStream2>,
2224    from_aliased_row_inits_my: Vec<TokenStream2>,
2225    /// Static column-name list — used by the simple insert path
2226    /// (no `Auto<T>` fields). Aligned with `insert_values`.
2227    insert_columns: Vec<TokenStream2>,
2228    /// Static `Into<SqlValue>` expressions, one per field. Aligned
2229    /// with `insert_columns`. Used by the simple insert path only.
2230    insert_values: Vec<TokenStream2>,
2231    /// Per-field push expressions for the dynamic (Auto-aware)
2232    /// insert path. Each statement either unconditionally pushes
2233    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2234    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2235    insert_pushes: Vec<TokenStream2>,
2236    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2237    /// when `has_auto == false`.
2238    returning_cols: Vec<TokenStream2>,
2239    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2240    /// field. Run after `insert_returning` to populate the model.
2241    auto_assigns: Vec<TokenStream2>,
2242    /// `(ident, column_literal, is_uuid)` for every Auto field. Used by
2243    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2244    /// instead of `self`.
2245    auto_field_idents: Vec<(syn::Ident, String, bool)>,
2246    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2247    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2248    /// back into the struct after insert (MySQL has no RETURNING → the
2249    /// field stays at its placeholder until the next read).
2250    generated_field_idents: Vec<(syn::Ident, String)>,
2251    /// First `Auto<T>` field the database fills, with its inner `T`: the
2252    /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2253    first_db_auto: Option<(syn::Ident, Type)>,
2254    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2255    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2256    /// `columns`).
2257    bulk_pushes_no_auto: Vec<TokenStream2>,
2258    /// Bulk-insert per-row pushes for **all fields including Auto**.
2259    /// Used by the all-Auto-Set bulk path (Auto col included with the
2260    /// caller-supplied value).
2261    bulk_pushes_all: Vec<TokenStream2>,
2262    /// Column-name literals for non-Auto fields only (paired with
2263    /// `bulk_pushes_no_auto`).
2264    bulk_columns_no_auto: Vec<TokenStream2>,
2265    /// Column-name literals for every field including Auto (paired
2266    /// with `bulk_pushes_all`).
2267    bulk_columns_all: Vec<TokenStream2>,
2268    /// Fills each `default_uuid_v7` field of `_row` in place, so a
2269    /// mutable bulk insert hands the PKs back (#1934).
2270    bulk_fill_in_place: Vec<TokenStream2>,
2271    /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2272    rust_filled_idents: Vec<syn::Ident>,
2273    /// `auto_now` fields: `upsert` stamps them before binding.
2274    auto_now_idents: Vec<syn::Ident>,
2275    /// `default_uuid_v7` fields: an `Unset` one gets a fresh id, so the
2276    /// upsert marks it `generated` for the MySQL key guard.
2277    uuid_v7_fields: Vec<(syn::Ident, String)>,
2278    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2279    /// + the loop that asserts every row matches. One pair per Auto
2280    /// field. Empty when `has_auto == false`.
2281    bulk_auto_uniformity: Vec<TokenStream2>,
2282    /// Identifier of the first Auto field, used as the witness for
2283    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2284    first_auto_ident: Option<syn::Ident>,
2285    /// `true` if any field on the struct is `Auto<T>`.
2286    has_auto: bool,
2287    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2288    /// Gates `save()` codegen — only Auto PKs let us infer
2289    /// insert-vs-update from the in-memory value.
2290    pk_is_auto: bool,
2291    /// `Assignment` constructors for every non-PK column. Drives the
2292    /// UPDATE branch of `save()`.
2293    update_assignments: Vec<TokenStream2>,
2294    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2295    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2296    upsert_update_columns: Vec<TokenStream2>,
2297    primary_key: Option<(syn::Ident, String)>,
2298    column_entries: Vec<ColumnEntry>,
2299    /// Rust-side field names, in declaration order. Used to validate
2300    /// container attributes like `display = "…"`.
2301    field_names: Vec<String>,
2302    /// FK fields on this child model. Drives the reverse-relation
2303    /// helper emit — for each FK, the macro adds an inherent
2304    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2305    /// method on the parent type.
2306    fk_relations: Vec<FkRelation>,
2307    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2308    /// the model has one. Drives emission of the `soft_delete_on` /
2309    /// `restore_on` inherent methods. At most one such column per
2310    /// model is allowed; collect_fields rejects duplicates.
2311    soft_delete_column: Option<String>,
2312    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2313    /// companion to `soft_delete_column` for emitting predicates
2314    /// that need to read the field off `&self` (e.g. `trashed()`).
2315    soft_delete_field_ident: Option<syn::Ident>,
2316}
2317
2318#[derive(Clone)]
2319struct FkRelation {
2320    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2321    /// helper is emitted as `impl <ParentType> { … }`.
2322    parent_type: Type,
2323    /// Rust field ident of the FK. Its name is the key for select_related,
2324    /// prefetch and filters; the SQL column may differ (#1936).
2325    field_ident: syn::Ident,
2326    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2327    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2328    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2329    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2330    pk_kind: DetectedKind,
2331    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2332    /// FK column). Drives the `Some(...)` wrapping in load_related
2333    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2334    /// the codegen matches the field's declared shape.
2335    nullable: bool,
2336    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2337    /// override. When set, the reverse helper picks this name instead
2338    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2339    /// #816 (issue's "Related" note re: per-FK override).
2340    related_name: Option<String>,
2341}
2342
2343fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2344    let root = rustango_root();
2345    let cap = named.named.len();
2346    let mut out = CollectedFields {
2347        field_schemas: Vec::with_capacity(cap),
2348        from_row_inits: Vec::with_capacity(cap),
2349        from_aliased_row_inits: Vec::with_capacity(cap),
2350        from_row_inits_my: Vec::with_capacity(cap),
2351        from_aliased_row_inits_my: Vec::with_capacity(cap),
2352        insert_columns: Vec::with_capacity(cap),
2353        insert_values: Vec::with_capacity(cap),
2354        insert_pushes: Vec::with_capacity(cap),
2355        returning_cols: Vec::new(),
2356        auto_assigns: Vec::new(),
2357        auto_field_idents: Vec::new(),
2358        generated_field_idents: Vec::new(),
2359        first_db_auto: None,
2360        bulk_pushes_no_auto: Vec::with_capacity(cap),
2361        bulk_pushes_all: Vec::with_capacity(cap),
2362        bulk_columns_no_auto: Vec::with_capacity(cap),
2363        bulk_columns_all: Vec::with_capacity(cap),
2364        bulk_fill_in_place: Vec::new(),
2365        rust_filled_idents: Vec::new(),
2366        auto_now_idents: Vec::new(),
2367        uuid_v7_fields: Vec::new(),
2368        bulk_auto_uniformity: Vec::new(),
2369        first_auto_ident: None,
2370        has_auto: false,
2371        pk_is_auto: false,
2372        update_assignments: Vec::with_capacity(cap),
2373        upsert_update_columns: Vec::with_capacity(cap),
2374        primary_key: None,
2375        column_entries: Vec::with_capacity(cap),
2376        field_names: Vec::with_capacity(cap),
2377        fk_relations: Vec::new(),
2378        soft_delete_column: None,
2379        soft_delete_field_ident: None,
2380    };
2381
2382    for field in &named.named {
2383        let info = process_field(field, table)?;
2384        out.field_names.push(ident_name(info.ident));
2385        out.field_schemas.push(info.schema);
2386        out.from_row_inits.push(info.from_row_init);
2387        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2388        out.from_row_inits_my.push(info.from_row_init_my);
2389        out.from_aliased_row_inits_my
2390            .push(info.from_aliased_row_init_my);
2391        if let Some(parent_ty) = info.fk_inner.clone() {
2392            out.fk_relations.push(FkRelation {
2393                parent_type: parent_ty,
2394                field_ident: info.ident.clone(),
2395                pk_kind: info.fk_pk_kind,
2396                nullable: info.nullable,
2397                related_name: info.related_name.clone(),
2398            });
2399        }
2400        if info.soft_delete {
2401            if out.soft_delete_column.is_some() {
2402                return Err(syn::Error::new_spanned(
2403                    field,
2404                    "only one field may be marked `#[rustango(soft_delete)]`",
2405                ));
2406            }
2407            out.soft_delete_column = Some(info.column.clone());
2408            out.soft_delete_field_ident = Some(info.ident.clone());
2409        }
2410        let column = info.column.as_str();
2411        let ident = info.ident;
2412        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2413        // skip every write path — Postgres recomputes the value
2414        // from EXPR. Push only the column-entry record (so typed
2415        // column constants still exist for filtering / projection)
2416        // and the schema literal (already pushed above) and move
2417        // on. No insert_columns/values, no insert_pushes, no
2418        // bulk_*, no update_assignments, no upsert_update_columns,
2419        // no returning_cols.
2420        if info.generated_as.is_some() {
2421            out.column_entries.push(ColumnEntry {
2422                ident: ident.clone(),
2423                value_ty: info.value_ty.clone(),
2424                name: ident_name(ident),
2425                column: info.column.clone(),
2426                field_type_tokens: info.field_type_tokens,
2427                uuid_column: info.uuid_column,
2428            });
2429            // #1028 — refresh the DB-computed value after insert on
2430            // RETURNING-capable backends. The column joins `returning_cols`
2431            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2432            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2433            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2434            // it keeps the placeholder until the next read.
2435            out.returning_cols.push(quote!(#column));
2436            out.generated_field_idents
2437                .push((ident.clone(), info.column.clone()));
2438            continue;
2439        }
2440        out.insert_columns.push(quote!(#column));
2441        out.insert_values.push(quote! {
2442            ::core::convert::Into::<#root::core::SqlValue>::into(
2443                ::core::clone::Clone::clone(&self.#ident)
2444            )
2445        });
2446        if info.auto {
2447            out.has_auto = true;
2448            if out.first_auto_ident.is_none() {
2449                out.first_auto_ident = Some(ident.clone());
2450            }
2451            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2452            // before binding, so the value is already in
2453            // `self.#ident` after the insert_push — RETURNING is
2454            // redundant. Skip adding this column to returning_cols /
2455            // auto_assigns / auto_field_idents to avoid (a) an
2456            // unnecessary RETURNING column on every dialect, and (b)
2457            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2458            // integer PK.
2459            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2460            // for the same reason: they are filled Rust-side below and
2461            // bound, so the value is already in `self` and RETURNING
2462            // would be a redundant column on every dialect.
2463            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2464                if out.first_db_auto.is_none() {
2465                    out.first_db_auto = auto_inner_type(info.value_ty)
2466                        .cloned()
2467                        .map(|ty| (ident.clone(), ty));
2468                }
2469                out.returning_cols.push(quote!(#column));
2470                out.auto_field_idents.push((
2471                    ident.clone(),
2472                    info.column.clone(),
2473                    info.fk_pk_kind == DetectedKind::Uuid,
2474                ));
2475                out.auto_assigns.push(quote! {
2476                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2477                });
2478            }
2479            if info.default_uuid_v7 {
2480                // Rust-side UUIDv7 generation (issue #823, Eloquent
2481                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2482                // and bind; Auto::Set → bind the user's value. The
2483                // column is ALWAYS present in the INSERT statement —
2484                // no RETURNING / no DB DEFAULT needed.
2485                out.insert_pushes.push(quote! {
2486                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2487                        self.#ident = #root::sql::Auto::Set(
2488                            #root::__uuid::Uuid::now_v7(),
2489                        );
2490                    }
2491                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2492                        _columns.push(#column);
2493                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2494                            ::core::clone::Clone::clone(_v)
2495                        ));
2496                    }
2497                });
2498            } else if info.auto_now_add || info.auto_now {
2499                // Rust-side write timestamp (#1464), mirroring
2500                // `default_uuid_v7` above. `auto_now` has always bound
2501                // `Utc::now()` on UPDATE but not on INSERT, so the
2502                // first write of an `updated_at` took the DB default
2503                // while every later one took the clock — the same
2504                // column, filled two different ways.
2505                //
2506                // The default is the problem. `CREATE TABLE IF NOT
2507                // EXISTS` leaves an upgraded table's
2508                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2509                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2510                // statement that changes it — so every insert relying
2511                // on that default wrote the legacy shape forever, while
2512                // the migrate sweep converted the rows around it. The
2513                // column went permanently mixed and `ORDER BY` inverted
2514                // the admin audit log (#1616 rework review,
2515                // correctness-001 / dialects-005 / security-002).
2516                //
2517                // Binding it here makes the stale default unreachable
2518                // rather than trying to migrate it: the timestamp comes
2519                // from the application, not the database.
2520                out.insert_pushes.push(quote! {
2521                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2522                        self.#ident = #root::sql::Auto::Set(
2523                            #root::__chrono::Utc::now(),
2524                        );
2525                    }
2526                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2527                        _columns.push(#column);
2528                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2529                            ::core::clone::Clone::clone(_v)
2530                        ));
2531                    }
2532                });
2533            } else {
2534                out.insert_pushes.push(quote! {
2535                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2536                        _columns.push(#column);
2537                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2538                            ::core::clone::Clone::clone(_v)
2539                        ));
2540                    }
2541                });
2542            }
2543            // …except the timestamp columns, which must appear in BOTH
2544            // paths (#1464). The path is chosen by the *first* Auto
2545            // field — in practice the PK — so a bulk insert of rows
2546            // with an unset PK took the no-auto branch and dropped the
2547            // timestamp column, falling back to the DB default that
2548            // this change exists to stop depending on.
2549            //
2550            // `rows` is borrowed immutably here, so this fills per row
2551            // at push time rather than writing back into the struct. An
2552            // explicitly-Set value is still honoured; only `Unset`
2553            // takes the clock.
2554            // `default_uuid_v7` likewise: its column has no DB default (#1934).
2555            if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2556                out.rust_filled_idents.push(ident.clone());
2557            }
2558            if info.auto_now {
2559                out.auto_now_idents.push(ident.clone());
2560            }
2561            if info.default_uuid_v7 {
2562                out.uuid_v7_fields
2563                    .push((ident.clone(), info.column.clone()));
2564            }
2565            let rust_fill = if info.default_uuid_v7 {
2566                Some(quote!(#root::__uuid::Uuid::now_v7()))
2567            } else if info.auto_now_add || info.auto_now {
2568                Some(quote!(#root::__chrono::Utc::now()))
2569            } else {
2570                None
2571            };
2572            // Bulk: Auto fields appear in the all-Set path. An unset filled
2573            // field takes its fill there too, not NULL (#1950).
2574            out.bulk_columns_all.push(quote!(#column));
2575            if let Some(fill) = &rust_fill {
2576                let push = quote! {
2577                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2578                        match &_row.#ident {
2579                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2580                            #root::sql::Auto::Unset => #fill,
2581                        }
2582                    ));
2583                };
2584                out.bulk_columns_no_auto.push(quote!(#column));
2585                out.bulk_pushes_no_auto.push(push.clone());
2586                out.bulk_pushes_all.push(push);
2587            } else {
2588                out.bulk_pushes_all.push(quote! {
2589                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2590                        ::core::clone::Clone::clone(&_row.#ident)
2591                    ));
2592                });
2593            }
2594            if info.default_uuid_v7 {
2595                out.bulk_fill_in_place.push(quote! {
2596                    if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2597                        _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2598                    }
2599                });
2600            }
2601            // Uniformity check: every row's Auto state must match the
2602            // first row's. Mixed Set/Unset within one bulk_insert is
2603            // rejected here so the column list stays consistent.
2604            // A filled field may be unset on any row, so it is exempt.
2605            let ident_clone = ident.clone();
2606            if rust_fill.is_none() {
2607                out.bulk_auto_uniformity.push(quote! {
2608                    for _r in rows.iter().skip(1) {
2609                        if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2610                            return ::core::result::Result::Err(
2611                                #root::sql::ExecError::Sql(
2612                                    #root::sql::SqlError::BulkAutoMixed
2613                                )
2614                            );
2615                        }
2616                    }
2617                });
2618            }
2619        } else {
2620            out.insert_pushes.push(quote! {
2621                _columns.push(#column);
2622                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2623                    ::core::clone::Clone::clone(&self.#ident)
2624                ));
2625            });
2626            // Bulk: non-Auto fields appear in BOTH paths.
2627            out.bulk_columns_no_auto.push(quote!(#column));
2628            out.bulk_columns_all.push(quote!(#column));
2629            let push_expr = quote! {
2630                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2631                    ::core::clone::Clone::clone(&_row.#ident)
2632                ));
2633            };
2634            out.bulk_pushes_no_auto.push(push_expr.clone());
2635            out.bulk_pushes_all.push(push_expr);
2636        }
2637        if info.primary_key {
2638            if out.primary_key.is_some() {
2639                return Err(syn::Error::new_spanned(
2640                    field,
2641                    "only one field may be marked `#[rustango(primary_key)]`",
2642                ));
2643            }
2644            out.primary_key = Some((ident.clone(), info.column.clone()));
2645            if info.auto {
2646                out.pk_is_auto = true;
2647            }
2648        } else if info.auto_now_add {
2649            // Immutable post-insert: skip from UPDATE entirely.
2650        } else if info.auto_now {
2651            // `auto_now` columns: bind `chrono::Utc::now()` on every
2652            // UPDATE, and write it back so `self` matches the row.
2653            out.update_assignments.push(quote! {
2654                #root::core::Assignment::new(
2655                    #column,
2656                    {
2657                        let _now = #root::__chrono::Utc::now();
2658                        self.#ident = #root::sql::Auto::Set(_now);
2659                        ::core::convert::Into::<#root::core::SqlValue>::into(_now)
2660                    },
2661                )
2662            });
2663            out.upsert_update_columns.push(quote!(#column));
2664        } else {
2665            out.update_assignments.push(quote! {
2666                #root::core::Assignment::new(
2667                    #column,
2668                    ::core::convert::Into::<#root::core::SqlValue>::into(
2669                        ::core::clone::Clone::clone(&self.#ident)
2670                    ),
2671                )
2672            });
2673            out.upsert_update_columns.push(quote!(#column));
2674        }
2675        out.column_entries.push(ColumnEntry {
2676            ident: ident.clone(),
2677            value_ty: info.value_ty.clone(),
2678            name: ident_name(ident),
2679            column: info.column.clone(),
2680            field_type_tokens: info.field_type_tokens,
2681            uuid_column: info.uuid_column,
2682        });
2683    }
2684    Ok(out)
2685}
2686
2687/// A derived index name within PG's 63 bytes (MySQL takes 64); a longer one
2688/// keeps a prefix plus a hash of the whole, so two long names stay distinct.
2689fn capped_index_name(name: String) -> String {
2690    const MAX: usize = 63;
2691    if name.len() <= MAX {
2692        return name;
2693    }
2694    // FNV-1a: stable across builds, unlike `DefaultHasher`.
2695    let hash = name.bytes().fold(0x811c_9dc5_u32, |h, b| {
2696        (h ^ u32::from(b)).wrapping_mul(0x0100_0193)
2697    });
2698    let mut cut = MAX - 9;
2699    while !name.is_char_boundary(cut) {
2700        cut -= 1;
2701    }
2702    format!("{}_{hash:08x}", &name[..cut])
2703}
2704
2705fn model_impl_tokens(
2706    struct_name: &syn::Ident,
2707    model_name: &str,
2708    table: &str,
2709    display: Option<&str>,
2710    app_label: Option<&str>,
2711    admin: Option<&AdminAttrs>,
2712    default_order: &[(String, bool, proc_macro2::Span)],
2713    field_schemas: &[TokenStream2],
2714    soft_delete_column: Option<&str>,
2715    permissions: bool,
2716    audit_track: Option<&[String]>,
2717    m2m_relations: &[M2MAttr],
2718    indexes: &[IndexAttr],
2719    checks: &[CheckAttr],
2720    excludes: &[ExcludeAttr],
2721    composite_fks: &[CompositeFkAttr],
2722    generic_fks: &[GenericFkAttr],
2723    scope: Option<&str>,
2724    access: Option<&str>,
2725    is_view: bool,
2726    verbose_name: Option<&str>,
2727    verbose_name_plural: Option<&str>,
2728    managed: bool,
2729    base_manager_name: Option<&str>,
2730    order_with_respect_to: Option<&str>,
2731    proxy: bool,
2732    required_db_features: &[String],
2733    required_db_vendor: Option<&str>,
2734    default_related_name: Option<&str>,
2735    db_table_comment: Option<&str>,
2736    get_latest_by: Option<(&str, bool)>,
2737    extra_permissions: &[(String, String)],
2738    default_permissions: &[String],
2739    global_scopes: &[GlobalScopeAttr],
2740    reverse_has_relations: &[ReverseHasAttr],
2741    generic_has_relations: &[GenericHasAttr],
2742) -> TokenStream2 {
2743    let root = rustango_root();
2744    let display_tokens = if let Some(name) = display {
2745        quote!(::core::option::Option::Some(#name))
2746    } else {
2747        quote!(::core::option::Option::None)
2748    };
2749    let app_label_tokens = if let Some(name) = app_label {
2750        quote!(::core::option::Option::Some(#name))
2751    } else {
2752        quote!(::core::option::Option::None)
2753    };
2754    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2755        quote!(::core::option::Option::Some(#col))
2756    } else {
2757        quote!(::core::option::Option::None)
2758    };
2759    let audit_track_tokens = match audit_track {
2760        None => quote!(::core::option::Option::None),
2761        Some(names) => {
2762            let lits = names.iter().map(|n| n.as_str());
2763            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2764        }
2765    };
2766    let admin_tokens = admin_config_tokens(admin);
2767    // Default `tenant` so single-tenant projects (no `scope` attr
2768    // anywhere) keep the v0.24.x behavior. Container-attr parser
2769    // already validated the value is "registry" or "tenant".
2770    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2771        Some("registry") => quote!(#root::core::ModelScope::Registry),
2772        _ => quote!(#root::core::ModelScope::Tenant),
2773    };
2774    // The container-attr parser already validated the value.
2775    let access_tokens = match access {
2776        Some("admin_only") => {
2777            quote!(::core::option::Option::Some(#root::core::TableAccess::AdminOnly))
2778        }
2779        Some("never") => quote!(::core::option::Option::Some(#root::core::TableAccess::Never)),
2780        Some(_) => quote!(::core::option::Option::Some(#root::core::TableAccess::Open)),
2781        None => quote!(::core::option::Option::None),
2782    };
2783    let verbose_name_tokens = optional_str(verbose_name);
2784    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2785    let base_manager_name_tokens = optional_str(base_manager_name);
2786    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2787    let required_db_features_lits: Vec<&str> =
2788        required_db_features.iter().map(String::as_str).collect();
2789    let required_db_vendor_tokens = optional_str(required_db_vendor);
2790    let default_related_name_tokens = optional_str(default_related_name);
2791    let db_table_comment_tokens = optional_str(db_table_comment);
2792    let get_latest_by_tokens = match get_latest_by {
2793        Some((col, desc)) => {
2794            quote!(::core::option::Option::Some((#col, #desc)))
2795        }
2796        None => quote!(::core::option::Option::None),
2797    };
2798    let extra_permission_tokens: Vec<_> = extra_permissions
2799        .iter()
2800        .map(|(c, l)| quote!((#c, #l)))
2801        .collect();
2802    let default_permission_tokens: Vec<_> = default_permissions
2803        .iter()
2804        .map(|action| quote!(#action))
2805        .collect();
2806    let indexes_tokens = indexes.iter().map(|idx| {
2807        // When no explicit `name = "..."` was given, derive a stable,
2808        // collision-free name from the table + columns
2809        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2810        // would clash the moment a model declares two unnamed indexes.
2811        let derived_name = idx
2812            .name
2813            .clone()
2814            .unwrap_or_else(|| capped_index_name(format!("{table}_{}_idx", idx.columns.join("_"))));
2815        let name = derived_name.as_str();
2816        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2817        let unique = idx.unique;
2818        // Map the parsed method string onto the IndexMethod enum
2819        // variant — kept at the codegen layer so the IR doesn't
2820        // carry the string form.
2821        let method_variant = match idx.method.as_str() {
2822            "gin" => quote!(#root::core::IndexMethod::Gin),
2823            "gist" => quote!(#root::core::IndexMethod::Gist),
2824            "brin" => quote!(#root::core::IndexMethod::Brin),
2825            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2826            "hash" => quote!(#root::core::IndexMethod::Hash),
2827            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2828            _ => quote!(#root::core::IndexMethod::BTree),
2829        };
2830        let where_clause = match &idx.where_clause {
2831            Some(s) => quote!(::core::option::Option::Some(#s)),
2832            None => quote!(::core::option::Option::None),
2833        };
2834        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2835        quote! {{
2836            let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2837            i.unique = #unique;
2838            i.method = #method_variant;
2839            i.where_clause = #where_clause;
2840            i.include = &[ #(#include_lits),* ];
2841            i
2842        }}
2843    });
2844    let checks_tokens = checks.iter().map(|c| {
2845        let name = c.name.as_str();
2846        let expr = c.expr.as_str();
2847        quote!(#root::core::CheckConstraint::new(#name, #expr))
2848    });
2849    let excludes_tokens = excludes.iter().map(|e| {
2850        let name = e.name.as_str();
2851        let using = e.using.as_str();
2852        let element_tokens = e.elements.iter().map(|(col, op)| {
2853            let col_s = col.as_str();
2854            let op_s = op.as_str();
2855            quote!((#col_s, #op_s))
2856        });
2857        let where_tokens = match e.where_clause.as_deref() {
2858            Some(w) => quote!(::core::option::Option::Some(#w)),
2859            None => quote!(::core::option::Option::None),
2860        };
2861        quote! {{
2862            let mut e = #root::core::ExclusionConstraint::new(
2863                #name,
2864                #using,
2865                &[ #(#element_tokens),* ],
2866            );
2867            e.where_clause = #where_tokens;
2868            e
2869        }}
2870    });
2871    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2872        let name = rel.name.as_str();
2873        let to = rel.to.as_str();
2874        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2875        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2876        quote! {
2877            #root::core::CompositeFkRelation::new(
2878                #name,
2879                #to,
2880                &[ #(#from_cols),* ],
2881                &[ #(#on_cols),* ],
2882            )
2883        }
2884    });
2885    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2886        let name = rel.name.as_str();
2887        let ct_col = rel.ct_column.as_str();
2888        let pk_col = rel.pk_column.as_str();
2889        quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2890    });
2891    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2892    // no `#[rustango(default_order = "...")]` attribute was supplied.
2893    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2894        let col_lit = col.as_str();
2895        quote! { (#col_lit, #desc) }
2896    });
2897
2898    // Issue #820 — `global_scopes` slice literal. Empty when no
2899    // `#[rustango(global_scope(...))]` attribute was supplied. The
2900    // `apply` path is re-emitted verbatim so it resolves in the
2901    // consumer's scope; the name is stored as a string literal.
2902    let global_scope_tokens = global_scopes.iter().map(|s| {
2903        let name = s.name.as_str();
2904        // The slice sits in a nested `const`, where `Self` does not resolve.
2905        let mut apply = s.apply.clone();
2906        if let Some(first) = apply.segments.first_mut() {
2907            if first.ident == "Self" {
2908                first.ident = struct_name.clone();
2909            }
2910        }
2911        quote!(#root::core::GlobalScope::new(#name, #apply))
2912    });
2913
2914    let m2m_tokens = m2m_relations.iter().map(|rel| {
2915        let name = rel.name.as_str();
2916        let to = rel.to.as_str();
2917        let through = rel.through.as_str();
2918        let src = rel.src.as_str();
2919        let dst = rel.dst.as_str();
2920        let auto_create = rel.auto_create;
2921        quote! {{
2922            let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2923            m.auto_create = #auto_create;
2924            m
2925        }}
2926    });
2927    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2928    // when the model declares `#[rustango(reverse_has(...))]`. Each
2929    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2930    // a const expression. Models without reverse_has fall through to
2931    // the trait's empty default — no override emitted.
2932    let reverse_relations_override = if reverse_has_relations.is_empty() {
2933        quote!()
2934    } else {
2935        let entries = reverse_has_relations.iter().map(|rel| {
2936            let name = rel.name.as_str();
2937            let child = &rel.child;
2938            let child_fk_column = rel.child_fk_column.as_str();
2939            let self_pk_column = rel.self_pk_column();
2940            quote! {
2941                #root::core::ReverseRelation::new(
2942                    #name,
2943                    <#child as #root::core::Model>::SCHEMA,
2944                    #child_fk_column,
2945                    #self_pk_column,
2946                )
2947            }
2948        });
2949        quote! {
2950            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2951                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2952                RELS
2953            }
2954        }
2955    };
2956    // Issue #830 — `Model::generic_reverse_relations()` override for
2957    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2958    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2959        quote!()
2960    } else {
2961        let entries = generic_has_relations.iter().map(|rel| {
2962            let name = rel.name.as_str();
2963            let child = &rel.child;
2964            let ct_column = rel.ct_column.as_str();
2965            let pk_column = rel.pk_column.as_str();
2966            let self_pk_column = rel.self_pk_column();
2967            quote! {
2968                #root::core::GenericReverseRelation::new(
2969                    #name,
2970                    <#child as #root::core::Model>::SCHEMA,
2971                    #ct_column,
2972                    #pk_column,
2973                    #self_pk_column,
2974                )
2975            }
2976        });
2977        quote! {
2978            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2979                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2980                RELS
2981            }
2982        }
2983    };
2984    let fields_slice = const_slice(
2985        &quote!(#root::core::FieldSchema),
2986        field_schemas.iter().cloned(),
2987    );
2988    let m2m_slice = const_slice(&quote!(#root::core::M2MRelation), m2m_tokens);
2989    let indexes_slice = const_slice(&quote!(#root::core::IndexSchema), indexes_tokens);
2990    let checks_slice = const_slice(&quote!(#root::core::CheckConstraint), checks_tokens);
2991    let excludes_slice = const_slice(&quote!(#root::core::ExclusionConstraint), excludes_tokens);
2992    let composite_fk_slice = const_slice(
2993        &quote!(#root::core::CompositeFkRelation),
2994        composite_fk_tokens,
2995    );
2996    let generic_fk_slice = const_slice(&quote!(#root::core::GenericRelation), generic_fk_tokens);
2997    let audited_update_override = if audit_track.is_some() {
2998        quote! {
2999            fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
3000                ::core::option::Option::Some(Self::__rustango_update_audited)
3001            }
3002            fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
3003                ::core::option::Option::Some(Self::__rustango_delete_audited)
3004            }
3005            fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
3006                ::core::option::Option::Some(Self::__rustango_create_audited)
3007            }
3008            fn __rustango_audited_update_record(
3009            ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
3010                ::core::option::Option::Some(Self::__rustango_update_record_audited)
3011            }
3012            fn __rustango_audited_cascade() -> ::core::option::Option<#root::audit::AuditedCascade> {
3013                ::core::option::Option::Some(Self::__rustango_cascade_audited)
3014            }
3015        }
3016    } else {
3017        quote!()
3018    };
3019    let global_scope_slice = const_slice(&quote!(#root::core::GlobalScope), global_scope_tokens);
3020    quote! {
3021        impl #root::core::Model for #struct_name {
3022            const SCHEMA: &'static #root::core::ModelSchema = &{
3023                #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
3024                    name: #model_name,
3025                    table: #table,
3026                    fields: #fields_slice,
3027                    display: #display_tokens,
3028                    app_label: #app_label_tokens,
3029                    admin: #admin_tokens,
3030                    soft_delete_column: #soft_delete_tokens,
3031                    permissions: #permissions,
3032                    audit_track: #audit_track_tokens,
3033                    m2m: #m2m_slice,
3034                    indexes: #indexes_slice,
3035                    check_constraints: #checks_slice,
3036                    exclusion_constraints: #excludes_slice,
3037                    composite_relations: #composite_fk_slice,
3038                    generic_relations: #generic_fk_slice,
3039                    scope: #scope_tokens,
3040                    default_order: &[ #(#default_order_tokens),* ],
3041                    is_view: #is_view,
3042                    verbose_name: #verbose_name_tokens,
3043                    verbose_name_plural: #verbose_name_plural_tokens,
3044                    managed: #managed,
3045                    base_manager_name: #base_manager_name_tokens,
3046                    order_with_respect_to: #order_with_respect_to_tokens,
3047                    proxy: #proxy,
3048                    required_db_features: &[ #(#required_db_features_lits),* ],
3049                    required_db_vendor: #required_db_vendor_tokens,
3050                    default_related_name: #default_related_name_tokens,
3051                    db_table_comment: #db_table_comment_tokens,
3052                    get_latest_by: #get_latest_by_tokens,
3053                    extra_permissions: &[ #(#extra_permission_tokens),* ],
3054                    default_permissions: &[ #(#default_permission_tokens),* ],
3055                    global_scopes: #global_scope_slice,
3056                    access: #access_tokens,
3057                })
3058            };
3059
3060            #reverse_relations_override
3061            #generic_reverse_relations_override
3062            #audited_update_override
3063        }
3064    }
3065}
3066
3067/// Emit the `admin: Option<&'static AdminConfig>` field for the model
3068/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
3069/// otherwise a static reference to a populated `AdminConfig`.
3070fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
3071    let root = rustango_root();
3072    let Some(admin) = admin else {
3073        return quote!(::core::option::Option::None);
3074    };
3075
3076    let list_display = admin
3077        .list_display
3078        .as_ref()
3079        .map(|(v, _)| v.as_slice())
3080        .unwrap_or(&[]);
3081    let list_display_lits = list_display.iter().map(|s| s.as_str());
3082
3083    let search_fields = admin
3084        .search_fields
3085        .as_ref()
3086        .map(|(v, _)| v.as_slice())
3087        .unwrap_or(&[]);
3088    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
3089
3090    let readonly_fields = admin
3091        .readonly_fields
3092        .as_ref()
3093        .map(|(v, _)| v.as_slice())
3094        .unwrap_or(&[]);
3095    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
3096
3097    let list_filter = admin
3098        .list_filter
3099        .as_ref()
3100        .map(|(v, _)| v.as_slice())
3101        .unwrap_or(&[]);
3102    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
3103
3104    let actions = admin
3105        .actions
3106        .as_ref()
3107        .map(|(v, _)| v.as_slice())
3108        .unwrap_or(&[]);
3109    let actions_lits = actions.iter().map(|s| s.as_str());
3110
3111    let fieldsets = admin
3112        .fieldsets
3113        .as_ref()
3114        .map(|(v, _)| v.as_slice())
3115        .unwrap_or(&[]);
3116    let fieldset_tokens = const_slice(
3117        &quote!(#root::core::Fieldset),
3118        fieldsets.iter().map(|(title, fields)| {
3119            let title = title.as_str();
3120            let field_lits = fields.iter().map(String::as_str);
3121            quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
3122        }),
3123    );
3124
3125    let list_display_links = admin
3126        .list_display_links
3127        .as_ref()
3128        .map(|(v, _)| v.as_slice())
3129        .unwrap_or(&[]);
3130    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
3131
3132    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
3133    let actions_on_top = admin.actions_on_top.unwrap_or(true);
3134    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
3135    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
3136
3137    let prepopulated = admin
3138        .prepopulated_fields
3139        .as_ref()
3140        .map(|(v, _)| v.as_slice())
3141        .unwrap_or(&[]);
3142    let prepopulated_tokens = const_slice(
3143        &quote!(#root::core::PrepopulatedField),
3144        prepopulated.iter().map(|(target, sources)| {
3145            let target = target.as_str();
3146            let source_lits = sources.iter().map(String::as_str);
3147            quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
3148        }),
3149    );
3150
3151    let raw_id_fields = admin
3152        .raw_id_fields
3153        .as_ref()
3154        .map(|(v, _)| v.as_slice())
3155        .unwrap_or(&[]);
3156    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
3157
3158    let autocomplete_fields = admin
3159        .autocomplete_fields
3160        .as_ref()
3161        .map(|(v, _)| v.as_slice())
3162        .unwrap_or(&[]);
3163    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
3164
3165    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
3166    let list_select_related_tokens = match admin.list_select_related.as_deref() {
3167        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
3168        Some("none") => quote!(#root::core::ListSelectRelated::None),
3169        Some(raw) => {
3170            let names: Vec<&str> = raw
3171                .split(',')
3172                .map(str::trim)
3173                .filter(|s| !s.is_empty())
3174                .collect();
3175            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
3176        }
3177    };
3178
3179    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
3180    // a Vec<(String, String)>. Empty / unset → no overrides.
3181    let formfield_pairs: Vec<(&str, &str)> = admin
3182        .formfield_overrides
3183        .as_ref()
3184        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
3185        .unwrap_or_default();
3186    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
3187        let field = *field;
3188        let widget = *widget;
3189        quote!((#field, #widget))
3190    });
3191
3192    let list_per_page = admin.list_per_page.unwrap_or(0);
3193
3194    let ordering_pairs = admin
3195        .ordering
3196        .as_ref()
3197        .map(|(v, _)| v.as_slice())
3198        .unwrap_or(&[]);
3199    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3200        let name = name.as_str();
3201        let desc = *desc;
3202        quote!((#name, #desc))
3203    });
3204
3205    quote! {
3206        ::core::option::Option::Some({
3207            const ADMIN: &#root::core::AdminConfig = &{
3208                #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3209                    list_display: &[ #( #list_display_lits ),* ],
3210                    search_fields: &[ #( #search_fields_lits ),* ],
3211                    list_per_page: #list_per_page,
3212                    ordering: &[ #( #ordering_tokens ),* ],
3213                    readonly_fields: &[ #( #readonly_fields_lits ),* ],
3214                    list_filter: &[ #( #list_filter_lits ),* ],
3215                    actions: &[ #( #actions_lits ),* ],
3216                    fieldsets: #fieldset_tokens,
3217                    list_display_links: &[ #( #list_display_links_lits ),* ],
3218                    search_help_text: #search_help_text,
3219                    actions_on_top: #actions_on_top,
3220                    actions_on_bottom: #actions_on_bottom,
3221                    date_hierarchy: #date_hierarchy,
3222                    prepopulated_fields: #prepopulated_tokens,
3223                    raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3224                    autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3225                    list_select_related: #list_select_related_tokens,
3226                    formfield_overrides: &[ #( #formfield_tokens ),* ],
3227                })
3228            };
3229            ADMIN
3230        })
3231    }
3232}
3233
3234fn inherent_impl_tokens(
3235    struct_name: &syn::Ident,
3236    fields: &CollectedFields,
3237    primary_key: Option<&(syn::Ident, String)>,
3238    column_consts: &TokenStream2,
3239    audited_fields: Option<&[&ColumnEntry]>,
3240    upsert_unique: Option<&[String]>,
3241    manager_fns: &[syn::Ident],
3242) -> TokenStream2 {
3243    let root = rustango_root();
3244
3245    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3246    // (audit-on-connection over &Pool needs a bi-dialect transaction
3247    // helper, deferred). Two body shapes:
3248    // - has_auto: build InsertQuery skipping Auto::Unset columns,
3249    //   request Auto cols in `returning`, dispatch via
3250    //   `insert_returning_pool`, then on the returned `PgRow` /
3251    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
3252    //   row OR drop the single i64 into the first Auto field on MySQL
3253    //   (multi-Auto models on MySQL error at runtime since
3254    //   `LAST_INSERT_ID()` only reports one)
3255    // - non-Auto: build InsertQuery with explicit columns/values and
3256    //   call `insert_pool` (no returning needed)
3257    // pool_insert_method body for the audited Auto-PK case is moved
3258    // to after audit_pair_tokens / audit_pk_to_string (they live
3259    // ~150 lines below). This block keeps the non-audited and
3260    // non-Auto branches in place — the audited Auto-PK arm is
3261    // computed below and merged via the dispatch helper variable.
3262    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3263        // Audited models with explicit (non-Auto) PKs go through
3264        // the non-Auto insert path below — the audit emit is one
3265        // round-trip after the INSERT inside the same tx via
3266        // audit::save_one_with_audit? No, INSERT semantics
3267        // differ. For non-Auto PK + audited, route through a
3268        // dedicated insert + audit emit on the same tx, but defer
3269        // the macro emission to the audit-bundle-aware block below
3270        // — this `quote!()` placeholder gets overwritten there.
3271        quote!()
3272    } else if audited_fields.is_some() && fields.has_auto {
3273        // Audited Auto-PK insert_pool — assembled after the audit
3274        // bundles. Placeholder; real emission below.
3275        quote!()
3276    } else if fields.has_auto {
3277        let pushes = &fields.insert_pushes;
3278        // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3279        let filled = &fields.rust_filled_idents;
3280        let was_unset: Vec<syn::Ident> = filled
3281            .iter()
3282            .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3283            .collect();
3284        let insert_or_ignore_call = quote! {
3285            #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3286            let mut _columns: ::std::vec::Vec<&'static str> =
3287                ::std::vec::Vec::new();
3288            let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3289                ::std::vec::Vec::new();
3290            #( #pushes )*
3291            let _query = #root::core::InsertQuery::new(
3292                <Self as #root::core::Model>::SCHEMA,
3293                _columns,
3294                _values,
3295            );
3296            let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3297            if !matches!(_result, ::core::result::Result::Ok(true)) {
3298                #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3299            }
3300            _result
3301        };
3302        // When every `Auto<T>` field is filled Rust-side
3303        // (`default_uuid_v7`, issue #823), there is no column to read
3304        // back from the database — `returning_cols` is empty. Route
3305        // through plain `insert_pool` instead of
3306        // `insert_returning_pool` to skip the redundant RETURNING /
3307        // LAST_INSERT_ID round-trip.
3308        if fields.returning_cols.is_empty() {
3309            quote! {
3310
3311
3312                /// Eloquent `Model::insertOrIgnore()` — same shape
3313                /// as the auto-PK branch above. Returns `Ok(true)`
3314                /// when inserted, `Ok(false)` when a conflict caused
3315                /// the INSERT to silently skip.
3316                ///
3317                /// # Errors
3318                /// As [`Self::insert`].
3319                pub async fn insert_or_ignore(
3320                    &mut self,
3321                    pool: &#root::sql::Pool,
3322                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3323                    #insert_or_ignore_call
3324                }
3325            }
3326        } else {
3327            quote! {
3328
3329
3330                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3331                /// row or silently skip on unique-constraint
3332                /// violation. Returns `Ok(true)` when a row was
3333                /// inserted, `Ok(false)` when a conflict caused the
3334                /// INSERT to silently skip.
3335                ///
3336                /// **Caveat on auto-PK models**: when the row is
3337                /// skipped (conflict), this instance's `Auto<T>`
3338                /// fields stay `Unset` — no PK is back-populated
3339                /// because the server didn't auto-assign one. For
3340                /// "insert then read back the PK or the existing
3341                /// row's PK", use the `upsert` family or
3342                /// `get_or_create`.
3343                ///
3344                /// # Errors
3345                /// As [`Self::insert`].
3346                pub async fn insert_or_ignore(
3347                    &mut self,
3348                    pool: &#root::sql::Pool,
3349                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3350                    #insert_or_ignore_call
3351                }
3352            }
3353        }
3354    } else {
3355        let insert_columns = &fields.insert_columns;
3356        let insert_values = &fields.insert_values;
3357        quote! {
3358
3359
3360            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3361            /// or silently skip on unique-constraint violation. Maps
3362            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3363            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3364            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3365            /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3366            ///
3367            /// Returns `Ok(true)` when a row was inserted,
3368            /// `Ok(false)` when a conflict caused the INSERT to
3369            /// silently skip.
3370            ///
3371            /// Use for "create-if-absent" patterns where you don't
3372            /// need the row back. For "find-or-create with the row
3373            /// returned", use the queryset-level
3374            /// `crate::sql::get_or_create` free function.
3375            ///
3376            /// # Errors
3377            /// As [`Self::insert`], plus any dialect-specific
3378            /// translation error from the ConflictClause writer.
3379            pub async fn insert_or_ignore(
3380                &self,
3381                pool: &#root::sql::Pool,
3382            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3383                let _query = #root::core::InsertQuery::new(
3384                    <Self as #root::core::Model>::SCHEMA,
3385                    ::std::vec![ #( #insert_columns ),* ],
3386                    ::std::vec![ #( #insert_values ),* ],
3387                );
3388                #root::sql::insert_or_ignore(pool, &_query).await
3389            }
3390        }
3391    };
3392
3393    // The tracked (column, JSON value) pairs, spliced only into
3394    // `__rustango_audit_pairs`. Empty when the model isn't audited.
3395    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3396        .map(|tracked| {
3397            tracked
3398                .iter()
3399                .map(|c| {
3400                    let column_lit = c.column.as_str();
3401                    let ident = &c.ident;
3402                    quote! {
3403                        (
3404                            #column_lit,
3405                            #root::core::__json_value(&self.#ident),
3406                        )
3407                    }
3408                })
3409                .collect()
3410        })
3411        .unwrap_or_default();
3412    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3413        if fields.pk_is_auto {
3414            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3415        } else {
3416            quote!(::std::format!("{}", &self.#pk_ident))
3417        }
3418    } else {
3419        quote!(::std::string::String::new())
3420    };
3421    // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3422    // is built, so every audited write path records the same shape.
3423    let delete_audited_body = if primary_key.is_some() {
3424        quote! {
3425            ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3426                pool,
3427                query,
3428                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3429            ))
3430        }
3431    } else {
3432        quote! {
3433            let _ = (pool, query);
3434            ::std::boxed::Box::pin(async {
3435                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3436                    table: <Self as #root::core::Model>::SCHEMA.table,
3437                })
3438            })
3439        }
3440    };
3441    let cascade_audited_body = if primary_key.is_some() {
3442        quote! {
3443            ::std::boxed::Box::pin(#root::audit::cascade_rows::<Self>(
3444                tx,
3445                <Self as #root::core::Model>::SCHEMA,
3446                rows,
3447                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3448            ))
3449        }
3450    } else {
3451        quote! {
3452            let _ = (tx, rows);
3453            ::std::boxed::Box::pin(async {
3454                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3455                    table: <Self as #root::core::Model>::SCHEMA.table,
3456                })
3457            })
3458        }
3459    };
3460    let update_audited_body = if primary_key.is_some() {
3461        quote! {
3462            ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3463                pool,
3464                query,
3465                move |_r: &Self| _r.__rustango_audit_entry(op),
3466            ))
3467        }
3468    } else {
3469        quote! {
3470            let _ = (pool, query, op);
3471            ::std::boxed::Box::pin(async {
3472                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3473                    table: <Self as #root::core::Model>::SCHEMA.table,
3474                })
3475            })
3476        }
3477    };
3478    let audit_entry_method = if audited_fields.is_some() {
3479        let pairs = audit_pair_tokens.iter();
3480        let pk_str = audit_pk_to_string.clone();
3481        quote! {
3482            /// This row's tracked `(column, value)` pairs.
3483            #[doc(hidden)]
3484            pub fn __rustango_audit_pairs(
3485                &self,
3486            ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3487                ::std::vec![ #( #pairs ),* ]
3488            }
3489
3490            /// This row's audit PK.
3491            #[doc(hidden)]
3492            pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3493                #pk_str
3494            }
3495
3496            /// Snapshot audit entry for `operation` on this row.
3497            #[doc(hidden)]
3498            pub fn __rustango_audit_entry(
3499                &self,
3500                operation: #root::audit::AuditOp,
3501            ) -> #root::audit::PendingEntry {
3502                #root::audit::PendingEntry {
3503                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3504                    entity_pk: self.__rustango_audit_pk(),
3505                    operation,
3506                    source: #root::audit::current_source(),
3507                    changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3508                }
3509            }
3510
3511            /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3512            #[doc(hidden)]
3513            pub fn __rustango_update_audited<'a>(
3514                pool: &'a #root::sql::Pool,
3515                query: &'a #root::core::UpdateQuery,
3516                op: #root::audit::AuditOp,
3517            ) -> ::std::pin::Pin<::std::boxed::Box<
3518                dyn ::core::future::Future<
3519                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3520                > + ::core::marker::Send + 'a,
3521            >> {
3522                #update_audited_body
3523            }
3524
3525            /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3526            #[doc(hidden)]
3527            pub fn __rustango_create_audited<'a, 't>(
3528                tx: &'a mut #root::sql::PoolTx<'t>,
3529                pk: #root::core::SqlValue,
3530            ) -> ::std::pin::Pin<::std::boxed::Box<
3531                dyn ::core::future::Future<
3532                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3533                > + ::core::marker::Send + 'a,
3534            >> {
3535                ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3536                    tx,
3537                    <Self as #root::core::Model>::SCHEMA,
3538                    pk,
3539                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3540                ))
3541            }
3542
3543            /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3544            #[doc(hidden)]
3545            pub fn __rustango_update_record_audited<'a, 't>(
3546                tx: &'a mut #root::sql::PoolTx<'t>,
3547                pk: #root::core::SqlValue,
3548            ) -> ::std::pin::Pin<::std::boxed::Box<
3549                dyn ::core::future::Future<
3550                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3551                > + ::core::marker::Send + 'a,
3552            >> {
3553                ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3554                    tx,
3555                    <Self as #root::core::Model>::SCHEMA,
3556                    pk,
3557                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3558                ))
3559            }
3560
3561            /// Rows an FK cascade will delete, behind `Model::__rustango_audited_cascade`.
3562            #[doc(hidden)]
3563            pub fn __rustango_cascade_audited<'a, 't>(
3564                tx: &'a mut #root::sql::PoolTx<'t>,
3565                rows: #root::core::WhereExpr,
3566            ) -> ::std::pin::Pin<::std::boxed::Box<
3567                dyn ::core::future::Future<
3568                    Output = ::core::result::Result<
3569                        ::std::vec::Vec<(#root::core::SqlValue, #root::audit::PendingEntry)>,
3570                        #root::sql::ExecError,
3571                    >,
3572                > + ::core::marker::Send + 'a,
3573            >> {
3574                #cascade_audited_body
3575            }
3576
3577            /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3578            #[doc(hidden)]
3579            pub fn __rustango_delete_audited<'a>(
3580                pool: &'a #root::sql::Pool,
3581                query: &'a #root::core::DeleteQuery,
3582            ) -> ::std::pin::Pin<::std::boxed::Box<
3583                dyn ::core::future::Future<
3584                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3585                > + ::core::marker::Send + 'a,
3586            >> {
3587                #delete_audited_body
3588            }
3589        }
3590    } else {
3591        quote!()
3592    };
3593    // Builds `_audit_entry`; `written` is the soft-delete column's new
3594    // value, recorded in place of the stale `self` field.
3595    let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3596        if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3597            quote! {
3598                let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3599                _audit_entry.set_tracked(#col, #v);
3600            }
3601        } else {
3602            quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3603        }
3604    };
3605    let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3606        .unwrap_or(#root::__serde_json::Value::Null));
3607    let restored_json = quote!(#root::__serde_json::Value::Null);
3608    // `&Pool` update runner for soft_delete / restore: one tx with the
3609    // audit row when audited, a plain UPDATE otherwise.
3610    let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3611        if audited_fields.is_some() {
3612            let entry = make_entry(op_path, Some(written));
3613            quote! {
3614                #entry
3615                #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3616            }
3617        } else {
3618            quote!(#root::sql::update_pool(pool, &_query).await)
3619        }
3620    };
3621    let pool_softdelete_run =
3622        make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3623    let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3624
3625    // `save_pool(&Pool)` — emitted for every model with a PK. The
3626    // audited `save_pool` is built below on the diff path; both share
3627    // `save_partial_methods`, which differ only in how they run `_query`.
3628    let save_ret = quote!(::core::result::Result<u64, #root::sql::ExecError>);
3629    let save_keep = quote!(matches!(_saved, ::core::result::Result::Ok(n) if n > 0));
3630    let save_partial_methods = |pk_ident: &syn::Ident,
3631                                pk_column_lit: &str,
3632                                run: TokenStream2|
3633     -> TokenStream2 {
3634        let assignments = &fields.update_assignments;
3635        // Pushed in step with `update_assignments`: one column each.
3636        let assignment_cols = &fields.upsert_update_columns;
3637        let write = restore_auto_now_unless(
3638            &fields.auto_now_idents,
3639            &save_ret,
3640            &save_keep,
3641            &quote! {
3642                // Build only the wanted assignments, so an unnamed
3643                // `auto_now` field is not stamped.
3644                let mut _filtered: ::std::vec::Vec<#root::core::Assignment> =
3645                    ::std::vec::Vec::with_capacity(_wanted_cols.len());
3646                #(
3647                    if _wanted_cols.contains(#assignment_cols) {
3648                        _filtered.push(#assignments);
3649                    }
3650                )*
3651                if _filtered.is_empty() {
3652                    // All field names valid, but they all map to
3653                    // non-assignable slots (PK column, computed/
3654                    // virtual fields, relations without an
3655                    // assignment). Same no-op as an empty list.
3656                    #root::__tracing::warn!(
3657                        target: "rustango::save_partial",
3658                        model = _schema.name,
3659                        "save_partial: every named field maps to a non-assignable column — no-op"
3660                    );
3661                    return ::core::result::Result::Ok(0);
3662                }
3663                let _query = #root::core::UpdateQuery::new(
3664                    _schema,
3665                    _filtered,
3666                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3667                        #pk_column_lit,
3668                        #root::core::Op::Eq,
3669                        ::core::convert::Into::<#root::core::SqlValue>::into(
3670                            ::core::clone::Clone::clone(&self.#pk_ident)
3671                        ),
3672                    )),
3673                );
3674                #run
3675            },
3676        );
3677        quote! {
3678            /// Save (UPDATE) only the listed Rust-side fields,
3679            /// leaving every other column untouched. Issue #66.
3680            ///
3681            /// `fields` are Rust-side struct field names; the macro
3682            /// resolves each to its SQL column. Unknown field
3683            /// names return [`#root::core::QueryError::UnknownField`]
3684            /// wrapped in `ExecError::Query`. An empty list is a
3685            /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3686            /// On an audited model the audit diff covers only these fields.
3687            ///
3688            /// Use this when:
3689            /// * you only mutated a couple of fields on a wide row
3690            ///   (avoid re-writing every column on every save), or
3691            /// * two writers diverged after their initial read and
3692            ///   you want to preserve the other writer's changes to
3693            ///   columns you didn't touch.
3694            ///
3695            /// Auto-PK models with an unset PK return
3696            /// [`#root::core::QueryError::UnknownField`] with
3697            /// field name `<pk>` — `save_partial` is an
3698            /// UPDATE-only path. Call [`Self::insert`]
3699            /// (or [`Self::save`] which dispatches based on
3700            /// PK state) for the INSERT case.
3701            ///
3702            /// # Errors
3703            /// As [`Self::save`], plus `UnknownField` for
3704            /// unknown / empty / Auto-Unset cases.
3705            pub async fn save_partial(
3706                &mut self,
3707                fields: &[&str],
3708                pool: &#root::sql::Pool,
3709            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3710                if fields.is_empty() {
3711                    #root::__tracing::warn!(
3712                        target: "rustango::save_partial",
3713                        model = <Self as #root::core::Model>::SCHEMA.name,
3714                        "save_partial called with empty field list — no-op"
3715                    );
3716                    return ::core::result::Result::Ok(0);
3717                }
3718                let _schema = <Self as #root::core::Model>::SCHEMA;
3719                // Validate field names against the schema.
3720                let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3721                    ::std::collections::HashSet::with_capacity(fields.len());
3722                for f in fields {
3723                    match _schema.field(f) {
3724                        ::core::option::Option::Some(fs) => {
3725                            _wanted_cols.insert(fs.column);
3726                        }
3727                        ::core::option::Option::None => {
3728                            return ::core::result::Result::Err(
3729                                #root::sql::ExecError::Query(
3730                                    #root::core::QueryError::UnknownField {
3731                                        model: _schema.name,
3732                                        field: (*f).to_owned(),
3733                                    }
3734                                )
3735                            );
3736                        }
3737                    }
3738                }
3739                #write
3740            }
3741
3742            /// Typed-column counterpart of [`Self::save_partial`] —
3743            /// issue #67. `fields` is a tuple of [`Column`]
3744            /// constants whose `Model` matches `Self`; typos and
3745            /// model mismatches surface at *compile time*
3746            /// (`Author::name` inside a `Post::save_partial_typed`
3747            /// call is a type error, no runtime check).
3748            ///
3749            /// ```ignore
3750            /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3751            /// ```
3752            ///
3753            /// Lowers to [`Self::save_partial`] under the hood — the
3754            /// tuple is reduced to a `&[&str]` slice of Rust-side
3755            /// field names and forwarded.
3756            ///
3757            /// [`Column`]: #root::core::Column
3758            ///
3759            /// # Errors
3760            /// As [`Self::save_partial`].
3761            pub async fn save_partial_typed<
3762                L: #root::core::TypedFieldList<Self>,
3763            >(
3764                &mut self,
3765                fields: L,
3766                pool: &#root::sql::Pool,
3767            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3768                let _names = fields.rust_field_names();
3769                let _refs: ::std::vec::Vec<&str> =
3770                    _names.iter().copied().collect();
3771                self.save_partial(&_refs, pool).await
3772            }
3773        }
3774    };
3775    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3776        let pk_column_lit = pk_col.as_str();
3777        if audited_fields.is_some() {
3778            quote!()
3779        } else {
3780            let partial = save_partial_methods(
3781                pk_ident,
3782                pk_column_lit,
3783                quote!(#root::sql::update_pool(pool, &_query).await),
3784            );
3785            quote! {
3786                /// Save this row to its table against either backend.
3787                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3788                /// `UPDATE` keyed on the PK; global scopes do not apply.
3789                /// A PK with no row is `RowNotFound`: use `insert` for a new row.
3790                ///
3791                /// # Errors
3792                /// As [`Self::save`].
3793                pub async fn save(
3794                    &mut self,
3795                    pool: &#root::sql::Pool,
3796                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3797                    self.__rustango_save_exec(pool).await
3798                }
3799
3800                #partial
3801            }
3802        }
3803    } else {
3804        quote!()
3805    };
3806
3807    // Audited `insert_pool` (overrides the placeholder set higher up
3808    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3809    // audited models get insert_pool routing through
3810    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3811    // + auto-PK readback + audit emit). Snapshot-style audit (the
3812    // PendingEntry's `changes` carries post-write field values).
3813    let pool_insert_method = if audited_fields.is_some() {
3814        if let Some(_) = primary_key {
3815            // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
3816            let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
3817            {
3818                fields.returning_cols.clone()
3819            } else {
3820                // Non-Auto-PK: still need RETURNING something for the
3821                // audit helper's contract (it errors on empty
3822                // returning). Return the PK column so the audit row
3823                // can carry the assigned PK back. Some non-Auto PKs
3824                // are server-side-default (e.g. UUIDv4 default), so
3825                // RETURNING is genuinely useful.
3826                primary_key
3827                    .map(|(_, col)| {
3828                        let lit = col.as_str();
3829                        vec![quote!(#lit)]
3830                    })
3831                    .unwrap_or_default()
3832            };
3833            // A failed COMMIT rolls the row back: put the read-back values back too.
3834            let assigned: Vec<syn::Ident> = fields
3835                .auto_field_idents
3836                .iter()
3837                .map(|(ident, _, _)| ident.clone())
3838                .collect();
3839            let body = restore_auto_now_unless(
3840                &assigned,
3841                &quote!(::core::result::Result<(), #root::sql::ExecError>),
3842                &quote!(_saved.is_ok()),
3843                &quote! {
3844                    let mut _query = self.__rustango_insert_query();
3845                    // The audit row needs the PK even when Rust filled it.
3846                    if _query.returning.is_empty() {
3847                        _query.returning = ::std::vec![ #( #returning_cols ),* ];
3848                    }
3849                    #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self, _op| {
3850                        _m.__rustango_audit_entry(_op)
3851                    })
3852                    .await
3853                },
3854            );
3855            quote! {
3856                /// Insert this row on any backend, with its audit row in the
3857                /// same transaction.
3858                ///
3859                /// Snapshot-style audit (post-write field values).
3860                ///
3861                /// # Errors
3862                /// As [`Self::insert`].
3863                pub async fn insert(
3864                    &mut self,
3865                    pool: &#root::sql::Pool,
3866                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3867                    #body
3868                }
3869            }
3870        } else {
3871            quote!()
3872        }
3873    } else {
3874        // Keep the non-audited pool_insert_method we built earlier.
3875        pool_insert_method
3876    };
3877
3878    // Update audited save_pool: now that insert_pool is wired for
3879    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3880    // insert_pool. Non-audited save_pool already does this.
3881    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3882    // Replaces the snapshot-only emission with a per-backend transaction
3883    // body that:
3884    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3885    //     column), capturing BEFORE values
3886    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3887    //  3. builds AFTER pairs from &self
3888    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3889    //     AuditOp::Update + diff_changes(...) on the same tx connection
3890    //  5. commits
3891    //
3892    // Per-backend arms inline the SQL string + placeholder shape, then
3893    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3894    // is polymorphic over Row type — the same tokens work against
3895    // PgRow and MySqlRow as long as the field's Rust type implements
3896    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3897    // primitives + chrono/uuid/serde_json::Value all do).
3898    let pool_save_method = if let Some(tracked) = audited_fields {
3899        if let Some((pk_ident, pk_col)) = primary_key {
3900            let pk_column_lit = pk_col.as_str();
3901            let partial = save_partial_methods(
3902                pk_ident,
3903                pk_column_lit,
3904                quote! {
3905                    self.__rustango_save_with_diff(
3906                        pool,
3907                        &_query,
3908                        ::core::option::Option::Some(&_wanted_cols),
3909                    )
3910                    .await
3911                },
3912            );
3913            // Per-tracked-column BEFORE-pair token list. Each entry
3914            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3915            // The Row alias resolves to PgRow / MySqlRow per call site,
3916            // so the same template generates both the PG and MySQL bodies.
3917            let mk_before_pairs =
3918                |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
3919                    tracked
3920                        .iter()
3921                        .map(|c| {
3922                            let getter = getter(c);
3923                            let column_lit = c.column.as_str();
3924                            let value_ty = &c.value_ty;
3925                            quote! {
3926                                (
3927                                    #column_lit,
3928                                    match #getter::<#value_ty>(
3929                                        _audit_before_row, #column_lit,
3930                                    ) {
3931                                        ::core::result::Result::Ok(v) => {
3932                                            #root::core::__json_value(&v)
3933                                        }
3934                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3935                                    },
3936                                )
3937                            }
3938                        })
3939                        .collect()
3940                };
3941            let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
3942            let before_pairs_my = mk_before_pairs(&|c| {
3943                if c.uuid_column {
3944                    quote!(#root::sql::try_get_flat_my)
3945                } else {
3946                    quote!(#root::sql::try_get_returning_my)
3947                }
3948            });
3949            let before_pairs_sqlite =
3950                mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
3951            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
3952            let pk_value_for_bind = if fields.pk_is_auto {
3953                quote!(self.#pk_ident.get().copied().unwrap_or_default())
3954            } else {
3955                quote!(::core::clone::Clone::clone(&self.#pk_ident))
3956            };
3957            let assignments = &fields.update_assignments;
3958            let unset_dispatch = if fields.has_auto {
3959                quote! {
3960                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3961                        return self.insert(pool).await.map(|()| 1u64);
3962                    }
3963                }
3964            } else {
3965                quote!()
3966            };
3967            let write = restore_auto_now_unless(
3968                &fields.auto_now_idents,
3969                &save_ret,
3970                &save_keep,
3971                &quote! {
3972                    let _query = #root::core::UpdateQuery::new(
3973                        <Self as #root::core::Model>::SCHEMA,
3974                        ::std::vec![ #( #assignments ),* ],
3975                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3976                            #pk_column_lit,
3977                            #root::core::Op::Eq,
3978                            ::core::convert::Into::<#root::core::SqlValue>::into(
3979                                ::core::clone::Clone::clone(&self.#pk_ident)
3980                            ),
3981                        )),
3982                    );
3983                    self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
3984                        .await
3985                        .and_then(#root::sql::__macro_internals::row_saved)
3986                },
3987            );
3988            quote! {
3989                /// Save this row on any backend, with a diff audit row
3990                /// (before and after) in the same transaction. An `Unset`
3991                /// `Auto` PK inserts instead; a PK with no row is `RowNotFound`.
3992                ///
3993                /// The audit row's `changes` JSON contains one
3994                /// `{ "field": { "before": …, "after": … } }` entry
3995                /// per tracked column whose value actually changed
3996                /// Keyed on the PK; global scopes do not apply.
3997                ///
3998                /// # Errors
3999                /// As [`Self::save`].
4000                pub async fn save(
4001                    &mut self,
4002                    pool: &#root::sql::Pool,
4003                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4004                    #unset_dispatch
4005                    #write
4006                }
4007
4008                /// Run `query` and write its diff entry, limited to `only` columns.
4009                #[doc(hidden)]
4010                async fn __rustango_save_with_diff(
4011                    &self,
4012                    pool: &#root::sql::Pool,
4013                    query: &#root::core::UpdateQuery,
4014                    only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
4015                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4016                    let mut _after_pairs = self.__rustango_audit_pairs();
4017                    if let ::core::option::Option::Some(only) = only {
4018                        _after_pairs.retain(|(col, _)| only.contains(col));
4019                    }
4020                    let _before_query = #root::audit::before_image_query(
4021                        <Self as #root::core::Model>::SCHEMA,
4022                        #pk_column_lit,
4023                        ::core::convert::Into::<#root::core::SqlValue>::into(
4024                            #pk_value_for_bind,
4025                        ),
4026                        &[ #( #tracked_cols ),* ],
4027                    );
4028                    #root::audit::save_one_with_diff(
4029                        pool,
4030                        query,
4031                        &_before_query,
4032                        <Self as #root::core::Model>::SCHEMA.table,
4033                        self.__rustango_audit_pk(),
4034                        _after_pairs,
4035                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4036                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4037                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4038                    ).await
4039                }
4040
4041                #partial
4042            }
4043        } else {
4044            quote!()
4045        }
4046    } else {
4047        pool_save_method
4048    };
4049
4050    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4051    // body shapes:
4052    // - non-audited: simple dispatch through `sql::delete_pool`
4053    // - audited: routes through `audit::delete_one_with_audit`,
4054    //   which opens a per-backend transaction wrapping DELETE +
4055    //   audit emit so the data write and audit row commit atomically.
4056    let pool_delete_method = {
4057        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4058        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4059        if let Some(pk_ident) = pk_ident_for_pool {
4060            if audited_fields.is_some() {
4061                quote! {
4062                    /// Delete this row on any backend, with its audit row in
4063                    /// the same transaction.
4064                    /// Keyed on the PK; global scopes do not apply.
4065                    ///
4066                    /// # Errors
4067                    /// As [`Self::delete`].
4068                    pub async fn delete(
4069                        &self,
4070                        pool: &#root::sql::Pool,
4071                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4072                        let _query = #root::core::DeleteQuery::by_pk(
4073                            <Self as #root::core::Model>::SCHEMA,
4074                            #pk_column_lit,
4075                            ::core::convert::Into::<#root::core::SqlValue>::into(
4076                                ::core::clone::Clone::clone(&self.#pk_ident)
4077                            ),
4078                        );
4079                        let _audit_entry =
4080                            self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4081                        #root::audit::delete_one_with_audit(
4082                            pool, &_query, &_audit_entry,
4083                        ).await
4084                    }
4085                }
4086            } else {
4087                quote! {
4088                    /// Delete the row identified by this instance's primary key
4089                    /// against either backend. Equivalent to [`Self::delete`] but
4090                    /// takes [`#root::sql::Pool`] and dispatches per backend.
4091                    /// Global scopes do not apply.
4092                    ///
4093                    /// # Errors
4094                    /// As [`Self::delete`].
4095                    pub async fn delete(
4096                        &self,
4097                        pool: &#root::sql::Pool,
4098                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4099                        self.__rustango_delete_exec(pool).await
4100                    }
4101                }
4102            }
4103        } else {
4104            quote!()
4105        }
4106    };
4107
4108    // Bulk writers on audited models write one audit row per affected
4109    // row, in the write's transaction (#1747).
4110    let (bulk_delete_run, destroy_run, bulk_update_run, truncate_run, truncate_cascade_run) =
4111        if audited_fields.is_some() {
4112            (
4113                quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4114                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4115            })
4116            .await),
4117                quote!(#root::audit::delete_each_with_audit::<Self>(pool, &_queries, |_r: &Self| {
4118                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4119            })
4120            .await),
4121                quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4122                _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4123            })
4124            .await),
4125                quote!(#root::audit::truncate_with_audit(
4126                pool,
4127                <Self as #root::core::Model>::SCHEMA.table,
4128                #root::sql::TruncateMode::Plain,
4129            )
4130            .await),
4131                quote!(#root::audit::truncate_with_audit(
4132                pool,
4133                <Self as #root::core::Model>::SCHEMA.table,
4134                #root::sql::TruncateMode::Cascade,
4135            )
4136            .await
4137            .map(drop)),
4138            )
4139        } else {
4140            (
4141                quote!(#root::sql::delete_pool(pool, &_query).await),
4142                quote!(#root::sql::__rustango_delete_all_or_none(pool, _queries).await),
4143                quote!(#root::sql::update_pool(pool, &_query).await),
4144                quote!(#root::sql::truncate(pool, <Self as #root::core::Model>::SCHEMA).await),
4145                quote!(#root::sql::truncate_cascade(pool, <Self as #root::core::Model>::SCHEMA).await),
4146            )
4147        };
4148
4149    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4150    // matching this instance's PK and overwrite the in-memory state
4151    // with the freshly-fetched columns.
4152    // Issue #825. Only emitted when the model declares a PK; non-PK
4153    // models can't address a specific row.
4154    //
4155    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4156    // every field from `self`; resets the PK to `Auto::Unset` when
4157    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4158    // a fresh autoincrement value. Non-Auto PKs preserve the source
4159    // PK — the caller must overwrite before insert. Pure-Rust, no
4160    // I/O, no dialect surface.
4161    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4162        let other_field_clones: Vec<TokenStream2> = fields
4163            .column_entries
4164            .iter()
4165            .filter(|c| &c.ident != pk_ident)
4166            .map(|c| {
4167                let ident = &c.ident;
4168                quote! {
4169                    #ident: ::core::clone::Clone::clone(&self.#ident)
4170                }
4171            })
4172            .collect();
4173        let pk_clone_token = if fields.pk_is_auto {
4174            quote! { #pk_ident: #root::sql::Auto::Unset }
4175        } else {
4176            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4177        };
4178        let replicate_doc = if fields.pk_is_auto {
4179            quote! {
4180                /// Eloquent-style `replicate()` — return a clone of this
4181                /// row with the primary key reset to [`Auto::Unset`] so
4182                /// the copy is ready for `insert_pool` / `save_pool` to
4183                /// allocate a fresh autoincrement value. Every other
4184                /// field is `Clone`d verbatim. Issue #825.
4185                ///
4186                /// `auto_now_add` / `auto_now` timestamp fields are
4187                /// **not** reset (Eloquent's `replicate` doesn't reset
4188                /// them either) — pass them through the normal insert
4189                /// path if you want fresh values, or assign them
4190                /// explicitly after the call.
4191            }
4192        } else {
4193            quote! {
4194                /// Eloquent-style `replicate()` — clone this row
4195                /// verbatim. Because the primary key is **not** an
4196                /// `Auto<T>`, the clone keeps the source PK; the
4197                /// caller must overwrite `copy.<pk>` before inserting
4198                /// to avoid a unique-key violation. Issue #825.
4199            }
4200        };
4201        // 2026-06-07 — field-name / shortcut collision guard.
4202        //
4203        // The macro emits both `pub const <field>: <field>_col = ...`
4204        // (per-field typed-column const, used by the typed-builder
4205        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4206        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4207        // model has a field named e.g. `count`, both items would land
4208        // in the same inherent impl with the same name, and the
4209        // compiler rejects the derive with "duplicate definitions".
4210        //
4211        // Drop the conflicting shortcut for that model. Callers can
4212        // still reach the same behavior via
4213        // `QuerySet::<Model>::default().count(&pool)`.
4214        let column_names: ::std::collections::HashSet<String> = fields
4215            .column_entries
4216            .iter()
4217            .map(|c| ident_name(&c.ident))
4218            .collect();
4219        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4220            if column_names.contains(name) {
4221                quote! {}
4222            } else {
4223                tokens
4224            }
4225        };
4226        let count_method = emit_if_no_field_collision(
4227            "count",
4228            quote! {
4229                /// Count rows of this model — `SELECT COUNT(*) FROM
4230                /// <table>`. Eloquent `Model::count()` parity.
4231                ///
4232                /// Skipped on models that already declare a field named
4233                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4234                /// in that case.
4235                ///
4236                /// # Errors
4237                /// As [`CounterPool::count`].
4238                ///
4239                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4240                pub async fn count(
4241                    pool: &#root::sql::Pool,
4242                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4243                    use #root::sql::CounterPool as _;
4244                    #root::query::QuerySet::<Self>::default()
4245                        .count(pool)
4246                        .await
4247                }
4248            },
4249        );
4250        let value_method = emit_if_no_field_collision(
4251            "value",
4252            quote! {
4253                /// Pluck a single scalar from the first row.
4254                /// Eloquent `Model::query()->value($col)` parity.
4255                ///
4256                /// Skipped on models that already declare a field named
4257                /// `value`. Drop into
4258                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4259                ///
4260                /// # Errors
4261                /// As `ValuesFlatQuerySet::first`.
4262                pub async fn value<U>(
4263                    col: &str,
4264                    pool: &#root::sql::Pool,
4265                ) -> ::core::result::Result<
4266                    ::core::option::Option<U>,
4267                    #root::sql::ExecError,
4268                >
4269                where
4270                    U: #root::sql::FlatScalar,
4271                {
4272                    let _col_static: &'static str = Self::__resolve_col(col)?;
4273                    #root::query::QuerySet::<Self>::default()
4274                        .values_list_flat(_col_static)
4275                        .first::<U>(pool)
4276                        .await
4277                }
4278            },
4279        );
4280        let sum_method = emit_if_no_field_collision(
4281            "sum",
4282            quote! {
4283                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4284                /// Skipped on models that already declare a field named
4285                /// `sum`.
4286                ///
4287                /// # Errors
4288                /// As [`#root::sql::fetch_aggregate_pool`].
4289                pub async fn sum<U>(
4290                    col: &str,
4291                    pool: &#root::sql::Pool,
4292                ) -> ::core::result::Result<
4293                    ::core::option::Option<U>,
4294                    #root::sql::ExecError,
4295                >
4296                where
4297                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4298                        + #root::sql::MaybeMyFromRow
4299                        + #root::sql::MaybeSqliteFromRow
4300                        + ::core::marker::Send
4301                        + ::core::marker::Unpin,
4302                {
4303                    #root::query::QuerySet::<Self>::default()
4304                        .sum::<U>(col, pool)
4305                        .await
4306                }
4307            },
4308        );
4309        let avg_method = emit_if_no_field_collision(
4310            "avg",
4311            quote! {
4312                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4313                /// models that already declare a field named `avg`.
4314                ///
4315                /// # Errors
4316                /// As [`#root::sql::fetch_aggregate_pool`].
4317                pub async fn avg<U>(
4318                    col: &str,
4319                    pool: &#root::sql::Pool,
4320                ) -> ::core::result::Result<
4321                    ::core::option::Option<U>,
4322                    #root::sql::ExecError,
4323                >
4324                where
4325                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4326                        + #root::sql::MaybeMyFromRow
4327                        + #root::sql::MaybeSqliteFromRow
4328                        + ::core::marker::Send
4329                        + ::core::marker::Unpin,
4330                {
4331                    #root::query::QuerySet::<Self>::default()
4332                        .avg::<U>(col, pool)
4333                        .await
4334                }
4335            },
4336        );
4337        let min_method = emit_if_no_field_collision(
4338            "min",
4339            quote! {
4340                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4341                /// models that already declare a field named `min`.
4342                ///
4343                /// # Errors
4344                /// As [`#root::sql::fetch_aggregate_pool`].
4345                pub async fn min<U>(
4346                    col: &str,
4347                    pool: &#root::sql::Pool,
4348                ) -> ::core::result::Result<
4349                    ::core::option::Option<U>,
4350                    #root::sql::ExecError,
4351                >
4352                where
4353                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4354                        + #root::sql::MaybeMyFromRow
4355                        + #root::sql::MaybeSqliteFromRow
4356                        + ::core::marker::Send
4357                        + ::core::marker::Unpin,
4358                {
4359                    #root::query::QuerySet::<Self>::default()
4360                        .min::<U>(col, pool)
4361                        .await
4362                }
4363            },
4364        );
4365        let max_method = emit_if_no_field_collision(
4366            "max",
4367            quote! {
4368                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4369                /// models that already declare a field named `max`.
4370                ///
4371                /// # Errors
4372                /// As [`#root::sql::fetch_aggregate_pool`].
4373                pub async fn max<U>(
4374                    col: &str,
4375                    pool: &#root::sql::Pool,
4376                ) -> ::core::result::Result<
4377                    ::core::option::Option<U>,
4378                    #root::sql::ExecError,
4379                >
4380                where
4381                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4382                        + #root::sql::MaybeMyFromRow
4383                        + #root::sql::MaybeSqliteFromRow
4384                        + ::core::marker::Send
4385                        + ::core::marker::Unpin,
4386                {
4387                    #root::query::QuerySet::<Self>::default()
4388                        .max::<U>(col, pool)
4389                        .await
4390                }
4391            },
4392        );
4393        let first_method = emit_if_no_field_collision(
4394            "first",
4395            quote! {
4396                /// First row of this model. Eloquent `Model::first()`.
4397                /// Skipped on models that already declare a field named
4398                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4399                ///
4400                /// # Errors
4401                /// As `QuerySet::first`.
4402                pub async fn first(
4403                    pool: &#root::sql::Pool,
4404                ) -> ::core::result::Result<
4405                    ::core::option::Option<Self>,
4406                    #root::sql::ExecError,
4407                > {
4408                    #root::query::QuerySet::<Self>::default()
4409                        .first(pool)
4410                        .await
4411                }
4412            },
4413        );
4414        let last_method = emit_if_no_field_collision(
4415            "last",
4416            quote! {
4417                /// Last row of this model by primary-key DESC.
4418                /// Eloquent `Model::query()->latest('id')->first()`
4419                /// parity — fetches the highest-PK row without
4420                /// requiring the caller to spell the PK column.
4421                /// Returns `None` on an empty table.
4422                ///
4423                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4424                /// Skipped on models that already declare a field
4425                /// named `last`.
4426                ///
4427                /// # Errors
4428                /// As `QuerySet::last`.
4429                pub async fn last(
4430                    pool: &#root::sql::Pool,
4431                ) -> ::core::result::Result<
4432                    ::core::option::Option<Self>,
4433                    #root::sql::ExecError,
4434                > {
4435                    #root::query::QuerySet::<Self>::default()
4436                        .last(pool)
4437                        .await
4438                }
4439            },
4440        );
4441        quote! {
4442            /// Re-SELECT this row by its primary key and overwrite
4443            /// every in-memory field with the freshly-fetched value.
4444            /// Issue #825.
4445            ///
4446            /// Use this when the row may have been modified by another
4447            /// process / connection / job since you read it — e.g. after
4448            /// a queued task callback, or to re-sync stale UI state
4449            /// before re-saving.
4450            ///
4451            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4452            /// when the primary key no longer matches any row (e.g.
4453            /// the row was deleted concurrently).
4454            ///
4455            /// # Errors
4456            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4457            /// the PK no longer exists.
4458            ///
4459            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4460            pub async fn refresh_from_db(
4461                &mut self,
4462                pool: &#root::sql::Pool,
4463            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4464                match self.fresh(pool).await? {
4465                    ::core::option::Option::Some(_fresh) => {
4466                        *self = _fresh;
4467                        ::core::result::Result::Ok(())
4468                    }
4469                    ::core::option::Option::None => ::core::result::Result::Err(
4470                        #root::sql::ExecError::Driver(
4471                            #root::sql::sqlx::Error::RowNotFound,
4472                        ),
4473                    ),
4474                }
4475            }
4476
4477            /// Atomically increment the integer column `col` by
4478            /// `by` for this row. Equivalent to
4479            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4480            ///
4481            /// **Doesn't mutate `self`** — the in-memory copy is now
4482            /// stale; call [`Self::refresh_from_db`] /
4483            /// [`Self::fresh`] to re-sync. Returns the rows-
4484            /// affected count (0 when the PK doesn't match any row,
4485            /// 1 on success).
4486            ///
4487            /// `col` is the Rust field name as a string; unknown
4488            /// fields surface as `UnknownField` at runtime. Negative
4489            /// `by` values atomically decrement (see also
4490            /// [`Self::decrement`]).
4491            ///
4492            /// # Errors
4493            /// As [`UpdaterPool::execute_pool`].
4494            ///
4495            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4496            pub async fn increment(
4497                &self,
4498                col: &str,
4499                by: i64,
4500                pool: &#root::sql::Pool,
4501            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4502                Self::__increment_one(self, col, by, pool).await
4503            }
4504
4505            /// Sibling of [`Self::increment`] — atomically
4506            /// decrement this row's `col` by `by`. Eloquent
4507            /// `$model->decrement($col, $by)` parity. Equivalent to
4508            /// `self.increment(col, -by, &pool)`; the separate name
4509            /// keeps call sites readable.
4510            ///
4511            /// # Errors
4512            /// As [`Self::increment`].
4513            pub async fn decrement(
4514                &self,
4515                col: &str,
4516                by: i64,
4517                pool: &#root::sql::Pool,
4518            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4519                Self::__increment_one(self, col, -by, pool).await
4520            }
4521
4522            /// Bulk-increment: add `by` to `col` on every row of the
4523            /// table. Eloquent `Model::query()->increment($col, $by)`
4524            /// parity. Use for counters, score adjustments, view
4525            /// rollups.
4526            ///
4527            /// # Errors
4528            /// As [`UpdaterPool::execute_pool`].
4529            ///
4530            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4531            pub async fn increment_each(
4532                col: &str,
4533                by: i64,
4534                pool: &#root::sql::Pool,
4535            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4536                Self::__increment_all(col, by, pool).await
4537            }
4538
4539            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4540            ///
4541            /// # Errors
4542            /// As [`Self::increment_each`].
4543            pub async fn decrement_each(
4544                col: &str,
4545                by: i64,
4546                pool: &#root::sql::Pool,
4547            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4548                Self::__increment_all(col, -by, pool).await
4549            }
4550
4551            /// Internal: forward to
4552            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4553            /// One-line wrapper kept as a per-Model method so the
4554            /// macro's emitted `increment` / `decrement` instance
4555            /// calls don't have to thread `Self` through manually.
4556            #[doc(hidden)]
4557            pub async fn __increment_one(
4558                this: &Self,
4559                col: &str,
4560                by: i64,
4561                pool: &#root::sql::Pool,
4562            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4563                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4564                    ::core::clone::Clone::clone(&this.#pk_ident),
4565                );
4566                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4567                    ::core::stringify!(#pk_ident),
4568                    _pk_val,
4569                    col,
4570                    by,
4571                    pool,
4572                )
4573                .await
4574            }
4575
4576            /// Internal: run
4577            /// [`#root::sql::model_shortcuts::increment_all_query`].
4578            #[doc(hidden)]
4579            pub async fn __increment_all(
4580                col: &str,
4581                by: i64,
4582                pool: &#root::sql::Pool,
4583            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4584                let _query =
4585                    #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4586                #bulk_update_run
4587            }
4588
4589            /// Internal: forward to
4590            /// [`#root::sql::model_shortcuts::resolve_col`].
4591            #[doc(hidden)]
4592            pub fn __resolve_col(
4593                col: &str,
4594            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4595                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4596            }
4597
4598            /// Internal: forward to
4599            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4600            #[doc(hidden)]
4601            #[must_use]
4602            pub fn __add_signed_expr(
4603                col_static: &'static str,
4604                signed_by: i64,
4605            ) -> #root::core::Expr {
4606                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4607            }
4608
4609            /// Re-SELECT this row by its primary key and return a
4610            /// **new** instance with the freshly-fetched fields.
4611            /// Eloquent `Model::fresh()` parity — non-mutating
4612            /// counterpart of [`Self::refresh_from_db`].
4613            ///
4614            /// Returns `Ok(None)` when the row was deleted
4615            /// concurrently — vs [`Self::refresh_from_db`]
4616            /// which surfaces that as `RowNotFound` because
4617            /// in-place mutation has nothing to write to.
4618            ///
4619            /// Useful when you want to compare the in-memory
4620            /// instance against the persisted state (audit-style
4621            /// diffs, conflict detection) without mutating the
4622            /// reference you already hold.
4623            ///
4624            /// # Errors
4625            /// As [`FetcherPool::fetch`].
4626            ///
4627            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4628            pub async fn fresh(
4629                &self,
4630                pool: &#root::sql::Pool,
4631            ) -> ::core::result::Result<
4632                ::core::option::Option<Self>,
4633                #root::sql::ExecError,
4634            > {
4635                use #root::sql::FetcherPool as _;
4636                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4637                    ::core::clone::Clone::clone(&self.#pk_ident),
4638                );
4639                // By key alone, as `save()` and `delete()` write it.
4640                let _rows: ::std::vec::Vec<Self> =
4641                    #root::query::QuerySet::<Self>::default()
4642                        .without_global_scopes()
4643                        .filter(::core::stringify!(#pk_ident), _pk_val)
4644                        .limit(1)
4645                        .fetch(pool)
4646                        .await?;
4647                ::core::result::Result::Ok(_rows.into_iter().next())
4648            }
4649
4650            #replicate_doc
4651            #[must_use]
4652            pub fn replicate(&self) -> Self {
4653                Self {
4654                    #pk_clone_token,
4655                    #( #other_field_clones, )*
4656                }
4657            }
4658
4659            #first_method
4660
4661            #last_method
4662
4663            /// Throwing counterpart of [`Self::first`] —
4664            /// errors with `RowNotFound` when the table is empty.
4665            /// Eloquent `Model::firstOrFail()` parity.
4666            ///
4667            /// # Errors
4668            /// As [`Self::first`]; additionally
4669            /// [`sqlx::Error::RowNotFound`] on empty tables.
4670            ///
4671            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4672            pub async fn first_or_fail(
4673                pool: &#root::sql::Pool,
4674            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4675                // Route through the queryset rather than `Self::first`,
4676                // which is suppressed on models with a field named
4677                // `first` (field/shortcut collision guard).
4678                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4679                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4680                    ::core::option::Option::None => ::core::result::Result::Err(
4681                        #root::sql::ExecError::Driver(
4682                            #root::sql::sqlx::Error::RowNotFound,
4683                        ),
4684                    ),
4685                }
4686            }
4687
4688            /// Single-column projection — `SELECT <col> FROM
4689            /// <table>`. Returns `Vec<U>` where each element is the
4690            /// decoded value of the column. Eloquent
4691            /// `Model::pluck($column)` parity.
4692            ///
4693            /// Thin wrapper over `QuerySet::<Self>::default()
4694            /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4695            /// `FlatScalar`; use `Option<_>` for a nullable column.
4696            ///
4697            /// # Errors
4698            /// As `ValuesFlatQuerySet::fetch`.
4699            pub async fn pluck<U>(
4700                col: &'static str,
4701                pool: &#root::sql::Pool,
4702            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4703            where
4704                U: #root::sql::FlatScalar,
4705            {
4706                #root::query::QuerySet::<Self>::default()
4707                    .values_list_flat(col)
4708                    .fetch::<U>(pool)
4709                    .await
4710            }
4711
4712            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4713            /// stream every row of this model in batches of `n`,
4714            /// invoking the callback once per batch. Stable PK-ASC
4715            /// ordering so the LIMIT/OFFSET pagination is
4716            /// deterministic across drivers.
4717            ///
4718            /// The callback is async — it can do further DB work
4719            /// (writes, related-row lookups, queue dispatch) per
4720            /// batch. Return `Err(...)` from the callback to abort
4721            /// the iteration early; the error bubbles up.
4722            ///
4723            /// **When to use**: bulk processing flows that can't
4724            /// fit the whole table in memory — sending newsletters,
4725            /// running data migrations, computing summary
4726            /// statistics. For small / known-bounded tables, plain
4727            /// `Self::all(&pool)` is simpler.
4728            ///
4729            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4730            /// large tables. For multi-million-row scans prefer
4731            /// keyset-by-PK (the standard "WHERE id > last_seen"
4732            /// shape) over `chunk(...)`.
4733            ///
4734            /// Skipped on models without a primary key — chunking
4735            /// needs a stable order to avoid skipping / repeating
4736            /// rows across batches.
4737            pub async fn chunk<F, Fut>(
4738                n: i64,
4739                pool: &#root::sql::Pool,
4740                mut cb: F,
4741            ) -> ::core::result::Result<(), #root::sql::ExecError>
4742            where
4743                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4744                Fut: ::core::future::Future<
4745                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4746                >,
4747            {
4748                use #root::sql::FetcherPool as _;
4749                // The field name: `filter` / `order_by` take fields, not columns.
4750                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4751                    ::core::option::Option::Some(f) => f.name,
4752                    ::core::option::Option::None => {
4753                        return ::core::result::Result::Ok(());
4754                    }
4755                };
4756                let mut offset: i64 = 0;
4757                loop {
4758                    let rows: ::std::vec::Vec<Self> =
4759                        #root::query::QuerySet::<Self>::default()
4760                            .order_by(&[(pk_col, false)])
4761                            .limit(n)
4762                            .offset(offset)
4763                            .fetch(pool)
4764                            .await?;
4765                    if rows.is_empty() {
4766                        return ::core::result::Result::Ok(());
4767                    }
4768                    let len = rows.len() as i64;
4769                    cb(rows).await?;
4770                    if len < n {
4771                        return ::core::result::Result::Ok(());
4772                    }
4773                    offset += n;
4774                }
4775            }
4776
4777            /// Eloquent `Model::chunkById($n, fn (...))` — same
4778            /// per-batch callback shape as [`Self::chunk`], but uses
4779            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4780            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4781            /// — the right choice for multi-million-row sweeps.
4782            ///
4783            /// The keyset compares the primary key in its own type,
4784            /// so any orderable PK works (`i32`, `i64`, UUID, string).
4785            ///
4786            /// Callback errors abort iteration; the error bubbles up
4787            /// unchanged. Empty table → callback invoked zero times.
4788            pub async fn chunk_by_id<F, Fut>(
4789                n: i64,
4790                pool: &#root::sql::Pool,
4791                mut cb: F,
4792            ) -> ::core::result::Result<(), #root::sql::ExecError>
4793            where
4794                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4795                Fut: ::core::future::Future<
4796                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4797                >,
4798            {
4799                use #root::sql::FetcherPool as _;
4800                // The field name: `filter` / `order_by` take fields, not columns.
4801                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4802                    ::core::option::Option::Some(f) => f.name,
4803                    ::core::option::Option::None => {
4804                        return ::core::result::Result::Ok(());
4805                    }
4806                };
4807                // The largest PK seen, in the PK's own type; none before
4808                // the first batch.
4809                let key = ::std::format!("{}__gt", pk_col);
4810                let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
4811                    ::core::option::Option::None;
4812                loop {
4813                    let mut qs = #root::query::QuerySet::<Self>::default();
4814                    if let ::core::option::Option::Some(v) = last_seen.take() {
4815                        qs = qs.filter(key.as_str(), v);
4816                    }
4817                    let rows: ::std::vec::Vec<Self> = qs
4818                        .order_by(&[(pk_col, false)])
4819                        .limit(n)
4820                        .fetch(pool)
4821                        .await?;
4822                    let len = rows.len() as i64;
4823                    // Capture the last row's PK BEFORE moving rows
4824                    // into the callback.
4825                    let ::core::option::Option::Some(max_pk) =
4826                        rows.last().map(|r| r.__rustango_pk_value())
4827                    else {
4828                        return ::core::result::Result::Ok(());
4829                    };
4830                    cb(rows).await?;
4831                    if len < n {
4832                        return ::core::result::Result::Ok(());
4833                    }
4834                    last_seen = ::core::option::Option::Some(max_pk);
4835                }
4836            }
4837
4838            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4839            /// per-row callback companion to [`Self::chunk`].
4840            /// Streams every row in keyset-paginated batches of
4841            /// `batch` size, calling `cb` once per row.
4842            ///
4843            /// Inherits the keyset-paginated scan of
4844            /// [`Self::chunk_by_id`] (O(N) total).
4845            ///
4846            /// ```ignore
4847            /// Post::each(500, &pool, |p| async move {
4848            ///     reindex(p).await?;
4849            ///     Ok(())
4850            /// }).await?;
4851            /// ```
4852            ///
4853            /// Return `Err(...)` from the callback to abort
4854            /// iteration; the error bubbles up unchanged.
4855            pub async fn each<F, Fut>(
4856                batch: i64,
4857                pool: &#root::sql::Pool,
4858                mut cb: F,
4859            ) -> ::core::result::Result<(), #root::sql::ExecError>
4860            where
4861                F: ::core::ops::FnMut(Self) -> Fut,
4862                Fut: ::core::future::Future<
4863                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4864                >,
4865            {
4866                use #root::sql::FetcherPool as _;
4867                // The field name: `filter` / `order_by` take fields, not columns.
4868                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4869                    ::core::option::Option::Some(f) => f.name,
4870                    ::core::option::Option::None => {
4871                        return ::core::result::Result::Ok(());
4872                    }
4873                };
4874                let key = ::std::format!("{}__gt", pk_col);
4875                let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
4876                    ::core::option::Option::None;
4877                loop {
4878                    let mut qs = #root::query::QuerySet::<Self>::default();
4879                    if let ::core::option::Option::Some(v) = last_seen.take() {
4880                        qs = qs.filter(key.as_str(), v);
4881                    }
4882                    let rows: ::std::vec::Vec<Self> = qs
4883                        .order_by(&[(pk_col, false)])
4884                        .limit(batch)
4885                        .fetch(pool)
4886                        .await?;
4887                    let len = rows.len() as i64;
4888                    let ::core::option::Option::Some(max_pk) =
4889                        rows.last().map(|r| r.__rustango_pk_value())
4890                    else {
4891                        return ::core::result::Result::Ok(());
4892                    };
4893                    for row in rows {
4894                        cb(row).await?;
4895                    }
4896                    if len < batch {
4897                        return ::core::result::Result::Ok(());
4898                    }
4899                    last_seen = ::core::option::Option::Some(max_pk);
4900                }
4901            }
4902
4903            /// Delete every row of this model with one `DELETE FROM
4904            /// <table>` on every backend; returns the rows deleted. Each
4905            /// FK's `ON DELETE` applies, so a `RESTRICT` / `NO ACTION`
4906            /// row still pointing here refuses it; see
4907            /// [`truncate_cascade`](Self::truncate_cascade).
4908            ///
4909            /// **Use only in tests / fixture-reset flows.** No per-row
4910            /// `pre_delete` / `post_delete` signals fire. An audited model
4911            /// gets one bulk audit entry, not one per row. Global scopes
4912            /// do not apply: every row goes, generic M2M links too.
4913            ///
4914            /// # Errors
4915            /// As [`truncate`].
4916            ///
4917            /// [`truncate`]: rustango::sql::truncate
4918            pub async fn truncate(
4919                pool: &#root::sql::Pool,
4920            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4921                #truncate_run
4922            }
4923
4924            /// [`truncate`](Self::truncate), also emptying every table
4925            /// that references this one, transitively and whatever its
4926            /// `ON DELETE`. The same on every backend.
4927            ///
4928            /// # Errors
4929            /// As [`truncate_cascade`].
4930            ///
4931            /// [`truncate_cascade`]: rustango::sql::truncate_cascade
4932            pub async fn truncate_cascade(
4933                pool: &#root::sql::Pool,
4934            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4935                #truncate_cascade_run
4936            }
4937
4938            /// Bulk-delete every row whose primary key is in
4939            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4940            /// Returns the affected row count.
4941            ///
4942            /// Eloquent `Model::destroy([1, 2, 3])` parity.
4943            /// Empty `pks` is a no-op (returns 0). All or none: past
4944            /// the bind cap the batches share one transaction.
4945            ///
4946            /// Accepts any iterable whose elements are
4947            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4948            /// `[i64; N]`, etc.
4949            ///
4950            /// # Errors
4951            /// As `delete_pool`.
4952            pub async fn destroy<V>(
4953                pks: impl ::core::iter::IntoIterator<Item = V>,
4954                pool: &#root::sql::Pool,
4955            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4956            where
4957                V: ::core::convert::Into<#root::core::SqlValue>,
4958            {
4959                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4960                    pks.into_iter().map(::core::convert::Into::into).collect();
4961                if _values.is_empty() {
4962                    return ::core::result::Result::Ok(0);
4963                }
4964                let _pk = <Self as #root::core::Model>::SCHEMA
4965                    .primary_key()
4966                    .ok_or_else(|| {
4967                        #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
4968                    })?;
4969                // One statement per `IN` list the dialect can bind, all or none.
4970                let mut _queries = ::std::vec::Vec::new();
4971                for _keys in #root::sql::__rustango_in_batches::<Self>(pool, _values)? {
4972                    _queries.push(
4973                        #root::query::QuerySet::<Self>::default()
4974                            .filter_op(
4975                                _pk.name,
4976                                #root::core::Op::In,
4977                                #root::core::SqlValue::List(_keys),
4978                            )
4979                            .compile_delete()?,
4980                    );
4981                }
4982                #destroy_run
4983            }
4984
4985            /// Fetch every row where `<col> = <val>`. Eloquent
4986            /// `Model::where($col, $val)->get()` parity.
4987            ///
4988            /// Thin wrapper over `QuerySet::<Self>::default()
4989            /// .filter(col, val).fetch(pool)`. For one row,
4990            /// use [`Self::first_where`]; for a chain that
4991            /// needs further `.filter()` / `.order_by()` /
4992            /// `.limit()`, drop down to `Self::query().filter(...)`
4993            /// directly.
4994            ///
4995            /// `val` accepts any value `Into<SqlValue>` so plain
4996            /// strings, ints, UUIDs, etc. all work.
4997            ///
4998            /// # Errors
4999            /// As [`FetcherPool::fetch`].
5000            ///
5001            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5002            pub async fn where_(
5003                col: &str,
5004                val: impl ::core::convert::Into<#root::core::SqlValue>,
5005                pool: &#root::sql::Pool,
5006            ) -> ::core::result::Result<
5007                ::std::vec::Vec<Self>,
5008                #root::sql::ExecError,
5009            > {
5010                use #root::sql::FetcherPool as _;
5011                #root::query::QuerySet::<Self>::default()
5012                    .filter(col, val)
5013                    .fetch(pool)
5014                    .await
5015            }
5016
5017            /// Fetch every row where `<col> IN (vals)`. Eloquent
5018            /// `Model::whereIn($col, $vals)->get()` parity. Empty
5019            /// `vals` returns no rows (matches SQL's empty-IN
5020            /// semantics).
5021            ///
5022            /// `vals` accepts any iterable whose items are
5023            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5024            /// etc.
5025            ///
5026            /// # Errors
5027            /// As [`FetcherPool::fetch`].
5028            ///
5029            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5030            pub async fn where_in<V>(
5031                col: &str,
5032                vals: impl ::core::iter::IntoIterator<Item = V>,
5033                pool: &#root::sql::Pool,
5034            ) -> ::core::result::Result<
5035                ::std::vec::Vec<Self>,
5036                #root::sql::ExecError,
5037            >
5038            where
5039                V: ::core::convert::Into<#root::core::SqlValue>,
5040            {
5041                use #root::sql::FetcherPool as _;
5042                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5043                    vals.into_iter().map(::core::convert::Into::into).collect();
5044                if _values.is_empty() {
5045                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5046                }
5047                let _key = ::std::format!("{}__in", col);
5048                let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
5049                let _split = _batches.len() > 1;
5050                let mut _rows = ::std::vec::Vec::new();
5051                for _keys in _batches {
5052                    _rows.extend(
5053                        #root::query::QuerySet::<Self>::default()
5054                            .filter(&_key, #root::core::SqlValue::List(_keys))
5055                            .fetch(pool)
5056                            .await?,
5057                    );
5058                }
5059                if _split {
5060                    _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
5061                }
5062                ::core::result::Result::Ok(_rows)
5063            }
5064
5065            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5066            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5067            /// `vals` returns every row (matches SQL's empty-NOT-IN
5068            /// semantics — vacuously true for every row).
5069            ///
5070            /// # Errors
5071            /// As [`FetcherPool::fetch`]; `ExecError::InListUnsplittable`
5072            /// when `vals` pass the backend's bind cap.
5073            ///
5074            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5075            pub async fn where_not_in<V>(
5076                col: &str,
5077                vals: impl ::core::iter::IntoIterator<Item = V>,
5078                pool: &#root::sql::Pool,
5079            ) -> ::core::result::Result<
5080                ::std::vec::Vec<Self>,
5081                #root::sql::ExecError,
5082            >
5083            where
5084                V: ::core::convert::Into<#root::core::SqlValue>,
5085            {
5086                use #root::sql::FetcherPool as _;
5087                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5088                    vals.into_iter().map(::core::convert::Into::into).collect();
5089                if _values.is_empty() {
5090                    return #root::query::QuerySet::<Self>::default()
5091                        .fetch(pool)
5092                        .await;
5093                }
5094                let _key = ::std::format!("{}__not_in", col);
5095                // A `NOT IN` cannot be split: past the bind cap it is refused.
5096                let _values = #root::sql::__rustango_one_in_list::<Self>(pool, _values)?;
5097                #root::query::QuerySet::<Self>::default()
5098                    .filter(&_key, #root::core::SqlValue::List(_values))
5099                    .fetch(pool)
5100                    .await
5101            }
5102
5103            /// Fetch every row where `<col> IS NULL`. Eloquent
5104            /// `Model::whereNull($col)->get()` parity.
5105            ///
5106            /// # Errors
5107            /// As [`FetcherPool::fetch`].
5108            ///
5109            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5110            pub async fn where_null(
5111                col: &str,
5112                pool: &#root::sql::Pool,
5113            ) -> ::core::result::Result<
5114                ::std::vec::Vec<Self>,
5115                #root::sql::ExecError,
5116            > {
5117                use #root::sql::FetcherPool as _;
5118                let _key = ::std::format!("{}__isnull", col);
5119                #root::query::QuerySet::<Self>::default()
5120                    .filter(&_key, #root::core::SqlValue::Bool(true))
5121                    .fetch(pool)
5122                    .await
5123            }
5124
5125            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5126            /// `Model::whereNotNull($col)->get()` parity.
5127            ///
5128            /// # Errors
5129            /// As [`FetcherPool::fetch`].
5130            ///
5131            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5132            pub async fn where_not_null(
5133                col: &str,
5134                pool: &#root::sql::Pool,
5135            ) -> ::core::result::Result<
5136                ::std::vec::Vec<Self>,
5137                #root::sql::ExecError,
5138            > {
5139                use #root::sql::FetcherPool as _;
5140                let _key = ::std::format!("{}__isnull", col);
5141                #root::query::QuerySet::<Self>::default()
5142                    .filter(&_key, #root::core::SqlValue::Bool(false))
5143                    .fetch(pool)
5144                    .await
5145            }
5146
5147            /// Fetch up to `n` rows in random order. Eloquent
5148            /// `Model::inRandomOrder()->limit($n)->get()` /
5149            /// `Model::query()->inRandomOrder()->get()->take($n)`
5150            /// parity. **Performance caveat**: random ordering
5151            /// forces a full table scan + per-row random key sort;
5152            /// the optimizer cannot use an index. Prefer a
5153            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5154            ///
5155            /// # Errors
5156            /// As [`FetcherPool::fetch`].
5157            ///
5158            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5159            pub async fn random_n(
5160                n: i64,
5161                pool: &#root::sql::Pool,
5162            ) -> ::core::result::Result<
5163                ::std::vec::Vec<Self>,
5164                #root::sql::ExecError,
5165            > {
5166                use #root::sql::FetcherPool as _;
5167                #root::query::QuerySet::<Self>::default()
5168                    .order_random()
5169                    .limit(n)
5170                    .fetch(pool)
5171                    .await
5172            }
5173
5174            /// Fetch one row in random order. Eloquent
5175            /// `Model::inRandomOrder()->first()` parity. Same
5176            /// performance caveat as [`Self::random_n`].
5177            ///
5178            /// # Errors
5179            /// As [`FetcherPool::fetch`].
5180            ///
5181            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5182            pub async fn random(
5183                pool: &#root::sql::Pool,
5184            ) -> ::core::result::Result<
5185                ::core::option::Option<Self>,
5186                #root::sql::ExecError,
5187            > {
5188                ::core::result::Result::Ok(
5189                    Self::random_n(1, pool).await?.into_iter().next(),
5190                )
5191            }
5192
5193            /// Fetch every row ordered ASC by `field`. Eloquent
5194            /// `Model::oldest($field)->get()` parity — the multi-row
5195            /// counterpart of [`Self::earliest`].
5196            ///
5197            /// # Errors
5198            /// As [`FetcherPool::fetch`].
5199            ///
5200            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5201            pub async fn oldest(
5202                field: &str,
5203                pool: &#root::sql::Pool,
5204            ) -> ::core::result::Result<
5205                ::std::vec::Vec<Self>,
5206                #root::sql::ExecError,
5207            > {
5208                use #root::sql::FetcherPool as _;
5209                #root::query::QuerySet::<Self>::default()
5210                    .order_by(&[(field, false)])
5211                    .fetch(pool)
5212                    .await
5213            }
5214
5215            /// Fetch every row ordered DESC by `field`. Eloquent
5216            /// `Model::latest($field)->get()` parity — the multi-row
5217            /// counterpart of [`Self::latest`].
5218            ///
5219            /// # Errors
5220            /// As [`FetcherPool::fetch`].
5221            ///
5222            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5223            pub async fn newest(
5224                field: &str,
5225                pool: &#root::sql::Pool,
5226            ) -> ::core::result::Result<
5227                ::std::vec::Vec<Self>,
5228                #root::sql::ExecError,
5229            > {
5230                use #root::sql::FetcherPool as _;
5231                #root::query::QuerySet::<Self>::default()
5232                    .order_by(&[(field, true)])
5233                    .fetch(pool)
5234                    .await
5235            }
5236
5237            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5238            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5239            /// Routes through the existing `__year` lookup suffix
5240            /// (issue #829).
5241            ///
5242            /// # Errors
5243            /// As [`FetcherPool::fetch`].
5244            ///
5245            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5246            pub async fn where_year(
5247                col: &str,
5248                year: i64,
5249                pool: &#root::sql::Pool,
5250            ) -> ::core::result::Result<
5251                ::std::vec::Vec<Self>,
5252                #root::sql::ExecError,
5253            > {
5254                use #root::sql::FetcherPool as _;
5255                let _key = ::std::format!("{}__year", col);
5256                #root::query::QuerySet::<Self>::default()
5257                    .filter(&_key, #root::core::SqlValue::I64(year))
5258                    .fetch(pool)
5259                    .await
5260            }
5261
5262            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5263            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5264            /// `month` is 1–12.
5265            ///
5266            /// # Errors
5267            /// As [`FetcherPool::fetch`].
5268            ///
5269            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5270            pub async fn where_month(
5271                col: &str,
5272                month: i64,
5273                pool: &#root::sql::Pool,
5274            ) -> ::core::result::Result<
5275                ::std::vec::Vec<Self>,
5276                #root::sql::ExecError,
5277            > {
5278                use #root::sql::FetcherPool as _;
5279                let _key = ::std::format!("{}__month", col);
5280                #root::query::QuerySet::<Self>::default()
5281                    .filter(&_key, #root::core::SqlValue::I64(month))
5282                    .fetch(pool)
5283                    .await
5284            }
5285
5286            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5287            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5288            /// `day` is 1–31.
5289            ///
5290            /// # Errors
5291            /// As [`FetcherPool::fetch`].
5292            ///
5293            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5294            pub async fn where_day(
5295                col: &str,
5296                day: i64,
5297                pool: &#root::sql::Pool,
5298            ) -> ::core::result::Result<
5299                ::std::vec::Vec<Self>,
5300                #root::sql::ExecError,
5301            > {
5302                use #root::sql::FetcherPool as _;
5303                let _key = ::std::format!("{}__day", col);
5304                #root::query::QuerySet::<Self>::default()
5305                    .filter(&_key, #root::core::SqlValue::I64(day))
5306                    .fetch(pool)
5307                    .await
5308            }
5309
5310            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5311            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5312            /// `hour` is 0–23.
5313            ///
5314            /// # Errors
5315            /// As [`FetcherPool::fetch`].
5316            ///
5317            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5318            pub async fn where_hour(
5319                col: &str,
5320                hour: i64,
5321                pool: &#root::sql::Pool,
5322            ) -> ::core::result::Result<
5323                ::std::vec::Vec<Self>,
5324                #root::sql::ExecError,
5325            > {
5326                use #root::sql::FetcherPool as _;
5327                let _key = ::std::format!("{}__hour", col);
5328                #root::query::QuerySet::<Self>::default()
5329                    .filter(&_key, #root::core::SqlValue::I64(hour))
5330                    .fetch(pool)
5331                    .await
5332            }
5333
5334            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5335            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5336            /// `minute` is 0–59.
5337            ///
5338            /// # Errors
5339            /// As [`FetcherPool::fetch`].
5340            ///
5341            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5342            pub async fn where_minute(
5343                col: &str,
5344                minute: i64,
5345                pool: &#root::sql::Pool,
5346            ) -> ::core::result::Result<
5347                ::std::vec::Vec<Self>,
5348                #root::sql::ExecError,
5349            > {
5350                use #root::sql::FetcherPool as _;
5351                let _key = ::std::format!("{}__minute", col);
5352                #root::query::QuerySet::<Self>::default()
5353                    .filter(&_key, #root::core::SqlValue::I64(minute))
5354                    .fetch(pool)
5355                    .await
5356            }
5357
5358            /// Fetch every row where `<col> LIKE <pattern>` —
5359            /// caller-supplied pattern (must include `%` / `_`
5360            /// wildcards manually). Eloquent `Model::whereLike`
5361            /// parity.
5362            ///
5363            /// # Errors
5364            /// As [`FetcherPool::fetch`].
5365            ///
5366            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5367            pub async fn where_like(
5368                col: &str,
5369                pattern: impl ::core::convert::Into<::std::string::String>,
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!("{}__like", col);
5377                #root::query::QuerySet::<Self>::default()
5378                    .filter(
5379                        &_key,
5380                        #root::core::SqlValue::String(pattern.into()),
5381                    )
5382                    .fetch(pool)
5383                    .await
5384            }
5385
5386            /// Fetch every row where `<col> ILIKE <pattern>` —
5387            /// case-insensitive LIKE (PG native, MySQL/SQLite
5388            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5389            ///
5390            /// # Errors
5391            /// As [`FetcherPool::fetch`].
5392            ///
5393            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5394            pub async fn where_ilike(
5395                col: &str,
5396                pattern: impl ::core::convert::Into<::std::string::String>,
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                let _key = ::std::format!("{}__ilike", col);
5404                #root::query::QuerySet::<Self>::default()
5405                    .filter(
5406                        &_key,
5407                        #root::core::SqlValue::String(pattern.into()),
5408                    )
5409                    .fetch(pool)
5410                    .await
5411            }
5412
5413            /// Fetch every row where `<col>` starts with `prefix`
5414            /// (auto-appends `%`). Eloquent
5415            /// `whereLike("col", "$prefix%")` parity.
5416            ///
5417            /// # Errors
5418            /// As [`FetcherPool::fetch`].
5419            ///
5420            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5421            pub async fn where_starts_with(
5422                col: &str,
5423                prefix: impl ::core::convert::Into<::std::string::String>,
5424                pool: &#root::sql::Pool,
5425            ) -> ::core::result::Result<
5426                ::std::vec::Vec<Self>,
5427                #root::sql::ExecError,
5428            > {
5429                use #root::sql::FetcherPool as _;
5430                let _key = ::std::format!("{}__startswith", col);
5431                #root::query::QuerySet::<Self>::default()
5432                    .filter(
5433                        &_key,
5434                        #root::core::SqlValue::String(prefix.into()),
5435                    )
5436                    .fetch(pool)
5437                    .await
5438            }
5439
5440            /// Fetch every row where `<col>` ends with `suffix`
5441            /// (auto-prepends `%`). Eloquent
5442            /// `whereLike("col", "%$suffix")` parity.
5443            ///
5444            /// # Errors
5445            /// As [`FetcherPool::fetch`].
5446            ///
5447            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5448            pub async fn where_ends_with(
5449                col: &str,
5450                suffix: impl ::core::convert::Into<::std::string::String>,
5451                pool: &#root::sql::Pool,
5452            ) -> ::core::result::Result<
5453                ::std::vec::Vec<Self>,
5454                #root::sql::ExecError,
5455            > {
5456                use #root::sql::FetcherPool as _;
5457                let _key = ::std::format!("{}__endswith", col);
5458                #root::query::QuerySet::<Self>::default()
5459                    .filter(
5460                        &_key,
5461                        #root::core::SqlValue::String(suffix.into()),
5462                    )
5463                    .fetch(pool)
5464                    .await
5465            }
5466
5467            /// Fetch every row where `<col>` contains `substr`
5468            /// (auto-wraps with `%`).
5469            /// Eloquent `whereLike("col", "%$substr%")` parity.
5470            ///
5471            /// # Errors
5472            /// As [`FetcherPool::fetch`].
5473            ///
5474            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5475            pub async fn where_contains(
5476                col: &str,
5477                substr: impl ::core::convert::Into<::std::string::String>,
5478                pool: &#root::sql::Pool,
5479            ) -> ::core::result::Result<
5480                ::std::vec::Vec<Self>,
5481                #root::sql::ExecError,
5482            > {
5483                use #root::sql::FetcherPool as _;
5484                let _key = ::std::format!("{}__contains", col);
5485                #root::query::QuerySet::<Self>::default()
5486                    .filter(
5487                        &_key,
5488                        #root::core::SqlValue::String(substr.into()),
5489                    )
5490                    .fetch(pool)
5491                    .await
5492            }
5493
5494            /// Fetch every row where `<col> > val`. Eloquent
5495            /// `Model::where($col, ">", $val)->get()` parity.
5496            ///
5497            /// # Errors
5498            /// As [`FetcherPool::fetch`].
5499            ///
5500            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5501            pub async fn where_gt(
5502                col: &str,
5503                val: impl ::core::convert::Into<#root::core::SqlValue>,
5504                pool: &#root::sql::Pool,
5505            ) -> ::core::result::Result<
5506                ::std::vec::Vec<Self>,
5507                #root::sql::ExecError,
5508            > {
5509                use #root::sql::FetcherPool as _;
5510                let _key = ::std::format!("{}__gt", col);
5511                #root::query::QuerySet::<Self>::default()
5512                    .filter(&_key, val)
5513                    .fetch(pool)
5514                    .await
5515            }
5516
5517            /// Fetch every row where `<col> >= val`. Eloquent
5518            /// `Model::where($col, ">=", $val)->get()` parity.
5519            ///
5520            /// # Errors
5521            /// As [`FetcherPool::fetch`].
5522            ///
5523            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5524            pub async fn where_gte(
5525                col: &str,
5526                val: impl ::core::convert::Into<#root::core::SqlValue>,
5527                pool: &#root::sql::Pool,
5528            ) -> ::core::result::Result<
5529                ::std::vec::Vec<Self>,
5530                #root::sql::ExecError,
5531            > {
5532                use #root::sql::FetcherPool as _;
5533                let _key = ::std::format!("{}__gte", col);
5534                #root::query::QuerySet::<Self>::default()
5535                    .filter(&_key, val)
5536                    .fetch(pool)
5537                    .await
5538            }
5539
5540            /// Fetch every row where `<col> < val`. Eloquent
5541            /// `Model::where($col, "<", $val)->get()` parity.
5542            ///
5543            /// # Errors
5544            /// As [`FetcherPool::fetch`].
5545            ///
5546            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5547            pub async fn where_lt(
5548                col: &str,
5549                val: impl ::core::convert::Into<#root::core::SqlValue>,
5550                pool: &#root::sql::Pool,
5551            ) -> ::core::result::Result<
5552                ::std::vec::Vec<Self>,
5553                #root::sql::ExecError,
5554            > {
5555                use #root::sql::FetcherPool as _;
5556                let _key = ::std::format!("{}__lt", col);
5557                #root::query::QuerySet::<Self>::default()
5558                    .filter(&_key, val)
5559                    .fetch(pool)
5560                    .await
5561            }
5562
5563            /// Fetch every row where `<col> <= val`. Eloquent
5564            /// `Model::where($col, "<=", $val)->get()` parity.
5565            ///
5566            /// # Errors
5567            /// As [`FetcherPool::fetch`].
5568            ///
5569            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5570            pub async fn where_lte(
5571                col: &str,
5572                val: impl ::core::convert::Into<#root::core::SqlValue>,
5573                pool: &#root::sql::Pool,
5574            ) -> ::core::result::Result<
5575                ::std::vec::Vec<Self>,
5576                #root::sql::ExecError,
5577            > {
5578                use #root::sql::FetcherPool as _;
5579                let _key = ::std::format!("{}__lte", col);
5580                #root::query::QuerySet::<Self>::default()
5581                    .filter(&_key, val)
5582                    .fetch(pool)
5583                    .await
5584            }
5585
5586            /// Fetch every row where `<col> <> val`. Eloquent
5587            /// `Model::where($col, "!=", $val)->get()` parity.
5588            ///
5589            /// # Errors
5590            /// As [`FetcherPool::fetch`].
5591            ///
5592            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5593            pub async fn where_ne(
5594                col: &str,
5595                val: impl ::core::convert::Into<#root::core::SqlValue>,
5596                pool: &#root::sql::Pool,
5597            ) -> ::core::result::Result<
5598                ::std::vec::Vec<Self>,
5599                #root::sql::ExecError,
5600            > {
5601                use #root::sql::FetcherPool as _;
5602                let _key = ::std::format!("{}__ne", col);
5603                #root::query::QuerySet::<Self>::default()
5604                    .filter(&_key, val)
5605                    .fetch(pool)
5606                    .await
5607            }
5608
5609            /// Fetch every row matching `<col> = val` for ANY of the
5610            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5611            /// parity. Empty `cols` returns no rows.
5612            ///
5613            /// Resolves each `&str` column to its SCHEMA-registered
5614            /// `&'static str` once and builds a single OR-composed
5615            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5616            ///
5617            /// # Errors
5618            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5619            /// when any column is not declared on the model.
5620            ///
5621            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5622            pub async fn where_any(
5623                cols: &[&str],
5624                val: impl ::core::convert::Into<#root::core::SqlValue>,
5625                pool: &#root::sql::Pool,
5626            ) -> ::core::result::Result<
5627                ::std::vec::Vec<Self>,
5628                #root::sql::ExecError,
5629            > {
5630                Self::__where_multi(cols, val, false, pool).await
5631            }
5632
5633            /// Fetch every row matching `<col> = val` for ALL listed
5634            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5635            /// Empty `cols` returns every row (vacuous AND).
5636            ///
5637            /// # Errors
5638            /// As [`Self::where_any`].
5639            pub async fn where_all(
5640                cols: &[&str],
5641                val: impl ::core::convert::Into<#root::core::SqlValue>,
5642                pool: &#root::sql::Pool,
5643            ) -> ::core::result::Result<
5644                ::std::vec::Vec<Self>,
5645                #root::sql::ExecError,
5646            > {
5647                Self::__where_multi(cols, val, true, pool).await
5648            }
5649
5650            /// Internal: build a Q expression composing `cols` via
5651            /// AND (`all`) or OR (`!all`), then fetch. Backs
5652            /// `where_any` / `where_all`.
5653            #[doc(hidden)]
5654            pub async fn __where_multi(
5655                cols: &[&str],
5656                val: impl ::core::convert::Into<#root::core::SqlValue>,
5657                all: bool,
5658                pool: &#root::sql::Pool,
5659            ) -> ::core::result::Result<
5660                ::std::vec::Vec<Self>,
5661                #root::sql::ExecError,
5662            > {
5663                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5664                    .await
5665            }
5666
5667            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5668            /// parity. PK-ordered
5669            /// is NOT guaranteed without an explicit `order_by` —
5670            /// drop into `Self::query()` for that.
5671            ///
5672            /// # Errors
5673            /// As [`FetcherPool::fetch`].
5674            ///
5675            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5676            pub async fn take(
5677                n: i64,
5678                pool: &#root::sql::Pool,
5679            ) -> ::core::result::Result<
5680                ::std::vec::Vec<Self>,
5681                #root::sql::ExecError,
5682            > {
5683                use #root::sql::FetcherPool as _;
5684                #root::query::QuerySet::<Self>::default()
5685                    .limit(n)
5686                    .fetch(pool)
5687                    .await
5688            }
5689
5690            /// Fetch the page-th window of `per_page` rows
5691            /// (1-indexed). Eloquent
5692            /// `Model::query()->forPage($page, $perPage)->get()`
5693            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5694            /// LIMIT per_page`; for large offsets this is O(N) —
5695            /// prefer keyset pagination via PK on hot paths.
5696            ///
5697            /// # Errors
5698            /// As [`FetcherPool::fetch`].
5699            ///
5700            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5701            pub async fn for_page(
5702                page: i64,
5703                per_page: i64,
5704                pool: &#root::sql::Pool,
5705            ) -> ::core::result::Result<
5706                ::std::vec::Vec<Self>,
5707                #root::sql::ExecError,
5708            > {
5709                use #root::sql::FetcherPool as _;
5710                let _offset = #root::list_params::page_offset(page, per_page);
5711                #root::query::QuerySet::<Self>::default()
5712                    .limit(per_page)
5713                    .offset(_offset)
5714                    .fetch(pool)
5715                    .await
5716            }
5717
5718            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5719            /// one page of rows AND the total row count in one
5720            /// call. Returns `(rows, total)`. Two queries: a
5721            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5722            /// for the total.
5723            ///
5724            /// Useful for paginated UIs that need both the visible
5725            /// rows AND a "Page X of Y" / total-count widget. For
5726            /// large tables prefer keyset pagination + cached count;
5727            /// every call to `paginate` re-counts the full table.
5728            ///
5729            /// Same 1-indexed `page` convention as [`Self::for_page`].
5730            ///
5731            /// # Errors
5732            /// As [`Self::for_page`] and [`Self::count`].
5733            pub async fn paginate(
5734                page: i64,
5735                per_page: i64,
5736                pool: &#root::sql::Pool,
5737            ) -> ::core::result::Result<
5738                (::std::vec::Vec<Self>, i64),
5739                #root::sql::ExecError,
5740            > {
5741                // Route through the queryset rather than `Self::count`:
5742                // the `count` inherent method is suppressed on models
5743                // that declare a field named `count` (field/shortcut
5744                // collision guard), and `paginate` must still compile
5745                // for those models.
5746                let total = {
5747                    use #root::sql::CounterPool as _;
5748                    #root::query::QuerySet::<Self>::default()
5749                        .count(pool)
5750                        .await?
5751                };
5752                let rows = Self::for_page(page, per_page, pool).await?;
5753                ::core::result::Result::Ok((rows, total))
5754            }
5755
5756            /// Bulk-update — set `set_col = set_val` on every row
5757            /// matching `where_col = where_val`. Returns affected row
5758            /// count. Eloquent
5759            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5760            /// parity.
5761            ///
5762            /// For multi-column updates drop into the queryset
5763            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5764            ///
5765            /// # Errors
5766            /// As [`UpdaterPool::execute_pool`].
5767            ///
5768            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5769            pub async fn update_where(
5770                where_col: &str,
5771                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5772                set_col: &str,
5773                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5774                pool: &#root::sql::Pool,
5775            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5776                let _query = #root::query::QuerySet::<Self>::default()
5777                    .filter(where_col, where_val)
5778                    .update()
5779                    .set(set_col, set_val)
5780                    .compile()?;
5781                #bulk_update_run
5782            }
5783
5784            /// Bulk-delete — remove every row matching
5785            /// `where_col = where_val`. Returns affected row count.
5786            /// Eloquent
5787            /// `Model::where($where_col, $where_val)->delete()` parity.
5788            ///
5789            /// # Errors
5790            /// As [`#root::sql::delete_pool`].
5791            pub async fn delete_where(
5792                where_col: &str,
5793                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5794                pool: &#root::sql::Pool,
5795            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5796                let _query = #root::query::QuerySet::<Self>::default()
5797                    .filter_op(where_col, #root::core::Op::Eq, where_val)
5798                    .compile_delete()?;
5799                #bulk_delete_run
5800            }
5801
5802            /// Bulk-update — set `set_col = set_val` on EVERY row of
5803            /// this model's table. **No WHERE clause** — use with
5804            /// care. Eloquent `Model::query()->update([$col => $val])`
5805            /// parity.
5806            ///
5807            /// Use for backfills, one-shot reset flows, etc. The
5808            /// returned count is rows affected.
5809            ///
5810            /// # Errors
5811            /// As [`UpdaterPool::execute_pool`].
5812            ///
5813            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5814            pub async fn update_all(
5815                set_col: &str,
5816                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5817                pool: &#root::sql::Pool,
5818            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5819                let _query = #root::query::QuerySet::<Self>::default()
5820                    .update()
5821                    .set(set_col, set_val)
5822                    .compile()?;
5823                #bulk_update_run
5824            }
5825
5826            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5827            /// Eloquent `Model::whereNotLike` parity. Pattern is
5828            /// passed verbatim — caller controls `%` / `_`.
5829            ///
5830            /// # Errors
5831            /// As [`FetcherPool::fetch`].
5832            ///
5833            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5834            pub async fn where_not_like(
5835                col: &str,
5836                pattern: impl ::core::convert::Into<::std::string::String>,
5837                pool: &#root::sql::Pool,
5838            ) -> ::core::result::Result<
5839                ::std::vec::Vec<Self>,
5840                #root::sql::ExecError,
5841            > {
5842                use #root::sql::FetcherPool as _;
5843                let _key = ::std::format!("{}__not_like", col);
5844                #root::query::QuerySet::<Self>::default()
5845                    .filter(
5846                        &_key,
5847                        #root::core::SqlValue::String(pattern.into()),
5848                    )
5849                    .fetch(pool)
5850                    .await
5851            }
5852
5853            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5854            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5855            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5856            ///
5857            /// # Errors
5858            /// As [`FetcherPool::fetch`].
5859            ///
5860            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5861            pub async fn where_not_ilike(
5862                col: &str,
5863                pattern: impl ::core::convert::Into<::std::string::String>,
5864                pool: &#root::sql::Pool,
5865            ) -> ::core::result::Result<
5866                ::std::vec::Vec<Self>,
5867                #root::sql::ExecError,
5868            > {
5869                use #root::sql::FetcherPool as _;
5870                let _key = ::std::format!("{}__not_ilike", col);
5871                #root::query::QuerySet::<Self>::default()
5872                    .filter(
5873                        &_key,
5874                        #root::core::SqlValue::String(pattern.into()),
5875                    )
5876                    .fetch(pool)
5877                    .await
5878            }
5879
5880            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5881            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5882            /// parity.
5883            ///
5884            /// # Errors
5885            /// As [`FetcherPool::fetch`].
5886            ///
5887            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5888            pub async fn where_not_between(
5889                col: &str,
5890                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5891                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5892                pool: &#root::sql::Pool,
5893            ) -> ::core::result::Result<
5894                ::std::vec::Vec<Self>,
5895                #root::sql::ExecError,
5896            > {
5897                use #root::sql::FetcherPool as _;
5898                let _key = ::std::format!("{}__not_between", col);
5899                let _vals = #root::core::SqlValue::List(::std::vec![
5900                    ::core::convert::Into::into(lo),
5901                    ::core::convert::Into::into(hi),
5902                ]);
5903                #root::query::QuerySet::<Self>::default()
5904                    .filter(&_key, _vals)
5905                    .fetch(pool)
5906                    .await
5907            }
5908
5909            /// Returns the SQL table name for this model. Eloquent
5910            /// `$model->getTable()` parity.
5911            #[must_use]
5912            pub fn table_name() -> &'static str {
5913                <Self as #root::core::Model>::SCHEMA.table
5914            }
5915
5916            /// Returns the SQL column name of this model's primary
5917            /// key, or `None` when the model has no
5918            /// `#[rustango(primary_key)]`. Eloquent
5919            /// `$model->getKeyName()` parity.
5920            #[must_use]
5921            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5922                <Self as #root::core::Model>::SCHEMA
5923                    .primary_key()
5924                    .map(|f| f.column)
5925            }
5926
5927            /// Fetch every row where `<col> BETWEEN lo AND hi`
5928            /// (inclusive on both ends — same as SQL). Eloquent
5929            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5930            ///
5931            /// # Errors
5932            /// As [`FetcherPool::fetch`].
5933            ///
5934            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5935            pub async fn where_between(
5936                col: &str,
5937                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5938                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5939                pool: &#root::sql::Pool,
5940            ) -> ::core::result::Result<
5941                ::std::vec::Vec<Self>,
5942                #root::sql::ExecError,
5943            > {
5944                use #root::sql::FetcherPool as _;
5945                let _key = ::std::format!("{}__between", col);
5946                let _vals = #root::core::SqlValue::List(::std::vec![
5947                    ::core::convert::Into::into(lo),
5948                    ::core::convert::Into::into(hi),
5949                ]);
5950                #root::query::QuerySet::<Self>::default()
5951                    .filter(&_key, _vals)
5952                    .fetch(pool)
5953                    .await
5954            }
5955
5956            /// Fetch the first row where `<col> = <val>`. Returns
5957            /// `Ok(None)` when no row matches. Eloquent
5958            /// `Model::firstWhere($col, $val)` parity.
5959            ///
5960            /// Thin wrapper over `QuerySet::<Self>::default()
5961            /// .filter(col, val).first(pool)`. Use this when you
5962            /// want one row identified by a non-PK column (e.g.
5963            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5964            ///
5965            /// `val` accepts any value `Into<SqlValue>` so plain
5966            /// strings, ints, UUIDs, etc. all work.
5967            ///
5968            /// # Errors
5969            /// As `QuerySet::first`.
5970            pub async fn first_where(
5971                col: &str,
5972                val: impl ::core::convert::Into<#root::core::SqlValue>,
5973                pool: &#root::sql::Pool,
5974            ) -> ::core::result::Result<
5975                ::core::option::Option<Self>,
5976                #root::sql::ExecError,
5977            > {
5978                #root::query::QuerySet::<Self>::default()
5979                    .filter(col, val)
5980                    .first(pool)
5981                    .await
5982            }
5983
5984            #value_method
5985
5986            /// Fetch the row with the largest non-NULL `field` value.
5987            /// Returns `Ok(None)` when the table is empty or every
5988            /// `field` is NULL. Thin wrapper over
5989            /// `QuerySet::<Self>::default().latest(field, pool)`.
5990            ///
5991            /// **Field name** is the Rust field ident as a string
5992            /// (not the SQL column). It is a runtime `&str`, so an
5993            /// unknown one compiles and surfaces as
5994            /// `ExecError::Query(QueryError::UnknownField)` when the
5995            /// query runs.
5996            ///
5997            /// # Errors
5998            /// As `QuerySet::latest`.
5999            pub async fn latest(
6000                field: &str,
6001                pool: &#root::sql::Pool,
6002            ) -> ::core::result::Result<
6003                ::core::option::Option<Self>,
6004                #root::sql::ExecError,
6005            > {
6006                #root::query::QuerySet::<Self>::default()
6007                    .latest(field, pool)
6008                    .await
6009            }
6010
6011            /// Sibling of [`Self::latest`]: fetches the row with the
6012            /// smallest non-NULL `field` value.
6013            ///
6014            /// # Errors
6015            /// As [`Self::latest`].
6016            pub async fn earliest(
6017                field: &str,
6018                pool: &#root::sql::Pool,
6019            ) -> ::core::result::Result<
6020                ::core::option::Option<Self>,
6021                #root::sql::ExecError,
6022            > {
6023                #root::query::QuerySet::<Self>::default()
6024                    .earliest(field, pool)
6025                    .await
6026            }
6027
6028            #count_method
6029
6030            /// `true` when the table contains at least one row.
6031            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6032            /// over `QuerySet::<Self>::default().exists(pool)`.
6033            ///
6034            /// # Errors
6035            /// As [`ExistsPool::exists`].
6036            ///
6037            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6038            pub async fn exists(
6039                pool: &#root::sql::Pool,
6040            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6041                use #root::sql::ExistsPool as _;
6042                #root::query::QuerySet::<Self>::default()
6043                    .exists(pool)
6044                    .await
6045            }
6046
6047            /// Inverse of [`Self::exists`] — returns `true` when
6048            /// the table has zero rows. Eloquent
6049            /// `Model::doesntExist()` parity.
6050            ///
6051            /// # Errors
6052            /// As [`Self::exists`].
6053            pub async fn doesnt_exist(
6054                pool: &#root::sql::Pool,
6055            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6056                Self::exists(pool).await.map(|e| !e)
6057            }
6058
6059            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6060            /// `true` when a row with primary key `pk` exists in
6061            /// the table. Sugar over
6062            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6063            ///
6064            /// Differs from [`Self::exists`] (which checks "any row
6065            /// in the table") by checking a specific PK existence.
6066            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6067            /// because the row is never materialized — the SQL is
6068            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6069            ///
6070            /// # Errors
6071            /// As [`#root::sql::ExistsPool::contains_pk`].
6072            pub async fn contains_pk(
6073                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6074                pool: &#root::sql::Pool,
6075            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6076                use #root::sql::ExistsPool as _;
6077                #root::query::QuerySet::<Self>::default()
6078                    .contains_pk(pool, pk)
6079                    .await
6080            }
6081
6082            #sum_method
6083            #avg_method
6084            #min_method
6085            #max_method
6086
6087            /// Fetch every row of this model from `pool`. Eloquent
6088            /// `Model::all()` parity — a thin wrapper over
6089            /// `QuerySet::<Self>::default().fetch(pool)`.
6090            ///
6091            /// **Use with care on large tables** — there's no
6092            /// pagination or limit; the entire table is materialized
6093            /// into memory. For anything beyond fixture / lookup
6094            /// tables, page through `QuerySet::<Self>::default()
6095            /// .order_by(...).limit(N).offset(M).fetch(pool)`
6096            /// or stream via `.iterator(chunk_size)`.
6097            ///
6098            /// # Errors
6099            /// As [`FetcherPool::fetch`].
6100            ///
6101            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6102            pub async fn all(
6103                pool: &#root::sql::Pool,
6104            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6105            {
6106                use #root::sql::FetcherPool as _;
6107                #root::query::QuerySet::<Self>::default()
6108                    .fetch(pool)
6109                    .await
6110            }
6111
6112            /// Look up every row whose primary key is in `pks`.
6113            /// Returns the matching rows in **inventory** order — NOT
6114            /// the order of `pks`. Empty `pks` returns an empty
6115            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6116            /// with a list) parity.
6117            ///
6118            /// Thin wrapper over `QuerySet::<Self>::default()
6119            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6120            /// Caller-supplied PKs that don't match a row are
6121            /// silently skipped (the returned vec is shorter than
6122            /// the input list). For an order-preserving / "fail
6123            /// when any missing" variant, build the queryset
6124            /// explicitly with `in_bulk` instead.
6125            ///
6126            /// Accepts any iterable whose elements are
6127            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6128            /// `Vec<Uuid>`, etc.
6129            ///
6130            /// # Errors
6131            /// As [`FetcherPool::fetch`].
6132            ///
6133            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6134            pub async fn find_many<V>(
6135                pks: impl ::core::iter::IntoIterator<Item = V>,
6136                pool: &#root::sql::Pool,
6137            ) -> ::core::result::Result<
6138                ::std::vec::Vec<Self>,
6139                #root::sql::ExecError,
6140            >
6141            where
6142                V: ::core::convert::Into<#root::core::SqlValue>,
6143            {
6144                use #root::sql::FetcherPool as _;
6145                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6146                    pks.into_iter().map(::core::convert::Into::into).collect();
6147                if _values.is_empty() {
6148                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6149                }
6150                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6151                let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
6152                let _split = _batches.len() > 1;
6153                let mut _rows = ::std::vec::Vec::new();
6154                for _keys in _batches {
6155                    _rows.extend(
6156                        #root::query::QuerySet::<Self>::default()
6157                            .filter(&_key, #root::core::SqlValue::List(_keys))
6158                            .fetch(pool)
6159                            .await?,
6160                    );
6161                }
6162                if _split {
6163                    _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
6164                }
6165                ::core::result::Result::Ok(_rows)
6166            }
6167
6168            /// Look up the row whose primary key equals `pk`. Returns
6169            /// `Ok(None)` when no row matches, rather than erroring.
6170            /// Eloquent `Model::find`
6171            /// shape — accepts any value `Into<SqlValue>`.
6172            ///
6173            /// One-liner shortcut for the common
6174            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6175            /// .limit(1).fetch(pool).await?.into_iter().next()`
6176            /// dance.
6177            ///
6178            /// # Errors
6179            /// As [`FetcherPool::fetch`].
6180            ///
6181            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6182            pub async fn find(
6183                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6184                pool: &#root::sql::Pool,
6185            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6186            {
6187                use #root::sql::FetcherPool as _;
6188                let _pk_val: #root::core::SqlValue = pk.into();
6189                let mut _rows: ::std::vec::Vec<Self> =
6190                    #root::query::QuerySet::<Self>::default()
6191                        .filter(::core::stringify!(#pk_ident), _pk_val)
6192                        .limit(1)
6193                        .fetch(pool)
6194                        .await?;
6195                ::core::result::Result::Ok(_rows.into_iter().next())
6196            }
6197
6198            /// Look up the row whose primary key equals `pk`. Errors
6199            /// when no row matches — the throwing counterpart of
6200            /// [`Self::find`]. Eloquent `Model::findOrFail`
6201            /// parity.
6202            ///
6203            /// Translates the miss into
6204            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6205            /// so callers can `?`-bubble straight through the typical
6206            /// `ExecError` error chain.
6207            ///
6208            /// # Errors
6209            /// As [`Self::find`]; additionally
6210            /// [`sqlx::Error::RowNotFound`] when no row matches.
6211            ///
6212            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6213            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6214            pub async fn find_or_fail(
6215                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6216                pool: &#root::sql::Pool,
6217            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6218                match Self::find(pk, pool).await? {
6219                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6220                    ::core::option::Option::None => ::core::result::Result::Err(
6221                        #root::sql::ExecError::Driver(
6222                            #root::sql::sqlx::Error::RowNotFound,
6223                        ),
6224                    ),
6225                }
6226            }
6227
6228            /// Look up multiple rows by primary key, **failing** if
6229            /// any requested PK is missing. Eloquent
6230            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6231            /// inventory order (NOT request order — same as
6232            /// [`Self::find_many`]).
6233            ///
6234            /// Empty `pks` returns an empty `Vec` (no rows requested
6235            /// → nothing to fail on).
6236            ///
6237            /// Differs from [`Self::find_many`] in that this method
6238            /// requires every PK to resolve. If even one is missing,
6239            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6240            /// in `ExecError::Driver`). A key counts as found when the
6241            /// database matches it to a row, so repeats and keys its
6242            /// collation folds together (`abc` / `ABC` on MySQL) pass.
6243            ///
6244            /// # Errors
6245            /// As [`Self::find_many`], plus `RowNotFound` when a
6246            /// requested PK matches no row.
6247            pub async fn find_many_or_fail<V>(
6248                pks: impl ::core::iter::IntoIterator<Item = V>,
6249                pool: &#root::sql::Pool,
6250            ) -> ::core::result::Result<
6251                ::std::vec::Vec<Self>,
6252                #root::sql::ExecError,
6253            >
6254            where
6255                V: ::core::convert::Into<#root::core::SqlValue>,
6256            {
6257                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6258                    pks.into_iter().map(::core::convert::Into::into).collect();
6259                if _values.is_empty() {
6260                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6261                }
6262                use #root::sql::ExistsPool as _;
6263                let mut _seen: ::std::collections::HashSet<
6264                    ::std::string::String,
6265                > = ::std::collections::HashSet::new();
6266                let mut _keys: ::std::vec::Vec<#root::core::SqlValue> =
6267                    ::std::vec::Vec::new();
6268                for v in &_values {
6269                    if _seen.insert(v.to_display_string()) {
6270                        _keys.push(::core::clone::Clone::clone(v));
6271                    }
6272                }
6273                let _rows = Self::find_many(_values, pool).await?;
6274                let _found: ::std::collections::HashSet<::std::string::String> = _rows
6275                    .iter()
6276                    .map(|r| r.__rustango_pk_value().to_display_string())
6277                    .collect();
6278                // A key with no exact twin among the rows may still match one
6279                // under the column's collation: the database decides.
6280                for _key in _keys {
6281                    if _found.contains(&_key.to_display_string()) {
6282                        continue;
6283                    }
6284                    let _hit = #root::query::QuerySet::<Self>::default()
6285                        .filter(::core::stringify!(#pk_ident), _key)
6286                        .exists(pool)
6287                        .await?;
6288                    if !_hit {
6289                        return ::core::result::Result::Err(
6290                            #root::sql::ExecError::Driver(
6291                                #root::sql::sqlx::Error::RowNotFound,
6292                            ),
6293                        );
6294                    }
6295                }
6296                ::core::result::Result::Ok(_rows)
6297            }
6298
6299            /// Find by primary key or run `fallback` to produce a
6300            /// default row to return. Eloquent
6301            /// `Model::findOr($pk, fn() => …)` parity.
6302            ///
6303            /// Unlike [`Self::find_or_fail`] (which raises on
6304            /// miss), this is the "give me something sensible"
6305            /// branch: typical use is "fetch the user's row, else
6306            /// fall back to an anonymous/guest stub".
6307            ///
6308            /// `fallback` runs only when no row matches — the DB
6309            /// round-trip happens unconditionally.
6310            ///
6311            /// # Errors
6312            /// As [`Self::find`].
6313            pub async fn find_or<F>(
6314                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6315                pool: &#root::sql::Pool,
6316                fallback: F,
6317            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6318            where
6319                F: ::core::ops::FnOnce() -> Self,
6320            {
6321                ::core::result::Result::Ok(
6322                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6323                )
6324            }
6325
6326            /// Same as [`Self::find_or`] but also returns a `bool`
6327            /// indicating whether the row was found in the DB
6328            /// (`true`) or freshly built from `fallback` (`false`).
6329            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6330            /// in PHP the returned model also exposes `->exists`;
6331            /// here that's surfaced as the second tuple element.
6332            ///
6333            /// Useful for edit-or-create form handlers where the
6334            /// caller needs to know whether to PATCH or POST when
6335            /// the user submits the form.
6336            ///
6337            /// # Errors
6338            /// As [`Self::find`].
6339            pub async fn find_or_new<F>(
6340                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6341                pool: &#root::sql::Pool,
6342                fallback: F,
6343            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6344            where
6345                F: ::core::ops::FnOnce() -> Self,
6346            {
6347                match Self::find(pk, pool).await? {
6348                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6349                    ::core::option::Option::None => {
6350                        ::core::result::Result::Ok((fallback(), false))
6351                    }
6352                }
6353            }
6354
6355            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6356            /// [`Self::find_or_new`] but **persists** the new row
6357            /// when the PK isn't found. Returns `(row, exists: bool)`
6358            /// — `exists=true` when the PK was found,
6359            /// `false` when a fresh row was inserted.
6360            ///
6361            /// ```ignore
6362            /// let (post, found) = Post::find_or_insert(
6363            ///     pk,
6364            ///     &pool,
6365            ///     || Post { id: Auto::default(), title: "new".into() },
6366            /// ).await?;
6367            /// // `found` true → returned existing row;
6368            /// // `found` false → fallback was inserted, `post.id` populated.
6369            /// ```
6370            ///
6371            /// **Caveat**: two concurrent calls that both miss the
6372            /// find can both INSERT, violating uniqueness. Wrap in a
6373            /// transaction or rely on a UNIQUE constraint + handle
6374            /// the conflict error if you need race-free semantics.
6375            ///
6376            /// # Errors
6377            /// As [`Self::find`] and [`Self::save`].
6378            pub async fn find_or_insert<F>(
6379                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6380                pool: &#root::sql::Pool,
6381                fallback: F,
6382            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6383            where
6384                F: ::core::ops::FnOnce() -> Self,
6385            {
6386                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6387                    return ::core::result::Result::Ok((_row, true));
6388                }
6389                let mut _new = fallback();
6390                _new.save(pool).await?;
6391                ::core::result::Result::Ok((_new, false))
6392            }
6393
6394            /// Fetch the first row of the table, or run `fallback`
6395            /// when the table is empty. Eloquent
6396            /// `Model::firstOr(fn() => …)` parity.
6397            ///
6398            /// # Errors
6399            /// As [`Self::first`].
6400            pub async fn first_or<F>(
6401                pool: &#root::sql::Pool,
6402                fallback: F,
6403            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6404            where
6405                F: ::core::ops::FnOnce() -> Self,
6406            {
6407                // Route through the queryset rather than `Self::first`,
6408                // which is suppressed on models with a field named
6409                // `first` (field/shortcut collision guard).
6410                ::core::result::Result::Ok(
6411                    #root::query::QuerySet::<Self>::default()
6412                        .first(pool)
6413                        .await?
6414                        .unwrap_or_else(fallback),
6415                )
6416            }
6417
6418            /// Fetch exactly one row matching `<col> = <val>`. Errors
6419            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6420            /// or more than one matches
6421            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6422            /// Eloquent `Model::sole($col, $val)` parity.
6423            ///
6424            /// # Errors
6425            /// As [`Self::where_`] plus the explicit
6426            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6427            pub async fn sole(
6428                col: &str,
6429                val: impl ::core::convert::Into<#root::core::SqlValue>,
6430                pool: &#root::sql::Pool,
6431            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6432                let mut _rows = Self::where_(col, val, pool).await?;
6433                match _rows.len() {
6434                    0 => ::core::result::Result::Err(
6435                        #root::sql::ExecError::Driver(
6436                            #root::sql::sqlx::Error::RowNotFound,
6437                        ),
6438                    ),
6439                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6440                    n => ::core::result::Result::Err(
6441                        #root::sql::ExecError::MultipleRowsReturned {
6442                            op: "sole",
6443                            table: <Self as #root::core::Model>::SCHEMA.name,
6444                            count: n,
6445                        },
6446                    ),
6447                }
6448            }
6449        }
6450    } else {
6451        quote!()
6452    };
6453
6454    // `_tx` family: the shared `__rustango_*_exec` bodies on an open
6455    // `PoolTx`; an audited model writes its snapshot entry in the same tx.
6456    let tx_emit = |op: TokenStream2| -> TokenStream2 {
6457        if audited_fields.is_some() && primary_key.is_some() {
6458            quote! {
6459                #root::audit::__emit_many_tx(
6460                    tx,
6461                    &[self.__rustango_audit_entry(#root::audit::AuditOp::#op)],
6462                )
6463                .await?;
6464            }
6465        } else {
6466            quote!()
6467        }
6468    };
6469    let tx_insert_method = {
6470        let self_ref = if fields.has_auto {
6471            quote!(&mut self)
6472        } else {
6473            quote!(&self)
6474        };
6475        let emit = tx_emit(quote!(Create));
6476        let refuse_pkless = if audited_fields.is_some() && primary_key.is_none() {
6477            quote! {
6478                return ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
6479                    table: <Self as #root::core::Model>::SCHEMA.table,
6480                });
6481            }
6482        } else {
6483            quote!()
6484        };
6485        quote! {
6486            /// Insert this row inside an open transaction; an audited model
6487            /// writes its audit row there too.
6488            ///
6489            /// # Errors
6490            /// As [`Self::insert`].
6491            #[allow(unreachable_code)]
6492            pub async fn insert_tx(
6493                #self_ref,
6494                tx: &mut #root::sql::PoolTx<'_>,
6495            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6496                #refuse_pkless
6497                self.__rustango_insert_exec(&mut *tx).await?;
6498                #emit
6499                ::core::result::Result::Ok(())
6500            }
6501        }
6502    };
6503    let tx_save_method = if let Some((pk_ident, _)) = primary_key {
6504        let emit_create = tx_emit(quote!(Create));
6505        let emit_update = tx_emit(quote!(Update));
6506        let was_unset = if fields.pk_is_auto {
6507            quote!(matches!(self.#pk_ident, #root::sql::Auto::Unset))
6508        } else {
6509            quote!(false)
6510        };
6511        quote! {
6512            /// Save this row inside an open transaction; an audited model
6513            /// writes its audit row there too.
6514            ///
6515            /// # Errors
6516            /// As [`Self::save`].
6517            pub async fn save_tx(
6518                &mut self,
6519                tx: &mut #root::sql::PoolTx<'_>,
6520            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6521                let _was_unset = #was_unset;
6522                let _n = self.__rustango_save_exec(&mut *tx).await?;
6523                if _n > 0 {
6524                    if _was_unset {
6525                        #emit_create
6526                    } else {
6527                        #emit_update
6528                    }
6529                }
6530                ::core::result::Result::Ok(_n)
6531            }
6532        }
6533    } else {
6534        quote!()
6535    };
6536    let tx_delete_method = if primary_key.is_some() {
6537        let emit = tx_emit(quote!(Delete));
6538        quote! {
6539            /// Delete this row inside an open transaction; an audited model
6540            /// writes its audit row there too.
6541            ///
6542            /// # Errors
6543            /// As [`Self::delete`].
6544            pub async fn delete_tx(
6545                &self,
6546                tx: &mut #root::sql::PoolTx<'_>,
6547            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6548                let _n = self.__rustango_delete_exec(&mut *tx).await?;
6549                if _n > 0 {
6550                    #emit
6551                }
6552                ::core::result::Result::Ok(_n)
6553            }
6554        }
6555    } else {
6556        quote!()
6557    };
6558
6559    // The one statement-level write path: `_on` (refusing audited models)
6560    // and `_tx` share these `__rustango_*_exec` bodies; non-audited `&Pool`
6561    // methods forward to them too.
6562    let refuse = |method: &str| -> TokenStream2 {
6563        let reason = format!("`{method}` cannot audit; use the `&Pool` method");
6564        quote!(#root::sql::__macro_internals::refuse_audited::<Self>(#reason)?;)
6565    };
6566    let refuse_save = refuse("save_on");
6567    let save_method = if let Some((pk_ident, pk_column)) = primary_key {
6568        let pk_column_lit = pk_column.as_str();
6569        let assignments = &fields.update_assignments;
6570        let dispatch_unset = if fields.pk_is_auto {
6571            quote! {
6572                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6573                    return self.__rustango_insert_exec(_executor).await.map(|()| 1u64);
6574                }
6575            }
6576        } else {
6577            quote!()
6578        };
6579        let write = restore_auto_now_unless(
6580            &fields.auto_now_idents,
6581            &save_ret,
6582            &save_keep,
6583            &quote! {
6584                let _query = #root::core::UpdateQuery::new(
6585                    <Self as #root::core::Model>::SCHEMA,
6586                    ::std::vec![ #( #assignments ),* ],
6587                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6588                        #pk_column_lit,
6589                        #root::core::Op::Eq,
6590                        ::core::convert::Into::<#root::core::SqlValue>::into(
6591                            ::core::clone::Clone::clone(&self.#pk_ident)
6592                        ),
6593                    )),
6594                );
6595                #root::sql::__macro_internals::update_on(_executor, &_query)
6596                    .await
6597                    .and_then(#root::sql::__macro_internals::row_saved)
6598            },
6599        );
6600        let upsert = if fields.pk_is_auto {
6601            let upsert_cols = &fields.upsert_update_columns;
6602            let upsert_pushes = &fields.insert_pushes;
6603            let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6604                fields.uuid_v7_fields.iter().cloned().unzip();
6605            let upsert_returning = &fields.returning_cols;
6606            let auto_now_idents = &fields.auto_now_idents;
6607            // Prefer the first `unique_together`: a server-assigned PK never
6608            // collides, so a PK-only target would always insert.
6609            let upsert_target_columns: Vec<String> =
6610                upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6611            let upsert_target_lits = upsert_target_columns
6612                .iter()
6613                .map(String::as_str)
6614                .collect::<Vec<_>>();
6615            let conflict_clause = if fields.upsert_update_columns.is_empty() {
6616                quote!(#root::core::ConflictClause::DoNothing)
6617            } else {
6618                quote!(#root::core::ConflictClause::DoUpdate {
6619                    target: ::std::vec![ #( #upsert_target_lits ),* ],
6620                    update_columns: ::std::vec![ #( #upsert_cols ),* ],
6621                })
6622            };
6623            let unit_ret = quote!(::core::result::Result<(), #root::sql::ExecError>);
6624            let unit_keep = quote!(_saved.is_ok());
6625            let exec_body = restore_auto_now_unless(
6626                auto_now_idents,
6627                &unit_ret,
6628                &unit_keep,
6629                &quote! {
6630                    let _query = self.__rustango_upsert_query();
6631                    let _row = #root::sql::__macro_internals::insert_returning_on(
6632                        _executor, _query,
6633                    )
6634                    .await?;
6635                    #root::sql::apply_auto_pk(_row, self)
6636                },
6637            );
6638            let pool_body = if audited_fields.is_some() {
6639                restore_auto_now_unless(
6640                    auto_now_idents,
6641                    &unit_ret,
6642                    &unit_keep,
6643                    &quote! {
6644                        let _query = self.__rustango_upsert_query();
6645                        #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self, _op| {
6646                            _m.__rustango_audit_entry(_op)
6647                        })
6648                        .await
6649                    },
6650                )
6651            } else {
6652                quote!(self.__rustango_upsert_exec(pool).await)
6653            };
6654            let refuse_upsert = refuse("upsert_on");
6655            quote! {
6656                /// Insert this row, or update the row it conflicts with: one
6657                /// `INSERT … ON CONFLICT` (MySQL: `ON DUPLICATE KEY UPDATE`).
6658                /// The target is the first `unique_together`, else the PK.
6659                /// Fills the `Auto<T>` fields from the written row.
6660                ///
6661                /// # Errors
6662                /// As [`Self::insert`].
6663                pub async fn upsert(
6664                    &mut self,
6665                    pool: &#root::sql::Pool,
6666                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6667                    #pool_body
6668                }
6669
6670                /// [`Self::upsert`] on any [`#root::sql::SqlExecutor`]. An
6671                /// audited model refuses it: use [`Self::upsert`].
6672                ///
6673                /// # Errors
6674                /// As [`Self::upsert`].
6675                pub async fn upsert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6676                    &mut self,
6677                    _executor: _E,
6678                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6679                    #refuse_upsert
6680                    self.__rustango_upsert_exec(_executor).await
6681                }
6682
6683                #[doc(hidden)]
6684                pub async fn __rustango_upsert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6685                    &mut self,
6686                    _executor: _E,
6687                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6688                    #exec_body
6689                }
6690
6691                /// The upsert statement; stamps `auto_now` like `save`.
6692                #[doc(hidden)]
6693                pub fn __rustango_upsert_query(&mut self) -> #root::core::InsertQuery {
6694                    let mut _columns: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
6695                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6696                        ::std::vec::Vec::new();
6697                    #[allow(unused_mut)]
6698                    let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
6699                    #(
6700                        if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
6701                            _generated.push(#gen_cols);
6702                        }
6703                    )*
6704                    #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
6705                    #( #upsert_pushes )*
6706                    #root::core::InsertQuery::new(
6707                        <Self as #root::core::Model>::SCHEMA,
6708                        _columns,
6709                        _values,
6710                    )
6711                    .returning(::std::vec![ #( #upsert_returning ),* ])
6712                    .on_conflict(#conflict_clause)
6713                    .generated(_generated)
6714                }
6715            }
6716        } else {
6717            quote!()
6718        };
6719        Some(quote! {
6720            /// [`Self::save`] on any [`#root::sql::SqlExecutor`]: a held
6721            /// `PoolConn`, a `PoolTx`, a tenant connection or a Postgres sqlx
6722            /// executor. An audited model refuses it: use [`Self::save`].
6723            ///
6724            /// # Errors
6725            /// As [`Self::save`].
6726            pub async fn save_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6727                &mut self,
6728                _executor: _E,
6729            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6730                #refuse_save
6731                self.__rustango_save_exec(_executor).await
6732            }
6733
6734            #[doc(hidden)]
6735            pub async fn __rustango_save_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6736                &mut self,
6737                _executor: _E,
6738            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6739                #dispatch_unset
6740                #write
6741            }
6742
6743            /// Old name of [`Self::save`].
6744            ///
6745            /// # Errors
6746            /// As [`Self::save`].
6747            #[deprecated(since = "0.62.0", note = "use `save`")]
6748            pub async fn save_pool(
6749                &mut self,
6750                pool: &#root::sql::Pool,
6751            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6752                self.save(pool).await
6753            }
6754
6755            #upsert
6756        })
6757    } else {
6758        None
6759    };
6760
6761    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6762        let pk_column_lit = pk_column.as_str();
6763        let refuse_delete = refuse("delete_on");
6764        // Optional `soft_delete_on` / `restore_on` companions when the
6765        // model has a `#[rustango(soft_delete)]` column. They land
6766        // alongside the regular `delete_on` so callers have both
6767        // options — a hard delete (audit-tracked as a real DELETE) and
6768        // a logical delete (audit-tracked as an UPDATE setting the
6769        // deleted_at column to NOW()).
6770        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6771            let col_lit = col;
6772            let sd_field_ident = fields
6773                .soft_delete_field_ident
6774                .clone()
6775                .expect("soft_delete_column without ident");
6776            let refuse_soft_delete = refuse("soft_delete_on");
6777            let refuse_restore = refuse("restore_on");
6778            // `self` takes the written value, so a later `save` keeps it.
6779            let mark_deleted = quote! {
6780                if _n > 0 {
6781                    self.#sd_field_ident = ::core::option::Option::Some(_now);
6782                }
6783            };
6784            let mark_restored = quote! {
6785                if _n > 0 {
6786                    self.#sd_field_ident = ::core::option::Option::None;
6787                }
6788            };
6789            quote! {
6790                /// Soft-delete this row on any [`#root::sql::SqlExecutor`]: set
6791                /// its `#[rustango(soft_delete)]` column to now. An audited
6792                /// model refuses it: use [`Self::soft_delete`].
6793                ///
6794                /// # Errors
6795                /// As [`Self::delete`].
6796                pub async fn soft_delete_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6797                    &mut self,
6798                    _executor: _E,
6799                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6800                    #refuse_soft_delete
6801                    let _now = #root::__chrono::Utc::now();
6802                    let _query = #root::soft_delete::__mark_query(
6803                        <Self as #root::core::Model>::SCHEMA,
6804                        #col_lit,
6805                        #pk_column_lit,
6806                        ::core::convert::Into::<#root::core::SqlValue>::into(
6807                            ::core::clone::Clone::clone(&self.#pk_ident)
6808                        ),
6809                        ::core::option::Option::Some(_now),
6810                    );
6811                    let _n = #root::sql::__macro_internals::update_on(_executor, &_query).await?;
6812                    #mark_deleted
6813                    ::core::result::Result::Ok(_n)
6814                }
6815
6816                /// Inverse of [`Self::soft_delete_on`]: clear the soft-delete
6817                /// column. An audited model refuses it: use [`Self::restore`].
6818                ///
6819                /// # Errors
6820                /// As [`Self::delete`].
6821                pub async fn restore_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6822                    &mut self,
6823                    _executor: _E,
6824                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6825                    #refuse_restore
6826                    let _query = #root::soft_delete::__mark_query(
6827                        <Self as #root::core::Model>::SCHEMA,
6828                        #col_lit,
6829                        #pk_column_lit,
6830                        ::core::convert::Into::<#root::core::SqlValue>::into(
6831                            ::core::clone::Clone::clone(&self.#pk_ident)
6832                        ),
6833                        ::core::option::Option::None,
6834                    );
6835                    let _n = #root::sql::__macro_internals::update_on(_executor, &_query).await?;
6836                    #mark_restored
6837                    ::core::result::Result::Ok(_n)
6838                }
6839
6840                /// Soft-delete this row on any backend. Eloquent `Model::delete()` semantics on
6841                /// soft-delete-enabled models (closes #821 partial).
6842                ///
6843                /// Sets the `#[rustango(soft_delete)]` column to
6844                /// `NOW()` on every backend. Query helpers
6845                /// (`QuerySet::active()` / `only_trashed()`,
6846                /// `soft_delete::active_filter` /
6847                /// `compose_with_active`) filter trashed rows out by
6848                /// reading `IS NULL` on the same column. Keyed on the PK;
6849                /// global scopes do not apply. When a row changed, `self`
6850                /// takes the stamp too, so a later `save` keeps it.
6851                ///
6852                /// # Errors
6853                /// As [`#root::sql::update_pool`].
6854                pub async fn soft_delete(
6855                    &mut self,
6856                    pool: &#root::sql::Pool,
6857                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6858                    let _now = #root::__chrono::Utc::now();
6859                    let _query = #root::soft_delete::__mark_query(
6860                        <Self as #root::core::Model>::SCHEMA,
6861                        #col_lit,
6862                        #pk_column_lit,
6863                        ::core::convert::Into::<#root::core::SqlValue>::into(
6864                            ::core::clone::Clone::clone(&self.#pk_ident)
6865                        ),
6866                        ::core::option::Option::Some(_now),
6867                    );
6868                    let _n = { #pool_softdelete_run }?;
6869                    #mark_deleted
6870                    ::core::result::Result::Ok(_n)
6871                }
6872
6873                /// Tri-dialect counterpart of [`Self::restore_on`].
6874                /// Clears the `#[rustango(soft_delete)]` column back
6875                /// to `NULL`, marking the row live again. Eloquent
6876                /// `Model::restore()` parity. Keyed on the PK; global
6877                /// scopes do not apply. When a row changed, `self` is
6878                /// cleared too.
6879                ///
6880                /// # Errors
6881                /// As [`#root::sql::update_pool`].
6882                pub async fn restore(
6883                    &mut self,
6884                    pool: &#root::sql::Pool,
6885                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6886                    let _query = #root::soft_delete::__mark_query(
6887                        <Self as #root::core::Model>::SCHEMA,
6888                        #col_lit,
6889                        #pk_column_lit,
6890                        ::core::convert::Into::<#root::core::SqlValue>::into(
6891                            ::core::clone::Clone::clone(&self.#pk_ident)
6892                        ),
6893                        ::core::option::Option::None,
6894                    );
6895                    let _n = { #pool_restore_run }?;
6896                    #mark_restored
6897                    ::core::result::Result::Ok(_n)
6898                }
6899
6900                /// Hard-delete this row, ignoring the soft-delete
6901                /// column. Eloquent `Model::forceDelete()` parity —
6902                /// the escape hatch when you need to actually purge
6903                /// data (GDPR, fixture cleanup, etc.).
6904                ///
6905                /// Equivalent to [`Self::delete_pool`] (the framework's
6906                /// non-soft delete) — exposed under the Eloquent name
6907                /// for muscle-memory + so soft-delete-enabled models
6908                /// have all three operations (soft / restore / force)
6909                /// in one place.
6910                ///
6911                /// # Errors
6912                /// As [`Self::delete_pool`].
6913                pub async fn force_delete(
6914                    &self,
6915                    pool: &#root::sql::Pool,
6916                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6917                    Self::delete(self, pool).await
6918                }
6919
6920                /// Returns `true` when this row is soft-deleted (its
6921                /// `#[rustango(soft_delete)]` column is currently
6922                /// set — Eloquent `$model->trashed()` parity).
6923                ///
6924                /// Pure in-memory predicate over `&self`; does not
6925                /// hit the database. Useful in admin/template code
6926                /// like `{% if post.trashed() %}…{% endif %}` and in
6927                /// guard clauses on restore/force-delete flows.
6928                pub fn trashed(&self) -> bool {
6929                    ::core::option::Option::is_some(&self.#sd_field_ident)
6930                }
6931
6932                /// Fetch every row whose `#[rustango(soft_delete)]`
6933                /// column is `NULL` (a.k.a. the live, non-trashed
6934                /// rows). Eloquent's default `Model::all()` behavior on
6935                /// a soft-delete model (Eloquent auto-scopes trashed
6936                /// rows out; rustango doesn't have auto-scoping yet —
6937                /// see issue #820 — so this is the explicit shortcut).
6938                ///
6939                /// One-liner over `QuerySet::<Self>::default()
6940                /// .active().fetch(pool)`. Closes #821 partial.
6941                ///
6942                /// # Errors
6943                /// As [`#root::sql::FetcherPool::fetch`].
6944                pub async fn active(
6945                    pool: &#root::sql::Pool,
6946                ) -> ::core::result::Result<
6947                    ::std::vec::Vec<Self>,
6948                    #root::sql::ExecError,
6949                > {
6950                    use #root::sql::FetcherPool as _;
6951                    #root::query::QuerySet::<Self>::default()
6952                        .active()
6953                        .fetch(pool)
6954                        .await
6955                }
6956
6957                /// Fetch ONLY soft-deleted rows. Eloquent
6958                /// `Model::onlyTrashed()->get()` parity — drives the
6959                /// admin "Trash" page, restore flows, GDPR purge
6960                /// scans, etc.
6961                ///
6962                /// One-liner over `QuerySet::<Self>::default()
6963                /// .only_trashed().fetch(pool)`. Closes #821
6964                /// partial.
6965                ///
6966                /// # Errors
6967                /// As [`#root::sql::FetcherPool::fetch`].
6968                pub async fn only_trashed(
6969                    pool: &#root::sql::Pool,
6970                ) -> ::core::result::Result<
6971                    ::std::vec::Vec<Self>,
6972                    #root::sql::ExecError,
6973                > {
6974                    use #root::sql::FetcherPool as _;
6975                    #root::query::QuerySet::<Self>::default()
6976                        .only_trashed()
6977                        .fetch(pool)
6978                        .await
6979                }
6980
6981                /// Fetch every row, both live and soft-deleted.
6982                /// Eloquent `Model::withTrashed()->get()` parity.
6983                ///
6984                /// Today every queryset already includes trashed rows
6985                /// (rustango has no global-scope tracking yet — issue
6986                /// #820), so this is functionally equivalent to
6987                /// [`Self::all`]. Exposed as a named shortcut so
6988                /// soft-delete-aware code reads `Model::with_trashed_pool`
6989                /// rather than `Model::all_pool` — keeps intent visible
6990                /// in callers and stays correct when auto-scoping lands.
6991                ///
6992                /// Closes #821 partial.
6993                ///
6994                /// # Errors
6995                /// As [`#root::sql::FetcherPool::fetch`].
6996                pub async fn with_trashed(
6997                    pool: &#root::sql::Pool,
6998                ) -> ::core::result::Result<
6999                    ::std::vec::Vec<Self>,
7000                    #root::sql::ExecError,
7001                > {
7002                    use #root::sql::FetcherPool as _;
7003                    #root::query::QuerySet::<Self>::default()
7004                        .with_trashed()
7005                        .fetch(pool)
7006                        .await
7007                }
7008
7009            }
7010        } else {
7011            quote!()
7012        };
7013        quote! {
7014
7015
7016            /// [`Self::delete`] on any [`#root::sql::SqlExecutor`]. An audited
7017            /// model refuses it: use [`Self::delete`].
7018            ///
7019            /// # Errors
7020            /// As [`Self::delete`].
7021            pub async fn delete_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7022                &self,
7023                _executor: _E,
7024            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7025                #refuse_delete
7026                self.__rustango_delete_exec(_executor).await
7027            }
7028
7029            #[doc(hidden)]
7030            pub async fn __rustango_delete_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7031                &self,
7032                _executor: _E,
7033            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7034                let _query = #root::core::DeleteQuery::by_pk(
7035                    <Self as #root::core::Model>::SCHEMA,
7036                    #pk_column_lit,
7037                    ::core::convert::Into::<#root::core::SqlValue>::into(
7038                        ::core::clone::Clone::clone(&self.#pk_ident)
7039                    ),
7040                );
7041                #root::sql::__macro_internals::delete_on(_executor, &_query).await
7042            }
7043
7044            /// Old name of [`Self::delete`].
7045            ///
7046            /// # Errors
7047            /// As [`Self::delete`].
7048            #[deprecated(since = "0.62.0", note = "use `delete`")]
7049            pub async fn delete_pool(
7050                &self,
7051                pool: &#root::sql::Pool,
7052            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7053                self.delete(pool).await
7054            }
7055
7056
7057            #pool_delete_method
7058            #pool_insert_method
7059            #audit_entry_method
7060            #pool_save_method
7061            #refresh_replicate_methods
7062            #tx_delete_method
7063            #tx_insert_method
7064            #tx_save_method
7065            #soft_delete_methods
7066
7067            /// Returns `true` when `other` represents the same DB
7068            /// row as `self` — i.e. their primary keys compare
7069            /// equal. Eloquent `$model->is($other)` parity.
7070            ///
7071            /// Because both arguments are typed `&Self`, the
7072            /// model/table check is automatic — `Post::is` cannot
7073            /// be invoked against a `Comment` at compile time. Only
7074            /// the PK has to be compared at runtime.
7075            pub fn is(&self, other: &Self) -> bool {
7076                self.#pk_ident == other.#pk_ident
7077            }
7078
7079            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7080            /// parity.
7081            pub fn is_not(&self, other: &Self) -> bool {
7082                self.#pk_ident != other.#pk_ident
7083            }
7084
7085            /// Returns this row's primary-key value as an
7086            /// [`#root::core::SqlValue`]. Eloquent
7087            /// `$model->getKey()` parity.
7088            ///
7089            /// Useful when you need to thread the PK through a
7090            /// generic `Into<SqlValue>`-bound API without knowing the
7091            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7092            #[must_use]
7093            pub fn get_key(&self) -> #root::core::SqlValue {
7094                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7095            }
7096
7097        }
7098    });
7099
7100    let refuse_insert = refuse("insert_on");
7101    // The audited `insert` is built with the PK methods; an audited model
7102    // without a PK has no row to name, so its insert refuses.
7103    let audited_insert = audited_fields.is_some() && primary_key.is_some();
7104    let insert_pool_body = if audited_fields.is_some() {
7105        quote! {
7106            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7107                table: <Self as #root::core::Model>::SCHEMA.table,
7108            })
7109        }
7110    } else {
7111        quote!(self.__rustango_insert_exec(pool).await)
7112    };
7113    let plain_insert_auto = if audited_insert {
7114        quote!()
7115    } else {
7116        quote! {
7117            /// Insert this row on any backend, reading each `Auto` field
7118            /// back from the written row.
7119            ///
7120            /// # Errors
7121            /// Returns [`#root::sql::ExecError`] for SQL-writing or driver failures.
7122            pub async fn insert(
7123                &mut self,
7124                pool: &#root::sql::Pool,
7125            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7126                #insert_pool_body
7127            }
7128        }
7129    };
7130    let plain_insert_plain = if audited_insert {
7131        quote!()
7132    } else {
7133        quote! {
7134            /// Insert this row on any backend.
7135            ///
7136            /// # Errors
7137            /// Returns [`#root::sql::ExecError`] for SQL-writing or driver failures.
7138            pub async fn insert(
7139                &self,
7140                pool: &#root::sql::Pool,
7141            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7142                #insert_pool_body
7143            }
7144        }
7145    };
7146    let insert_method = if fields.has_auto {
7147        let pushes = &fields.insert_pushes;
7148        let returning_cols = &fields.returning_cols;
7149        quote! {
7150            #plain_insert_auto
7151            /// [`Self::insert`] on any [`#root::sql::SqlExecutor`]. An audited
7152            /// model refuses it: use [`Self::insert`].
7153            ///
7154            /// # Errors
7155            /// As [`Self::insert`].
7156            pub async fn insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7157                &mut self,
7158                _executor: _E,
7159            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7160                #refuse_insert
7161                self.__rustango_insert_exec(_executor).await
7162            }
7163
7164            #[doc(hidden)]
7165            pub async fn __rustango_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7166                &mut self,
7167                _executor: _E,
7168            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7169                let _query = self.__rustango_insert_query();
7170                // Every `Auto` filled Rust-side: nothing to read back.
7171                if _query.returning.is_empty() {
7172                    return #root::sql::__macro_internals::insert_owned_on(_executor, _query).await;
7173                }
7174                let _row = #root::sql::__macro_internals::insert_returning_on(
7175                    _executor, _query,
7176                )
7177                .await?;
7178                #root::sql::apply_auto_pk(_row, self)
7179            }
7180
7181            /// The INSERT for this row; fills Rust-side `Auto` values first.
7182            #[doc(hidden)]
7183            pub fn __rustango_insert_query(&mut self) -> #root::core::InsertQuery {
7184                let mut _columns: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7185                let mut _values: ::std::vec::Vec<#root::core::SqlValue> = ::std::vec::Vec::new();
7186                #( #pushes )*
7187                #root::core::InsertQuery::new(
7188                    <Self as #root::core::Model>::SCHEMA,
7189                    _columns,
7190                    _values,
7191                )
7192                .returning(::std::vec![ #( #returning_cols ),* ])
7193            }
7194
7195            /// Old name of [`Self::insert`].
7196            ///
7197            /// # Errors
7198            /// As [`Self::insert`].
7199            #[deprecated(since = "0.62.0", note = "use `insert`")]
7200            pub async fn insert_pool(
7201                &mut self,
7202                pool: &#root::sql::Pool,
7203            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7204                self.insert(pool).await
7205            }
7206        }
7207    } else {
7208        let insert_columns = &fields.insert_columns;
7209        let insert_values = &fields.insert_values;
7210        // The audited `insert` takes `&mut self` for its audit helper.
7211        let pool_self = if audited_fields.is_some() {
7212            quote!(&mut self)
7213        } else {
7214            quote!(&self)
7215        };
7216        quote! {
7217            #plain_insert_plain
7218            /// [`Self::insert`] on any [`#root::sql::SqlExecutor`]. An audited
7219            /// model refuses it: use [`Self::insert`].
7220            ///
7221            /// # Errors
7222            /// As [`Self::insert`].
7223            pub async fn insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7224                &self,
7225                _executor: _E,
7226            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7227                #refuse_insert
7228                self.__rustango_insert_exec(_executor).await
7229            }
7230
7231            #[doc(hidden)]
7232            pub async fn __rustango_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7233                &self,
7234                _executor: _E,
7235            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7236                #root::sql::__macro_internals::insert_owned_on(
7237                    _executor,
7238                    self.__rustango_insert_query(),
7239                )
7240                .await
7241            }
7242
7243            /// The INSERT for this row.
7244            #[doc(hidden)]
7245            pub fn __rustango_insert_query(&self) -> #root::core::InsertQuery {
7246                #root::core::InsertQuery::new(
7247                    <Self as #root::core::Model>::SCHEMA,
7248                    ::std::vec![ #( #insert_columns ),* ],
7249                    ::std::vec![ #( #insert_values ),* ],
7250                )
7251            }
7252
7253            /// Old name of [`Self::insert`].
7254            ///
7255            /// # Errors
7256            /// As [`Self::insert`].
7257            #[deprecated(since = "0.62.0", note = "use `insert`")]
7258            pub async fn insert_pool(
7259                #pool_self,
7260                pool: &#root::sql::Pool,
7261            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7262                self.insert(pool).await
7263            }
7264        }
7265    };
7266
7267    // `bulk_insert(&Pool)`: audited models write their rows and one
7268    // `Create` entry each in one transaction.
7269    let bulk_pool_body = if audited_fields.is_some() {
7270        quote! {
7271            let mut _tx = #root::sql::__macro_internals::write_tx(pool).await?;
7272            Self::__rustango_bulk_insert_exec(rows, &mut *_tx).await?;
7273            // MySQL has no RETURNING: a database-filled PK stays unset.
7274            if rows.iter().any(|_row| _row.__rustango_audit_pk().is_empty()) {
7275                return ::core::result::Result::Err(#root::sql::ExecError::AuditUnsupported {
7276                    table: <Self as #root::core::Model>::SCHEMA.table,
7277                    reason: "MySQL has no RETURNING to read the written rows",
7278                });
7279            }
7280            let _entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
7281                .iter()
7282                .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
7283                .collect();
7284            #root::audit::__emit_many_tx(&mut _tx, &_entries).await?;
7285            _tx.commit().await?;
7286            ::core::result::Result::Ok(())
7287        }
7288    } else {
7289        quote!(Self::__rustango_bulk_insert_exec(rows, pool).await)
7290    };
7291    let refuse_bulk = refuse("bulk_insert_on");
7292    let bulk_insert_method = if fields.has_auto {
7293        let cols_no_auto = &fields.bulk_columns_no_auto;
7294        let cols_all = &fields.bulk_columns_all;
7295        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7296        let pushes_all = &fields.bulk_pushes_all;
7297        let returning_cols = &fields.returning_cols;
7298        let uniformity = &fields.bulk_auto_uniformity;
7299        let fill_in_place = &fields.bulk_fill_in_place;
7300        // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7301        let first_auto_ident = fields
7302            .first_db_auto
7303            .as_ref()
7304            .map(|(ident, _)| ident)
7305            .or(fields.first_auto_ident.as_ref())
7306            .expect("has_auto implies first_auto_ident is Some");
7307        quote! {
7308            /// Bulk-insert `rows`: one statement, or one per chunk in one
7309            /// transaction past the bind cap. Every row's `Auto<T>` PK must
7310            /// be `Auto::Unset` (the database fills it) or every one `Set`.
7311            /// Each row's `Auto` fields are read back from `RETURNING`; MySQL
7312            /// has none, so it leaves them unset. An empty slice is a no-op.
7313            ///
7314            /// # Errors
7315            /// Returns [`#root::sql::ExecError`] for validation,
7316            /// SQL-writing, mixed-Auto rejection, or driver failures.
7317            pub async fn bulk_insert(
7318                rows: &mut [Self],
7319                pool: &#root::sql::Pool,
7320            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7321                #bulk_pool_body
7322            }
7323
7324            /// [`Self::bulk_insert`] on any [`#root::sql::SqlExecutor`]. An
7325            /// audited model refuses it: use [`Self::bulk_insert`].
7326            ///
7327            /// # Errors
7328            /// As [`Self::bulk_insert`].
7329            pub async fn bulk_insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7330                rows: &mut [Self],
7331                _executor: _E,
7332            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7333                #refuse_bulk
7334                Self::__rustango_bulk_insert_exec(rows, _executor).await
7335            }
7336
7337            #[doc(hidden)]
7338            pub async fn __rustango_bulk_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7339                rows: &mut [Self],
7340                _executor: _E,
7341            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7342                if rows.is_empty() {
7343                    return ::core::result::Result::Ok(());
7344                }
7345                for _row in rows.iter_mut() {
7346                    #( #fill_in_place )*
7347                }
7348                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7349                let _query = #root::core::BulkInsertQuery::new(
7350                    <Self as #root::core::Model>::SCHEMA,
7351                    _columns,
7352                    _all_rows,
7353                )
7354                .returning(::std::vec![ #( #returning_cols ),* ]);
7355                let _returned =
7356                    #root::sql::__macro_internals::bulk_insert_owned_on(_executor, _query).await?;
7357                if _returned.is_empty() {
7358                    return ::core::result::Result::Ok(());
7359                }
7360                if _returned.len() != rows.len() {
7361                    return ::core::result::Result::Err(#root::sql::ExecError::Sql(
7362                        #root::sql::SqlError::BulkInsertReturningMismatch {
7363                            expected: rows.len(),
7364                            actual: _returned.len(),
7365                        },
7366                    ));
7367                }
7368                for (_row, _row_mut) in _returned.into_iter().zip(rows.iter_mut()) {
7369                    #root::sql::apply_auto_pk(_row, _row_mut)?;
7370                }
7371                ::core::result::Result::Ok(())
7372            }
7373
7374            /// Columns and per-row values for every bulk write. A database-filled
7375            /// `Auto` is left out when every row leaves it `Unset`, written when
7376            /// every row sets it; a mix is `BulkAutoMixed`.
7377            #[doc(hidden)]
7378            pub fn __rustango_bulk_rows(
7379                rows: &[Self],
7380            ) -> ::core::result::Result<
7381                (
7382                    ::std::vec::Vec<&'static str>,
7383                    ::std::vec::Vec<::std::vec::Vec<#root::core::SqlValue>>,
7384                ),
7385                #root::sql::ExecError,
7386            > {
7387                let _first_unset = match rows.first() {
7388                    ::core::option::Option::Some(_r) => {
7389                        matches!(_r.#first_auto_ident, #root::sql::Auto::Unset)
7390                    }
7391                    ::core::option::Option::None => true,
7392                };
7393                #( #uniformity )*
7394                let mut _all_rows: ::std::vec::Vec<
7395                    ::std::vec::Vec<#root::core::SqlValue>,
7396                > = ::std::vec::Vec::with_capacity(rows.len());
7397                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7398                    for _row in rows.iter() {
7399                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7400                            ::std::vec::Vec::new();
7401                        #( #pushes_no_auto )*
7402                        _all_rows.push(_row_vals);
7403                    }
7404                    ::std::vec![ #( #cols_no_auto ),* ]
7405                } else {
7406                    for _row in rows.iter() {
7407                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7408                            ::std::vec::Vec::new();
7409                        #( #pushes_all )*
7410                        _all_rows.push(_row_vals);
7411                    }
7412                    ::std::vec![ #( #cols_all ),* ]
7413                };
7414                ::core::result::Result::Ok((_columns, _all_rows))
7415            }
7416        }
7417    } else {
7418        let cols_all = &fields.bulk_columns_all;
7419        let pushes_all = &fields.bulk_pushes_all;
7420        quote! {
7421            /// Bulk-insert `rows`: one statement, or one per chunk in one
7422            /// transaction past the bind cap. An empty slice is a no-op.
7423            ///
7424            /// # Errors
7425            /// Returns [`#root::sql::ExecError`] for validation,
7426            /// SQL-writing, or driver failures.
7427            pub async fn bulk_insert(
7428                rows: &[Self],
7429                pool: &#root::sql::Pool,
7430            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7431                #bulk_pool_body
7432            }
7433
7434            /// [`Self::bulk_insert`] on any [`#root::sql::SqlExecutor`]. An
7435            /// audited model refuses it: use [`Self::bulk_insert`].
7436            ///
7437            /// # Errors
7438            /// As [`Self::bulk_insert`].
7439            pub async fn bulk_insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7440                rows: &[Self],
7441                _executor: _E,
7442            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7443                #refuse_bulk
7444                Self::__rustango_bulk_insert_exec(rows, _executor).await
7445            }
7446
7447            #[doc(hidden)]
7448            pub async fn __rustango_bulk_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7449                rows: &[Self],
7450                _executor: _E,
7451            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7452                if rows.is_empty() {
7453                    return ::core::result::Result::Ok(());
7454                }
7455                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7456                let _query = #root::core::BulkInsertQuery::new(
7457                    <Self as #root::core::Model>::SCHEMA,
7458                    _columns,
7459                    _all_rows,
7460                );
7461                #root::sql::__macro_internals::bulk_insert_owned_on(_executor, _query)
7462                    .await
7463                    .map(drop)
7464            }
7465
7466            /// Columns and per-row values for every bulk write.
7467            #[doc(hidden)]
7468            pub fn __rustango_bulk_rows(
7469                rows: &[Self],
7470            ) -> ::core::result::Result<
7471                (
7472                    ::std::vec::Vec<&'static str>,
7473                    ::std::vec::Vec<::std::vec::Vec<#root::core::SqlValue>>,
7474                ),
7475                #root::sql::ExecError,
7476            > {
7477                let mut _all_rows: ::std::vec::Vec<
7478                    ::std::vec::Vec<#root::core::SqlValue>,
7479                > = ::std::vec::Vec::with_capacity(rows.len());
7480                for _row in rows.iter() {
7481                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7482                        ::std::vec::Vec::new();
7483                    #( #pushes_all )*
7484                    _all_rows.push(_row_vals);
7485                }
7486                ::core::result::Result::Ok((::std::vec![ #( #cols_all ),* ], _all_rows))
7487            }
7488        }
7489    };
7490
7491    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7492    // (no postgres-feature gate); routes through the existing
7493    // `bulk_insert_pool` + per-dialect conflict writer. Its columns come
7494    // from `__rustango_bulk_rows`, as `bulk_insert`'s do.
7495    // Audited models audit each written row; MySQL, with no RETURNING,
7496    // is refused there (#1747, #1795).
7497    let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7498        (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7499        (true, true) => quote! {
7500            #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7501                _r.__rustango_audit_entry(_op)
7502            })
7503            .await
7504        },
7505        (true, false) => quote! {
7506            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7507                table: <Self as #root::core::Model>::SCHEMA.table,
7508            })
7509        },
7510    };
7511    let bulk_upsert_pool_method = {
7512        let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7513            fields.uuid_v7_fields.iter().cloned().unzip();
7514        quote! {
7515            /// Tri-dialect bulk insert that updates on conflict — the
7516            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7517            ///
7518            /// Per-row values are extracted and lowered into a
7519            /// [`#root::core::BulkInsertQuery`] with
7520            /// `on_conflict = DoUpdate { target, update_columns }`. The
7521            /// writer dispatches per-dialect:
7522            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7523            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7524            ///   `target` is not the only unique key, since MySQL matches every UNIQUE index
7525            ///
7526            /// `target` names the column(s) whose unique constraint
7527            /// defines the conflict (typically a `unique` or
7528            /// `unique_together` natural-key column, NOT the `Auto<T>`
7529            /// PK). `update_cols` names the columns to overwrite on
7530            /// conflict — every other column is left untouched on the
7531            /// existing row.
7532            ///
7533            /// A database-filled `Auto` PK follows [`Self::bulk_insert`]: all
7534            /// `Unset` leaves it to the database, all `Set` writes it (so
7535            /// `target = ["id"]` updates by id), a mix is `BulkAutoMixed`.
7536            /// Rows repeating a `target` key: the last one wins.
7537            /// Empty slice is a no-op. On an audited model each written
7538            /// row gets a `create` or `update` audit row; MySQL refuses it.
7539            ///
7540            /// # Errors
7541            /// Returns [`#root::sql::ExecError`] for validation,
7542            /// SQL-writing, or driver failures.
7543            pub async fn bulk_upsert_pool(
7544                rows: &[Self],
7545                target: &[&'static str],
7546                update_cols: &[&'static str],
7547                pool: &#root::sql::Pool,
7548            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7549                if rows.is_empty() {
7550                    return ::core::result::Result::Ok(());
7551                }
7552                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7553                #[allow(unused_mut)]
7554                let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7555                #(
7556                    if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7557                        _generated.push(#gen_cols);
7558                    }
7559                )*
7560                let _query = #root::core::BulkInsertQuery::new(
7561                    <Self as #root::core::Model>::SCHEMA,
7562                    _columns,
7563                    _all_rows,
7564                )
7565                .on_conflict_do_update(target, update_cols)
7566                .generated(_generated);
7567                #run_conflict_bulk_insert
7568            }
7569
7570            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7571            /// skip rows that would violate a unique constraint. Issue
7572            /// #267 / T1.5. Same per-dialect dispatch as
7573            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7574            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7575            /// <pivot> = <pivot>` (MySQL no-op write).
7576            ///
7577            /// # Errors
7578            /// As [`Self::bulk_upsert_pool`].
7579            pub async fn bulk_insert_or_ignore_pool(
7580                rows: &[Self],
7581                pool: &#root::sql::Pool,
7582            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7583                if rows.is_empty() {
7584                    return ::core::result::Result::Ok(());
7585                }
7586                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7587                let _query = #root::core::BulkInsertQuery::new(
7588                    <Self as #root::core::Model>::SCHEMA,
7589                    _columns,
7590                    _all_rows,
7591                )
7592                .on_conflict_do_nothing();
7593                #run_conflict_bulk_insert
7594            }
7595        }
7596    };
7597
7598    // Ergonomic `Model::bulk_update(objs, fields)`.
7599    // The SQL/IR/executor stack
7600    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7601    // `write_bulk_update_*` writers) already existed; what was missing
7602    // was the per-model constructor that maps `&[Self]` + a runtime
7603    // column list into rows of `[pk, col_vals…]` so callers don't
7604    // hand-build the IR. Emitted only when the model has a primary key
7605    // (the PK is the join key and can't itself be updated).
7606    let bulk_update_rows_run = if audited_fields.is_some() {
7607        quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7608            _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7609        })
7610        .await)
7611    } else {
7612        quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7613    };
7614    let bulk_update_method = match &fields.primary_key {
7615        None => quote! {},
7616        Some((pk_ident, pk_col)) => {
7617            // One pair of match arms per non-PK column: a validation arm
7618            // resolving the runtime name to its `&'static str` column,
7619            // and a value arm pushing that field off the row.
7620            let mut col_arms: Vec<TokenStream2> = Vec::new();
7621            let mut val_arms: Vec<TokenStream2> = Vec::new();
7622            for entry in &fields.column_entries {
7623                if &entry.column == pk_col {
7624                    continue;
7625                }
7626                let col = &entry.column;
7627                let ident = &entry.ident;
7628                col_arms.push(quote! { #col => #col, });
7629                val_arms.push(quote! {
7630                    #col => _row_vals.push(
7631                        ::core::convert::Into::<#root::core::SqlValue>::into(
7632                            ::core::clone::Clone::clone(&_o.#ident)
7633                        )
7634                    ),
7635                });
7636            }
7637            quote! {
7638                /// Write per-row-different values for the named `fields`
7639                /// across every object in `objs`, matched by primary key.
7640                /// Past the backend's bind cap the rows go in batches inside
7641                /// one transaction, so the update is all or none.
7642                ///
7643                /// `fields` names the **columns** to update. The primary
7644                /// key identifies each row and cannot itself be updated
7645                /// (pass it and you get
7646                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7647                /// Empty `objs` or `fields` is a no-op returning `0`.
7648                /// Objects whose PK matches no row are simply not updated.
7649                /// Returns the number of rows affected.
7650                ///
7651                /// Tri-dialect: lowers to a
7652                /// [`#root::core::BulkUpdateQuery`] and dispatches
7653                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7654                /// a CTE + correlated subquery on SQLite, an inner
7655                /// `JOIN (VALUES …)` on MySQL.
7656                ///
7657                /// # Errors
7658                /// [`#root::core::QueryError::UnknownField`] for a name
7659                /// that isn't a column on this model,
7660                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7661                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7662                /// SQL-writing / driver failures.
7663                pub async fn bulk_update(
7664                    objs: &[Self],
7665                    fields: &[&str],
7666                    pool: &#root::sql::Pool,
7667                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7668                    if objs.is_empty() || fields.is_empty() {
7669                        return ::core::result::Result::Ok(0);
7670                    }
7671                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7672                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7673                        ::std::vec::Vec::with_capacity(fields.len());
7674                    for &_f in fields {
7675                        let _col: &'static str = match _f {
7676                            #pk_col => {
7677                                return ::core::result::Result::Err(
7678                                    ::core::convert::Into::into(
7679                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7680                                            model: _model_name,
7681                                            field: ::std::string::ToString::to_string(_f),
7682                                        }
7683                                    )
7684                                );
7685                            }
7686                            #( #col_arms )*
7687                            _ => {
7688                                return ::core::result::Result::Err(
7689                                    ::core::convert::Into::into(
7690                                        #root::core::QueryError::UnknownField {
7691                                            model: _model_name,
7692                                            field: ::std::string::ToString::to_string(_f),
7693                                        }
7694                                    )
7695                                );
7696                            }
7697                        };
7698                        // A model whose only field is the primary key emits no
7699                        // non-PK arms, so every arm above diverges and this push
7700                        // is genuinely unreachable — correct, since such a model
7701                        // has nothing to bulk-update. Scoped to the statement
7702                        // rather than the fn so a real unreachability elsewhere
7703                        // in this body still warns. Without it every PK-only
7704                        // model warns in the *user's* crate, which they cannot
7705                        // act on (#1290).
7706                        #[allow(unreachable_code)]
7707                        _update_columns.push(_col);
7708                    }
7709                    let mut _rows: ::std::vec::Vec<
7710                        ::std::vec::Vec<#root::core::SqlValue>,
7711                    > = ::std::vec::Vec::with_capacity(objs.len());
7712                    for _o in objs.iter() {
7713                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7714                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7715                        // PK first — the writers expect `[pk, …update cols]`.
7716                        _row_vals.push(
7717                            ::core::convert::Into::<#root::core::SqlValue>::into(
7718                                ::core::clone::Clone::clone(&_o.#pk_ident)
7719                            )
7720                        );
7721                        for &_f in fields {
7722                            match _f {
7723                                #( #val_arms )*
7724                                // Unreachable: every name was validated
7725                                // against the same arm set above.
7726                                _ => {}
7727                            }
7728                        }
7729                        _rows.push(_row_vals);
7730                    }
7731                    let _query = #root::core::BulkUpdateQuery::new(
7732                        <Self as #root::core::Model>::SCHEMA,
7733                        _update_columns,
7734                        _rows,
7735                    );
7736                    #bulk_update_rows_run
7737                }
7738            }
7739        }
7740    };
7741
7742    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7743        quote! {
7744            /// Hidden runtime accessor for the primary-key value as a
7745            /// [`SqlValue`]. Used by reverse-relation helpers
7746            /// (`<parent>::<child>_set`) emitted from sibling models'
7747            /// FK fields. Not part of the public API.
7748            #[doc(hidden)]
7749            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7750                ::core::convert::Into::<#root::core::SqlValue>::into(
7751                    ::core::clone::Clone::clone(&self.#pk_ident)
7752                )
7753            }
7754        }
7755    });
7756
7757    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7758        let column_arms = fields.column_entries.iter().map(|entry| {
7759            let col = &entry.column;
7760            let ident = &entry.ident;
7761            quote! {
7762                #col => ::core::option::Option::Some(
7763                    ::core::convert::Into::<#root::core::SqlValue>::into(
7764                        ::core::clone::Clone::clone(&self.#ident)
7765                    )
7766                ),
7767            }
7768        });
7769        quote! {
7770            impl #root::sql::HasPkValue for #struct_name {
7771                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7772                    ::core::convert::Into::<#root::core::SqlValue>::into(
7773                        ::core::clone::Clone::clone(&self.#pk_ident)
7774                    )
7775                }
7776                fn __rustango_column_value(
7777                    &self,
7778                    column: &str,
7779                ) -> ::core::option::Option<#root::core::SqlValue> {
7780                    match column {
7781                        #( #column_arms )*
7782                        _ => ::core::option::Option::None,
7783                    }
7784                }
7785            }
7786        }
7787    });
7788
7789    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7790
7791    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7792    // dispatch to the right per-backend body without the macro emitting
7793    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7794    // so audited models with non-Auto PKs (which still go through
7795    // `insert_one_with_audit` → `apply_auto_pk`) link.
7796    let assign_auto_pk_pool_impl = {
7797        let auto_assigns = &fields.auto_assigns;
7798        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7799        // so the body is structurally identical to `auto_assigns` —
7800        // only the helper name swaps from `try_get_returning` to
7801        // `try_get_returning_sqlite` so the closure typechecks against
7802        // a `SqliteRow` instead of a `PgRow`.
7803        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7804            .auto_field_idents
7805            .iter()
7806            .map(|(ident, column, uuid)| {
7807                // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
7808                if *uuid {
7809                    return quote! {
7810                        self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
7811                            _returning_row, #column
7812                        )?);
7813                    };
7814                }
7815                quote! {
7816                    self.#ident = #root::sql::try_get_returning_sqlite(
7817                        _returning_row, #column
7818                    )?;
7819                }
7820            })
7821            .collect();
7822        // #1028 — decode each `generated_as` column from the same
7823        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7824        // own type drives the decode (no `Auto<T>` wrapper).
7825        let generated_assigns: Vec<TokenStream2> = fields
7826            .generated_field_idents
7827            .iter()
7828            .map(|(ident, column)| {
7829                quote! {
7830                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7831                }
7832            })
7833            .collect();
7834        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7835            .generated_field_idents
7836            .iter()
7837            .map(|(ident, column)| {
7838                quote! {
7839                    self.#ident = #root::sql::try_get_returning_sqlite(
7840                        _returning_row, #column
7841                    )?;
7842                }
7843            })
7844            .collect();
7845        let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
7846            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7847            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7848            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7849            // those models can't use AUTO_INCREMENT). The trait is only
7850            // touched on the MySQL arm at runtime, so PG-only consumers
7851            // never see the bound failure.
7852            //
7853            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7854            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7855            // runtime with "multi-column RETURNING". MySQL has no
7856            // multi-column RETURNING semantic and a follow-up SELECT
7857            // would need cross-trait plumbing. Pragmatic shape: succeed
7858            // with the FIRST Auto field populated from LAST_INSERT_ID();
7859            // any other Auto fields stay `Auto::Unset`. Callers that
7860            // need the DB-defaulted timestamp / UUID can re-fetch the
7861            // row by PK after `save_pool`. Fixes the cookbook chapter
7862            // 12 dialect divergence.
7863            quote! {
7864                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7865                    ::rustango_from_mysql_auto_id(_id)?;
7866                self.#first = #root::sql::Auto::Set(_converted);
7867                ::core::result::Result::Ok(())
7868            }
7869        } else {
7870            quote! {
7871                let _ = _id;
7872                ::core::result::Result::Ok(())
7873            }
7874        };
7875        quote! {
7876            impl #root::sql::AssignAutoPkPool for #struct_name {
7877                fn __rustango_assign_from_pg_row(
7878                    &mut self,
7879                    _returning_row: &#root::sql::PgReturningRow,
7880                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7881                    #( #auto_assigns )*
7882                    #( #generated_assigns )*
7883                    ::core::result::Result::Ok(())
7884                }
7885                fn __rustango_assign_from_mysql_id(
7886                    &mut self,
7887                    _id: i64,
7888                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7889                    #mysql_body
7890                }
7891                fn __rustango_assign_from_sqlite_row(
7892                    &mut self,
7893                    _returning_row: &#root::sql::SqliteReturningRow,
7894                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7895                    #( #auto_assigns_sqlite )*
7896                    #( #generated_assigns_sqlite )*
7897                    ::core::result::Result::Ok(())
7898                }
7899            }
7900        }
7901    };
7902
7903    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7904    let aliased_row_helper = quote! {
7905        /// Decode a row's aliased target columns (produced by
7906        /// `select_related`'s LEFT JOIN) into a fresh instance of
7907        /// this model. Reads each column via
7908        /// `format!("{prefix}__{col}")`, matching the alias the
7909        /// SELECT writer emitted. Slice 9.0d.
7910        #[doc(hidden)]
7911        #[cfg(feature = "postgres")]
7912        pub fn __rustango_from_aliased_row(
7913            row: &#root::sql::sqlx::postgres::PgRow,
7914            prefix: &str,
7915        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7916            ::core::result::Result::Ok(Self {
7917                #( #from_aliased_row_inits ),*
7918            })
7919        }
7920    };
7921    // v0.23.0-batch8 — MySQL counterpart, gated through the
7922    // cfg-aware macro_rules so PG-only builds expand to nothing.
7923    let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
7924    let aliased_row_helper_my = quote! {
7925        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7926            #( #from_aliased_row_inits_my ),*
7927        });
7928    };
7929
7930    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7931    // pattern + cfg gate on the `sqlite` feature.
7932    let aliased_row_helper_sqlite = quote! {
7933        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7934            #( #from_aliased_row_inits ),*
7935        });
7936    };
7937
7938    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7939    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7940    let load_related_impl_sqlite =
7941        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7942
7943    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7944    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7945    // default `Self::objects()`. Each accessor returns a fresh
7946    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7947    // methods the user defined.
7948    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7949        .iter()
7950        .map(|fn_ident| {
7951            let model_name_str = struct_name.to_string();
7952            let fn_name_str = fn_ident.to_string();
7953            let doc = format!(
7954                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7955                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7956                 equivalent to `Self::objects()`. Chains with any \
7957                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7958                 extension methods."
7959            );
7960            quote! {
7961                #[doc = #doc]
7962                #[must_use]
7963                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7964                    #root::query::QuerySet::new()
7965                }
7966            }
7967        })
7968        .collect();
7969
7970    quote! {
7971        impl #struct_name {
7972            /// Start a new `QuerySet` over this model.
7973            #[must_use]
7974            pub fn objects() -> #root::query::QuerySet<#struct_name> {
7975                #root::query::QuerySet::new()
7976            }
7977
7978            /// Eloquent-shape alias of [`Self::objects`]. Returns
7979            /// a fresh `QuerySet<Self>` ready for `.filter()` /
7980            /// `.where_()` / etc. Matches Laravel muscle-memory:
7981            ///
7982            /// ```ignore
7983            /// // Eloquent:    Post::query()->where('published', true)
7984            /// // rustango:    Post::query().filter("published", true)
7985            /// //         or:  Post::objects().filter("published", true)
7986            /// ```
7987            ///
7988            /// Both names point at the same underlying constructor;
7989            /// neither is preferred.
7990            #[must_use]
7991            pub fn query() -> #root::query::QuerySet<#struct_name> {
7992                #root::query::QuerySet::new()
7993            }
7994
7995            #( #extra_manager_fns )*
7996
7997            #insert_method
7998
7999            #bulk_insert_method
8000
8001            #bulk_upsert_pool_method
8002
8003            #bulk_update_method
8004
8005            #save_method
8006
8007            #pk_methods
8008
8009            #pk_value_helper
8010
8011            #aliased_row_helper
8012
8013            #column_consts
8014        }
8015
8016        #aliased_row_helper_my
8017
8018        #aliased_row_helper_sqlite
8019
8020        #load_related_impl
8021
8022        #load_related_impl_my
8023
8024        #load_related_impl_sqlite
8025
8026        #has_pk_value_impl
8027
8028        #fk_pk_access_impl
8029
8030        #assign_auto_pk_pool_impl
8031    }
8032}
8033
8034/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8035fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8036    let lines = entries.iter().map(|e| {
8037        let ident = &e.ident;
8038        let col_ty = column_type_ident(ident);
8039        quote! {
8040            #[allow(non_upper_case_globals)]
8041            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8042        }
8043    });
8044    quote! { #(#lines)* }
8045}
8046
8047/// Emit a hidden per-model module carrying one zero-sized type per field,
8048/// each with a `Column` impl pointing back at the model.
8049fn column_module_tokens(
8050    module_ident: &syn::Ident,
8051    struct_name: &syn::Ident,
8052    entries: &[ColumnEntry],
8053) -> TokenStream2 {
8054    let root = rustango_root();
8055    let items = entries.iter().map(|e| {
8056        let col_ty = column_type_ident(&e.ident);
8057        let value_ty = &e.value_ty;
8058        let name = &e.name;
8059        let column = &e.column;
8060        let field_type_tokens = &e.field_type_tokens;
8061        quote! {
8062            #[derive(::core::clone::Clone, ::core::marker::Copy)]
8063            pub struct #col_ty;
8064
8065            impl #root::core::Column for #col_ty {
8066                type Model = super::#struct_name;
8067                type Value = #value_ty;
8068                const NAME: &'static str = #name;
8069                const COLUMN: &'static str = #column;
8070                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8071            }
8072        }
8073    });
8074    quote! {
8075        #[doc(hidden)]
8076        #[allow(non_camel_case_types, non_snake_case)]
8077        pub mod #module_ident {
8078            // Re-import the parent scope so field types referencing
8079            // sibling models (e.g. `ForeignKey<Author>`) resolve
8080            // inside this submodule. Without this we'd hit
8081            // `proc_macro_derive_resolution_fallback` warnings.
8082            #[allow(unused_imports)]
8083            use super::*;
8084            #(#items)*
8085        }
8086    }
8087}
8088
8089fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8090    let name = ident_name(field_ident);
8091    syn::Ident::new(&format!("{name}_col"), field_ident.span())
8092}
8093
8094/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8095/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8096fn restore_auto_now_unless(
8097    idents: &[syn::Ident],
8098    ret: &TokenStream2,
8099    keep: &TokenStream2,
8100    body: &TokenStream2,
8101) -> TokenStream2 {
8102    if idents.is_empty() {
8103        return body.clone();
8104    }
8105    let prev: Vec<syn::Ident> = (0..idents.len())
8106        .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8107        .collect();
8108    quote! {
8109        #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8110        let _saved: #ret = async { #body }.await;
8111        if !(#keep) {
8112            #( self.#idents = #prev; )*
8113        }
8114        _saved
8115    }
8116}
8117
8118/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8119fn ident_name(ident: &syn::Ident) -> String {
8120    syn::ext::IdentExt::unraw(ident).to_string()
8121}
8122
8123/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8124fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8125    match syn::parse_str::<syn::Ident>(name) {
8126        Ok(mut ident) => {
8127            ident.set_span(span);
8128            ident
8129        }
8130        Err(_) => syn::Ident::new_raw(name, span),
8131    }
8132}
8133
8134fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8135    syn::Ident::new(
8136        &format!("__rustango_cols_{struct_name}"),
8137        struct_name.span(),
8138    )
8139}
8140
8141fn from_row_impl_tokens(
8142    struct_name: &syn::Ident,
8143    from_row_inits: &[TokenStream2],
8144    from_row_inits_my: &[TokenStream2],
8145) -> TokenStream2 {
8146    let root = rustango_root();
8147    // The Postgres impl is always emitted — every rustango build pulls in
8148    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8149    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8150    // macro_rules whose body collapses to nothing when rustango is built
8151    // without the `mysql` feature. No user-facing feature shim required.
8152    //
8153    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8154    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8155    // back into a comma-separated list inside square brackets. Since each
8156    // entry is already in `field: expr` shape, the existing tokens slot in.
8157    quote! {
8158        #[cfg(feature = "postgres")]
8159        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8160            for #struct_name
8161        {
8162            fn from_row(
8163                row: &'r #root::sql::sqlx::postgres::PgRow,
8164            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8165                ::core::result::Result::Ok(Self {
8166                    #( #from_row_inits ),*
8167                })
8168            }
8169        }
8170
8171        #root::__impl_my_from_row!(#struct_name, |row| {
8172            #( #from_row_inits_my ),*
8173        });
8174
8175        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8176            #( #from_row_inits ),*
8177        });
8178    }
8179}
8180
8181struct ContainerAttrs {
8182    table: Option<String>,
8183    display: Option<(String, proc_macro2::Span)>,
8184    /// Explicit app label from `#[rustango(app = "blog")]`.
8185    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8186    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8187    /// at runtime — this attribute is the override for cases where
8188    /// the inference is wrong (e.g. a model that conceptually belongs
8189    /// to one app but is physically in another module).
8190    app: Option<String>,
8191    /// Per-model admin knobs from
8192    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8193    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8194    /// admin code falls back to `AdminConfig::DEFAULT`.
8195    admin: Option<AdminAttrs>,
8196    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8197    /// `None` when the model isn't audited — write paths emit no
8198    /// audit entries. When present, single-row writes capture
8199    /// before/after for the listed fields and bulk writes batch
8200    /// snapshots into one INSERT into `rustango_audit_log`.
8201    audit: Option<AuditAttrs>,
8202    /// `true` when `#[rustango(permissions)]` is present. Signals that
8203    /// `auto_create_permissions` should seed the four CRUD codenames for
8204    /// this model.
8205    permissions: bool,
8206    /// Many-to-many relations declared via
8207    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8208    ///                 src = "post_id", dst = "tag_id"))]`.
8209    m2m: Vec<M2MAttr>,
8210    /// Polymorphic M2M relations declared via
8211    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8212    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8213    ///   related_column = "tag_id"))]` (issue #818).
8214    generic_m2m: Vec<GenericM2MAttr>,
8215    /// Composite indexes declared via
8216    /// `#[rustango(index("col1, col2"))]` or
8217    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8218    /// Single-column indexes from `#[rustango(index)]` on fields are
8219    /// accumulated here during field collection.
8220    indexes: Vec<IndexAttr>,
8221    /// Table-level CHECK constraints declared via
8222    /// `#[rustango(check(name = "…", expr = "…"))]`.
8223    checks: Vec<CheckAttr>,
8224    /// Table-level PG `EXCLUDE` constraints declared via
8225    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8226    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8227    /// migration writer renders them on Postgres and skips with a
8228    /// warning on MySQL/SQLite. Issue #319.
8229    excludes: Vec<ExcludeAttr>,
8230    /// Composite (multi-column) FKs declared via
8231    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8232    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8233    composite_fks: Vec<CompositeFkAttr>,
8234    /// Generic ("any model") FKs declared via
8235    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8236    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8237    generic_fks: Vec<GenericFkAttr>,
8238    /// Where this model lives in a tenancy deployment, declared via
8239    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8240    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8241    /// to partition diff output between registry-scoped and
8242    /// tenant-scoped migration files.
8243    scope: Option<String>,
8244    /// `#[rustango(access = "open" | "admin_only" | "never")]`: which
8245    /// generic surfaces may serve the table. Unset emits `None`.
8246    access: Option<String>,
8247    /// Extra QuerySet accessor names from
8248    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8249    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8250    /// the default `Self::objects()`. Multiple attributes allowed.
8251    manager_fns: Vec<syn::Ident>,
8252    /// Default ordering declared via `#[rustango(default_order =
8253    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8254    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8255    /// `-` prefix means descending; the `+` prefix or no prefix means
8256    /// ascending.
8257    default_order: Vec<(String, bool, proc_macro2::Span)>,
8258    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8259    /// Routes the emitted schema's `is_view = true` so the migration
8260    /// snapshot skips this model (its underlying SQL view is operator-
8261    /// managed, not rustango-managed).
8262    is_view: bool,
8263    /// `#[rustango(managed = false)]` — who owns the table.
8264    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8265    /// the migration snapshot skips this model so `makemigrations` /
8266    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8267    /// TABLE` against it (operator-managed schema).
8268    managed: bool,
8269    /// Name of the model's base manager, from
8270    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8271    /// `ModelSchema::base_manager_name`. Declarative-only today.
8272    base_manager_name: Option<String>,
8273    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8274    /// field this model's instances are ordered relative to.
8275    /// Declarative-only today; threaded onto
8276    /// `ModelSchema::order_with_respect_to`.
8277    order_with_respect_to: Option<String>,
8278    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8279    /// Marks the model as a proxy that
8280    /// shares its DB table with another struct. Threaded into
8281    /// `ModelSchema::proxy` so future codegen can skip table-owning
8282    /// behavior for proxies.
8283    proxy: bool,
8284    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8285    /// Each comma-separated capability token surfaces on
8286    /// `ModelSchema::required_db_features` so `manage check --deploy`
8287    /// can warn when the active dialect lacks one.
8288    required_db_features: Vec<String>,
8289    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8290    /// Normalized to the dialect name `manage check --deploy`
8291    /// compares against `Settings.database.backend`. Aliases
8292    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8293    /// stored under the canonical name.
8294    required_db_vendor: Option<String>,
8295    /// `#[rustango(default_related_name = "...")]`. Threaded into
8296    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8297    /// name to use when an FK / M2M field doesn't override it.
8298    /// Today rustango doesn't auto-emit reverse managers; the
8299    /// metadata is the foundation for that work.
8300    default_related_name: Option<String>,
8301    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8302    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8303    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8304    /// inline `COMMENT='...'`, SQLite: no-op).
8305    db_table_comment: Option<String>,
8306    /// `#[rustango(get_latest_by = "created_at")]` /
8307    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8308    /// `(column, descending)` where `descending = true` when the
8309    /// attribute value starts with `-`. Threaded into
8310    /// `ModelSchema::get_latest_by`.
8311    get_latest_by: Option<(String, bool, proc_macro2::Span)>,
8312    /// Extra `(codename, label)` permissions
8313    /// from `#[rustango(extra_permissions = "approve:Can approve,
8314    /// archive:Can archive")]`. Comma-separated `codename:label`
8315    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8316    extra_permissions: Vec<(String, String)>,
8317    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8318    /// `"view"`) the framework
8319    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8320    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8321    /// Validated at parse time; unknown actions fail with a span-pointing
8322    /// error.
8323    default_permissions: Vec<String>,
8324    /// `#[rustango(verbose_name = "blog post")]` — the
8325    /// human-readable singular label for the model. Threaded into
8326    /// `ModelSchema::verbose_name` so admin section headers /
8327    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8328    /// over the Rust struct identifier.
8329    verbose_name: Option<String>,
8330    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8331    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8332    /// When unset, `display_label_plural()` auto-suffixes `s`.
8333    verbose_name_plural: Option<String>,
8334    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8335    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8336    /// a name (used by `QuerySet::without_global_scope`) with a
8337    /// `fn() -> WhereExpr` path that the macro emits into
8338    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8339    global_scopes: Vec<GlobalScopeAttr>,
8340    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8341    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8342    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8343    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8344    /// entry emits an inherent `<name>_through(&self)` accessor that
8345    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8346    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8347    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8348    through_relations: Vec<ThroughAttr>,
8349    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8350    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8351    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8352    /// Each entry emits two associated functions on the parent —
8353    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8354    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8355    /// over a correlated subquery against the child table. Users
8356    /// drop the result into `QuerySet::where_raw(...)`.
8357    reverse_has_relations: Vec<ReverseHasAttr>,
8358    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8359    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8360    /// entry so the relation-existence family resolves polymorphic
8361    /// children by name.
8362    generic_has_relations: Vec<GenericHasAttr>,
8363}
8364
8365/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8366/// declaration. Each entry becomes one `core::GlobalScope` in the
8367/// emitted schema literal; `apply` resolves at macro-expand time
8368/// against the consumer's scope so the function must be in scope at
8369/// the use site. Issue #820.
8370struct GlobalScopeAttr {
8371    name: String,
8372    apply: syn::Path,
8373}
8374
8375/// Parsed `#[rustango(through(...))]` declaration. Issue
8376/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8377/// `hasManyThrough` / `hasOneThrough` parity.
8378///
8379/// `Country hasManyThrough Post via User` declares as:
8380///
8381/// ```ignore
8382/// #[rustango(through(
8383///     name                   = "posts",
8384///     far                    = "Post",
8385///     far_fk_column          = "author_id",
8386///     intermediate           = "User",
8387///     intermediate_fk_column = "country_id",
8388/// ))]
8389/// struct Country { ... }
8390/// ```
8391///
8392/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8393/// which returns a queryset filtered via a correlated subquery —
8394/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8395/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8396/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8397/// because the subquery lives inside a `WhereExpr::InSubquery` node
8398/// and the outer queryset's pending list stays empty.
8399///
8400/// All four required arguments use **SQL column / table names**
8401/// (not Rust field names) to sidestep the multi-hop-filter substrate
8402/// gap. Once that substrate lands, a higher-level Rust-field-name
8403/// shorthand can be added without breaking this surface.
8404struct ThroughAttr {
8405    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8406    name: String,
8407    /// Far model type identifier. `far = "Post"` → returns
8408    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8409    /// the derive expands.
8410    far: syn::Ident,
8411    /// SQL column on the far model's table that references the
8412    /// intermediate model's primary key. For `Post`'s
8413    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8414    /// default `<field>_id` convention) or whatever the user
8415    /// declared via `#[rustango(db_column = "...")]`.
8416    far_fk_column: String,
8417    /// Intermediate model type identifier. Needed to look up its
8418    /// `SCHEMA` so the subquery's `FROM` clause points at the
8419    /// intermediate table. `intermediate = "User"`.
8420    intermediate: syn::Ident,
8421    /// SQL column on the intermediate's table that references the
8422    /// source (this) model's primary key. For `User`'s
8423    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8424    intermediate_fk_column: String,
8425    /// SQL primary-key column on the intermediate's table — the
8426    /// column the subquery projects. Optional; defaults to `"id"`
8427    /// (rustango's default PK column name). Override when the
8428    /// intermediate declares a custom PK column.
8429    intermediate_pk_column: String,
8430}
8431
8432/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8433/// child_fk_column = "..."))]` declaration. Issue
8434/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8435/// `whereHas` / `whereDoesntHave` parity.
8436///
8437/// `Post hasMany Comment` declares as:
8438///
8439/// ```ignore
8440/// #[rustango(reverse_has(
8441///     name             = "comments",
8442///     child            = "Comment",
8443///     child_fk_column  = "post_id",
8444/// ))]
8445/// struct Post { ... }
8446/// ```
8447///
8448/// The macro emits two associated functions on `Post`:
8449///
8450/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8451///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8452///   <outer>.<self_pk_column>)`.
8453/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8454///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8455///
8456/// User code:
8457///
8458/// ```ignore
8459/// // Posts with at least one comment:
8460/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8461/// // Posts with no comments:
8462/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8463/// ```
8464///
8465/// As with #817, all column / table identifiers are **SQL names**
8466/// (not Rust field names) so the substrate is independent of the
8467/// outstanding multi-hop filter resolver gap. The emitted
8468/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8469/// SQL-emit time via the writer's scope stack.
8470struct ReverseHasAttr {
8471    /// Accessor name. `name = "comments"` → emits
8472    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8473    name: String,
8474    /// Child model type identifier. `child = "Comment"` — needed to
8475    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8476    /// points at the child table.
8477    child: syn::Ident,
8478    /// SQL column on the child's table that references this model's
8479    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8480    /// column is `"post_id"`.
8481    child_fk_column: String,
8482    /// Column on **this** model's table the child FK references — the
8483    /// `OuterRef` target. Defaults to the PK column (`"id"` without one).
8484    self_pk_column: Option<String>,
8485    /// Where an explicit `self_pk_column` was written, for its error.
8486    self_pk_span: Option<proc_macro2::Span>,
8487}
8488
8489impl ReverseHasAttr {
8490    fn self_pk_column(&self) -> &str {
8491        self.self_pk_column.as_deref().unwrap_or("id")
8492    }
8493}
8494
8495/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8496/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8497/// relation-existence family (issue #830). The child is a polymorphic,
8498/// content-type-discriminated model; emits a `Model::
8499/// generic_reverse_relations()` entry the queryset resolves by name.
8500struct GenericHasAttr {
8501    /// Accessor name, e.g. `name = "tags"`.
8502    name: String,
8503    /// Child model type identifier (`child = "Tag"`) — looked up for its
8504    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8505    child: syn::Ident,
8506    /// Column on the child table holding the parent's content-type id.
8507    /// Optional; defaults to `"content_type_id"`.
8508    ct_column: String,
8509    /// Column on the child table holding the parent's PK value.
8510    /// Optional; defaults to `"object_pk"`.
8511    pk_column: String,
8512    /// SQL primary-key column on **this** (parent) model's table.
8513    /// Defaults to the PK column (`"id"` without one).
8514    self_pk_column: Option<String>,
8515    /// Where an explicit `self_pk_column` was written, for its error.
8516    self_pk_span: Option<proc_macro2::Span>,
8517}
8518
8519impl GenericHasAttr {
8520    fn self_pk_column(&self) -> &str {
8521        self.self_pk_column.as_deref().unwrap_or("id")
8522    }
8523}
8524
8525/// Parsed form of one index declaration (field-level or container-level).
8526struct IndexAttr {
8527    /// The declaring attribute, for diagnostics.
8528    span: proc_macro2::Span,
8529    /// Index name; auto-derived when `None` at parse time.
8530    name: Option<String>,
8531    /// Column names in the index.
8532    columns: Vec<String>,
8533    /// `true` for `CREATE UNIQUE INDEX`.
8534    unique: bool,
8535    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8536    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8537    /// `"btree"` when the attribute is absent — the DDL writer omits
8538    /// the `USING` clause and the backend uses its own default
8539    /// (btree on every supported dialect).
8540    method: String,
8541    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8542    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8543    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8544    where_clause: Option<String>,
8545    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8546    /// Empty `Vec` (the default) means "no covering columns".
8547    include: Vec<String>,
8548}
8549
8550/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8551struct CheckAttr {
8552    name: String,
8553    expr: String,
8554}
8555
8556/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8557/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8558/// PG-only — surfaced on every backend in the macro emit; the migration
8559/// writer skips the op on MySQL/SQLite. Issue #319.
8560struct ExcludeAttr {
8561    /// Constraint name (free-form Rust identifier).
8562    name: String,
8563    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8564    using: String,
8565    /// Comma-separated `(column, operator)` pairs, in declaration
8566    /// order. Parsed from `"col WITH op, col WITH op"`.
8567    elements: Vec<(String, String)>,
8568    /// Optional `WHERE` predicate (raw SQL).
8569    where_clause: Option<String>,
8570}
8571
8572/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8573/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8574/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8575/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8576/// the model, not the field.
8577struct CompositeFkAttr {
8578    /// Logical relation name (free-form Rust identifier).
8579    name: String,
8580    /// SQL table name of the target.
8581    to: String,
8582    /// Source-side column names, in declaration order.
8583    from: Vec<String>,
8584    /// Target-side column names, same length / order as `from`.
8585    on: Vec<String>,
8586}
8587
8588/// Parsed form of one `#[rustango(generic_fk(name = "target",
8589/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8590/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8591/// generic ("any model") FKs live on the model, not the field.
8592struct GenericFkAttr {
8593    /// Logical relation name (free-form Rust identifier).
8594    name: String,
8595    /// Source-side column carrying the `content_type_id` value.
8596    ct_column: String,
8597    /// Source-side column carrying the target row's primary key.
8598    pk_column: String,
8599}
8600
8601/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8602struct M2MAttr {
8603    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8604    name: String,
8605    /// Target table (e.g. `"app_tags"`).
8606    to: String,
8607    /// Junction table (e.g. `"post_tags"`).
8608    through: String,
8609    /// Source FK column in the junction table (e.g. `"post_id"`).
8610    src: String,
8611    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8612    dst: String,
8613    /// Whether the migration writer should auto-create the junction
8614    /// table. Default `true`. Set `auto_create = false` (#324) when
8615    /// the operator declares the through table as its own
8616    /// `#[derive(Model)]` struct with extra columns.
8617    auto_create: bool,
8618}
8619
8620/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8621/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8622/// junction carries a ContentType discriminator so unrelated models
8623/// share one pivot + related set.
8624struct GenericM2MAttr {
8625    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8626    name: String,
8627    /// Polymorphic junction table (e.g. `"taggables"`).
8628    through: String,
8629    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8630    pk_column: String,
8631    /// Junction column holding the owning model's `content_type_id`
8632    /// discriminator (e.g. `"taggable_type"`).
8633    ct_column: String,
8634    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8635    related_column: String,
8636}
8637
8638/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8639/// "user"))]`. `track` is a comma-separated list of field names whose
8640/// before/after values land in the JSONB `changes` column. `source`
8641/// is informational only — it pins a default source when the model
8642/// is written outside any `audit::with_source(...)` scope (rare).
8643#[derive(Default)]
8644struct AuditAttrs {
8645    /// Field names to capture in the `changes` JSONB. Validated
8646    /// against declared scalar fields at compile time. Empty means
8647    /// "track every scalar field".
8648    track: Option<(Vec<String>, proc_macro2::Span)>,
8649}
8650
8651/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8652/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8653/// comma-separated strings; we validate each ident against the model's
8654/// declared fields at compile time.
8655#[derive(Default)]
8656struct AdminAttrs {
8657    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8658    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8659    list_per_page: Option<usize>,
8660    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8661    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8662    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8663    /// Bulk action names. No field-validation against model fields —
8664    /// these are action handlers, not column references.
8665    actions: Option<(Vec<String>, proc_macro2::Span)>,
8666    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8667    /// sections, comma-separated fields per section, optional
8668    /// `Title:` prefix. Empty title omits the `<legend>`.
8669    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8670    /// `admin(list_display_links = "title")`. Names
8671    /// from `list_display` whose cells should link to detail/edit.
8672    /// Issue #350.
8673    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8674    /// `admin(search_help_text = "...")`. Short
8675    /// caption rendered beside the admin list view's search box.
8676    /// Issue #353.
8677    search_help_text: Option<String>,
8678    /// `admin(actions_on_top = false)`. Hides the
8679    /// action-bar above the table. Default `true`. Issue #354.
8680    actions_on_top: Option<bool>,
8681    /// `admin(actions_on_bottom = true)`. Renders an
8682    /// additional action-bar below the table. Default `false`.
8683    /// Issue #354.
8684    actions_on_bottom: Option<bool>,
8685    /// `admin(date_hierarchy = "created_at")`. Name of
8686    /// a date / datetime field whose values render as a clickable
8687    /// year / month / day drill-down strip above the list table.
8688    /// Empty / unset disables the strip. Issue #355.
8689    date_hierarchy: Option<String>,
8690    /// `admin(prepopulated_fields = "slug:title")`.
8691    /// Each entry is `target:source[+source2]`; multiple entries are
8692    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8693    /// The admin change-form emits JS that slugifies the source values
8694    /// into the target field on every keystroke. Issue #356.
8695    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8696    /// `admin(raw_id_fields = "parent, owner")`. Names
8697    /// of FK fields whose change-form widget renders a lookup link
8698    /// next to the input. Issue #357.
8699    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8700    /// `admin(autocomplete_fields = "author_id")`.
8701    /// Names of FK fields whose change-form widget renders an
8702    /// Ajax-driven typeahead populated from a `__autocomplete`
8703    /// endpoint on the target model. Issue #358.
8704    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8705    /// `admin(list_select_related = "all" | "none" | "author, …")`
8706    /// — tunes the admin list view's FK auto-JOIN
8707    /// policy. Default `"all"` matches rustango's join-everything
8708    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8709    list_select_related: Option<String>,
8710    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8711    /// Each entry is `field_name:widget_name`; multiple
8712    /// entries comma-separated. Empty / unset → no overrides. The
8713    /// list of widget names supported is documented on
8714    /// `AdminConfig::formfield_overrides`. Issue #359.
8715    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8716}
8717
8718/// The names `#[derive(Model)]` reads its attributes under: `rgo` is a
8719/// short alias of `rustango` with the same meaning.
8720#[derive(Clone, Copy, PartialEq, Eq)]
8721enum ModelAttrName {
8722    Rustango,
8723    Rgo,
8724}
8725
8726impl ModelAttrName {
8727    fn of(attr: &syn::Attribute) -> Option<Self> {
8728        let path = attr.path();
8729        if path.is_ident("rustango") {
8730            Some(Self::Rustango)
8731        } else if path.is_ident("rgo") {
8732            Some(Self::Rgo)
8733        } else {
8734            None
8735        }
8736    }
8737
8738    fn as_str(self) -> &'static str {
8739        match self {
8740            Self::Rustango => "rustango",
8741            Self::Rgo => "rgo",
8742        }
8743    }
8744}
8745
8746/// The model attributes in `attrs`, under either name. One item or field
8747/// uses one name, so a mix reads as a mistake rather than a merge.
8748fn model_attrs(attrs: &[syn::Attribute]) -> syn::Result<Vec<&syn::Attribute>> {
8749    let mut first: Option<ModelAttrName> = None;
8750    let mut out = Vec::new();
8751    for attr in attrs {
8752        let Some(name) = ModelAttrName::of(attr) else {
8753            continue;
8754        };
8755        match first {
8756            Some(seen) if seen != name => {
8757                return Err(syn::Error::new_spanned(
8758                    attr.path(),
8759                    format!(
8760                        "`#[{}(...)]` and `#[{}(...)]` are the same attribute; \
8761                         use one name on an item or field",
8762                        seen.as_str(),
8763                        name.as_str(),
8764                    ),
8765                ));
8766            }
8767            _ => first = Some(name),
8768        }
8769        out.push(attr);
8770    }
8771    Ok(out)
8772}
8773
8774fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8775    let mut out = ContainerAttrs {
8776        table: None,
8777        display: None,
8778        app: None,
8779        admin: None,
8780        audit: None,
8781        // Default `permissions = true` so every `#[derive(Model)]`
8782        // gets the four CRUD codenames seeded by `auto_create_permissions`
8783        // and is visible to non-superusers in the tenant admin without
8784        // manual per-model annotation. Models that intentionally don't
8785        // want permission rows (registry-internal types, framework
8786        // tables operators shouldn't manage directly) opt out via
8787        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8788        // out-of-the-box admin invisibility regression (#62).
8789        permissions: true,
8790        m2m: Vec::new(),
8791        generic_m2m: Vec::new(),
8792        indexes: Vec::new(),
8793        checks: Vec::new(),
8794        excludes: Vec::new(),
8795        composite_fks: Vec::new(),
8796        generic_fks: Vec::new(),
8797        scope: None,
8798        access: None,
8799        manager_fns: Vec::new(),
8800        default_order: Vec::new(),
8801        is_view: false,
8802        managed: true,
8803        verbose_name: None,
8804        verbose_name_plural: None,
8805        base_manager_name: None,
8806        order_with_respect_to: None,
8807        proxy: false,
8808        required_db_features: Vec::new(),
8809        required_db_vendor: None,
8810        default_related_name: None,
8811        db_table_comment: None,
8812        get_latest_by: None,
8813        extra_permissions: Vec::new(),
8814        default_permissions: Vec::new(),
8815        global_scopes: Vec::new(),
8816        through_relations: Vec::new(),
8817        reverse_has_relations: Vec::new(),
8818        generic_has_relations: Vec::new(),
8819    };
8820    for attr in model_attrs(&input.attrs)? {
8821        // The name the user wrote, for hints that quote the attribute.
8822        let written = ModelAttrName::of(attr).map_or("rustango", ModelAttrName::as_str);
8823        attr.parse_nested_meta(|meta| {
8824            if meta.path.is_ident("table") {
8825                let s: LitStr = meta.value()?.parse()?;
8826                let name = s.value();
8827                // v0.27.3 (#65) — macro-time guard against table names
8828                // that compile but break SQL downstream. Hyphens are
8829                // the common footgun: PostgreSQL accepts a quoted
8830                // `"intermediate-region"` in CREATE TABLE, but the
8831                // FK / index name derivation in `migrate::ddl`
8832                // emits `intermediate-region_field_fkey` unquoted,
8833                // which then fails the SQL parser. Same shape rule
8834                // as Postgres regular identifiers so the safe path
8835                // is the only path.
8836                validate_table_name(&name, s.span())?;
8837                out.table = Some(name);
8838                return Ok(());
8839            }
8840            if meta.path.is_ident("display") {
8841                let s: LitStr = meta.value()?.parse()?;
8842                out.display = Some((s.value(), s.span()));
8843                return Ok(());
8844            }
8845            if meta.path.is_ident("app") {
8846                let s: LitStr = meta.value()?.parse()?;
8847                out.app = Some(s.value());
8848                return Ok(());
8849            }
8850            if meta.path.is_ident("scope") {
8851                let s: LitStr = meta.value()?.parse()?;
8852                let val = s.value();
8853                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8854                    return Err(meta.error(format!(
8855                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8856                    )));
8857                }
8858                out.scope = Some(val);
8859                return Ok(());
8860            }
8861            if meta.path.is_ident("access") {
8862                let s: LitStr = meta.value()?.parse()?;
8863                let val = s.value();
8864                if !matches!(val.as_str(), "open" | "admin_only" | "never") {
8865                    return Err(meta.error(format!(
8866                        "`access` must be \"open\", \"admin_only\" or \"never\", got {val:?}"
8867                    )));
8868                }
8869                out.access = Some(val);
8870                return Ok(());
8871            }
8872            if meta.path.is_ident("admin") {
8873                let mut admin = AdminAttrs::default();
8874                meta.parse_nested_meta(|inner| {
8875                    if inner.path.is_ident("list_display") {
8876                        let s: LitStr = inner.value()?.parse()?;
8877                        admin.list_display =
8878                            Some((split_field_list(&s.value()), s.span()));
8879                        return Ok(());
8880                    }
8881                    if inner.path.is_ident("search_fields") {
8882                        let s: LitStr = inner.value()?.parse()?;
8883                        admin.search_fields =
8884                            Some((split_field_list(&s.value()), s.span()));
8885                        return Ok(());
8886                    }
8887                    if inner.path.is_ident("readonly_fields") {
8888                        let s: LitStr = inner.value()?.parse()?;
8889                        admin.readonly_fields =
8890                            Some((split_field_list(&s.value()), s.span()));
8891                        return Ok(());
8892                    }
8893                    if inner.path.is_ident("list_per_page") {
8894                        let lit: syn::LitInt = inner.value()?.parse()?;
8895                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8896                        return Ok(());
8897                    }
8898                    if inner.path.is_ident("ordering") {
8899                        let s: LitStr = inner.value()?.parse()?;
8900                        admin.ordering = Some((
8901                            parse_ordering_list(&s.value()),
8902                            s.span(),
8903                        ));
8904                        return Ok(());
8905                    }
8906                    if inner.path.is_ident("list_filter") {
8907                        let s: LitStr = inner.value()?.parse()?;
8908                        admin.list_filter =
8909                            Some((split_field_list(&s.value()), s.span()));
8910                        return Ok(());
8911                    }
8912                    if inner.path.is_ident("actions") {
8913                        let s: LitStr = inner.value()?.parse()?;
8914                        admin.actions =
8915                            Some((split_field_list(&s.value()), s.span()));
8916                        return Ok(());
8917                    }
8918                    if inner.path.is_ident("fieldsets") {
8919                        let s: LitStr = inner.value()?.parse()?;
8920                        admin.fieldsets =
8921                            Some((parse_fieldset_list(&s.value()), s.span()));
8922                        return Ok(());
8923                    }
8924                    if inner.path.is_ident("list_display_links") {
8925                        let s: LitStr = inner.value()?.parse()?;
8926                        admin.list_display_links =
8927                            Some((split_field_list(&s.value()), s.span()));
8928                        return Ok(());
8929                    }
8930                    if inner.path.is_ident("search_help_text") {
8931                        let s: LitStr = inner.value()?.parse()?;
8932                        admin.search_help_text = Some(s.value());
8933                        return Ok(());
8934                    }
8935                    if inner.path.is_ident("actions_on_top") {
8936                        let lit: syn::LitBool = inner.value()?.parse()?;
8937                        admin.actions_on_top = Some(lit.value);
8938                        return Ok(());
8939                    }
8940                    if inner.path.is_ident("actions_on_bottom") {
8941                        let lit: syn::LitBool = inner.value()?.parse()?;
8942                        admin.actions_on_bottom = Some(lit.value);
8943                        return Ok(());
8944                    }
8945                    if inner.path.is_ident("date_hierarchy") {
8946                        let s: LitStr = inner.value()?.parse()?;
8947                        admin.date_hierarchy = Some(s.value());
8948                        return Ok(());
8949                    }
8950                    if inner.path.is_ident("prepopulated_fields") {
8951                        let s: LitStr = inner.value()?.parse()?;
8952                        admin.prepopulated_fields =
8953                            Some((parse_prepopulated_list(&s.value()), s.span()));
8954                        return Ok(());
8955                    }
8956                    if inner.path.is_ident("raw_id_fields") {
8957                        let s: LitStr = inner.value()?.parse()?;
8958                        admin.raw_id_fields =
8959                            Some((split_field_list(&s.value()), s.span()));
8960                        return Ok(());
8961                    }
8962                    if inner.path.is_ident("autocomplete_fields") {
8963                        let s: LitStr = inner.value()?.parse()?;
8964                        admin.autocomplete_fields =
8965                            Some((split_field_list(&s.value()), s.span()));
8966                        return Ok(());
8967                    }
8968                    if inner.path.is_ident("list_select_related") {
8969                        let s: LitStr = inner.value()?.parse()?;
8970                        admin.list_select_related = Some(s.value());
8971                        return Ok(());
8972                    }
8973                    if inner.path.is_ident("formfield_overrides") {
8974                        let s: LitStr = inner.value()?.parse()?;
8975                        admin.formfield_overrides =
8976                            Some((parse_formfield_overrides(&s.value()), s.span()));
8977                        return Ok(());
8978                    }
8979                    Err(inner.error(
8980                        "unknown admin attribute (supported: \
8981                         `list_display`, `list_display_links`, \
8982                         `search_fields`, `search_help_text`, \
8983                         `readonly_fields`, \
8984                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8985                         `actions_on_top`, `actions_on_bottom`, \
8986                         `date_hierarchy`, \
8987                         `prepopulated_fields`, \
8988                         `raw_id_fields`, \
8989                         `autocomplete_fields`, \
8990                         `list_select_related`, \
8991                         `formfield_overrides`, \
8992                         `fieldsets`)",
8993                    ))
8994                })?;
8995                out.admin = Some(admin);
8996                return Ok(());
8997            }
8998            if meta.path.is_ident("manager") {
8999                // Its empty trait could not take methods (#2132).
9000                return Err(meta.error(
9001                    "`manager(ext = ...)` was removed (#2132): declare your own \
9002                     `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
9003                ));
9004            }
9005            if meta.path.is_ident("manager_fn") {
9006                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
9007                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
9008                // next to the default `Self::objects()`. Multiple
9009                // attributes accumulate.
9010                let s: LitStr = meta.value()?.parse()?;
9011                let name = s.value();
9012                if name.is_empty() {
9013                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
9014                }
9015                if name == "objects" {
9016                    return Err(meta.error(
9017                        "`manager_fn = \"objects\"` collides with the default \
9018                         accessor — pick a different name",
9019                    ));
9020                }
9021                let ident = syn::Ident::new(&name, s.span());
9022                if out.manager_fns.iter().any(|prev| *prev == ident) {
9023                    return Err(meta.error(format!(
9024                        "duplicate `manager_fn = \"{name}\"`"
9025                    )));
9026                }
9027                out.manager_fns.push(ident);
9028                return Ok(());
9029            }
9030            if meta.path.is_ident("default_order") {
9031                // `#[rustango(default_order = "-created_at, status")]`
9032                // — issue #291 / T2.5. Comma-separated list; `-prefix`
9033                // means descending, `+prefix` or bare name means ascending.
9034                // Per-query opt-in via `QuerySet::with_default_order()`.
9035                let s: LitStr = meta.value()?.parse()?;
9036                let raw = s.value();
9037                let span = s.span();
9038                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9039                    Vec::new();
9040                for entry in raw.split(',') {
9041                    let trimmed = entry.trim();
9042                    if trimmed.is_empty() {
9043                        return Err(syn::Error::new(
9044                            span,
9045                            "`default_order = \"...\"` has an empty entry — \
9046                             check for a stray comma",
9047                        ));
9048                    }
9049                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9050                        (true, rest.trim().to_owned())
9051                    } else if let Some(rest) = trimmed.strip_prefix('+') {
9052                        (false, rest.trim().to_owned())
9053                    } else {
9054                        (false, trimmed.to_owned())
9055                    };
9056                    if name.is_empty() {
9057                        return Err(syn::Error::new(
9058                            span,
9059                            "`default_order` entry has no column name after the prefix",
9060                        ));
9061                    }
9062                    if parsed.iter().any(|(n, _, _)| *n == name) {
9063                        return Err(syn::Error::new(
9064                            span,
9065                            format!("duplicate column `{name}` in `default_order`"),
9066                        ));
9067                    }
9068                    parsed.push((name, desc, span));
9069                }
9070                if parsed.is_empty() {
9071                    return Err(syn::Error::new(
9072                        span,
9073                        "`default_order = \"...\"` cannot be empty",
9074                    ));
9075                }
9076                out.default_order = parsed;
9077                return Ok(());
9078            }
9079            if meta.path.is_ident("global_scope") {
9080                // `#[rustango(global_scope(name = "active", apply =
9081                //  path::to::fn))]` — issue #820. The apply function
9082                // path resolves at macro-expand time in the consumer's
9083                // scope; the macro re-emits it verbatim into the
9084                // `ModelSchema::global_scopes` slice literal.
9085                let span = meta.path.span();
9086                let mut scope_name: Option<String> = None;
9087                let mut apply_path: Option<syn::Path> = None;
9088                meta.parse_nested_meta(|inner| {
9089                    if inner.path.is_ident("name") {
9090                        let s: LitStr = inner.value()?.parse()?;
9091                        let raw = s.value();
9092                        if raw.trim().is_empty() {
9093                            return Err(syn::Error::new(
9094                                s.span(),
9095                                "`global_scope(name = \"...\")` must not be empty",
9096                            ));
9097                        }
9098                        scope_name = Some(raw);
9099                        return Ok(());
9100                    }
9101                    if inner.path.is_ident("apply") {
9102                        let p: syn::Path = inner.value()?.parse()?;
9103                        apply_path = Some(p);
9104                        return Ok(());
9105                    }
9106                    Err(inner.error(
9107                        "unknown `global_scope` attribute (supported: \
9108                         `name`, `apply`)",
9109                    ))
9110                })?;
9111                let Some(name) = scope_name else {
9112                    return Err(syn::Error::new(
9113                        span,
9114                        "`global_scope` requires `name = \"...\"`",
9115                    ));
9116                };
9117                let Some(apply) = apply_path else {
9118                    return Err(syn::Error::new(
9119                        span,
9120                        "`global_scope` requires `apply = fn_path`",
9121                    ));
9122                };
9123                if out.global_scopes.iter().any(|s| s.name == name) {
9124                    return Err(syn::Error::new(
9125                        span,
9126                        format!(
9127                            "duplicate global scope name `{name}` — \
9128                             pick a unique identifier so \
9129                             `QuerySet::without_global_scope(\"{name}\")` \
9130                             is unambiguous"
9131                        ),
9132                    ));
9133                }
9134                out.global_scopes.push(GlobalScopeAttr { name, apply });
9135                return Ok(());
9136            }
9137            if meta.path.is_ident("through") {
9138                // `#[rustango(through(name = "posts", far = "Post",
9139                //  far_fk_column = "author_id", intermediate = "User",
9140                //  intermediate_fk_column = "country_id"
9141                //  [, intermediate_pk_column = "id"]))]` — issue #817.
9142                let span = meta.path.span();
9143                let mut accessor_name: Option<String> = None;
9144                let mut far_ident: Option<syn::Ident> = None;
9145                let mut far_fk_column: Option<String> = None;
9146                let mut intermediate_ident: Option<syn::Ident> = None;
9147                let mut intermediate_fk_column: Option<String> = None;
9148                let mut intermediate_pk_column: Option<String> = None;
9149                fn parse_nonempty_string(
9150                    inner: &syn::meta::ParseNestedMeta<'_>,
9151                    field: &str,
9152                ) -> syn::Result<String> {
9153                    let s: LitStr = inner.value()?.parse()?;
9154                    let raw = s.value();
9155                    let trimmed = raw.trim();
9156                    if trimmed.is_empty() {
9157                        return Err(syn::Error::new(
9158                            s.span(),
9159                            format!("`through({field} = \"...\")` must not be empty"),
9160                        ));
9161                    }
9162                    Ok(trimmed.to_owned())
9163                }
9164                meta.parse_nested_meta(|inner| {
9165                    if inner.path.is_ident("name") {
9166                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9167                        return Ok(());
9168                    }
9169                    if inner.path.is_ident("far") {
9170                        // Far model name accepted as a string literal
9171                        // so the attribute fits inside the existing
9172                        // `parse_nested_meta` shape; parsed back into a
9173                        // `syn::Ident` so the emitted accessor can
9174                        // reference the type directly.
9175                        let s: LitStr = inner.value()?.parse()?;
9176                        let raw = s.value();
9177                        let trimmed = raw.trim();
9178                        if trimmed.is_empty() {
9179                            return Err(syn::Error::new(
9180                                s.span(),
9181                                "`through(far = \"...\")` must not be empty",
9182                            ));
9183                        }
9184                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9185                        return Ok(());
9186                    }
9187                    if inner.path.is_ident("far_fk_column") {
9188                        far_fk_column =
9189                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9190                        return Ok(());
9191                    }
9192                    if inner.path.is_ident("intermediate") {
9193                        let s: LitStr = inner.value()?.parse()?;
9194                        let raw = s.value();
9195                        let trimmed = raw.trim();
9196                        if trimmed.is_empty() {
9197                            return Err(syn::Error::new(
9198                                s.span(),
9199                                "`through(intermediate = \"...\")` must not be empty",
9200                            ));
9201                        }
9202                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9203                        return Ok(());
9204                    }
9205                    if inner.path.is_ident("intermediate_fk_column") {
9206                        intermediate_fk_column =
9207                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9208                        return Ok(());
9209                    }
9210                    if inner.path.is_ident("intermediate_pk_column") {
9211                        intermediate_pk_column =
9212                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9213                        return Ok(());
9214                    }
9215                    Err(inner.error(
9216                        "unknown `through` attribute (supported: \
9217                         `name`, `far`, `far_fk_column`, \
9218                         `intermediate`, `intermediate_fk_column`, \
9219                         `intermediate_pk_column`)",
9220                    ))
9221                })?;
9222                let Some(name) = accessor_name else {
9223                    return Err(syn::Error::new(
9224                        span,
9225                        "`through` requires `name = \"...\"`",
9226                    ));
9227                };
9228                let Some(far) = far_ident else {
9229                    return Err(syn::Error::new(
9230                        span,
9231                        "`through` requires `far = \"FarModelType\"`",
9232                    ));
9233                };
9234                let Some(far_fk_column) = far_fk_column else {
9235                    return Err(syn::Error::new(
9236                        span,
9237                        "`through` requires `far_fk_column = \"<column>\"`",
9238                    ));
9239                };
9240                let Some(intermediate) = intermediate_ident else {
9241                    return Err(syn::Error::new(
9242                        span,
9243                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9244                    ));
9245                };
9246                let Some(intermediate_fk_column) = intermediate_fk_column else {
9247                    return Err(syn::Error::new(
9248                        span,
9249                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9250                    ));
9251                };
9252                let intermediate_pk_column =
9253                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9254                if out.through_relations.iter().any(|t| t.name == name) {
9255                    return Err(syn::Error::new(
9256                        span,
9257                        format!(
9258                            "duplicate `through(name = \"{name}\")` — \
9259                             pick a unique accessor name"
9260                        ),
9261                    ));
9262                }
9263                out.through_relations.push(ThroughAttr {
9264                    name,
9265                    far,
9266                    far_fk_column,
9267                    intermediate,
9268                    intermediate_fk_column,
9269                    intermediate_pk_column,
9270                });
9271                return Ok(());
9272            }
9273            if meta.path.is_ident("reverse_has") {
9274                // `#[rustango(reverse_has(name = "comments",
9275                //  child = "Comment", child_fk_column = "post_id"
9276                //  [, self_pk_column = "id"]))]` — issue #830.
9277                let span = meta.path.span();
9278                let mut accessor_name: Option<String> = None;
9279                let mut child_ident: Option<syn::Ident> = None;
9280                let mut child_fk_column: Option<String> = None;
9281                let mut self_pk_column: Option<String> = None;
9282                let mut self_pk_span: Option<proc_macro2::Span> = None;
9283                meta.parse_nested_meta(|inner| {
9284                    if inner.path.is_ident("name") {
9285                        let s: LitStr = inner.value()?.parse()?;
9286                        let raw = s.value();
9287                        if raw.trim().is_empty() {
9288                            return Err(syn::Error::new(
9289                                s.span(),
9290                                "`reverse_has(name = \"...\")` must not be empty",
9291                            ));
9292                        }
9293                        accessor_name = Some(raw);
9294                        return Ok(());
9295                    }
9296                    if inner.path.is_ident("child") {
9297                        let s: LitStr = inner.value()?.parse()?;
9298                        let raw = s.value();
9299                        let trimmed = raw.trim();
9300                        if trimmed.is_empty() {
9301                            return Err(syn::Error::new(
9302                                s.span(),
9303                                "`reverse_has(child = \"...\")` must not be empty",
9304                            ));
9305                        }
9306                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9307                        return Ok(());
9308                    }
9309                    if inner.path.is_ident("child_fk_column") {
9310                        let s: LitStr = inner.value()?.parse()?;
9311                        let raw = s.value();
9312                        let trimmed = raw.trim();
9313                        if trimmed.is_empty() {
9314                            return Err(syn::Error::new(
9315                                s.span(),
9316                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9317                            ));
9318                        }
9319                        child_fk_column = Some(trimmed.to_owned());
9320                        return Ok(());
9321                    }
9322                    if inner.path.is_ident("self_pk_column") {
9323                        let s: LitStr = inner.value()?.parse()?;
9324                        let raw = s.value();
9325                        let trimmed = raw.trim();
9326                        if trimmed.is_empty() {
9327                            return Err(syn::Error::new(
9328                                s.span(),
9329                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9330                            ));
9331                        }
9332                        self_pk_column = Some(trimmed.to_owned());
9333                        self_pk_span = Some(s.span());
9334                        return Ok(());
9335                    }
9336                    Err(inner.error(
9337                        "unknown `reverse_has` attribute (supported: \
9338                         `name`, `child`, `child_fk_column`, \
9339                         `self_pk_column`)",
9340                    ))
9341                })?;
9342                let Some(name) = accessor_name else {
9343                    return Err(syn::Error::new(
9344                        span,
9345                        "`reverse_has` requires `name = \"...\"`",
9346                    ));
9347                };
9348                let Some(child) = child_ident else {
9349                    return Err(syn::Error::new(
9350                        span,
9351                        "`reverse_has` requires `child = \"ChildModelType\"`",
9352                    ));
9353                };
9354                let Some(child_fk_column) = child_fk_column else {
9355                    return Err(syn::Error::new(
9356                        span,
9357                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9358                    ));
9359                };
9360                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9361                    return Err(syn::Error::new(
9362                        span,
9363                        format!(
9364                            "duplicate `reverse_has(name = \"{name}\")` — \
9365                             pick a unique accessor name"
9366                        ),
9367                    ));
9368                }
9369                out.reverse_has_relations.push(ReverseHasAttr {
9370                    name,
9371                    child,
9372                    child_fk_column,
9373                    self_pk_column,
9374                    self_pk_span,
9375                });
9376                return Ok(());
9377            }
9378            if meta.path.is_ident("generic_has") {
9379                // `#[rustango(generic_has(name = "tags",
9380                //  child = "Tag", ct_column = "content_type_id",
9381                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9382                //  — issue #830, the reverse generic-FK (GFK) arm of the
9383                //  relation-existence family.
9384                let span = meta.path.span();
9385                let mut accessor_name: Option<String> = None;
9386                let mut child_ident: Option<syn::Ident> = None;
9387                let mut ct_column: Option<String> = None;
9388                let mut pk_column: Option<String> = None;
9389                let mut self_pk_column: Option<String> = None;
9390                let mut self_pk_span: Option<proc_macro2::Span> = None;
9391                meta.parse_nested_meta(|inner| {
9392                    if inner.path.is_ident("name") {
9393                        let s: LitStr = inner.value()?.parse()?;
9394                        let raw = s.value();
9395                        if raw.trim().is_empty() {
9396                            return Err(syn::Error::new(
9397                                s.span(),
9398                                "`generic_has(name = \"...\")` must not be empty",
9399                            ));
9400                        }
9401                        accessor_name = Some(raw);
9402                        return Ok(());
9403                    }
9404                    if inner.path.is_ident("child") {
9405                        let s: LitStr = inner.value()?.parse()?;
9406                        let trimmed = s.value().trim().to_owned();
9407                        if trimmed.is_empty() {
9408                            return Err(syn::Error::new(
9409                                s.span(),
9410                                "`generic_has(child = \"...\")` must not be empty",
9411                            ));
9412                        }
9413                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9414                        return Ok(());
9415                    }
9416                    if inner.path.is_ident("ct_column") {
9417                        let s: LitStr = inner.value()?.parse()?;
9418                        let trimmed = s.value().trim().to_owned();
9419                        if trimmed.is_empty() {
9420                            return Err(syn::Error::new(
9421                                s.span(),
9422                                "`generic_has(ct_column = \"...\")` must not be empty",
9423                            ));
9424                        }
9425                        ct_column = Some(trimmed);
9426                        return Ok(());
9427                    }
9428                    if inner.path.is_ident("pk_column") {
9429                        let s: LitStr = inner.value()?.parse()?;
9430                        let trimmed = s.value().trim().to_owned();
9431                        if trimmed.is_empty() {
9432                            return Err(syn::Error::new(
9433                                s.span(),
9434                                "`generic_has(pk_column = \"...\")` must not be empty",
9435                            ));
9436                        }
9437                        pk_column = Some(trimmed);
9438                        return Ok(());
9439                    }
9440                    if inner.path.is_ident("self_pk_column") {
9441                        let s: LitStr = inner.value()?.parse()?;
9442                        let trimmed = s.value().trim().to_owned();
9443                        if trimmed.is_empty() {
9444                            return Err(syn::Error::new(
9445                                s.span(),
9446                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9447                            ));
9448                        }
9449                        self_pk_column = Some(trimmed);
9450                        self_pk_span = Some(s.span());
9451                        return Ok(());
9452                    }
9453                    Err(inner.error(
9454                        "unknown `generic_has` attribute (supported: \
9455                         `name`, `child`, `ct_column`, `pk_column`, \
9456                         `self_pk_column`)",
9457                    ))
9458                })?;
9459                let Some(name) = accessor_name else {
9460                    return Err(syn::Error::new(
9461                        span,
9462                        "`generic_has` requires `name = \"...\"`",
9463                    ));
9464                };
9465                let Some(child) = child_ident else {
9466                    return Err(syn::Error::new(
9467                        span,
9468                        "`generic_has` requires `child = \"ChildModelType\"`",
9469                    ));
9470                };
9471                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9472                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9473                if out.generic_has_relations.iter().any(|r| r.name == name) {
9474                    return Err(syn::Error::new(
9475                        span,
9476                        format!(
9477                            "duplicate `generic_has(name = \"{name}\")` — \
9478                             pick a unique accessor name"
9479                        ),
9480                    ));
9481                }
9482                out.generic_has_relations.push(GenericHasAttr {
9483                    name,
9484                    child,
9485                    ct_column,
9486                    pk_column,
9487                    self_pk_column,
9488                    self_pk_span,
9489                });
9490                return Ok(());
9491            }
9492            if meta.path.is_ident("audit") {
9493                let mut audit = AuditAttrs::default();
9494                meta.parse_nested_meta(|inner| {
9495                    if inner.path.is_ident("track") {
9496                        let s: LitStr = inner.value()?.parse()?;
9497                        audit.track =
9498                            Some((split_field_list(&s.value()), s.span()));
9499                        return Ok(());
9500                    }
9501                    Err(inner.error(
9502                        "unknown audit attribute (supported: `track`)",
9503                    ))
9504                })?;
9505                out.audit = Some(audit);
9506                return Ok(());
9507            }
9508            if meta.path.is_ident("permissions") {
9509                // Two forms accepted:
9510                //   #[rustango(permissions)]          — flag form, true
9511                //   #[rustango(permissions = false)]  — explicit opt-out
9512                //   #[rustango(permissions = true)]   — explicit opt-in
9513                if let Ok(v) = meta.value() {
9514                    let lit: syn::LitBool = v.parse()?;
9515                    out.permissions = lit.value;
9516                } else {
9517                    out.permissions = true;
9518                }
9519                return Ok(());
9520            }
9521            if meta.path.is_ident("view") {
9522                // Issue #293 / T2.10. Two forms accepted, matching
9523                // the `permissions` flag pattern:
9524                //   #[rustango(view)]          — flag form, true
9525                //   #[rustango(view = false)]  — explicit opt-out
9526                //   #[rustango(view = true)]   — explicit opt-in
9527                if let Ok(v) = meta.value() {
9528                    let lit: syn::LitBool = v.parse()?;
9529                    out.is_view = lit.value;
9530                } else {
9531                    out.is_view = true;
9532                }
9533                return Ok(());
9534            }
9535            if meta.path.is_ident("managed") {
9536                // Who owns the table. Issue #321.
9537                //   #[rustango(managed = false)]  — operator-managed table
9538                //   #[rustango(managed = true)]   — rustango-managed (the default)
9539                // Bare-flag form is intentionally not accepted: writing
9540                // `#[rustango(managed)]` reads as "yes please manage it"
9541                // which is already the default. The opt-out is the only
9542                // useful state, so it must be explicit.
9543                let v = meta.value()?;
9544                let lit: syn::LitBool = v.parse()?;
9545                out.managed = lit.value;
9546                return Ok(());
9547            }
9548            if meta.path.is_ident("verbose_name") {
9549                let s: LitStr = meta.value()?.parse()?;
9550                out.verbose_name = Some(s.value());
9551                return Ok(());
9552            }
9553            if meta.path.is_ident("verbose_name_plural") {
9554                let s: LitStr = meta.value()?.parse()?;
9555                out.verbose_name_plural = Some(s.value());
9556                return Ok(());
9557            }
9558            if meta.path.is_ident("db_table_comment") {
9559                // Free-form table-level comment attached to the DB
9560                // catalog.
9561                let s: LitStr = meta.value()?.parse()?;
9562                out.db_table_comment = Some(s.value());
9563                return Ok(());
9564            }
9565            if meta.path.is_ident("proxy") {
9566                // Declarative flag
9567                // marking the struct as a proxy of another model that
9568                // shares its DB table. Stored on `ModelSchema::proxy`
9569                // so future codegen can skip `CreateTable` emission
9570                // for proxies (parent owns the table) and route
9571                // per-instance method resolution to the proxy class.
9572                //
9573                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9574                let value = if meta.input.peek(syn::Token![=]) {
9575                    meta.value()?.parse::<syn::LitBool>()?.value
9576                } else {
9577                    true
9578                };
9579                out.proxy = value;
9580                return Ok(());
9581            }
9582            if meta.path.is_ident("order_with_respect_to") {
9583                // `order_with_respect_to = "parent_fk"` — the model's
9584                // instances are intrinsically ordered relative to
9585                // their parent FK.
9586                //
9587                // rustango stores the FK field name on
9588                // `ModelSchema::order_with_respect_to`. Declarative-only
9589                // today: the migration writer + admin surfaces still
9590                // treat every model identically. Future codegen will
9591                // key off the metadata to auto-emit the `_order`
9592                // column and reorder helpers.
9593                //
9594                // Validated as a Rust-shape identifier so the macro
9595                // can reject typos at derive time.
9596                let s: LitStr = meta.value()?.parse()?;
9597                let raw = s.value();
9598                if raw.is_empty() {
9599                    return Err(syn::Error::new(
9600                        s.span(),
9601                        "`order_with_respect_to` must be a non-empty FK field name",
9602                    ));
9603                }
9604                let valid = raw
9605                    .chars()
9606                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9607                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9608                if !valid {
9609                    return Err(syn::Error::new(
9610                        s.span(),
9611                        format!(
9612                            "`order_with_respect_to` must be a valid Rust \
9613                             identifier (letters / digits / underscores, \
9614                             not starting with a digit); got `{raw}`"
9615                        ),
9616                    ));
9617                }
9618                out.order_with_respect_to = Some(raw);
9619                return Ok(());
9620            }
9621            if meta.path.is_ident("required_db_features") {
9622                // Capability tokens the model needs (e.g.
9623                // `"json_extract"`, `"window_functions"`,
9624                // `"row_security"`). Comma-separated.
9625                // `manage check --deploy` walks every model and warns
9626                // when the active backend doesn't advertise the
9627                // capability.
9628                //
9629                // rustango ships a small registry of capability tokens
9630                // each dialect supports — see `Dialect::supports`.
9631                // Unknown tokens still parse (they end up on the
9632                // schema and show up in the warning) so projects can
9633                // declare aspirational capabilities and the check
9634                // verb will keep nagging until the dialect implements
9635                // them.
9636                let s: LitStr = meta.value()?.parse()?;
9637                let raw = s.value();
9638                let features: Vec<String> = raw
9639                    .split(',')
9640                    .map(str::trim)
9641                    .filter(|s| !s.is_empty())
9642                    .map(str::to_owned)
9643                    .collect();
9644                if features.is_empty() {
9645                    return Err(syn::Error::new(
9646                        s.span(),
9647                        "`required_db_features` must list at least one \
9648                         comma-separated capability token",
9649                    ));
9650                }
9651                out.required_db_features = features;
9652                return Ok(());
9653            }
9654            if meta.path.is_ident("required_db_vendor") {
9655                // The model is only meant to run against the named DB
9656                // backend. `manage check --deploy` flags a mismatch so
9657                // ops catches "I forgot to switch DATABASE_URL" at
9658                // deploy time rather than runtime.
9659                //
9660                // Restricted to the three backends rustango ships
9661                // dialects for so the check verb can compare reliably.
9662                let s: LitStr = meta.value()?.parse()?;
9663                let raw = s.value().to_ascii_lowercase();
9664                match raw.as_str() {
9665                    "postgresql" | "postgres" | "pg" => {
9666                        out.required_db_vendor = Some("postgres".to_owned());
9667                    }
9668                    "mysql" | "mariadb" => {
9669                        out.required_db_vendor = Some("mysql".to_owned());
9670                    }
9671                    "sqlite" | "sqlite3" => {
9672                        out.required_db_vendor = Some("sqlite".to_owned());
9673                    }
9674                    _ => {
9675                        return Err(syn::Error::new(
9676                            s.span(),
9677                            format!(
9678                                "unknown required_db_vendor `{raw}` — \
9679                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9680                                 `mysql` (alias: `mariadb`), or `sqlite` \
9681                                 (alias: `sqlite3`)"
9682                            ),
9683                        ));
9684                    }
9685                }
9686                return Ok(());
9687            }
9688            if meta.path.is_ident("base_manager_name") {
9689                // Name of the manager that `<instance>.<relation>_set`
9690                // uses when resolving reverse-relation managers.
9691                // Distinct from `default_manager_name` (what
9692                // `Self::objects()` returns). Stored on
9693                // `ModelSchema::base_manager_name`.
9694                //
9695                // Validated as a Rust identifier so it stays safe to
9696                // re-emit as code in future reverse-manager codegen.
9697                let s: LitStr = meta.value()?.parse()?;
9698                let raw = s.value();
9699                if raw.is_empty() {
9700                    return Err(syn::Error::new(
9701                        s.span(),
9702                        "`base_manager_name` must be a non-empty string",
9703                    ));
9704                }
9705                let valid = raw
9706                    .chars()
9707                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9708                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9709                if !valid {
9710                    return Err(syn::Error::new(
9711                        s.span(),
9712                        format!(
9713                            "`base_manager_name` must be a valid Rust \
9714                             identifier (letters / digits / underscores, \
9715                             not starting with a digit); got `{raw}`"
9716                        ),
9717                    ));
9718                }
9719                out.base_manager_name = Some(raw);
9720                return Ok(());
9721            }
9722            if meta.path.is_ident("default_related_name") {
9723                // The name reverse-relation accessors use when callers
9724                // don't override `related_name = "..."` on the FK / M2M
9725                // field. Stored on `ModelSchema::default_related_name`
9726                // so future reverse-manager codegen / schema emit /
9727                // admin templates can pick the right accessor name
9728                // (today rustango doesn't auto-emit reverse managers;
9729                // the metadata is the foundation for that work).
9730                //
9731                // Enforced non-empty + ASCII identifier-shape so the
9732                // string is safe to use as a Rust ident later.
9733                let s: LitStr = meta.value()?.parse()?;
9734                let raw = s.value();
9735                if raw.is_empty() {
9736                    return Err(syn::Error::new(
9737                        s.span(),
9738                        "`default_related_name` must be a non-empty string",
9739                    ));
9740                }
9741                let valid = raw
9742                    .chars()
9743                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9744                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9745                if !valid {
9746                    return Err(syn::Error::new(
9747                        s.span(),
9748                        format!(
9749                            "`default_related_name` must be snake_case ASCII \
9750                             (lowercase letters / digits / underscores, not \
9751                             starting with a digit); got `{raw}`"
9752                        ),
9753                    ));
9754                }
9755                out.default_related_name = Some(raw);
9756                return Ok(());
9757            }
9758            if meta.path.is_ident("extra_permissions") {
9759                // Comma-separated `codename:label` pairs.
9760                let s: LitStr = meta.value()?.parse()?;
9761                let raw = s.value();
9762                let mut pairs = Vec::new();
9763                for entry in raw.split(',') {
9764                    let entry = entry.trim();
9765                    if entry.is_empty() {
9766                        continue;
9767                    }
9768                    let (codename, label) = match entry.split_once(':') {
9769                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9770                        None => (entry.to_owned(), entry.to_owned()),
9771                    };
9772                    if codename.is_empty() {
9773                        return Err(meta.error(
9774                            "`extra_permissions` entries must be `codename:label` pairs",
9775                        ));
9776                    }
9777                    pairs.push((codename, label));
9778                }
9779                if pairs.is_empty() {
9780                    return Err(meta
9781                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9782                }
9783                out.extra_permissions = pairs;
9784                return Ok(());
9785            }
9786            if meta.path.is_ident("default_permissions") {
9787                // Comma-separated subset of the CRUD action set.
9788                // Empty means all four (the framework default).
9789                let s: LitStr = meta.value()?.parse()?;
9790                let raw = s.value();
9791                let mut actions: Vec<String> = Vec::new();
9792                for entry in raw.split(',') {
9793                    let action = entry.trim().to_ascii_lowercase();
9794                    if action.is_empty() {
9795                        continue;
9796                    }
9797                    match action.as_str() {
9798                        "add" | "change" | "delete" | "view" => {}
9799                        other => {
9800                            return Err(syn::Error::new(
9801                                s.span(),
9802                                format!(
9803                                    "unknown default_permissions action `{other}` — \
9804                                     expected one of `add`, `change`, `delete`, `view`"
9805                                ),
9806                            ));
9807                        }
9808                    }
9809                    if !actions.contains(&action) {
9810                        actions.push(action);
9811                    }
9812                }
9813                if actions.is_empty() {
9814                    return Err(syn::Error::new(
9815                        s.span(),
9816                        "`default_permissions = \"…\"` must list at least one action; \
9817                         use `permissions = false` on the container if you want NO \
9818                         permissions seeded for this model.",
9819                    ));
9820                }
9821                out.default_permissions = actions;
9822                return Ok(());
9823            }
9824            if meta.path.is_ident("get_latest_by") {
9825                // The `-` prefix selects descending order.
9826                let s: LitStr = meta.value()?.parse()?;
9827                let raw = s.value();
9828                let trimmed = raw.trim();
9829                if trimmed.is_empty() {
9830                    return Err(meta.error("`get_latest_by` must name a column"));
9831                }
9832                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9833                    (stripped.to_owned(), true)
9834                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9835                    (stripped.to_owned(), false)
9836                } else {
9837                    (trimmed.to_owned(), false)
9838                };
9839                if col.is_empty() {
9840                    return Err(meta.error("`get_latest_by` must name a column"));
9841                }
9842                out.get_latest_by = Some((col, desc, s.span()));
9843                return Ok(());
9844            }
9845            if meta.path.is_ident("unique_together") {
9846                // Composite UNIQUE index. Two syntaxes:
9847                //
9848                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9849                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9850                //
9851                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9852                // (col1, col2)`, where <name> defaults to
9853                // `<table>_<col1>_<col2>_uq` when not supplied.
9854                let (columns, name) = parse_together_attr(&meta, "unique_together", written)?;
9855                out.indexes.push(IndexAttr {
9856                    span: meta.path.span(),
9857                    name,
9858                    columns,
9859                    unique: true,
9860                    method: "btree".to_owned(),
9861                    where_clause: None,
9862                include: Vec::new(),
9863                });
9864                return Ok(());
9865            }
9866            if meta.path.is_ident("index_together") {
9867                // Composite (non-unique) index. Two syntaxes
9868                // mirroring `unique_together`.
9869                //
9870                //   #[rustango(index_together = "created_at, status")]
9871                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9872                let (columns, name) = parse_together_attr(&meta, "index_together", written)?;
9873                out.indexes.push(IndexAttr {
9874                    span: meta.path.span(),
9875                    name,
9876                    columns,
9877                    unique: false,
9878                    method: "btree".to_owned(),
9879                    where_clause: None,
9880                include: Vec::new(),
9881                });
9882                return Ok(());
9883            }
9884            if meta.path.is_ident("unique_when") {
9885                // Partial unique index. Issue #265 / T1.3.
9886                //
9887                //   #[rustango(unique_when(
9888                //       columns   = "email",
9889                //       condition = "deleted_at IS NULL",
9890                //       name      = "unique_active_email"
9891                //   ))]
9892                //
9893                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9894                // on PG / SQLite (both ship partial indexes natively).
9895                // MySQL falls back to a plain UNIQUE index — the
9896                // condition is lost; document the limitation in the
9897                // generated migration.
9898                let mut columns: Option<Vec<String>> = None;
9899                let mut condition: Option<String> = None;
9900                let mut name: Option<String> = None;
9901                let mut include: Vec<String> = Vec::new();
9902                meta.parse_nested_meta(|inner| {
9903                    if inner.path.is_ident("columns") {
9904                        let s: LitStr = inner.value()?.parse()?;
9905                        columns = Some(split_field_list(&s.value()));
9906                        return Ok(());
9907                    }
9908                    if inner.path.is_ident("condition") {
9909                        let s: LitStr = inner.value()?.parse()?;
9910                        condition = Some(s.value());
9911                        return Ok(());
9912                    }
9913                    if inner.path.is_ident("name") {
9914                        let s: LitStr = inner.value()?.parse()?;
9915                        name = Some(s.value());
9916                        return Ok(());
9917                    }
9918                    if inner.path.is_ident("include") {
9919                        // PG 11+ covering-index columns. Non-key columns
9920                        // travel with the index leaf for index-only
9921                        // scans. Dropped on MySQL/SQLite by the writer.
9922                        let s: LitStr = inner.value()?.parse()?;
9923                        include = split_field_list(&s.value());
9924                        return Ok(());
9925                    }
9926                    Err(inner.error(
9927                        "unknown unique_when attribute (supported: \
9928                         `columns = \"...\"`, `condition = \"...\"`, \
9929                         `name = \"...\"`, `include = \"...\"`)",
9930                    ))
9931                })?;
9932                let columns = columns.ok_or_else(|| {
9933                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9934                })?;
9935                let condition = condition.ok_or_else(|| {
9936                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9937                })?;
9938                if columns.is_empty() {
9939                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9940                }
9941                out.indexes.push(IndexAttr {
9942                    span: meta.path.span(),
9943                    name,
9944                    columns,
9945                    unique: true,
9946                    method: "btree".to_owned(),
9947                    where_clause: Some(condition),
9948                    include,
9949                });
9950                return Ok(());
9951            }
9952            if meta.path.is_ident("index_when") {
9953                // Non-unique partial index. Sibling of `unique_when`
9954                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9955                //
9956                //   #[rustango(index_when(
9957                //       columns   = "status, created_at",
9958                //       condition = "deleted_at IS NULL",
9959                //       name      = "active_status_created_idx"
9960                //   ))]
9961                //
9962                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9963                // on PG / SQLite (both ship partial indexes natively).
9964                // MySQL has no native partial-index support — the writer
9965                // emits a plain CREATE INDEX and the condition is lost;
9966                // operators wanting that selectivity on MySQL should
9967                // declare a covering index plus an application-level
9968                // filter.
9969                let mut columns: Option<Vec<String>> = None;
9970                let mut condition: Option<String> = None;
9971                let mut name: Option<String> = None;
9972                let mut method: String = "btree".to_owned();
9973                let mut include: Vec<String> = Vec::new();
9974                meta.parse_nested_meta(|inner| {
9975                    if inner.path.is_ident("columns") {
9976                        let s: LitStr = inner.value()?.parse()?;
9977                        columns = Some(split_field_list(&s.value()));
9978                        return Ok(());
9979                    }
9980                    if inner.path.is_ident("condition") {
9981                        let s: LitStr = inner.value()?.parse()?;
9982                        condition = Some(s.value());
9983                        return Ok(());
9984                    }
9985                    if inner.path.is_ident("name") {
9986                        let s: LitStr = inner.value()?.parse()?;
9987                        name = Some(s.value());
9988                        return Ok(());
9989                    }
9990                    if inner.path.is_ident("method") {
9991                        let s: LitStr = inner.value()?.parse()?;
9992                        method = s.value();
9993                        return Ok(());
9994                    }
9995                    if inner.path.is_ident("include") {
9996                        // PG 11+ covering-index columns; non-key
9997                        // columns travel with the index leaf. Dropped
9998                        // on MySQL/SQLite.
9999                        let s: LitStr = inner.value()?.parse()?;
10000                        include = split_field_list(&s.value());
10001                        return Ok(());
10002                    }
10003                    Err(inner.error(
10004                        "unknown index_when attribute (supported: \
10005                         `columns = \"...\"`, `condition = \"...\"`, \
10006                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
10007                         `include = \"...\"`)",
10008                    ))
10009                })?;
10010                let columns = columns
10011                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
10012                let condition = condition.ok_or_else(|| {
10013                    meta.error("`index_when(...)` requires `condition = \"...\"`")
10014                })?;
10015                if columns.is_empty() {
10016                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
10017                }
10018                out.indexes.push(IndexAttr {
10019                    span: meta.path.span(),
10020                    name,
10021                    columns,
10022                    unique: false,
10023                    method,
10024                    where_clause: Some(condition),
10025                    include,
10026                });
10027                return Ok(());
10028            }
10029            if meta.path.is_ident("index") {
10030                // Container-level composite index. Two syntaxes:
10031                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
10032                //   #[rustango(index("col1, col2"))]    — call form (same result)
10033                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
10034                // The call form takes the column list as a leading string
10035                // literal, then optional `unique` / `name = "..."` /
10036                // `method = "..."` flags (a leading literal can't compose
10037                // under `parse_nested_meta`, so the paren body is parsed by
10038                // hand). `unique_together` / `index_together` remain
10039                // aliases for the same feature.
10040                let cols_lit: LitStr;
10041                let mut unique = false;
10042                let mut name: Option<String> = None;
10043                let mut method = "btree".to_owned();
10044                if meta.input.peek(syn::token::Paren) {
10045                    let content;
10046                    syn::parenthesized!(content in meta.input);
10047                    cols_lit = content.parse()?;
10048                    while content.peek(syn::Token![,]) {
10049                        content.parse::<syn::Token![,]>()?;
10050                        if content.is_empty() {
10051                            break;
10052                        }
10053                        let flag: syn::Ident = content.parse()?;
10054                        if flag == "unique" {
10055                            unique = true;
10056                        } else if flag == "name" {
10057                            content.parse::<syn::Token![=]>()?;
10058                            let s: LitStr = content.parse()?;
10059                            name = Some(s.value());
10060                        } else if flag == "method" {
10061                            content.parse::<syn::Token![=]>()?;
10062                            let s: LitStr = content.parse()?;
10063                            let v = s.value();
10064                            match v.as_str() {
10065                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10066                                | "bloom" => method = v,
10067                                other => {
10068                                    return Err(syn::Error::new(
10069                                        s.span(),
10070                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10071                                    ));
10072                                }
10073                            }
10074                        } else {
10075                            return Err(syn::Error::new(
10076                                flag.span(),
10077                                "unknown index flag (supported: `unique`, `name`, `method`)",
10078                            ));
10079                        }
10080                    }
10081                } else {
10082                    cols_lit = meta.value()?.parse()?;
10083                }
10084                let columns = split_field_list(&cols_lit.value());
10085                out.indexes.push(IndexAttr {
10086                    span: meta.path.span(),
10087                    name,
10088                    columns,
10089                    unique,
10090                    method,
10091                    where_clause: None,
10092                    include: Vec::new(),
10093                });
10094                return Ok(());
10095            }
10096            if meta.path.is_ident("check") {
10097                // #[rustango(check(name = "…", expr = "…"))]
10098                let mut name: Option<String> = None;
10099                let mut expr: Option<String> = None;
10100                meta.parse_nested_meta(|inner| {
10101                    if inner.path.is_ident("name") {
10102                        let s: LitStr = inner.value()?.parse()?;
10103                        name = Some(s.value());
10104                        return Ok(());
10105                    }
10106                    if inner.path.is_ident("expr") {
10107                        let s: LitStr = inner.value()?.parse()?;
10108                        expr = Some(s.value());
10109                        return Ok(());
10110                    }
10111                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10112                })?;
10113                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10114                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10115                out.checks.push(CheckAttr { name, expr });
10116                return Ok(());
10117            }
10118            if meta.path.is_ident("exclude") {
10119                // #[rustango(exclude(name = "…", using = "gist",
10120                //                    elements = "col WITH op, col WITH op",
10121                //                    where = "…"))]
10122                let mut name: Option<String> = None;
10123                let mut using: Option<String> = None;
10124                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10125                let mut where_clause: Option<String> = None;
10126                meta.parse_nested_meta(|inner| {
10127                    if inner.path.is_ident("name") {
10128                        let s: LitStr = inner.value()?.parse()?;
10129                        name = Some(s.value());
10130                        return Ok(());
10131                    }
10132                    if inner.path.is_ident("using") {
10133                        let s: LitStr = inner.value()?.parse()?;
10134                        using = Some(s.value());
10135                        return Ok(());
10136                    }
10137                    if inner.path.is_ident("elements") {
10138                        let s: LitStr = inner.value()?.parse()?;
10139                        elements_raw = Some((s.value(), s.span()));
10140                        return Ok(());
10141                    }
10142                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10143                        let s: LitStr = inner.value()?.parse()?;
10144                        where_clause = Some(s.value());
10145                        return Ok(());
10146                    }
10147                    Err(inner.error(
10148                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10149                    ))
10150                })?;
10151                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10152                let using = using.unwrap_or_else(|| "gist".to_owned());
10153                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10154                    meta.error(
10155                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
10156                    )
10157                })?;
10158                // Parse `col WITH op` pairs separated by commas.
10159                let mut elements: Vec<(String, String)> = Vec::new();
10160                for pair in elements_str.split(',') {
10161                    let pair = pair.trim();
10162                    if pair.is_empty() {
10163                        continue;
10164                    }
10165                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10166                    let col = split.next().unwrap_or("").trim();
10167                    let rest = split.next().unwrap_or("").trim();
10168                    // `WITH op` — case-insensitive on `WITH`, then op.
10169                    let rest_lc = rest.to_ascii_lowercase();
10170                    let op = rest_lc
10171                        .strip_prefix("with")
10172                        .map(|r| r.trim_start())
10173                        .filter(|r| !r.is_empty())
10174                        .map(|_| {
10175                            // Pull the original-case op from `rest` after the
10176                            // `WITH ` token (5 chars).
10177                            rest[4..].trim_start().to_owned()
10178                        });
10179                    let Some(op) = op else {
10180                        return Err(syn::Error::new(
10181                            elements_span,
10182                            format!(
10183                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10184                                 (e.g. `room_id WITH =` or `during WITH &&`)"
10185                            ),
10186                        ));
10187                    };
10188                    if col.is_empty() || op.is_empty() {
10189                        return Err(syn::Error::new(
10190                            elements_span,
10191                            format!(
10192                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10193                                 (both sides non-empty)"
10194                            ),
10195                        ));
10196                    }
10197                    elements.push((col.to_owned(), op));
10198                }
10199                if elements.is_empty() {
10200                    return Err(syn::Error::new(
10201                        elements_span,
10202                        "exclude requires at least one `col WITH op` element",
10203                    ));
10204                }
10205                out.excludes.push(ExcludeAttr {
10206                    name,
10207                    using,
10208                    elements,
10209                    where_clause,
10210                });
10211                return Ok(());
10212            }
10213            if meta.path.is_ident("generic_fk") {
10214                let mut gfk = GenericFkAttr {
10215                    name: String::new(),
10216                    ct_column: String::new(),
10217                    pk_column: String::new(),
10218                };
10219                meta.parse_nested_meta(|inner| {
10220                    if inner.path.is_ident("name") {
10221                        let s: LitStr = inner.value()?.parse()?;
10222                        gfk.name = s.value();
10223                        return Ok(());
10224                    }
10225                    if inner.path.is_ident("ct_column") {
10226                        let s: LitStr = inner.value()?.parse()?;
10227                        gfk.ct_column = s.value();
10228                        return Ok(());
10229                    }
10230                    if inner.path.is_ident("pk_column") {
10231                        let s: LitStr = inner.value()?.parse()?;
10232                        gfk.pk_column = s.value();
10233                        return Ok(());
10234                    }
10235                    Err(inner.error(
10236                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10237                    ))
10238                })?;
10239                if gfk.name.is_empty() {
10240                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10241                }
10242                if gfk.ct_column.is_empty() {
10243                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10244                }
10245                if gfk.pk_column.is_empty() {
10246                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10247                }
10248                out.generic_fks.push(gfk);
10249                return Ok(());
10250            }
10251            if meta.path.is_ident("fk_composite") {
10252                let mut fk = CompositeFkAttr {
10253                    name: String::new(),
10254                    to: String::new(),
10255                    from: Vec::new(),
10256                    on: Vec::new(),
10257                };
10258                meta.parse_nested_meta(|inner| {
10259                    if inner.path.is_ident("name") {
10260                        let s: LitStr = inner.value()?.parse()?;
10261                        fk.name = s.value();
10262                        return Ok(());
10263                    }
10264                    if inner.path.is_ident("to") {
10265                        let s: LitStr = inner.value()?.parse()?;
10266                        fk.to = s.value();
10267                        return Ok(());
10268                    }
10269                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10270                    // comma-list of string literals.
10271                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10272                        let value = inner.value()?;
10273                        let content;
10274                        syn::parenthesized!(content in value);
10275                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10276                            content.parse_terminated(
10277                                |p| p.parse::<syn::LitStr>(),
10278                                syn::Token![,],
10279                            )?;
10280                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10281                        if inner.path.is_ident("on") {
10282                            fk.on = cols;
10283                        } else {
10284                            fk.from = cols;
10285                        }
10286                        return Ok(());
10287                    }
10288                    Err(inner.error(
10289                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10290                    ))
10291                })?;
10292                if fk.name.is_empty() {
10293                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10294                }
10295                if fk.to.is_empty() {
10296                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10297                }
10298                if fk.from.is_empty() || fk.on.is_empty() {
10299                    return Err(meta.error(
10300                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10301                    ));
10302                }
10303                if fk.from.len() != fk.on.len() {
10304                    return Err(meta.error(format!(
10305                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10306                        fk.from.len(),
10307                        fk.on.len(),
10308                    )));
10309                }
10310                out.composite_fks.push(fk);
10311                return Ok(());
10312            }
10313            if meta.path.is_ident("m2m") {
10314                let mut m2m = M2MAttr {
10315                    name: String::new(),
10316                    to: String::new(),
10317                    through: String::new(),
10318                    src: String::new(),
10319                    dst: String::new(),
10320                    auto_create: true,
10321                };
10322                meta.parse_nested_meta(|inner| {
10323                    if inner.path.is_ident("name") {
10324                        let s: LitStr = inner.value()?.parse()?;
10325                        m2m.name = s.value();
10326                        return Ok(());
10327                    }
10328                    if inner.path.is_ident("to") {
10329                        let s: LitStr = inner.value()?.parse()?;
10330                        m2m.to = s.value();
10331                        return Ok(());
10332                    }
10333                    if inner.path.is_ident("through") {
10334                        let s: LitStr = inner.value()?.parse()?;
10335                        m2m.through = s.value();
10336                        return Ok(());
10337                    }
10338                    if inner.path.is_ident("src") {
10339                        let s: LitStr = inner.value()?.parse()?;
10340                        m2m.src = s.value();
10341                        return Ok(());
10342                    }
10343                    if inner.path.is_ident("dst") {
10344                        let s: LitStr = inner.value()?.parse()?;
10345                        m2m.dst = s.value();
10346                        return Ok(());
10347                    }
10348                    if inner.path.is_ident("auto_create") {
10349                        let lit: syn::LitBool = inner.value()?.parse()?;
10350                        m2m.auto_create = lit.value;
10351                        return Ok(());
10352                    }
10353                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10354                })?;
10355                if m2m.name.is_empty() {
10356                    return Err(meta.error("m2m requires `name = \"...\"`"));
10357                }
10358                if m2m.to.is_empty() {
10359                    return Err(meta.error("m2m requires `to = \"...\"`"));
10360                }
10361                if m2m.through.is_empty() {
10362                    return Err(meta.error("m2m requires `through = \"...\"`"));
10363                }
10364                if m2m.src.is_empty() {
10365                    return Err(meta.error("m2m requires `src = \"...\"`"));
10366                }
10367                if m2m.dst.is_empty() {
10368                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10369                }
10370                out.m2m.push(m2m);
10371                return Ok(());
10372            }
10373            if meta.path.is_ident("generic_m2m") {
10374                let mut gm = GenericM2MAttr {
10375                    name: String::new(),
10376                    through: String::new(),
10377                    pk_column: String::new(),
10378                    ct_column: String::new(),
10379                    related_column: String::new(),
10380                };
10381                meta.parse_nested_meta(|inner| {
10382                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10383                        let s: LitStr = inner.value()?.parse()?;
10384                        Ok(s.value())
10385                    };
10386                    if inner.path.is_ident("name") {
10387                        gm.name = field(&inner)?;
10388                        return Ok(());
10389                    }
10390                    if inner.path.is_ident("through") {
10391                        gm.through = field(&inner)?;
10392                        return Ok(());
10393                    }
10394                    if inner.path.is_ident("pk_column") {
10395                        gm.pk_column = field(&inner)?;
10396                        return Ok(());
10397                    }
10398                    if inner.path.is_ident("ct_column") {
10399                        gm.ct_column = field(&inner)?;
10400                        return Ok(());
10401                    }
10402                    if inner.path.is_ident("related_column") {
10403                        gm.related_column = field(&inner)?;
10404                        return Ok(());
10405                    }
10406                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10407                })?;
10408                for (val, label) in [
10409                    (&gm.name, "name"),
10410                    (&gm.through, "through"),
10411                    (&gm.pk_column, "pk_column"),
10412                    (&gm.ct_column, "ct_column"),
10413                    (&gm.related_column, "related_column"),
10414                ] {
10415                    if val.is_empty() {
10416                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10417                    }
10418                }
10419                out.generic_m2m.push(gm);
10420                return Ok(());
10421            }
10422            Err(unknown_key_error(&meta, "container", CONTAINER_ATTR_KEYS))
10423        })?;
10424    }
10425    Ok(out)
10426}
10427
10428/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10429/// owned field names, trimming whitespace and skipping empty entries.
10430/// Field-name validation against the model is done by the caller.
10431fn split_field_list(raw: &str) -> Vec<String> {
10432    raw.split(',')
10433        .map(str::trim)
10434        .filter(|s| !s.is_empty())
10435        .map(str::to_owned)
10436        .collect()
10437}
10438
10439/// Shared parser for `unique_together` and `index_together` container
10440/// attrs. Accepts both shapes:
10441///
10442///   * `attr = "col1, col2"`              — auto-derived index name.
10443///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10444///
10445/// Returns `(columns, name)`.
10446fn parse_together_attr(
10447    meta: &syn::meta::ParseNestedMeta<'_>,
10448    attr: &str,
10449    written: &str,
10450) -> syn::Result<(Vec<String>, Option<String>)> {
10451    // Disambiguate by whether the next token is `=` (key-value) or
10452    // `(` (parenthesized).
10453    if meta.input.peek(syn::Token![=]) {
10454        let cols_lit: LitStr = meta.value()?.parse()?;
10455        let columns = split_field_list(&cols_lit.value());
10456        check_together_columns(meta, attr, written, &columns)?;
10457        return Ok((columns, None));
10458    }
10459    let mut columns: Option<Vec<String>> = None;
10460    let mut name: Option<String> = None;
10461    meta.parse_nested_meta(|inner| {
10462        if inner.path.is_ident("columns") {
10463            let s: LitStr = inner.value()?.parse()?;
10464            columns = Some(split_field_list(&s.value()));
10465            return Ok(());
10466        }
10467        if inner.path.is_ident("name") {
10468            let s: LitStr = inner.value()?.parse()?;
10469            name = Some(s.value());
10470            return Ok(());
10471        }
10472        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10473    })?;
10474    let columns = columns.ok_or_else(|| {
10475        meta.error(format!(
10476            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10477        ))
10478    })?;
10479    check_together_columns(meta, attr, written, &columns)?;
10480    Ok((columns, name))
10481}
10482
10483fn check_together_columns(
10484    meta: &syn::meta::ParseNestedMeta<'_>,
10485    attr: &str,
10486    written: &str,
10487    columns: &[String],
10488) -> syn::Result<()> {
10489    if columns.len() < 2 {
10490        let flag = if attr == "unique_together" {
10491            "unique"
10492        } else {
10493            "index"
10494        };
10495        let single = format!("#[{written}({flag})] on the field");
10496        return Err(meta.error(format!(
10497            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10498        )));
10499    }
10500    Ok(())
10501}
10502
10503/// Parse the fieldsets DSL: pipe-separated sections, optional
10504/// `"Title:"` prefix on each, comma-separated field names after.
10505/// Examples:
10506/// * `"name, office"` → one untitled section with two fields
10507/// * `"Identity: name, office | Metadata: created_at"` → two titled
10508///   sections
10509///
10510/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10511fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10512    raw.split('|')
10513        .map(str::trim)
10514        .filter(|s| !s.is_empty())
10515        .map(|section| {
10516            // Split off an optional `Title:` prefix (first colon).
10517            let (title, rest) = match section.split_once(':') {
10518                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10519                _ => (String::new(), section),
10520            };
10521            let fields = split_field_list(rest);
10522            (title, fields)
10523        })
10524        .collect()
10525}
10526
10527/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10528/// comma-separated entry maps a target field to one or more source
10529/// fields joined with `+`. Whitespace around tokens is trimmed.
10530/// Entries missing `:` or with empty target/source lists are dropped.
10531fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10532    raw.split(',')
10533        .map(str::trim)
10534        .filter(|s| !s.is_empty())
10535        .filter_map(|entry| {
10536            let (target, sources_raw) = entry.split_once(':')?;
10537            let target = target.trim().to_owned();
10538            if target.is_empty() {
10539                return None;
10540            }
10541            let sources: Vec<String> = sources_raw
10542                .split('+')
10543                .map(|s| s.trim().to_owned())
10544                .filter(|s| !s.is_empty())
10545                .collect();
10546            if sources.is_empty() {
10547                return None;
10548            }
10549            Some((target, sources))
10550        })
10551        .collect()
10552}
10553
10554/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10555/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10556/// and empty halves drop silently — the macro layer only enforces shape,
10557/// not field-name vs. widget-name validity (those checks happen at
10558/// `AdminConfig` consumption time). Issue #359.
10559fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10560    raw.split(',')
10561        .map(str::trim)
10562        .filter(|s| !s.is_empty())
10563        .filter_map(|entry| {
10564            let (field, widget) = entry.split_once(':')?;
10565            let field = field.trim().to_owned();
10566            let widget = widget.trim().to_owned();
10567            if field.is_empty() || widget.is_empty() {
10568                return None;
10569            }
10570            Some((field, widget))
10571        })
10572        .collect()
10573}
10574
10575/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10576/// Returns `(field_name, desc)` pairs in the same order as the input.
10577fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10578    raw.split(',')
10579        .map(str::trim)
10580        .filter(|s| !s.is_empty())
10581        .map(|spec| {
10582            spec.strip_prefix('-')
10583                .map_or((spec.to_owned(), false), |rest| {
10584                    (rest.trim().to_owned(), true)
10585                })
10586        })
10587        .collect()
10588}
10589
10590struct FieldAttrs {
10591    column: Option<String>,
10592    primary_key: bool,
10593    fk: Option<String>,
10594    o2o: Option<String>,
10595    on: Option<String>,
10596    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10597    /// "set_default" | "no_action")]` — what happens to this row when
10598    /// the row it points at is deleted.
10599    /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10600    /// set; the macro errors at compile time if applied to a
10601    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10602    /// DDL writer renders `ON DELETE <action>` after the constraint
10603    /// clause when this is `Some`; `None` falls back to the database
10604    /// default (NO ACTION on every backend rustango supports).
10605    on_delete: Option<String>,
10606    /// `#[rustango(related_name = "...")]` — per-FK
10607    /// reverse-accessor override. When set, the derive emits
10608    /// `Parent::<related_name>[_pool]` instead of the container-level
10609    /// `default_related_name` or the `<child_snake>_set[_pool]`
10610    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10611    /// silently ignored on non-FK fields. Follow-up to #816.
10612    /// `"+"` emits none, for a parent defined in another crate.
10613    related_name: Option<String>,
10614    max_length: Option<u32>,
10615    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10616    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10617    /// unconstrained `vector` column.
10618    vector_dims: Option<u32>,
10619    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10620    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10621    /// unconstrained `geometry(Point)` column (SRID 0).
10622    geometry_srid: Option<u32>,
10623    min: Option<i64>,
10624    max: Option<i64>,
10625    default: Option<String>,
10626    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10627    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10628    /// "gen_random_uuid()"`. The Rust field type must be
10629    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10630    /// INSERTs so the DB DEFAULT fires.
10631    auto_uuid: bool,
10632    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10633    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10634    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10635    /// the field is `Auto::Unset`, then bound as a normal parameter
10636    /// rather than relying on a per-dialect DB function. Issue #823
10637    /// (Eloquent `HasUuids`).
10638    ///
10639    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10640    /// Composes with every backend (PG / MySQL / SQLite) — no
10641    /// `gen_random_uuid()` requirement on the database.
10642    default_uuid_v7: bool,
10643    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10644    /// Server-set on insert, immutable from app code afterwards.
10645    /// Implies `auto + default = "now()"`. Field type must be
10646    /// `DateTime<Utc>`.
10647    auto_now_add: bool,
10648    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10649    /// every insert AND every update. Implies `auto + default =
10650    /// "now()"`; the macro additionally rewrites `update_on` /
10651    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10652    /// field value.
10653    auto_now: bool,
10654    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10655    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10656    /// `soft_delete_on(executor)` and `restore_on(executor)`
10657    /// methods on the model.
10658    soft_delete: bool,
10659    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10660    /// the column in the generated DDL.
10661    unique: bool,
10662    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10663    /// generates a `CREATE INDEX` for this column. `unique` here means
10664    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10665    index: bool,
10666    index_unique: bool,
10667    index_name: Option<String>,
10668    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10669    /// `"btree"`. Issue #34.
10670    index_method: String,
10671    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10672    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10673    /// the macro skips this column from every INSERT and UPDATE
10674    /// path, so the database always recomputes the value from
10675    /// `EXPR`. Backlog item #35.
10676    generated_as: Option<String>,
10677    /// `#[rustango(help_text = "…")]` — help text
10678    /// rendered below the admin form's input. Threaded into
10679    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10680    /// layers can read it.
10681    help_text: Option<String>,
10682    /// `#[rustango(choices = "value:Label, value:Label")]` — the
10683    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10684    /// as a `&'static [(&'static str, &'static str)]` slice. When
10685    /// present, the admin form renders a `<select>` instead of `<input>`
10686    /// and the validator rejects values not in the list. Only meaningful
10687    /// for `FieldType::String`; the macro errors at compile time if
10688    /// applied to a non-string field.
10689    choices: Option<Vec<(String, String)>>,
10690    /// `#[rustango(db_comment = "…")]` — DB-side column
10691    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10692    /// the comment in CREATE TABLE; Postgres emits a separate
10693    /// `COMMENT ON COLUMN` statement after the table is created;
10694    /// SQLite silently drops the value (no native column comments).
10695    db_comment: Option<String>,
10696    /// `#[rustango(verbose_name = "…")]` — human-readable
10697    /// label for the field. Threaded into `FieldSchema::verbose_name`
10698    /// so admin column headers, form labels, and other display
10699    /// surfaces can prefer the friendly caption over the Rust
10700    /// identifier. `None` means renderers fall back to the field name.
10701    verbose_name: Option<String>,
10702    /// `#[rustango(editable = false)]` — opt out of
10703    /// auto-generated form rendering. Defaults to `true` so existing
10704    /// fields keep their current admin / form behavior; setting
10705    /// `false` removes the field from the admin change-form entirely
10706    /// (the value is still visible on detail / list views, just not
10707    /// editable).
10708    editable: bool,
10709    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10710    /// "form may submit empty even when DB is NOT NULL". Threaded into
10711    /// `FieldSchema::blank`. Defaults to `false`.
10712    blank: bool,
10713    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10714    /// case-insensitive text column. Threaded into
10715    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10716    /// fields; the macro errors at derive time if applied elsewhere.
10717    case_insensitive: bool,
10718    /// `#[rustango(validators = "email,url")]` — the
10719    /// model-level validator chain. Comma-separated names that
10720    /// dispatch to the `validators::*` family in `validate_value`.
10721    /// Empty by default; fires on every typed INSERT/UPDATE. Each is
10722    /// spanned on the attribute string, so an unknown name errors there.
10723    validators: Vec<syn::Ident>,
10724    /// `#[rustango(secret)]` / `secret = false`; `None` defers to the name rules.
10725    secret: Option<bool>,
10726}
10727
10728/// Keys `parse_field_attrs` accepts, listed in its unknown-key error.
10729const FIELD_ATTR_KEYS: &[&str] = &[
10730    "column",
10731    "primary_key",
10732    "fk",
10733    "o2o",
10734    "on",
10735    "on_delete",
10736    "related_name",
10737    "max_length",
10738    "vector",
10739    "geometry",
10740    "min",
10741    "max",
10742    "default",
10743    "generated_as",
10744    "help_text",
10745    "choices",
10746    "db_comment",
10747    "verbose_name",
10748    "editable",
10749    "secret",
10750    "blank",
10751    "citext",
10752    "validators",
10753    "auto_uuid",
10754    "default_uuid_v7",
10755    "auto_now_add",
10756    "auto_now",
10757    "soft_delete",
10758    "unique",
10759    "index",
10760];
10761
10762/// Keys `parse_container_attrs` accepts, listed in its unknown-key error.
10763const CONTAINER_ATTR_KEYS: &[&str] = &[
10764    "table",
10765    "display",
10766    "app",
10767    "scope",
10768    "access",
10769    "admin",
10770    "manager",
10771    "manager_fn",
10772    "default_order",
10773    "global_scope",
10774    "through",
10775    "reverse_has",
10776    "generic_has",
10777    "audit",
10778    "permissions",
10779    "view",
10780    "managed",
10781    "verbose_name",
10782    "verbose_name_plural",
10783    "db_table_comment",
10784    "proxy",
10785    "order_with_respect_to",
10786    "required_db_features",
10787    "required_db_vendor",
10788    "base_manager_name",
10789    "default_related_name",
10790    "extra_permissions",
10791    "default_permissions",
10792    "get_latest_by",
10793    "unique_together",
10794    "index_together",
10795    "unique_when",
10796    "index_when",
10797    "index",
10798    "check",
10799    "exclude",
10800    "generic_fk",
10801    "fk_composite",
10802    "m2m",
10803    "generic_m2m",
10804];
10805
10806/// A flag attribute: `#[rustango(key)]` or `#[rustango(key = true)]`.
10807/// `= false` is refused: off is the default, so the attribute is a no-op.
10808fn parse_flag(meta: &syn::meta::ParseNestedMeta<'_>, key: &str) -> syn::Result<()> {
10809    if meta.input.peek(syn::Token![=]) {
10810        let lit: syn::LitBool = meta.value()?.parse()?;
10811        if !lit.value {
10812            return Err(syn::Error::new(
10813                lit.span(),
10814                format!("`{key} = false` is the default; remove the attribute"),
10815            ));
10816        }
10817    }
10818    Ok(())
10819}
10820
10821/// "unknown rustango <what> attribute `key`", with the nearest key and the full list.
10822fn unknown_key_error(
10823    meta: &syn::meta::ParseNestedMeta<'_>,
10824    what: &str,
10825    keys: &[&str],
10826) -> syn::Error {
10827    let path = &meta.path;
10828    let got = path
10829        .get_ident()
10830        .map_or_else(|| quote!(#path).to_string(), ToString::to_string);
10831    let mut msg = format!("unknown rustango {what} attribute `{got}`");
10832    if let Some(near) = keys
10833        .iter()
10834        .map(|k| (edit_distance(&got, k), *k))
10835        .filter(|(d, k)| *d <= 2.max(k.len() / 4))
10836        .min_by_key(|(d, _)| *d)
10837        .map(|(_, k)| k)
10838    {
10839        msg.push_str(&format!("; did you mean `{near}`?"));
10840    }
10841    msg.push_str(&format!(" Supported: {}", keys.join(", ")));
10842    meta.error(msg)
10843}
10844
10845/// Levenshtein distance, for "did you mean" hints.
10846fn edit_distance(a: &str, b: &str) -> usize {
10847    let b: Vec<char> = b.chars().collect();
10848    let mut prev: Vec<usize> = (0..=b.len()).collect();
10849    for (i, ca) in a.chars().enumerate() {
10850        let mut cur = vec![i + 1];
10851        for (j, cb) in b.iter().enumerate() {
10852            let sub = prev[j] + usize::from(ca != *cb);
10853            cur.push(sub.min(prev[j + 1] + 1).min(cur[j] + 1));
10854        }
10855        prev = cur;
10856    }
10857    prev[b.len()]
10858}
10859
10860fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10861    let mut out = FieldAttrs {
10862        column: None,
10863        primary_key: false,
10864        fk: None,
10865        o2o: None,
10866        on: None,
10867        on_delete: None,
10868        related_name: None,
10869        max_length: None,
10870        vector_dims: None,
10871        geometry_srid: None,
10872        min: None,
10873        max: None,
10874        default: None,
10875        auto_uuid: false,
10876        default_uuid_v7: false,
10877        auto_now_add: false,
10878        auto_now: false,
10879        soft_delete: false,
10880        unique: false,
10881        index: false,
10882        index_unique: false,
10883        index_name: None,
10884        index_method: "btree".to_owned(),
10885        generated_as: None,
10886        help_text: None,
10887        choices: None,
10888        db_comment: None,
10889        verbose_name: None,
10890        editable: true,
10891        blank: false,
10892        case_insensitive: false,
10893        validators: Vec::new(),
10894        secret: None,
10895    };
10896    for attr in model_attrs(&field.attrs)? {
10897        attr.parse_nested_meta(|meta| {
10898            if meta.path.is_ident("column") {
10899                let s: LitStr = meta.value()?.parse()?;
10900                let name = s.value();
10901                validate_sql_identifier(&name, "column", s.span())?;
10902                out.column = Some(name);
10903                return Ok(());
10904            }
10905            if meta.path.is_ident("primary_key") {
10906                parse_flag(&meta, "primary_key")?;
10907                out.primary_key = true;
10908                return Ok(());
10909            }
10910            if meta.path.is_ident("fk") {
10911                let s: LitStr = meta.value()?.parse()?;
10912                out.fk = Some(s.value());
10913                return Ok(());
10914            }
10915            if meta.path.is_ident("o2o") {
10916                let s: LitStr = meta.value()?.parse()?;
10917                out.o2o = Some(s.value());
10918                return Ok(());
10919            }
10920            if meta.path.is_ident("on") {
10921                let s: LitStr = meta.value()?.parse()?;
10922                out.on = Some(s.value());
10923                return Ok(());
10924            }
10925            if meta.path.is_ident("on_delete") {
10926                let s: LitStr = meta.value()?.parse()?;
10927                let raw = s.value();
10928                let normalized = raw.trim().to_ascii_lowercase();
10929                // Validate at parse time so the user gets a clear span
10930                // rather than a downstream compile error in the emit.
10931                match normalized.as_str() {
10932                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10933                    _ => {
10934                        return Err(syn::Error::new(
10935                            s.span(),
10936                            format!(
10937                                "unknown on_delete action `{raw}`; expected one of \
10938                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10939                            ),
10940                        ));
10941                    }
10942                }
10943                out.on_delete = Some(normalized);
10944                return Ok(());
10945            }
10946            if meta.path.is_ident("related_name") {
10947                let s: LitStr = meta.value()?.parse()?;
10948                let raw = s.value();
10949                if raw.trim().is_empty() {
10950                    return Err(syn::Error::new(
10951                        s.span(),
10952                        "`related_name` must be a non-empty identifier",
10953                    ));
10954                }
10955                // Validate as a Rust-ident shape — the value becomes
10956                // a method name on the parent type. Same rule as
10957                // `default_related_name` so the two surfaces match.
10958                // `"+"` emits no accessor at all.
10959                let snake = raw
10960                    .chars()
10961                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10962                    && !raw.starts_with(char::is_numeric);
10963                if !snake && raw != NO_REVERSE_ACCESSOR {
10964                    return Err(syn::Error::new(
10965                        s.span(),
10966                        "`related_name` must be snake_case ASCII (lowercase letters, \
10967                         digits, underscores; no leading digit), or \"+\" for none",
10968                    ));
10969                }
10970                out.related_name = Some(raw);
10971                return Ok(());
10972            }
10973            if meta.path.is_ident("max_length") {
10974                let lit: syn::LitInt = meta.value()?.parse()?;
10975                out.max_length = Some(lit.base10_parse::<u32>()?);
10976                return Ok(());
10977            }
10978            // `#[rustango(vector(dims = N))]` — pgvector column
10979            // dimension (#824). Nested-meta form so it reads like the
10980            // other typed-column attrs.
10981            if meta.path.is_ident("vector") {
10982                meta.parse_nested_meta(|inner| {
10983                    if inner.path.is_ident("dims") {
10984                        let lit: syn::LitInt = inner.value()?.parse()?;
10985                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10986                        return Ok(());
10987                    }
10988                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10989                })?;
10990                return Ok(());
10991            }
10992            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10993            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10994            if meta.path.is_ident("geometry") {
10995                meta.parse_nested_meta(|inner| {
10996                    if inner.path.is_ident("srid") {
10997                        let lit: syn::LitInt = inner.value()?.parse()?;
10998                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10999                        return Ok(());
11000                    }
11001                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
11002                })?;
11003                return Ok(());
11004            }
11005            if meta.path.is_ident("min") {
11006                out.min = Some(parse_signed_i64(&meta)?);
11007                return Ok(());
11008            }
11009            if meta.path.is_ident("max") {
11010                out.max = Some(parse_signed_i64(&meta)?);
11011                return Ok(());
11012            }
11013            if meta.path.is_ident("default") {
11014                let s: LitStr = meta.value()?.parse()?;
11015                out.default = Some(s.value());
11016                return Ok(());
11017            }
11018            if meta.path.is_ident("generated_as") {
11019                let s: LitStr = meta.value()?.parse()?;
11020                out.generated_as = Some(s.value());
11021                return Ok(());
11022            }
11023            if meta.path.is_ident("help_text") {
11024                let s: LitStr = meta.value()?.parse()?;
11025                out.help_text = Some(s.value());
11026                return Ok(());
11027            }
11028            if meta.path.is_ident("choices") {
11029                let s: LitStr = meta.value()?.parse()?;
11030                let raw = s.value();
11031                let mut pairs: Vec<(String, String)> = Vec::new();
11032                for chunk in raw.split(',') {
11033                    let chunk = chunk.trim();
11034                    if chunk.is_empty() {
11035                        continue;
11036                    }
11037                    let (value, label) = match chunk.split_once(':') {
11038                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
11039                        None => (chunk.to_owned(), chunk.to_owned()),
11040                    };
11041                    if value.is_empty() {
11042                        return Err(syn::Error::new(
11043                            s.span(),
11044                            "`choices` entry has empty value before `:`",
11045                        ));
11046                    }
11047                    pairs.push((value, label));
11048                }
11049                if pairs.is_empty() {
11050                    return Err(syn::Error::new(
11051                        s.span(),
11052                        "`choices = \"…\"` must contain at least one value",
11053                    ));
11054                }
11055                out.choices = Some(pairs);
11056                return Ok(());
11057            }
11058            if meta.path.is_ident("db_comment") {
11059                let s: LitStr = meta.value()?.parse()?;
11060                out.db_comment = Some(s.value());
11061                return Ok(());
11062            }
11063            if meta.path.is_ident("verbose_name") {
11064                let s: LitStr = meta.value()?.parse()?;
11065                out.verbose_name = Some(s.value());
11066                return Ok(());
11067            }
11068            if meta.path.is_ident("secret") {
11069                // `#[rustango(secret)]` or `#[rustango(secret = false)]`.
11070                out.secret = Some(if let Ok(v) = meta.value() {
11071                    v.parse::<syn::LitBool>()?.value
11072                } else {
11073                    true
11074                });
11075                return Ok(());
11076            }
11077            if meta.path.is_ident("editable") {
11078                // Two forms accepted:
11079                //   #[rustango(editable = false)] / true — explicit
11080                //   #[rustango(editable)] — flag form (= true, the
11081                //   default, so harmless; included for symmetry)
11082                if let Ok(v) = meta.value() {
11083                    let lit: syn::LitBool = v.parse()?;
11084                    out.editable = lit.value;
11085                } else {
11086                    out.editable = true;
11087                }
11088                return Ok(());
11089            }
11090            if meta.path.is_ident("blank") {
11091                // Two forms accepted:
11092                //   #[rustango(blank)] — flag form, true
11093                //   #[rustango(blank = false)] / true — explicit
11094                if let Ok(v) = meta.value() {
11095                    let lit: syn::LitBool = v.parse()?;
11096                    out.blank = lit.value;
11097                } else {
11098                    out.blank = true;
11099                }
11100                return Ok(());
11101            }
11102            if meta.path.is_ident("citext") {
11103                // Case-insensitive text column (#344). Two forms:
11104                //   #[rustango(citext)]          — flag form, true
11105                //   #[rustango(citext = true)]   — explicit
11106                //   #[rustango(citext = false)]  — explicit opt-out
11107                // String-only validation lives in the field-type
11108                // emitter (the FieldType discriminant is computed in
11109                // `detect_type`); the macro records the flag and the
11110                // DDL writer emits dialect-specific COLLATE / CITEXT.
11111                if let Ok(v) = meta.value() {
11112                    let lit: syn::LitBool = v.parse()?;
11113                    out.case_insensitive = lit.value;
11114                } else {
11115                    out.case_insensitive = true;
11116                }
11117                return Ok(());
11118            }
11119            if meta.path.is_ident("validators") {
11120                let s: LitStr = meta.value()?.parse()?;
11121                let raw = s.value();
11122                out.validators = raw
11123                    .split(',')
11124                    .map(str::trim)
11125                    .filter(|s| !s.is_empty())
11126                    .map(|name| {
11127                        let mut ident = syn::parse_str::<syn::Ident>(name).map_err(|_| {
11128                            syn::Error::new(s.span(), format!("unknown validator `{name}`"))
11129                        })?;
11130                        ident.set_span(s.span());
11131                        Ok(ident)
11132                    })
11133                    .collect::<syn::Result<_>>()?;
11134                if out.validators.is_empty() {
11135                    return Err(syn::Error::new(
11136                        s.span(),
11137                        "`validators = \"…\"` must list at least one name",
11138                    ));
11139                }
11140                return Ok(());
11141            }
11142            if meta.path.is_ident("auto_uuid") {
11143                parse_flag(&meta, "auto_uuid")?;
11144                out.auto_uuid = true;
11145                // Implied: PK + auto + DEFAULT gen_random_uuid().
11146                // Each is also explicitly settable; the explicit
11147                // value wins if conflicting.
11148                out.primary_key = true;
11149                if out.default.is_none() {
11150                    out.default = Some("gen_random_uuid()".into());
11151                }
11152                return Ok(());
11153            }
11154            if meta.path.is_ident("default_uuid_v7") {
11155                parse_flag(&meta, "default_uuid_v7")?;
11156                // Backend-neutral counterpart of `auto_uuid` — issue #823.
11157                // No SQL DEFAULT (the macro fills the value Rust-side
11158                // before binding); just mark the field as PK + Auto
11159                // so the insert path is the `Auto::Unset → generate`
11160                // branch.
11161                out.default_uuid_v7 = true;
11162                out.primary_key = true;
11163                return Ok(());
11164            }
11165            if meta.path.is_ident("auto_now_add") {
11166                parse_flag(&meta, "auto_now_add")?;
11167                out.auto_now_add = true;
11168                if out.default.is_none() {
11169                    out.default = Some("now()".into());
11170                }
11171                return Ok(());
11172            }
11173            if meta.path.is_ident("auto_now") {
11174                parse_flag(&meta, "auto_now")?;
11175                out.auto_now = true;
11176                if out.default.is_none() {
11177                    out.default = Some("now()".into());
11178                }
11179                return Ok(());
11180            }
11181            if meta.path.is_ident("soft_delete") {
11182                parse_flag(&meta, "soft_delete")?;
11183                out.soft_delete = true;
11184                return Ok(());
11185            }
11186            if meta.path.is_ident("unique") {
11187                parse_flag(&meta, "unique")?;
11188                out.unique = true;
11189                return Ok(());
11190            }
11191            if meta.path.is_ident("index") {
11192                parse_flag(&meta, "index")?;
11193                out.index = true;
11194                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11195                if meta.input.peek(syn::token::Paren) {
11196                    meta.parse_nested_meta(|inner| {
11197                        if inner.path.is_ident("unique") {
11198                            out.index_unique = true;
11199                            return Ok(());
11200                        }
11201                        if inner.path.is_ident("name") {
11202                            let s: LitStr = inner.value()?.parse()?;
11203                            out.index_name = Some(s.value());
11204                            return Ok(());
11205                        }
11206                        if inner.path.is_ident("method") {
11207                            let s: LitStr = inner.value()?.parse()?;
11208                            let v = s.value();
11209                            match v.as_str() {
11210                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11211                                    out.index_method = v;
11212                                }
11213                                other => {
11214                                    return Err(inner.error(format!(
11215                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11216                                    )));
11217                                }
11218                            }
11219                            return Ok(());
11220                        }
11221                        Err(inner.error(
11222                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11223                        ))
11224                    })?;
11225                }
11226                return Ok(());
11227            }
11228            Err(unknown_key_error(&meta, "field", FIELD_ATTR_KEYS))
11229        })?;
11230    }
11231    Ok(out)
11232}
11233
11234/// Parse a signed integer literal, accepting optional leading `-`.
11235fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11236    let expr: syn::Expr = meta.value()?.parse()?;
11237    match expr {
11238        syn::Expr::Lit(syn::ExprLit {
11239            lit: syn::Lit::Int(lit),
11240            ..
11241        }) => lit.base10_parse::<i64>(),
11242        syn::Expr::Unary(syn::ExprUnary {
11243            op: syn::UnOp::Neg(_),
11244            expr,
11245            ..
11246        }) => {
11247            if let syn::Expr::Lit(syn::ExprLit {
11248                lit: syn::Lit::Int(lit),
11249                ..
11250            }) = *expr
11251            {
11252                let v: i64 = lit.base10_parse()?;
11253                Ok(-v)
11254            } else {
11255                Err(syn::Error::new_spanned(expr, "expected integer literal"))
11256            }
11257        }
11258        other => Err(syn::Error::new_spanned(
11259            other,
11260            "expected integer literal (signed)",
11261        )),
11262    }
11263}
11264
11265struct FieldInfo<'a> {
11266    ident: &'a syn::Ident,
11267    column: String,
11268    primary_key: bool,
11269    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11270    /// skip this column when `Auto::Unset` and emit it under
11271    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11272    auto: bool,
11273    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11274    /// the `Column::Value` associated type for typed-column tokens.
11275    value_ty: &'a Type,
11276    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11277    field_type_tokens: TokenStream2,
11278    schema: TokenStream2,
11279    from_row_init: TokenStream2,
11280    /// Variant of [`Self::from_row_init`] that reads the column via
11281    /// `format!("{prefix}__{col}")` so a model can be decoded out of
11282    /// the aliased columns of a JOINed row. Drives slice 9.0d's
11283    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11284    /// helper that `select_related` calls when stitching loaded FKs.
11285    from_aliased_row_init: TokenStream2,
11286    /// MySQL twins of the two inits above. They differ only for a UUID
11287    /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11288    from_row_init_my: TokenStream2,
11289    from_aliased_row_init_my: TokenStream2,
11290    /// A bare or optional `Uuid` field.
11291    uuid_column: bool,
11292    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11293    /// relation helper emit (`Author::<child>_set`) needs to know `T`
11294    /// to point the generated method at the right child model.
11295    fk_inner: Option<Type>,
11296    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11297    /// `kind` (since ForeignKey detection sets `kind` to K's
11298    /// underlying type) but stored separately for clarity at the
11299    /// `FkRelation` construction site, which only sees the FK's
11300    /// surface fields.
11301    fk_pk_kind: DetectedKind,
11302    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11303    /// the bare `ForeignKey<T, K>`. Routes the load_related and
11304    /// fk_pk_access emitters to wrap assignments / accessors in
11305    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11306    /// FK column compiles end-to-end. The DDL writer reads this off
11307    /// the field schema (`nullable` flag); the macro just needs to
11308    /// keep the Rust-side codegen consistent.
11309    nullable: bool,
11310    /// `true` when this column was marked `#[rustango(auto_now)]` —
11311    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11312    /// column instead of the user-supplied value, so `updated_at`
11313    /// always reflects the latest write without the caller having
11314    /// to remember to set it.
11315    auto_now: bool,
11316    /// `true` when this column was marked `#[rustango(auto_now_add)]`
11317    /// — the column is server-set on INSERT (DB DEFAULT) and
11318    /// **immutable** afterwards. `update_on` / `save_on` skip the
11319    /// column entirely so a stale `created_at` value in memory never
11320    /// rewrites the persisted timestamp.
11321    auto_now_add: bool,
11322    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11323    /// Triggers emission of `soft_delete_on(executor)` and
11324    /// `restore_on(executor)` on the model's inherent impl. There is
11325    /// at most one such column per model — emission asserts this.
11326    soft_delete: bool,
11327    /// `Some` when this column was marked
11328    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11329    /// every INSERT and UPDATE path; the database recomputes the
11330    /// value from `EXPR`. Backlog item #35.
11331    generated_as: Option<String>,
11332    /// `true` when this column was marked
11333    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11334    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11335    /// with `Uuid::now_v7()` before binding, so the PK is generated
11336    /// Rust-side and the column is always present in the INSERT
11337    /// statement (no DB DEFAULT requirement). Issue #823.
11338    default_uuid_v7: bool,
11339    /// `Some` when this FK field carried `#[rustango(related_name =
11340    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11341    /// consumed by [`reverse_helper_tokens`] to override the default
11342    /// `<child_snake>_set` accessor name. Follow-up to #816.
11343    related_name: Option<String>,
11344}
11345
11346/// Reject table names that won't survive SQL identifier
11347/// derivation downstream. Postgres' regular-identifier rule
11348/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11349/// through the framework's unquoted FK / index / constraint name
11350/// emission without surprises. We also disallow leading-digit and
11351/// the empty string for clarity.
11352///
11353/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11354/// here — those produce a runtime error from the SQL parser,
11355/// which is loud enough; statically enumerating reserved words
11356/// across the three supported dialects is more friction than help.
11357///
11358/// Backlog item #65.
11359fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11360    validate_sql_identifier(name, "table", span)
11361}
11362
11363/// Reject SQL identifiers that compile but break downstream SQL
11364/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11365/// `kind` is "table" / "column" — used for the error message so users
11366/// see which attribute caused the failure.
11367fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11368    if name.is_empty() {
11369        return Err(syn::Error::new(
11370            span,
11371            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11372        ));
11373    }
11374    let mut chars = name.chars();
11375    let first = chars.next().unwrap();
11376    if !(first.is_ascii_alphabetic() || first == '_') {
11377        return Err(syn::Error::new(
11378            span,
11379            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11380        ));
11381    }
11382    for c in chars {
11383        if !(c.is_ascii_alphanumeric() || c == '_') {
11384            return Err(syn::Error::new(
11385                span,
11386                format!(
11387                    "{kind} name `{name}` contains invalid character {c:?} — \
11388                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11389                     Hyphens in particular break FK / index name derivation \
11390                     downstream; use underscores instead (e.g. `{}`)",
11391                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11392                ),
11393            ));
11394        }
11395    }
11396    Ok(())
11397}
11398
11399fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11400    let root = rustango_root();
11401    let attrs = parse_field_attrs(field)?;
11402    let ident = field
11403        .ident
11404        .as_ref()
11405        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11406    let name = ident_name(ident);
11407    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11408    let primary_key = attrs.primary_key;
11409    let DetectedType {
11410        kind,
11411        nullable,
11412        auto: detected_auto,
11413        fk_inner,
11414    } = detect_type(&field.ty)?;
11415    check_bound_compatibility(field, &attrs, kind)?;
11416    let auto = detected_auto;
11417    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11418    // INSERT path: the user must wrap the field in `Auto<T>`, which
11419    // marks the column as DB-default-supplied. The mixin attrs then
11420    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11421    // for `auto_now`, force the value on UPDATE too.
11422    if attrs.auto_uuid {
11423        if kind != DetectedKind::Uuid {
11424            return Err(syn::Error::new_spanned(
11425                field,
11426                "`#[rustango(auto_uuid)]` requires the field type to be \
11427                 `Auto<uuid::Uuid>`",
11428            ));
11429        }
11430        if !detected_auto {
11431            return Err(syn::Error::new_spanned(
11432                field,
11433                "`#[rustango(auto_uuid)]` requires the field type to be \
11434                 wrapped in `Auto<...>` so the macro skips the column on \
11435                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11436            ));
11437        }
11438    }
11439    if attrs.default_uuid_v7 {
11440        if kind != DetectedKind::Uuid {
11441            return Err(syn::Error::new_spanned(
11442                field,
11443                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11444                 `Auto<uuid::Uuid>`",
11445            ));
11446        }
11447        if !detected_auto {
11448            return Err(syn::Error::new_spanned(
11449                field,
11450                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11451                 wrapped in `Auto<...>` so the macro can detect the \
11452                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11453            ));
11454        }
11455        if attrs.auto_uuid {
11456            return Err(syn::Error::new_spanned(
11457                field,
11458                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11459                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11460                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11461                 Pick one.",
11462            ));
11463        }
11464    }
11465    if attrs.auto_now_add || attrs.auto_now {
11466        if kind != DetectedKind::DateTime {
11467            return Err(syn::Error::new_spanned(
11468                field,
11469                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11470                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11471            ));
11472        }
11473        if !detected_auto {
11474            return Err(syn::Error::new_spanned(
11475                field,
11476                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11477                 the field type to be wrapped in `Auto<...>` so the macro skips \
11478                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11479            ));
11480        }
11481    }
11482    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11483        return Err(syn::Error::new_spanned(
11484            field,
11485            "`#[rustango(soft_delete)]` requires the field type to be \
11486             `Option<chrono::DateTime<chrono::Utc>>`",
11487        ));
11488    }
11489    let is_mixin_auto =
11490        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11491    if detected_auto && !primary_key && !is_mixin_auto {
11492        return Err(syn::Error::new_spanned(
11493            field,
11494            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11495             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11496             `auto_now`",
11497        ));
11498    }
11499    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11500        return Err(syn::Error::new_spanned(
11501            field,
11502            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11503             SERIAL / BIGSERIAL already supplies a default sequence.",
11504        ));
11505    }
11506    if fk_inner.is_some() && primary_key {
11507        return Err(syn::Error::new_spanned(
11508            field,
11509            "`ForeignKey<T>` is not allowed on a primary-key field — \
11510             a row's PK is its own identity, not a reference to a parent.",
11511        ));
11512    }
11513    if attrs.generated_as.is_some() {
11514        if primary_key {
11515            return Err(syn::Error::new_spanned(
11516                field,
11517                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11518                 primary-key field — a PK must be writable so the row \
11519                 has an identity at INSERT time.",
11520            ));
11521        }
11522        if attrs.default.is_some() {
11523            return Err(syn::Error::new_spanned(
11524                field,
11525                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11526                 `default = \"…\"` — Postgres rejects DEFAULT on \
11527                 generated columns. The expression IS the default.",
11528            ));
11529        }
11530        if detected_auto {
11531            return Err(syn::Error::new_spanned(
11532                field,
11533                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11534                 an `Auto<T>` field — generated columns are computed \
11535                 by the DB, not server-assigned via a sequence. Use a \
11536                 plain Rust type (e.g. `f64`).",
11537            ));
11538        }
11539        if fk_inner.is_some() {
11540            return Err(syn::Error::new_spanned(
11541                field,
11542                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11543                 ForeignKey field.",
11544            ));
11545        }
11546    }
11547    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11548    let column_lit = column.as_str();
11549    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11550    // dimension that the bare `Vector` Rust type can't carry, so emit
11551    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11552    // fallback of `Vector(0)`.
11553    let field_type_tokens = if kind == DetectedKind::Vector {
11554        let root = rustango_root();
11555        let dims = attrs.vector_dims.unwrap_or(0);
11556        quote!(#root::core::FieldType::Vector(#dims))
11557    } else if kind == DetectedKind::Geometry {
11558        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11559        // SRID that the bare `Point` Rust type can't carry, so emit
11560        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11561        let root = rustango_root();
11562        let srid = attrs.geometry_srid.unwrap_or(0);
11563        quote!(#root::core::FieldType::Geometry(#srid))
11564    } else {
11565        kind.variant_tokens()
11566    };
11567    let max_length = optional_u32(attrs.max_length);
11568    let min = optional_i64(attrs.min);
11569    let max = optional_i64(attrs.max);
11570    let default = optional_str(attrs.default.as_deref());
11571
11572    let unique = attrs.unique;
11573    let generated_as = optional_str(attrs.generated_as.as_deref());
11574    let help_text = optional_str(attrs.help_text.as_deref());
11575    let choices = optional_choices(attrs.choices.as_deref());
11576    let db_comment = optional_str(attrs.db_comment.as_deref());
11577    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11578    let editable = attrs.editable;
11579    let secret = match attrs.secret {
11580        Some(v) => quote!(::core::option::Option::Some(#v)),
11581        None => quote!(::core::option::Option::None),
11582    };
11583    let blank = attrs.blank;
11584    let case_insensitive = attrs.case_insensitive;
11585    // A path per name: one the runtime does not know fails to resolve.
11586    let validators = &attrs.validators;
11587    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11588    {
11589        return Err(syn::Error::new_spanned(
11590            field,
11591            "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11592             or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11593             has no meaning on a non-FK column.",
11594        ));
11595    }
11596    if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11597        return Err(syn::Error::new_spanned(
11598            field,
11599            "`on_delete = \"set_null\"` needs a nullable column: use \
11600             `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11601             alias for `Option` is not seen).",
11602        ));
11603    }
11604    let fk_on_delete = match attrs.on_delete.as_deref() {
11605        None => quote!(::core::option::Option::None),
11606        Some(action) => {
11607            let variant = match action {
11608                "cascade" => quote!(Cascade),
11609                "restrict" => quote!(Restrict),
11610                "set_null" => quote!(SetNull),
11611                "set_default" => quote!(SetDefault),
11612                "no_action" => quote!(NoAction),
11613                // parse_field_attrs already validated this — guard against future drift.
11614                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11615            };
11616            quote!(::core::option::Option::Some(
11617                #root::core::OnDeleteAction::#variant
11618            ))
11619        }
11620    };
11621    let auto_now_flag = attrs.auto_now;
11622    let cast = match cast_type(&field.ty) {
11623        Some(ty) => quote!(::core::option::Option::Some(<#ty>::FIELD_CAST)),
11624        None => quote!(::core::option::Option::None),
11625    };
11626    // A sealed cast is always a credential: `secret = false` cannot opt it out.
11627    let sealed_guard = match (attrs.secret, cast_type(&field.ty)) {
11628        (Some(false), Some(ty)) => quote! {
11629            const _: () = ::core::assert!(
11630                !::core::matches!(<#ty>::FIELD_CAST, #root::core::FieldCast::Sealed),
11631                "`#[rustango(secret = false)]` on a sealed cast: it is always a credential",
11632            );
11633        },
11634        _ => quote!(),
11635    };
11636    let schema = quote! {{
11637        #sealed_guard
11638        #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11639            name: #name,
11640            column: #column_lit,
11641            ty: #field_type_tokens,
11642            nullable: #nullable,
11643            primary_key: #primary_key,
11644            relation: #relation,
11645            max_length: #max_length,
11646            min: #min,
11647            max: #max,
11648            default: #default,
11649            auto: #auto,
11650            auto_now: #auto_now_flag,
11651            unique: #unique,
11652            generated_as: #generated_as,
11653            help_text: #help_text,
11654            choices: #choices,
11655            db_comment: #db_comment,
11656            verbose_name: #verbose_name,
11657            editable: #editable,
11658            blank: #blank,
11659            case_insensitive: #case_insensitive,
11660            fk_on_delete: #fk_on_delete,
11661            validators: &[ #( #root::core::__validator_names::#validators ),* ],
11662            cast: #cast,
11663            secret: #secret,
11664        })
11665    }};
11666
11667    // sqlx-sqlite has no `Decimal` decode: it goes through `FlatScalar`'s cell.
11668    let decimal_column = kind == DetectedKind::Decimal && !detected_auto && fk_inner.is_none();
11669    let (from_row_init, from_aliased_row_init) = if decimal_column {
11670        (
11671            quote! { #ident: #root::sql::try_get_flat(row, #column_lit)? },
11672            quote! {
11673                #ident: #root::sql::try_get_flat(
11674                    row,
11675                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11676                )?
11677            },
11678        )
11679    } else {
11680        (
11681            quote! { #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)? },
11682            quote! {
11683                #ident: #root::sql::sqlx::Row::try_get(
11684                    row,
11685                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11686                )?
11687            },
11688        )
11689    };
11690    // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11691    // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11692    let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11693    let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11694        (
11695            quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11696            quote! {
11697                #ident: #root::sql::try_get_flat_my(
11698                    row,
11699                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11700                )?
11701            },
11702        )
11703    } else {
11704        (from_row_init.clone(), from_aliased_row_init.clone())
11705    };
11706
11707    Ok(FieldInfo {
11708        ident,
11709        column,
11710        primary_key,
11711        auto,
11712        value_ty: &field.ty,
11713        field_type_tokens,
11714        schema,
11715        from_row_init,
11716        from_aliased_row_init,
11717        from_row_init_my,
11718        from_aliased_row_init_my,
11719        uuid_column,
11720        fk_inner: fk_inner.cloned(),
11721        fk_pk_kind: kind,
11722        nullable,
11723        auto_now: attrs.auto_now,
11724        auto_now_add: attrs.auto_now_add,
11725        soft_delete: attrs.soft_delete,
11726        generated_as: attrs.generated_as.clone(),
11727        default_uuid_v7: attrs.default_uuid_v7,
11728        related_name: attrs.related_name.clone(),
11729    })
11730}
11731
11732fn check_bound_compatibility(
11733    field: &syn::Field,
11734    attrs: &FieldAttrs,
11735    kind: DetectedKind,
11736) -> syn::Result<()> {
11737    if attrs.max_length.is_some() && kind != DetectedKind::String {
11738        return Err(syn::Error::new_spanned(
11739            field,
11740            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11741        ));
11742    }
11743    // These were silently dropped on the wrong type (#1937).
11744    if attrs.case_insensitive && kind != DetectedKind::String {
11745        return Err(syn::Error::new_spanned(
11746            field,
11747            "`citext` is only valid on `String` fields (or `Option<String>`)",
11748        ));
11749    }
11750    if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11751        return Err(syn::Error::new_spanned(
11752            field,
11753            "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11754        ));
11755    }
11756    if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11757        return Err(syn::Error::new_spanned(
11758            field,
11759            "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11760        ));
11761    }
11762    if attrs.choices.is_some() && kind != DetectedKind::String {
11763        return Err(syn::Error::new_spanned(
11764            field,
11765            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11766             integer-valued enumerations should be modeled with a Rust enum and \
11767             custom (de)serializer for now",
11768        ));
11769    }
11770    // They run only on string values, so elsewhere they would never fire.
11771    if !attrs.validators.is_empty() && kind != DetectedKind::String {
11772        return Err(syn::Error::new_spanned(
11773            field,
11774            "`validators` is only valid on `String` fields (or `Option<String>`)",
11775        ));
11776    }
11777    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11778        return Err(syn::Error::new_spanned(
11779            field,
11780            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11781        ));
11782    }
11783    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11784        if min > max {
11785            return Err(syn::Error::new_spanned(
11786                field,
11787                format!("`min` ({min}) is greater than `max` ({max})"),
11788            ));
11789        }
11790    }
11791    if let Some(default) = &attrs.default {
11792        check_default_expr(default, kind).map_err(|msg| syn::Error::new_spanned(field, msg))?;
11793    }
11794    Ok(())
11795}
11796
11797/// SQL words a `default` may be on its own; any other bare word is a
11798/// column reference, which Postgres and MySQL refuse in a DEFAULT.
11799const DEFAULT_KEYWORDS: &[&str] = &[
11800    "NULL",
11801    "TRUE",
11802    "FALSE",
11803    "CURRENT_TIMESTAMP",
11804    "CURRENT_DATE",
11805    "CURRENT_TIME",
11806    "LOCALTIMESTAMP",
11807    "LOCALTIME",
11808];
11809
11810/// `default` is raw SQL. Refuse the shapes only SQLite accepts: a bare
11811/// word (read as a column on PG/MySQL) and a non-numeric literal on a
11812/// numeric field.
11813fn check_default_expr(default: &str, kind: DetectedKind) -> Result<(), String> {
11814    let expr = default.trim();
11815    let is_word = expr.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
11816        && expr
11817            .chars()
11818            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ' ');
11819    let upper = expr.to_ascii_uppercase();
11820    if is_word && ["CURRENT_USER", "SESSION_USER", "USER"].contains(&upper.as_str()) {
11821        return Err(format!(
11822            "`default = \"{expr}\"` works on Postgres only: MySQL refuses it as a default \
11823             and SQLite stores the text `{expr}`; set the value in code instead"
11824        ));
11825    }
11826    if is_word && !DEFAULT_KEYWORDS.contains(&upper.as_str()) {
11827        return Err(format!(
11828            "`default = \"{expr}\"` is raw SQL, so `{expr}` is read as a column name; \
11829             for the text '{expr}' write `default = \"'{expr}'\"`"
11830        ));
11831    }
11832    let numeric = kind.is_integer()
11833        || matches!(
11834            kind,
11835            DetectedKind::F32 | DetectedKind::F64 | DetectedKind::Decimal
11836        );
11837    if numeric {
11838        let unquoted = expr
11839            .strip_prefix('\'')
11840            .and_then(|e| e.strip_suffix('\''))
11841            .unwrap_or(expr);
11842        let is_number = if kind.is_integer() {
11843            unquoted.parse::<i64>().is_ok()
11844        } else {
11845            unquoted.parse::<f64>().is_ok_and(f64::is_finite)
11846        };
11847        // Keep expressions (`(1 + 2)`, `abs(-1)`) and NULL; refuse a
11848        // literal that is no number. `""` renders as `''`.
11849        let is_literal = is_word || expr.is_empty() || expr.starts_with('\'');
11850        if is_literal && !is_number && upper != "NULL" {
11851            return Err(format!(
11852                "`default = \"{expr}\"` is not a number, but the field is numeric"
11853            ));
11854        }
11855    }
11856    Ok(())
11857}
11858
11859/// A `&'static [ty]` of `const fn` results. The slice needs its own
11860/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11861fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11862    quote!({
11863        const ITEMS: &[#ty] = &[ #(#items),* ];
11864        ITEMS
11865    })
11866}
11867
11868fn optional_u32(value: Option<u32>) -> TokenStream2 {
11869    if let Some(v) = value {
11870        quote!(::core::option::Option::Some(#v))
11871    } else {
11872        quote!(::core::option::Option::None)
11873    }
11874}
11875
11876fn optional_i64(value: Option<i64>) -> TokenStream2 {
11877    if let Some(v) = value {
11878        quote!(::core::option::Option::Some(#v))
11879    } else {
11880        quote!(::core::option::Option::None)
11881    }
11882}
11883
11884fn optional_str(value: Option<&str>) -> TokenStream2 {
11885    if let Some(v) = value {
11886        quote!(::core::option::Option::Some(#v))
11887    } else {
11888        quote!(::core::option::Option::None)
11889    }
11890}
11891
11892fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11893    let Some(pairs) = pairs else {
11894        return quote!(::core::option::Option::None);
11895    };
11896    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11897    quote!(::core::option::Option::Some(&[#(#entries),*]))
11898}
11899
11900fn relation_tokens(
11901    field: &syn::Field,
11902    attrs: &FieldAttrs,
11903    fk_inner: Option<&syn::Type>,
11904    table: &str,
11905) -> syn::Result<TokenStream2> {
11906    let root = rustango_root();
11907    if let Some(inner) = fk_inner {
11908        if attrs.fk.is_some() || attrs.o2o.is_some() {
11909            return Err(syn::Error::new_spanned(
11910                field,
11911                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11912                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11913            ));
11914        }
11915        // No `on`: the target's PK column, which may be renamed.
11916        let on = match attrs.on.as_deref() {
11917            Some(on) => quote!(#on),
11918            None => quote! {
11919                match <#inner as #root::core::Model>::SCHEMA.pk_column() {
11920                    ::core::option::Option::Some(c) => c,
11921                    ::core::option::Option::None => "id",
11922                }
11923            },
11924        };
11925        return Ok(quote! {
11926            ::core::option::Option::Some(#root::core::Relation::fk(
11927                <#inner as #root::core::Model>::SCHEMA.table,
11928                #on,
11929            ))
11930        });
11931    }
11932    match (&attrs.fk, &attrs.o2o) {
11933        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11934            field,
11935            "`fk` and `o2o` are mutually exclusive",
11936        )),
11937        (Some(to), None) => {
11938            let on = attrs.on.as_deref().unwrap_or("id");
11939            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11940            // the model's own table. Threaded as a literal string at
11941            // macro-expansion time to sidestep the const-eval cycle
11942            // that `Self::SCHEMA.table` would create when referenced
11943            // inside Self::SCHEMA's own initializer.
11944            let resolved = if to == "self" { table } else { to };
11945            Ok(quote! {
11946                ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11947            })
11948        }
11949        (None, Some(to)) => {
11950            let on = attrs.on.as_deref().unwrap_or("id");
11951            let resolved = if to == "self" { table } else { to };
11952            Ok(quote! {
11953                ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11954            })
11955        }
11956        (None, None) => {
11957            if attrs.on.is_some() {
11958                return Err(syn::Error::new_spanned(
11959                    field,
11960                    "`on` requires `fk` or `o2o`",
11961                ));
11962            }
11963            Ok(quote!(::core::option::Option::None))
11964        }
11965    }
11966}
11967
11968/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11969/// about kinds without depending on `rustango-core` (which would require a
11970/// proc-macro/normal split it doesn't have today).
11971#[derive(Clone, Copy, PartialEq, Eq)]
11972enum DetectedKind {
11973    I16,
11974    I32,
11975    I64,
11976    F32,
11977    F64,
11978    Bool,
11979    String,
11980    DateTime,
11981    Date,
11982    Time,
11983    Uuid,
11984    Json,
11985    Decimal,
11986    Binary,
11987    /// `Array<String>` → PG `text[]` (#341).
11988    ArrayText,
11989    /// `Array<i32>` → PG `integer[]` (#341).
11990    ArrayInt,
11991    /// `Array<i64>` → PG `bigint[]` (#341).
11992    ArrayBigInt,
11993    /// `Range<i32>` → PG `int4range` (#343).
11994    RangeInt,
11995    /// `Range<i64>` → PG `int8range` (#343).
11996    RangeBigInt,
11997    /// `Range<Decimal>` → PG `numrange` (#343).
11998    RangeNumeric,
11999    /// `Range<NaiveDate>` → PG `daterange` (#343).
12000    RangeDate,
12001    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
12002    RangeDateTime,
12003    /// `HStore` → PG `hstore` (#342).
12004    HStore,
12005    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
12006    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
12007    /// in at the `FieldType` emission site (not carried on this enum).
12008    Vector,
12009    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
12010    /// from the `#[rustango(geometry(srid = N))]` field attribute,
12011    /// threaded in at the `FieldType` emission site.
12012    Geometry,
12013}
12014
12015impl DetectedKind {
12016    fn variant_tokens(self) -> TokenStream2 {
12017        let root = rustango_root();
12018        match self {
12019            Self::I16 => quote!(#root::core::FieldType::I16),
12020            Self::I32 => quote!(#root::core::FieldType::I32),
12021            Self::I64 => quote!(#root::core::FieldType::I64),
12022            Self::F32 => quote!(#root::core::FieldType::F32),
12023            Self::F64 => quote!(#root::core::FieldType::F64),
12024            Self::Bool => quote!(#root::core::FieldType::Bool),
12025            Self::String => quote!(#root::core::FieldType::String),
12026            Self::DateTime => quote!(#root::core::FieldType::DateTime),
12027            Self::Date => quote!(#root::core::FieldType::Date),
12028            Self::Time => quote!(#root::core::FieldType::Time),
12029            Self::Uuid => quote!(#root::core::FieldType::Uuid),
12030            Self::Json => quote!(#root::core::FieldType::Json),
12031            Self::Decimal => quote!(#root::core::FieldType::Decimal),
12032            Self::Binary => quote!(#root::core::FieldType::Binary),
12033            Self::ArrayText => {
12034                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
12035            }
12036            Self::ArrayInt => {
12037                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
12038            }
12039            Self::ArrayBigInt => {
12040                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
12041            }
12042            Self::RangeInt => {
12043                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
12044            }
12045            Self::RangeBigInt => {
12046                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
12047            }
12048            Self::RangeNumeric => {
12049                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
12050            }
12051            Self::RangeDate => {
12052                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
12053            }
12054            Self::RangeDateTime => {
12055                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
12056            }
12057            Self::HStore => quote!(#root::core::FieldType::HStore),
12058            // Dimension comes from the `vector(dims = N)` attribute,
12059            // applied at the emission site; `0` here is just a fallback.
12060            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
12061            // SRID comes from the `geometry(srid = N)` attribute, applied
12062            // at the emission site; `0` here is just a fallback.
12063            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
12064        }
12065    }
12066
12067    fn is_integer(self) -> bool {
12068        matches!(self, Self::I16 | Self::I32 | Self::I64)
12069    }
12070}
12071
12072/// Result of walking a field's Rust type. `kind` is the underlying
12073/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
12074/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
12075/// `Some(<T>)` when the field was `ForeignKey<T>` (or
12076/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
12077#[derive(Clone, Copy)]
12078struct DetectedType<'a> {
12079    kind: DetectedKind,
12080    nullable: bool,
12081    auto: bool,
12082    fk_inner: Option<&'a syn::Type>,
12083}
12084
12085/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
12086/// non-`Auto` types — the caller should already have routed Auto-only
12087/// codegen through this helper, so a `None` indicates a macro-internal
12088/// invariant break.
12089fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
12090    let Type::Path(TypePath { path, qself: None }) = ty else {
12091        return None;
12092    };
12093    let last = path.segments.last()?;
12094    if last.ident != "Auto" {
12095        return None;
12096    }
12097    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12098        return None;
12099    };
12100    args.args.iter().find_map(|a| match a {
12101        syn::GenericArgument::Type(t) => Some(t),
12102        _ => None,
12103    })
12104}
12105
12106/// The `Cast<C>` inside `ty` (bare or in an `Option`), for its schema record.
12107fn cast_type(ty: &syn::Type) -> Option<&syn::Type> {
12108    let Type::Path(TypePath { path, qself: None }) = ty else {
12109        return None;
12110    };
12111    let last = path.segments.last()?;
12112    if last.ident == "Cast" {
12113        return Some(ty);
12114    }
12115    if last.ident != "Option" {
12116        return None;
12117    }
12118    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12119        return None;
12120    };
12121    match args.args.first()? {
12122        syn::GenericArgument::Type(inner) => cast_type(inner).filter(|_| args.args.len() == 1),
12123        _ => None,
12124    }
12125}
12126
12127fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
12128    let Type::Path(TypePath { path, qself: None }) = ty else {
12129        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
12130    };
12131    let last = path
12132        .segments
12133        .last()
12134        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
12135
12136    if last.ident == "Option" {
12137        let inner = generic_inner(ty, &last.arguments, "Option")?;
12138        let inner_det = detect_type(inner)?;
12139        if inner_det.nullable {
12140            return Err(syn::Error::new_spanned(
12141                ty,
12142                "nested Option is not supported",
12143            ));
12144        }
12145        if inner_det.auto {
12146            return Err(syn::Error::new_spanned(
12147                ty,
12148                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12149            ));
12150        }
12151        return Ok(DetectedType {
12152            nullable: true,
12153            ..inner_det
12154        });
12155    }
12156
12157    if last.ident == "Auto" {
12158        let inner = generic_inner(ty, &last.arguments, "Auto")?;
12159        let inner_det = detect_type(inner)?;
12160        if inner_det.auto {
12161            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
12162        }
12163        if inner_det.nullable {
12164            return Err(syn::Error::new_spanned(
12165                ty,
12166                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12167            ));
12168        }
12169        if inner_det.fk_inner.is_some() {
12170            return Err(syn::Error::new_spanned(
12171                ty,
12172                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
12173            ));
12174        }
12175        if !matches!(
12176            inner_det.kind,
12177            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
12178        ) {
12179            return Err(syn::Error::new_spanned(
12180                ty,
12181                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
12182                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
12183                 (DEFAULT now())",
12184            ));
12185        }
12186        return Ok(DetectedType {
12187            auto: true,
12188            ..inner_det
12189        });
12190    }
12191
12192    if last.ident == "ForeignKey" {
12193        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
12194        // Resolve the FK column's underlying SQL type from `K`. When the
12195        // user wrote `ForeignKey<T>` without a key parameter, the type
12196        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
12197        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
12198        // recurse into K so the column DDL emits the right SQL type
12199        // (VARCHAR for String, UUID for Uuid, …) and the load_related
12200        // emitter knows which `SqlValue` variant to match.
12201        let kind = match key_ty {
12202            Some(k) => detect_type(k)?.kind,
12203            None => DetectedKind::I64,
12204        };
12205        return Ok(DetectedType {
12206            kind,
12207            nullable: false,
12208            auto: false,
12209            fk_inner: Some(inner),
12210        });
12211    }
12212
12213    let kind = match last.ident.to_string().as_str() {
12214        "i16" => DetectedKind::I16,
12215        "i32" => DetectedKind::I32,
12216        "i64" => DetectedKind::I64,
12217        "f32" => DetectedKind::F32,
12218        "f64" => DetectedKind::F64,
12219        "bool" => DetectedKind::Bool,
12220        "String" => DetectedKind::String,
12221        "DateTime" => DetectedKind::DateTime,
12222        "NaiveDate" => DetectedKind::Date,
12223        "NaiveTime" => DetectedKind::Time,
12224        "Uuid" => DetectedKind::Uuid,
12225        "Value" => DetectedKind::Json,
12226        "Decimal" => DetectedKind::Decimal,
12227        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
12228        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
12229        // — that would emit Binary DDL and decode-fail at runtime.
12230        "Vec" => {
12231            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
12232            if let Type::Path(TypePath { path, qself: None }) = inner {
12233                if let Some(seg) = path.segments.last() {
12234                    if seg.ident == "u8" && seg.arguments.is_empty() {
12235                        return Ok(DetectedType {
12236                            kind: DetectedKind::Binary,
12237                            nullable: false,
12238                            auto: false,
12239                            fk_inner: None,
12240                        });
12241                    }
12242                }
12243            }
12244            return Err(syn::Error::new_spanned(
12245                ty,
12246                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
12247                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
12248            ));
12249        }
12250        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
12251        // `integer[]` / `bigint[]` (#341).
12252        "Array" => {
12253            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
12254            let elem = match inner {
12255                Type::Path(TypePath { path, qself: None }) => {
12256                    path.segments.last().map(|s| s.ident.to_string())
12257                }
12258                _ => None,
12259            };
12260            let kind = match elem.as_deref() {
12261                Some("String") => DetectedKind::ArrayText,
12262                Some("i32") => DetectedKind::ArrayInt,
12263                Some("i64") => DetectedKind::ArrayBigInt,
12264                _ => {
12265                    return Err(syn::Error::new_spanned(
12266                        ty,
12267                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12268                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12269                    ));
12270                }
12271            };
12272            return Ok(DetectedType {
12273                kind,
12274                nullable: false,
12275                auto: false,
12276                fk_inner: None,
12277            });
12278        }
12279        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12280        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12281        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12282        "Range" => {
12283            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12284            let elem = match inner {
12285                Type::Path(TypePath { path, qself: None }) => {
12286                    path.segments.last().map(|s| s.ident.to_string())
12287                }
12288                _ => None,
12289            };
12290            let kind = match elem.as_deref() {
12291                Some("i32") => DetectedKind::RangeInt,
12292                Some("i64") => DetectedKind::RangeBigInt,
12293                Some("Decimal") => DetectedKind::RangeNumeric,
12294                Some("NaiveDate") => DetectedKind::RangeDate,
12295                Some("DateTime") => DetectedKind::RangeDateTime,
12296                _ => {
12297                    return Err(syn::Error::new_spanned(
12298                        ty,
12299                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12300                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12301                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12302                         (→ tstzrange) are supported (#343)",
12303                    ));
12304                }
12305            };
12306            return Ok(DetectedType {
12307                kind,
12308                nullable: false,
12309                auto: false,
12310                fk_inner: None,
12311            });
12312        }
12313        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12314        // (the `CastValue` impl bridges logical↔stored); the field's own
12315        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12316        // schema just needs `FieldType::String`.
12317        "Cast" => {
12318            return Ok(DetectedType {
12319                kind: DetectedKind::String,
12320                nullable: false,
12321                auto: false,
12322                fk_inner: None,
12323            });
12324        }
12325        // `HStore` → PG `hstore` (#342). No generic
12326        // parameter — always a string→string map.
12327        "HStore" => {
12328            return Ok(DetectedType {
12329                kind: DetectedKind::HStore,
12330                nullable: false,
12331                auto: false,
12332                fk_inner: None,
12333            });
12334        }
12335        // `Vector` → pgvector `vector(N)` (#824). The dimension is
12336        // supplied by `#[rustango(vector(dims = N))]`, not the type.
12337        "Vector" => {
12338            return Ok(DetectedType {
12339                kind: DetectedKind::Vector,
12340                nullable: false,
12341                auto: false,
12342                fk_inner: None,
12343            });
12344        }
12345        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12346        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12347        "Point" => {
12348            return Ok(DetectedType {
12349                kind: DetectedKind::Geometry,
12350                nullable: false,
12351                auto: false,
12352                fk_inner: None,
12353            });
12354        }
12355        other => {
12356            return Err(syn::Error::new_spanned(
12357                ty,
12358                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)"),
12359            ));
12360        }
12361    };
12362    Ok(DetectedType {
12363        kind,
12364        nullable: false,
12365        auto: false,
12366        fk_inner: None,
12367    })
12368}
12369
12370fn generic_inner<'a>(
12371    ty: &'a Type,
12372    arguments: &'a PathArguments,
12373    wrapper: &str,
12374) -> syn::Result<&'a Type> {
12375    let PathArguments::AngleBracketed(args) = arguments else {
12376        return Err(syn::Error::new_spanned(
12377            ty,
12378            format!("{wrapper} requires a generic argument"),
12379        ));
12380    };
12381    args.args
12382        .iter()
12383        .find_map(|a| match a {
12384            GenericArgument::Type(t) => Some(t),
12385            _ => None,
12386        })
12387        .ok_or_else(|| {
12388            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12389        })
12390}
12391
12392/// Like [`generic_inner`] but pulls *two* type args — the first is
12393/// required, the second is optional. Used by the `ForeignKey<T, K>`
12394/// detection where K defaults to `i64` when omitted.
12395fn generic_pair<'a>(
12396    ty: &'a Type,
12397    arguments: &'a PathArguments,
12398    wrapper: &str,
12399) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12400    let PathArguments::AngleBracketed(args) = arguments else {
12401        return Err(syn::Error::new_spanned(
12402            ty,
12403            format!("{wrapper} requires a generic argument"),
12404        ));
12405    };
12406    let mut types = args.args.iter().filter_map(|a| match a {
12407        GenericArgument::Type(t) => Some(t),
12408        _ => None,
12409    });
12410    let first = types.next().ok_or_else(|| {
12411        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12412    })?;
12413    let second = types.next();
12414    Ok((first, second))
12415}
12416
12417fn to_snake_case(s: &str) -> String {
12418    let mut out = String::with_capacity(s.len() + 4);
12419    for (i, ch) in s.chars().enumerate() {
12420        if ch.is_ascii_uppercase() {
12421            if i > 0 {
12422                out.push('_');
12423            }
12424            out.push(ch.to_ascii_lowercase());
12425        } else {
12426            out.push(ch);
12427        }
12428    }
12429    out
12430}
12431
12432// ============================================================
12433//  #[derive(Form)]  —  slice 8.4B
12434// ============================================================
12435
12436/// Per-field `#[form(...)]` attributes recognised by the derive.
12437#[derive(Default)]
12438struct FormFieldAttrs {
12439    min: Option<i64>,
12440    max: Option<i64>,
12441    min_length: Option<u32>,
12442    max_length: Option<u32>,
12443    /// `#[form(clean = "fn_name")]` — per-field clean hook.
12444    /// The named static method on the form struct is called after the
12445    /// field's typed parse + length/range checks; it gets the parsed
12446    /// value by reference and returns `Result<<FieldType>, String>`.
12447    /// On Ok, the returned value replaces the parsed one; on Err, the
12448    /// message is attached to the field error list. Issue #372.
12449    clean: Option<syn::Ident>,
12450}
12451
12452/// Container-level `#[form(...)]` attributes. Currently only the
12453/// cross-field `validate` hook (issue #373).
12454#[derive(Default)]
12455struct FormContainerAttrs {
12456    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12457    /// After every per-field parse succeeds, the named method on the
12458    /// form struct is called with `&self` and may return
12459    /// `Result<(), FormErrors>`. Errors merge into the field error
12460    /// list. Issue #373.
12461    validate: Option<syn::Ident>,
12462}
12463
12464/// Detected shape of a form field's Rust type.
12465#[derive(Clone, Copy)]
12466enum FormFieldKind {
12467    String,
12468    I16,
12469    I32,
12470    I64,
12471    F32,
12472    F64,
12473    Bool,
12474}
12475
12476impl FormFieldKind {
12477    fn parse_method(self) -> &'static str {
12478        match self {
12479            Self::I16 => "i16",
12480            Self::I32 => "i32",
12481            Self::I64 => "i64",
12482            Self::F32 => "f32",
12483            Self::F64 => "f64",
12484            // String + Bool don't go through `str::parse`; the codegen
12485            // handles them inline.
12486            Self::String | Self::Bool => "",
12487        }
12488    }
12489}
12490
12491/// A generated `derive(Form)` local; mixed-site so a same-named field can't shadow it.
12492fn form_local(name: &str) -> syn::Ident {
12493    syn::Ident::new(name, proc_macro2::Span::mixed_site())
12494}
12495
12496fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12497    let root = rustango_root();
12498    let (errs, cleaned, data_param) = (
12499        form_local("__errors"),
12500        form_local("__cleaned"),
12501        form_local("data"),
12502    );
12503    let struct_name = &input.ident;
12504
12505    let Data::Struct(data) = &input.data else {
12506        return Err(syn::Error::new_spanned(
12507            struct_name,
12508            "Form can only be derived on structs",
12509        ));
12510    };
12511    let Fields::Named(named) = &data.fields else {
12512        return Err(syn::Error::new_spanned(
12513            struct_name,
12514            "Form requires a struct with named fields",
12515        ));
12516    };
12517
12518    // #373 — container-level `#[form(validate = "fn")]` hook.
12519    let container = parse_form_container_attrs(input)?;
12520    let post_field_clean: Vec<TokenStream2> = Vec::new();
12521    let _ = post_field_clean;
12522
12523    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12524    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12525
12526    for field in &named.named {
12527        let ident = field
12528            .ident
12529            .as_ref()
12530            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12531        let attrs = parse_form_field_attrs(field)?;
12532        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12533        let is_string = matches!(kind, FormFieldKind::String);
12534        let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12535        if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12536            return Err(syn::Error::new(
12537                field.span(),
12538                "`min_length` / `max_length` need a String field",
12539            ));
12540        }
12541        if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12542            return Err(syn::Error::new(
12543                field.span(),
12544                "`min` / `max` need a numeric field",
12545            ));
12546        }
12547
12548        let name_lit = ident_name(ident);
12549        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12550        // #372 — append the per-field `clean_<field>` call right after
12551        // the parse block when the attribute is set. The clean fn
12552        // takes &T and returns Result<T, String>; on Err we attach
12553        // the message to the field error list without aborting, so
12554        // the caller sees every field error at once.
12555        let clean_block = if let Some(clean_fn) = &attrs.clean {
12556            quote! {
12557                if #errs.fields().get(#name_lit).is_none() {
12558                    match Self::#clean_fn(&#ident) {
12559                        ::core::result::Result::Ok(#cleaned) => { #ident = #cleaned; }
12560                        ::core::result::Result::Err(__msg) => {
12561                            #errs.add(#name_lit, __msg);
12562                        }
12563                    }
12564                }
12565            }
12566        } else {
12567            quote! {}
12568        };
12569        field_blocks.push(quote! {
12570            #parse_block
12571            #clean_block
12572        });
12573        field_idents.push(ident);
12574    }
12575
12576    // #373 — after every per-field parse + clean succeeds, call the
12577    // cross-field validator if declared. Errors merge into the
12578    // outgoing FormErrors via the existing `FormErrors::merge` helper
12579    // (same primitive the serializer cross-field hook uses).
12580    let cross_field_call = if let Some(validate_fn) = &container.validate {
12581        quote! {
12582            if #errs.is_empty() {
12583                let __candidate = Self { #( #field_idents ),* };
12584                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12585                    #errs.merge(__other);
12586                }
12587                if !#errs.is_empty() {
12588                    return ::core::result::Result::Err(#errs);
12589                }
12590                return ::core::result::Result::Ok(__candidate);
12591            }
12592        }
12593    } else {
12594        quote! {}
12595    };
12596
12597    Ok(quote! {
12598        impl #root::forms::Form for #struct_name {
12599            fn parse(
12600                #data_param: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12601            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12602                let mut #errs = #root::forms::FormErrors::default();
12603                #( #field_blocks )*
12604                #cross_field_call
12605                if !#errs.is_empty() {
12606                    return ::core::result::Result::Err(#errs);
12607                }
12608                ::core::result::Result::Ok(Self {
12609                    #( #field_idents ),*
12610                })
12611            }
12612        }
12613    })
12614}
12615
12616fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12617    let mut out = FormContainerAttrs::default();
12618    for attr in &input.attrs {
12619        if !attr.path().is_ident("form") {
12620            continue;
12621        }
12622        attr.parse_nested_meta(|meta| {
12623            if meta.path.is_ident("validate") {
12624                let s: LitStr = meta.value()?.parse()?;
12625                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12626                return Ok(());
12627            }
12628            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12629        })?;
12630    }
12631    Ok(out)
12632}
12633
12634fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12635    let mut out = FormFieldAttrs::default();
12636    for attr in &field.attrs {
12637        if !attr.path().is_ident("form") {
12638            continue;
12639        }
12640        attr.parse_nested_meta(|meta| {
12641            if meta.path.is_ident("min") {
12642                let lit: syn::LitInt = meta.value()?.parse()?;
12643                out.min = Some(lit.base10_parse::<i64>()?);
12644                return Ok(());
12645            }
12646            if meta.path.is_ident("max") {
12647                let lit: syn::LitInt = meta.value()?.parse()?;
12648                out.max = Some(lit.base10_parse::<i64>()?);
12649                return Ok(());
12650            }
12651            if meta.path.is_ident("min_length") {
12652                let lit: syn::LitInt = meta.value()?.parse()?;
12653                out.min_length = Some(lit.base10_parse::<u32>()?);
12654                return Ok(());
12655            }
12656            if meta.path.is_ident("max_length") {
12657                let lit: syn::LitInt = meta.value()?.parse()?;
12658                out.max_length = Some(lit.base10_parse::<u32>()?);
12659                return Ok(());
12660            }
12661            if meta.path.is_ident("clean") {
12662                let s: LitStr = meta.value()?.parse()?;
12663                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12664                return Ok(());
12665            }
12666            Err(meta.error(
12667                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12668            ))
12669        })?;
12670    }
12671    Ok(out)
12672}
12673
12674fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12675    let Type::Path(TypePath { path, qself: None }) = ty else {
12676        return Err(syn::Error::new(
12677            span,
12678            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12679        ));
12680    };
12681    let last = path
12682        .segments
12683        .last()
12684        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12685
12686    if last.ident == "Option" {
12687        let inner = generic_inner(ty, &last.arguments, "Option")?;
12688        let (kind, nested) = detect_form_field(inner, span)?;
12689        if nested {
12690            return Err(syn::Error::new(
12691                span,
12692                "nested Option in Form fields is not supported",
12693            ));
12694        }
12695        return Ok((kind, true));
12696    }
12697
12698    let kind = match last.ident.to_string().as_str() {
12699        "String" => FormFieldKind::String,
12700        "i16" => FormFieldKind::I16,
12701        "i32" => FormFieldKind::I32,
12702        "i64" => FormFieldKind::I64,
12703        "f32" => FormFieldKind::F32,
12704        "f64" => FormFieldKind::F64,
12705        "bool" => FormFieldKind::Bool,
12706        other => {
12707            return Err(syn::Error::new(
12708                span,
12709                format!(
12710                    "Form field type `{other}` is not supported in v0.8 — use String / \
12711                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12712                ),
12713            ));
12714        }
12715    };
12716    Ok((kind, false))
12717}
12718
12719#[allow(clippy::too_many_lines)]
12720fn render_form_field_parse(
12721    ident: &syn::Ident,
12722    name_lit: &str,
12723    kind: FormFieldKind,
12724    nullable: bool,
12725    attrs: &FormFieldAttrs,
12726) -> TokenStream2 {
12727    let (errs, data_param) = (form_local("__errors"), form_local("data"));
12728    // Pull the raw &str from the payload `Form::parse(data: &HashMap<…>)`.
12729    let lookup = quote! {
12730        let __raw: ::core::option::Option<&::std::string::String> = #data_param.get(#name_lit);
12731    };
12732
12733    let root = rustango_root();
12734    // Strict, as ModelForm: an unknown spelling is an error, not `true`.
12735    let checkbox = quote! {
12736        match #root::forms::checkbox(__s) {
12737            ::core::option::Option::Some(__b) => __b,
12738            ::core::option::Option::None => {
12739                #errs.add(#name_lit, "Enter true or false.");
12740                false
12741            }
12742        }
12743    };
12744    let parsed_value = match kind {
12745        // `Option<bool>`: absent or blank is `None`, as for the other types.
12746        FormFieldKind::Bool if nullable => quote! {
12747            let __v: ::core::option::Option<bool> = match __raw {
12748                ::core::option::Option::None => ::core::option::Option::None,
12749                ::core::option::Option::Some(__s) if __s.is_empty() => {
12750                    ::core::option::Option::None
12751                }
12752                ::core::option::Option::Some(__s) => ::core::option::Option::Some(#checkbox),
12753            };
12754        },
12755        FormFieldKind::Bool => quote! {
12756            let __v: bool = match __raw {
12757                ::core::option::Option::None => false,
12758                ::core::option::Option::Some(__s) => #checkbox,
12759            };
12760        },
12761        FormFieldKind::String => {
12762            if nullable {
12763                quote! {
12764                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12765                        ::core::option::Option::None => ::core::option::Option::None,
12766                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12767                            ::core::option::Option::None
12768                        }
12769                        ::core::option::Option::Some(__s) => {
12770                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12771                        }
12772                    };
12773                }
12774            } else {
12775                quote! {
12776                    let __v: ::std::string::String = match __raw {
12777                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12778                            ::core::clone::Clone::clone(__s)
12779                        }
12780                        _ => {
12781                            #errs.add(#name_lit, "This field is required.");
12782                            ::std::string::String::new()
12783                        }
12784                    };
12785                }
12786            }
12787        }
12788        FormFieldKind::I16
12789        | FormFieldKind::I32
12790        | FormFieldKind::I64
12791        | FormFieldKind::F32
12792        | FormFieldKind::F64 => {
12793            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12794            let ty_lit = kind.parse_method();
12795            let default_val = match kind {
12796                FormFieldKind::I16 => quote! { 0i16 },
12797                FormFieldKind::I32 => quote! { 0i32 },
12798                FormFieldKind::I64 => quote! { 0i64 },
12799                FormFieldKind::F32 => quote! { 0f32 },
12800                FormFieldKind::F64 => quote! { 0f64 },
12801                _ => quote! { Default::default() },
12802            };
12803            // NaN and inf parse, then pass every min/max compare.
12804            let finite = if matches!(kind, FormFieldKind::F32 | FormFieldKind::F64) {
12805                quote! {
12806                    .and_then(|__n| if __n.is_finite() {
12807                        ::core::result::Result::Ok(__n)
12808                    } else {
12809                        ::core::result::Result::Err(::std::string::String::from("not a finite number"))
12810                    })
12811                }
12812            } else {
12813                quote! {}
12814            };
12815            if nullable {
12816                quote! {
12817                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12818                        ::core::option::Option::None => ::core::option::Option::None,
12819                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12820                            ::core::option::Option::None
12821                        }
12822                        ::core::option::Option::Some(__s) => {
12823                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12824                                ::core::result::Result::Ok(__n) => {
12825                                    ::core::option::Option::Some(__n)
12826                                }
12827                                ::core::result::Result::Err(__e) => {
12828                                    #errs.add(
12829                                        #name_lit,
12830                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12831                                    );
12832                                    ::core::option::Option::None
12833                                }
12834                            }
12835                        }
12836                    };
12837                }
12838            } else {
12839                quote! {
12840                    let __v: #parse_ty = match __raw {
12841                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12842                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12843                                ::core::result::Result::Ok(__n) => __n,
12844                                ::core::result::Result::Err(__e) => {
12845                                    #errs.add(
12846                                        #name_lit,
12847                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12848                                    );
12849                                    #default_val
12850                                }
12851                            }
12852                        }
12853                        _ => {
12854                            #errs.add(#name_lit, "This field is required.");
12855                            #default_val
12856                        }
12857                    };
12858                }
12859            }
12860        }
12861    };
12862
12863    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12864
12865    quote! {
12866        // `mut` so the per-field `clean` hook (#372) can rewrite the
12867        // parsed value in-place when it returns Ok with a normalized
12868        // form (e.g. trim / lowercase).
12869        let mut #ident = {
12870            #lookup
12871            #parsed_value
12872            #validators
12873            __v
12874        };
12875    }
12876}
12877
12878fn render_form_validators(
12879    name_lit: &str,
12880    kind: FormFieldKind,
12881    nullable: bool,
12882    attrs: &FormFieldAttrs,
12883) -> TokenStream2 {
12884    let errs = form_local("__errors");
12885    let mut checks: Vec<TokenStream2> = Vec::new();
12886
12887    let val_ref = if nullable {
12888        quote! { __v.as_ref() }
12889    } else {
12890        quote! { ::core::option::Option::Some(&__v) }
12891    };
12892
12893    let is_string = matches!(kind, FormFieldKind::String);
12894    let is_numeric = matches!(
12895        kind,
12896        FormFieldKind::I16
12897            | FormFieldKind::I32
12898            | FormFieldKind::I64
12899            | FormFieldKind::F32
12900            | FormFieldKind::F64
12901    );
12902
12903    if is_string {
12904        if let Some(min_len) = attrs.min_length {
12905            let min_len_usize = min_len as usize;
12906            checks.push(quote! {
12907                if let ::core::option::Option::Some(__s) = #val_ref {
12908                    if __s.chars().count() < #min_len_usize {
12909                        #errs.add(
12910                            #name_lit,
12911                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12912                        );
12913                    }
12914                }
12915            });
12916        }
12917        if let Some(max_len) = attrs.max_length {
12918            let max_len_usize = max_len as usize;
12919            checks.push(quote! {
12920                if let ::core::option::Option::Some(__s) = #val_ref {
12921                    if __s.chars().count() > #max_len_usize {
12922                        #errs.add(
12923                            #name_lit,
12924                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12925                        );
12926                    }
12927                }
12928            });
12929        }
12930    }
12931
12932    if is_numeric {
12933        if let Some(min) = attrs.min {
12934            checks.push(quote! {
12935                if let ::core::option::Option::Some(__n) = #val_ref {
12936                    if (*__n as f64) < (#min as f64) {
12937                        #errs.add(
12938                            #name_lit,
12939                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12940                        );
12941                    }
12942                }
12943            });
12944        }
12945        if let Some(max) = attrs.max {
12946            checks.push(quote! {
12947                if let ::core::option::Option::Some(__n) = #val_ref {
12948                    if (*__n as f64) > (#max as f64) {
12949                        #errs.add(
12950                            #name_lit,
12951                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12952                        );
12953                    }
12954                }
12955            });
12956        }
12957    }
12958
12959    quote! { #( #checks )* }
12960}
12961
12962// ============================================================
12963//  #[derive(ViewSet)]
12964// ============================================================
12965
12966struct ViewSetAttrs {
12967    model: syn::Path,
12968    fields: Vec<String>,
12969    filter_fields: Vec<String>,
12970    search_fields: Vec<String>,
12971    /// (field_name, desc)
12972    ordering: Vec<(String, bool)>,
12973    page_size: Option<usize>,
12974    read_only: bool,
12975    allow_anonymous: bool,
12976    perms: ViewSetPermsAttrs,
12977    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12978    /// retrieve / create responses through this `#[derive(Serializer)]`
12979    /// type instead of the default field-level projection (requires the
12980    /// `serializer` feature). Tri-dialect.
12981    serializer: Option<syn::Path>,
12982}
12983
12984#[derive(Default)]
12985struct ViewSetPermsAttrs {
12986    list: Vec<String>,
12987    retrieve: Vec<String>,
12988    create: Vec<String>,
12989    update: Vec<String>,
12990    destroy: Vec<String>,
12991}
12992
12993fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12994    let root = rustango_root();
12995    let struct_name = &input.ident;
12996
12997    // Must be a unit struct or an empty named struct.
12998    match &input.data {
12999        Data::Struct(s) => match &s.fields {
13000            Fields::Unit | Fields::Named(_) => {}
13001            Fields::Unnamed(_) => {
13002                return Err(syn::Error::new_spanned(
13003                    struct_name,
13004                    "ViewSet can only be derived on a unit struct or an empty named struct",
13005                ));
13006            }
13007        },
13008        _ => {
13009            return Err(syn::Error::new_spanned(
13010                struct_name,
13011                "ViewSet can only be derived on a struct",
13012            ));
13013        }
13014    }
13015
13016    let attrs = parse_viewset_attrs(input)?;
13017    let model_path = &attrs.model;
13018
13019    let field_lits = attrs.fields.iter().map(|f| f.as_str());
13020
13021    let filter_fields_call = if attrs.filter_fields.is_empty() {
13022        quote!()
13023    } else {
13024        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
13025        quote!(.filter_fields(&[ #(#lits),* ]))
13026    };
13027
13028    let search_fields_call = if attrs.search_fields.is_empty() {
13029        quote!()
13030    } else {
13031        let lits = attrs.search_fields.iter().map(|f| f.as_str());
13032        quote!(.search_fields(&[ #(#lits),* ]))
13033    };
13034
13035    let ordering_call = if attrs.ordering.is_empty() {
13036        quote!()
13037    } else {
13038        let pairs = attrs.ordering.iter().map(|(f, desc)| {
13039            let f = f.as_str();
13040            quote!((#f, #desc))
13041        });
13042        quote!(.ordering(&[ #(#pairs),* ]))
13043    };
13044
13045    let page_size_call = if let Some(n) = attrs.page_size {
13046        quote!(.page_size(#n))
13047    } else {
13048        quote!()
13049    };
13050
13051    let read_only_call = if attrs.read_only {
13052        quote!(.read_only())
13053    } else {
13054        quote!()
13055    };
13056    let allow_anonymous_call = if attrs.allow_anonymous {
13057        quote!(.allow_anonymous())
13058    } else {
13059        quote!()
13060    };
13061
13062    // `.serializer::<S>()` — reshape responses through a derived
13063    // serializer. Requires the downstream crate to enable the
13064    // `serializer` feature (the method is gated on it).
13065    let serializer_call = if let Some(ref ser) = attrs.serializer {
13066        quote!(.serializer::<#ser>())
13067    } else {
13068        quote!()
13069    };
13070
13071    let perms = &attrs.perms;
13072    let perms_call = if perms.list.is_empty()
13073        && perms.retrieve.is_empty()
13074        && perms.create.is_empty()
13075        && perms.update.is_empty()
13076        && perms.destroy.is_empty()
13077    {
13078        quote!()
13079    } else {
13080        let list_lits = perms.list.iter().map(|s| s.as_str());
13081        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
13082        let create_lits = perms.create.iter().map(|s| s.as_str());
13083        let update_lits = perms.update.iter().map(|s| s.as_str());
13084        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
13085        quote! {
13086            .permissions(#root::viewset::ViewSetPerms {
13087                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
13088                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
13089                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
13090                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
13091                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
13092            })
13093        }
13094    };
13095
13096    Ok(quote! {
13097        impl #struct_name {
13098            /// Build an `axum::Router` with the six standard REST endpoints
13099            /// for this ViewSet, mounted at `prefix`.
13100            ///
13101            /// Accepts any backend's pool — a `sqlx::PgPool`,
13102            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
13103            /// enum — because they all `Into<Pool>` (#1273). This is why
13104            /// the derive no longer names `PgPool`, which would not
13105            /// exist on a project built without the `postgres` feature.
13106            pub fn router(
13107                prefix: &str,
13108                pool: impl ::core::convert::Into<#root::sql::Pool>,
13109            ) -> #root::__axum::Router {
13110                #root::viewset::ViewSet::for_model(
13111                    <#model_path as #root::core::Model>::SCHEMA,
13112                    &[ #(#field_lits),* ],
13113                )
13114                    #filter_fields_call
13115                    #search_fields_call
13116                    #ordering_call
13117                    #page_size_call
13118                    #perms_call
13119                    #read_only_call
13120                    #allow_anonymous_call
13121                    #serializer_call
13122                    .router(prefix, pool.into())
13123            }
13124        }
13125    })
13126}
13127
13128fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
13129    let mut model: Option<syn::Path> = None;
13130    let mut fields: Option<Vec<String>> = None;
13131    let mut filter_fields: Vec<String> = Vec::new();
13132    let mut search_fields: Vec<String> = Vec::new();
13133    let mut ordering: Vec<(String, bool)> = Vec::new();
13134    let mut page_size: Option<usize> = None;
13135    let mut read_only = false;
13136    let mut allow_anonymous = false;
13137    let mut perms = ViewSetPermsAttrs::default();
13138    let mut serializer: Option<syn::Path> = None;
13139
13140    for attr in &input.attrs {
13141        if !attr.path().is_ident("viewset") {
13142            continue;
13143        }
13144        attr.parse_nested_meta(|meta| {
13145            if meta.path.is_ident("model") {
13146                let path: syn::Path = meta.value()?.parse()?;
13147                model = Some(path);
13148                return Ok(());
13149            }
13150            if meta.path.is_ident("serializer") {
13151                let path: syn::Path = meta.value()?.parse()?;
13152                serializer = Some(path);
13153                return Ok(());
13154            }
13155            if meta.path.is_ident("fields") {
13156                let s: LitStr = meta.value()?.parse()?;
13157                fields = Some(split_field_list(&s.value()));
13158                return Ok(());
13159            }
13160            if meta.path.is_ident("filter_fields") {
13161                let s: LitStr = meta.value()?.parse()?;
13162                filter_fields = split_field_list(&s.value());
13163                return Ok(());
13164            }
13165            if meta.path.is_ident("search_fields") {
13166                let s: LitStr = meta.value()?.parse()?;
13167                search_fields = split_field_list(&s.value());
13168                return Ok(());
13169            }
13170            if meta.path.is_ident("ordering") {
13171                let s: LitStr = meta.value()?.parse()?;
13172                ordering = parse_ordering_list(&s.value());
13173                return Ok(());
13174            }
13175            if meta.path.is_ident("page_size") {
13176                let lit: syn::LitInt = meta.value()?.parse()?;
13177                page_size = Some(lit.base10_parse::<usize>()?);
13178                return Ok(());
13179            }
13180            if meta.path.is_ident("read_only") {
13181                read_only = true;
13182                return Ok(());
13183            }
13184            if meta.path.is_ident("allow_anonymous") {
13185                allow_anonymous = true;
13186                return Ok(());
13187            }
13188            if meta.path.is_ident("permissions") {
13189                meta.parse_nested_meta(|inner| {
13190                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
13191                        let s: LitStr = inner.value()?.parse()?;
13192                        Ok(split_field_list(&s.value()))
13193                    };
13194                    if inner.path.is_ident("list") {
13195                        perms.list = parse_codenames(&inner)?;
13196                    } else if inner.path.is_ident("retrieve") {
13197                        perms.retrieve = parse_codenames(&inner)?;
13198                    } else if inner.path.is_ident("create") {
13199                        perms.create = parse_codenames(&inner)?;
13200                    } else if inner.path.is_ident("update") {
13201                        perms.update = parse_codenames(&inner)?;
13202                    } else if inner.path.is_ident("destroy") {
13203                        perms.destroy = parse_codenames(&inner)?;
13204                    } else {
13205                        return Err(inner.error(
13206                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
13207                        ));
13208                    }
13209                    Ok(())
13210                })?;
13211                return Ok(());
13212            }
13213            Err(meta.error(
13214                "unknown viewset attribute (supported: model, fields, filter_fields, \
13215                 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
13216                 permissions(...))",
13217            ))
13218        })?;
13219    }
13220
13221    let model = model.ok_or_else(|| {
13222        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
13223    })?;
13224    // The field list is the write allow-list, so it is never implied.
13225    let fields = fields.ok_or_else(|| {
13226        syn::Error::new_spanned(
13227            &input.ident,
13228            "`#[viewset(fields = \"id, title\")]` is required: the fields the API renders and writes",
13229        )
13230    })?;
13231
13232    Ok(ViewSetAttrs {
13233        model,
13234        fields,
13235        filter_fields,
13236        search_fields,
13237        ordering,
13238        page_size,
13239        read_only,
13240        allow_anonymous,
13241        perms,
13242        serializer,
13243    })
13244}
13245
13246// ============================================================ #[derive(Serializer)]
13247
13248struct SerializerContainerAttrs {
13249    model: syn::Path,
13250    /// `#[serializer(validate = "fn_name")]` on the struct — the
13251    /// cross-field validation hook (#436). The named inherent method
13252    /// must take `&self` and return
13253    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
13254    /// `validate()` runs every per-field validator first; then calls
13255    /// the cross-field method if declared; aggregates all errors into
13256    /// one `FormErrors`.
13257    cross_validate: Option<syn::Ident>,
13258}
13259
13260#[derive(Default)]
13261struct SerializerFieldAttrs {
13262    read_only: bool,
13263    write_only: bool,
13264    source: Option<String>,
13265    skip: bool,
13266    /// `#[serializer(method = "fn_name")]` — computed field. The macro
13267    /// emits a `from_model` initializer that calls
13268    /// `Self::fn_name(&model)` and stores the return value.
13269    method: Option<String>,
13270    /// `#[serializer(validate = "fn_name")]` — per-field validator
13271    /// callable run by `Self::validate(&self)`. Must return
13272    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
13273    /// the field name.
13274    validate: Option<String>,
13275    /// `#[serializer(nested)]` on a field whose type is another
13276    /// `Serializer` — the macro emits `from_model` initializer that
13277    /// reads the parent via `model.<source>.value()` then calls the
13278    /// child serializer's `from_model(parent)`. When the FK is
13279    /// unloaded the field falls back to `Default::default()` (does
13280    /// NOT panic) so a missing prefetch in prod degrades gracefully.
13281    /// Source field on the model defaults to the field name; override
13282    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
13283    /// panic-on-unloaded behavior for tests.
13284    nested: bool,
13285    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
13286    /// strict behavior: panic when the FK isn't loaded. Useful in
13287    /// test code where forgetting select_related must trip a hard
13288    /// failure rather than render a blank nested object.
13289    nested_strict: bool,
13290    /// `#[serializer(many = TagSerializer)]` — declare the field as
13291    /// a list of nested serializers. Field type must be `Vec<S>`
13292    /// where `S` is the inner serializer. The macro initializes the
13293    /// field to `Vec::new()` in `from_model` and emits a typed
13294    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13295    /// maps each model row through `S::from_model`. Auto-load isn't
13296    /// possible (the M2M / one-to-many accessor is async); callers
13297    /// fetch the children + call the setter post-from_model.
13298    many: Option<syn::Type>,
13299    /// `#[serializer(slug = "name")]` — render a relation as one of
13300    /// its fields.
13301    /// Source field on the model must be a `ForeignKey<T>`; the
13302    /// macro emits `from_model` glue that walks
13303    /// `model.<source>.value()?.<slug>` and clones it. Field type on
13304    /// the serializer is typically `String` (whatever type the slug
13305    /// column has). When the FK is unloaded the field falls back to
13306    /// `Default::default()`, same graceful-degrade contract as
13307    /// `nested`. Source defaults to the field name; override with
13308    /// `source = "..."`. v0.44.
13309    slug: Option<String>,
13310    /// `#[serializer(max_length = N)]` — caps
13311    /// the character count of a string field on write. Overrides the
13312    /// model's `max_length`; when absent the model value is inherited.
13313    max_length: Option<u64>,
13314    /// `#[serializer(min_length = N)]` — minimum character count.
13315    /// Serializer-only (the model has no `min_length` column).
13316    min_length: Option<u64>,
13317    /// `#[serializer(min = N)]` — inclusive
13318    /// integer lower bound; overrides the model's `min` when given.
13319    min: Option<i64>,
13320    /// `#[serializer(max = N)]` — inclusive
13321    /// integer upper bound; overrides the model's `max` when given.
13322    max: Option<i64>,
13323}
13324
13325fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13326    let mut model: Option<syn::Path> = None;
13327    let mut cross_validate: Option<syn::Ident> = None;
13328    for attr in &input.attrs {
13329        if !attr.path().is_ident("serializer") {
13330            continue;
13331        }
13332        attr.parse_nested_meta(|meta| {
13333            if meta.path.is_ident("model") {
13334                let _eq: syn::Token![=] = meta.input.parse()?;
13335                model = Some(meta.input.parse()?);
13336                return Ok(());
13337            }
13338            if meta.path.is_ident("validate") {
13339                // #436 — container-level `validate = "fn_name"` for
13340                // cross-field validation. Field-level
13341                // `#[serializer(validate = "...")]` on a field is
13342                // parsed separately in `parse_serializer_field_attrs`.
13343                let s: LitStr = meta.value()?.parse()?;
13344                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13345                return Ok(());
13346            }
13347            Err(meta.error(
13348                "unknown serializer container attribute \
13349                 (supported: `model`, `validate`)",
13350            ))
13351        })?;
13352    }
13353    let model = model.ok_or_else(|| {
13354        syn::Error::new_spanned(
13355            &input.ident,
13356            "`#[serializer(model = SomeModel)]` is required",
13357        )
13358    })?;
13359    Ok(SerializerContainerAttrs {
13360        model,
13361        cross_validate,
13362    })
13363}
13364
13365/// The `#[serde(...)]` keys the Serializer derive honours. Its own
13366/// `Serialize` impl would otherwise drop them silently.
13367#[derive(Default)]
13368struct SerdeWireAttrs {
13369    rename_ser: Option<String>,
13370    rename_de: Option<String>,
13371    skip_ser: bool,
13372    skip_de: bool,
13373    skip_ser_if: Option<syn::ExprPath>,
13374}
13375
13376/// A serde `rename_all` rule, applied to a `snake_case` field name.
13377#[derive(Clone, Copy)]
13378enum SerdeRenameRule {
13379    Lower,
13380    Upper,
13381    Pascal,
13382    Camel,
13383    ScreamingSnake,
13384    Kebab,
13385    ScreamingKebab,
13386}
13387
13388impl SerdeRenameRule {
13389    fn parse(s: &LitStr) -> syn::Result<Self> {
13390        Ok(match s.value().as_str() {
13391            "lowercase" | "snake_case" => Self::Lower,
13392            "UPPERCASE" => Self::Upper,
13393            "PascalCase" => Self::Pascal,
13394            "camelCase" => Self::Camel,
13395            "SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
13396            "kebab-case" => Self::Kebab,
13397            "SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
13398            _ => {
13399                return Err(syn::Error::new_spanned(
13400                    s,
13401                    "unknown serde `rename_all` rule",
13402                ))
13403            }
13404        })
13405    }
13406
13407    /// Same output as serde's own field renaming.
13408    fn apply(self, field: &str) -> String {
13409        match self {
13410            Self::Lower => field.to_owned(),
13411            Self::Upper | Self::ScreamingSnake => field.to_ascii_uppercase(),
13412            Self::Pascal | Self::Camel => {
13413                let mut out = String::new();
13414                let mut cap = matches!(self, Self::Pascal);
13415                for ch in field.chars() {
13416                    if ch == '_' {
13417                        cap = !out.is_empty() || matches!(self, Self::Pascal);
13418                    } else if cap {
13419                        out.push(ch.to_ascii_uppercase());
13420                        cap = false;
13421                    } else {
13422                        out.push(ch);
13423                    }
13424                }
13425                out
13426            }
13427            Self::Kebab => field.replace('_', "-"),
13428            Self::ScreamingKebab => field.to_ascii_uppercase().replace('_', "-"),
13429        }
13430    }
13431}
13432
13433/// Consume the value of a `serde` key the derive does not act on.
13434fn skip_serde_meta_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
13435    if meta.input.peek(syn::Token![=]) {
13436        let _: syn::Expr = meta.value()?.parse()?;
13437    } else if meta.input.peek(syn::token::Paren) {
13438        let _: proc_macro2::Group = meta.input.parse()?;
13439    }
13440    Ok(())
13441}
13442
13443/// `rename = "x"` or `rename(serialize = "a", deserialize = "b")`.
13444fn parse_serde_rename(
13445    meta: &syn::meta::ParseNestedMeta,
13446    mut set: impl FnMut(bool, LitStr) -> syn::Result<()>,
13447) -> syn::Result<()> {
13448    if meta.input.peek(syn::Token![=]) {
13449        let s: LitStr = meta.value()?.parse()?;
13450        set(true, s.clone())?;
13451        return set(false, s);
13452    }
13453    meta.parse_nested_meta(|inner| {
13454        let is_ser = inner.path.is_ident("serialize");
13455        if !is_ser && !inner.path.is_ident("deserialize") {
13456            return Err(inner.error("expected `serialize` or `deserialize`"));
13457        }
13458        set(is_ser, inner.value()?.parse()?)
13459    })
13460}
13461
13462/// Container `rename_all` rules: (serialize, deserialize).
13463fn parse_serde_container_rules(
13464    input: &DeriveInput,
13465) -> syn::Result<(Option<SerdeRenameRule>, Option<SerdeRenameRule>)> {
13466    let (mut ser, mut de) = (None, None);
13467    for attr in input.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13468        attr.parse_nested_meta(|meta| {
13469            if meta.path.is_ident("rename_all") {
13470                return parse_serde_rename(&meta, |is_ser, s| {
13471                    let rule = Some(SerdeRenameRule::parse(&s)?);
13472                    if is_ser {
13473                        ser = rule;
13474                    } else {
13475                        de = rule;
13476                    }
13477                    Ok(())
13478                });
13479            }
13480            skip_serde_meta_value(&meta)
13481        })?;
13482    }
13483    Ok((ser, de))
13484}
13485
13486fn parse_serde_field_attrs(field: &syn::Field) -> syn::Result<SerdeWireAttrs> {
13487    let mut out = SerdeWireAttrs::default();
13488    for attr in field.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13489        attr.parse_nested_meta(|meta| {
13490            if meta.path.is_ident("rename") {
13491                return parse_serde_rename(&meta, |is_ser, s| {
13492                    if is_ser {
13493                        out.rename_ser = Some(s.value());
13494                    } else {
13495                        out.rename_de = Some(s.value());
13496                    }
13497                    Ok(())
13498                });
13499            }
13500            if meta.path.is_ident("skip") {
13501                out.skip_ser = true;
13502                out.skip_de = true;
13503                return Ok(());
13504            }
13505            if meta.path.is_ident("skip_serializing") {
13506                out.skip_ser = true;
13507                return Ok(());
13508            }
13509            if meta.path.is_ident("skip_deserializing") {
13510                out.skip_de = true;
13511                return Ok(());
13512            }
13513            if meta.path.is_ident("skip_serializing_if") {
13514                let s: LitStr = meta.value()?.parse()?;
13515                out.skip_ser_if = Some(s.parse()?);
13516                return Ok(());
13517            }
13518            skip_serde_meta_value(&meta)
13519        })?;
13520    }
13521    Ok(out)
13522}
13523
13524fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13525    let mut out = SerializerFieldAttrs::default();
13526    for attr in &field.attrs {
13527        if !attr.path().is_ident("serializer") {
13528            continue;
13529        }
13530        attr.parse_nested_meta(|meta| {
13531            if meta.path.is_ident("read_only") {
13532                out.read_only = true;
13533                return Ok(());
13534            }
13535            if meta.path.is_ident("write_only") {
13536                out.write_only = true;
13537                return Ok(());
13538            }
13539            if meta.path.is_ident("skip") {
13540                out.skip = true;
13541                return Ok(());
13542            }
13543            if meta.path.is_ident("source") {
13544                let s: LitStr = meta.value()?.parse()?;
13545                out.source = Some(s.value());
13546                return Ok(());
13547            }
13548            if meta.path.is_ident("method") {
13549                let s: LitStr = meta.value()?.parse()?;
13550                out.method = Some(s.value());
13551                return Ok(());
13552            }
13553            if meta.path.is_ident("validate") {
13554                let s: LitStr = meta.value()?.parse()?;
13555                out.validate = Some(s.value());
13556                return Ok(());
13557            }
13558            if meta.path.is_ident("many") {
13559                let _eq: syn::Token![=] = meta.input.parse()?;
13560                out.many = Some(meta.input.parse()?);
13561                return Ok(());
13562            }
13563            if meta.path.is_ident("nested") {
13564                out.nested = true;
13565                // Optional strict flag inside parentheses:
13566                //   #[serializer(nested(strict))]
13567                if meta.input.peek(syn::token::Paren) {
13568                    meta.parse_nested_meta(|inner| {
13569                        if inner.path.is_ident("strict") {
13570                            out.nested_strict = true;
13571                            return Ok(());
13572                        }
13573                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13574                    })?;
13575                }
13576                return Ok(());
13577            }
13578            if meta.path.is_ident("slug") {
13579                let s: LitStr = meta.value()?.parse()?;
13580                out.slug = Some(s.value());
13581                return Ok(());
13582            }
13583            if meta.path.is_ident("max_length") {
13584                let lit: syn::LitInt = meta.value()?.parse()?;
13585                out.max_length = Some(lit.base10_parse::<u64>()?);
13586                return Ok(());
13587            }
13588            if meta.path.is_ident("min_length") {
13589                let lit: syn::LitInt = meta.value()?.parse()?;
13590                out.min_length = Some(lit.base10_parse::<u64>()?);
13591                return Ok(());
13592            }
13593            if meta.path.is_ident("min") {
13594                let lit: syn::LitInt = meta.value()?.parse()?;
13595                out.min = Some(lit.base10_parse::<i64>()?);
13596                return Ok(());
13597            }
13598            if meta.path.is_ident("max") {
13599                let lit: syn::LitInt = meta.value()?.parse()?;
13600                out.max = Some(lit.base10_parse::<i64>()?);
13601                return Ok(());
13602            }
13603            Err(meta.error(
13604                "unknown serializer field attribute (supported: \
13605                 `read_only`, `write_only`, `source`, `skip`, `method`, \
13606                 `validate`, `nested`, `many`, `slug`, `max_length`, \
13607                 `min_length`, `min`, `max`)",
13608            ))
13609        })?;
13610    }
13611    // Validate: read_only + write_only is nonsensical
13612    if out.read_only && out.write_only {
13613        return Err(syn::Error::new_spanned(
13614            field,
13615            "a field cannot be both `read_only` and `write_only`",
13616        ));
13617    }
13618    if out.method.is_some() && out.source.is_some() {
13619        return Err(syn::Error::new_spanned(
13620            field,
13621            "`method` and `source` are mutually exclusive — `method` computes \
13622             the value from a method, `source` reads it from a different model field",
13623        ));
13624    }
13625    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13626        return Err(syn::Error::new_spanned(
13627            field,
13628            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13629             — pick one strategy for populating the field",
13630        ));
13631    }
13632    Ok(out)
13633}
13634
13635fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13636    let root = rustango_root();
13637    let struct_name = &input.ident;
13638    let struct_name_lit = struct_name.to_string();
13639
13640    let Data::Struct(data) = &input.data else {
13641        return Err(syn::Error::new_spanned(
13642            struct_name,
13643            "Serializer can only be derived on structs",
13644        ));
13645    };
13646    let Fields::Named(named) = &data.fields else {
13647        return Err(syn::Error::new_spanned(
13648            struct_name,
13649            "Serializer requires a struct with named fields",
13650        ));
13651    };
13652
13653    let container = parse_serializer_container_attrs(input)?;
13654    let model_path = &container.model;
13655
13656    // Classify each field. `ty` is only consumed by the
13657    // `#[cfg(feature = "openapi")]` block below, but we always
13658    // capture it to keep the field-info build a single pass.
13659    #[allow(dead_code)]
13660    struct FieldInfo {
13661        ident: syn::Ident,
13662        ty: syn::Type,
13663        attrs: SerializerFieldAttrs,
13664        serde: SerdeWireAttrs,
13665        /// JSON key in the output / in a request body.
13666        ser_name: String,
13667        de_name: String,
13668    }
13669    let (ser_rule, de_rule) = parse_serde_container_rules(input)?;
13670    let mut fields_info: Vec<FieldInfo> = Vec::new();
13671    for field in &named.named {
13672        let ident = field.ident.clone().expect("named field has ident");
13673        let attrs = parse_serializer_field_attrs(field)?;
13674        let serde = parse_serde_field_attrs(field)?;
13675        let name = ident_name(&ident);
13676        let wire = |rename: &Option<String>, rule: Option<SerdeRenameRule>| {
13677            rename
13678                .clone()
13679                .unwrap_or_else(|| rule.map_or_else(|| name.clone(), |r| r.apply(&name)))
13680        };
13681        fields_info.push(FieldInfo {
13682            ser_name: wire(&serde.rename_ser, ser_rule),
13683            de_name: wire(&serde.rename_de, de_rule),
13684            ident,
13685            ty: field.ty.clone(),
13686            attrs,
13687            serde,
13688        });
13689    }
13690
13691    // Generate from_model body: struct literal with each field assigned.
13692    let from_model_fields = fields_info.iter().map(|fi| {
13693        let ident = &fi.ident;
13694        if let Some(_inner) = &fi.attrs.many {
13695            // Many — collection field. Initialize empty; caller
13696            // populates via the macro-emitted set_<field> helper
13697            // after fetching the M2M children.
13698            quote! { #ident: ::std::vec::Vec::new() }
13699        } else if let Some(method) = &fi.attrs.method {
13700            // SerializerMethodField: call Self::<method>(&model) to
13701            // compute the value. Method signature must be
13702            // `fn <method>(model: &T) -> <field type>`.
13703            let method_ident = syn::Ident::new(method, ident.span());
13704            quote! { #ident: Self::#method_ident(model) }
13705        } else if let Some(slug_field) = &fi.attrs.slug {
13706            // v0.44 — SlugRelatedField. Source defaults to the field
13707            // name on this struct; override via `source = "..."`. The
13708            // source field on the model is expected to be a
13709            // `ForeignKey<T>`; the slug field on the parent is named
13710            // by the attribute value. When the FK is unloaded the
13711            // field falls back to `Default::default()` — same
13712            // graceful-degrade contract as `nested`; a NULL
13713            // `Option<ForeignKey>` renders `None`.
13714            let src_name = fi
13715                .attrs
13716                .source
13717                .as_deref()
13718                .unwrap_or(&ident_name(&fi.ident))
13719                .to_owned();
13720            let src_ident = ident_from_name(&src_name, ident.span());
13721            let slug_ident = syn::Ident::new(slug_field, ident.span());
13722            quote! {
13723                #ident: #root::serializer::LoadedRelation::render(
13724                    &model.#src_ident,
13725                    |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
13726                    ::core::default::Default::default,
13727                )
13728            }
13729        } else if fi.attrs.nested {
13730            // Nested serializer. Source defaults to the field name on
13731            // this struct; override via `source = "..."`. The source
13732            // field on the model is a `ForeignKey<T>` or
13733            // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
13734            //
13735            // Behavior matrix (tweakable per-field):
13736            //   * FK loaded   → nested object materializes via
13737            //                   ChildSerializer::from_model(parent).
13738            //   * FK unloaded → fall back to ChildSerializer::default()
13739            //                   (so prod doesn't crash on a missing
13740            //                   prefetch — just renders a blank nested
13741            //                   object). Add `#[serializer(nested(
13742            //                   strict))]` to keep the v0.18.1
13743            //                   panic-on-unloaded behavior for tests
13744            //                   that want hard guardrails.
13745            //   * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
13746            //                   field (JSON null), strict or not.
13747            let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13748            let src_ident = ident_from_name(&src_name, ident.span());
13749            let unloaded = if fi.attrs.nested_strict {
13750                let panic_msg = format!(
13751                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13752                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13753                );
13754                quote! { || ::core::panic!("{}", #panic_msg) }
13755            } else {
13756                quote! { ::core::default::Default::default }
13757            };
13758            quote! {
13759                #ident: #root::serializer::LoadedRelation::render(
13760                    &model.#src_ident,
13761                    |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
13762                    #unloaded,
13763                )
13764            }
13765        } else if fi.attrs.write_only || fi.attrs.skip {
13766            // Not read from model — use default
13767            quote! { #ident: ::core::default::Default::default() }
13768        } else if let Some(src) = &fi.attrs.source {
13769            let src_ident = ident_from_name(src, ident.span());
13770            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13771        } else {
13772            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13773        }
13774    });
13775
13776    // is_writable predicate (also used by writable_lits /
13777    // writable_source_lits below).
13778    let is_writable = |fi: &&FieldInfo| {
13779        !fi.attrs.read_only
13780            && !fi.attrs.skip
13781            && !fi.serde.skip_de
13782            && fi.attrs.method.is_none()
13783            && !fi.attrs.nested
13784            && fi.attrs.many.is_none()
13785            && fi.attrs.slug.is_none()
13786    };
13787
13788    // Declarative field constraints: one block
13789    // per writable field, run inside `validate()`. Each resolves its
13790    // bounds as the serializer attr when given (`#[serializer(max_length
13791    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13792    // `max` / `choices`), then dispatches on the field's JSON value via
13793    // `forms::validators::check_value` (string → length/choices, integer
13794    // → min/max).
13795    let opt_usize = |v: Option<u64>| match v {
13796        Some(n) => {
13797            let n = n as usize;
13798            quote!(::core::option::Option::Some(#n))
13799        }
13800        None => quote!(::core::option::Option::None),
13801    };
13802    #[cfg(feature = "openapi")]
13803    let opt_u64 = |v: Option<u64>| match v {
13804        Some(n) => quote!(::core::option::Option::Some(#n)),
13805        None => quote!(::core::option::Option::None),
13806    };
13807    let opt_i64 = |v: Option<i64>| match v {
13808        Some(n) => quote!(::core::option::Option::Some(#n)),
13809        None => quote!(::core::option::Option::None),
13810    };
13811    let constraint_blocks: Vec<_> = fields_info
13812        .iter()
13813        .filter(is_writable)
13814        .map(|fi| {
13815            let ident = &fi.ident;
13816            let fname = &fi.de_name;
13817            let mname = fi
13818                .attrs
13819                .source
13820                .clone()
13821                .unwrap_or_else(|| ident_name(&fi.ident));
13822            let attr_max_len = opt_usize(fi.attrs.max_length);
13823            let attr_min_len = opt_usize(fi.attrs.min_length);
13824            let attr_min = opt_i64(fi.attrs.min);
13825            let attr_max = opt_i64(fi.attrs.max);
13826            // A bound the field's type cannot take is a compile error, not a no-op.
13827            let ty = &fi.ty;
13828            let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
13829                .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
13830            let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
13831                .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
13832            quote! {
13833                #len_check
13834                #num_check
13835                if __sent(#fname) {
13836                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13837                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13838                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13839                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13840                    let __min: ::core::option::Option<i64> =
13841                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13842                    let __max: ::core::option::Option<i64> =
13843                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13844                    let __choices = __sf.and_then(|__f| __f.choices);
13845                    let __v = #root::__serde_json::to_value(&self.#ident)
13846                        .unwrap_or(#root::__serde_json::Value::Null);
13847                    #root::forms::validators::check_value(
13848                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13849                        &mut __errors,
13850                    );
13851                }
13852            }
13853        })
13854        .collect();
13855    let has_constraints = !constraint_blocks.is_empty();
13856
13857    // Per-field validators. Emit a
13858    // `validate(&self)` method that runs each user-defined validator
13859    // and aggregates errors into `FormErrors`.
13860    let validator_calls: Vec<_> = fields_info
13861        .iter()
13862        .filter_map(|fi| {
13863            let ident = &fi.ident;
13864            let name_lit = &fi.de_name;
13865            let method = fi.attrs.validate.as_ref()?;
13866            let method_ident = syn::Ident::new(method, ident.span());
13867            Some(quote! {
13868                if __sent(#name_lit) {
13869                    if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13870                        __errors.add(#name_lit.to_owned(), __e);
13871                    }
13872                }
13873            })
13874        })
13875        .collect();
13876    // #436 — cross-field `validate(&self)`. If the
13877    // container declared `#[serializer(validate = "fn_name")]`,
13878    // the macro-generated `validate(&self)` runs every per-field
13879    // validator first, then calls the user's cross-field method,
13880    // merging its `FormErrors` into the per-field errors. Either
13881    // alone is enough to emit the wrapper.
13882    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13883        quote! {
13884            // Merge cross-field errors into the per-field bucket so
13885            // a single .validate() call surfaces both layers.
13886            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13887                __errors.merge(__cross);
13888            }
13889        }
13890    });
13891    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13892    // Anything to run? Declarative constraints (for any writable field) +
13893    // per-field validators + cross-field hook.
13894    let has_run_validations = has_validators || has_constraints;
13895    // Shared body: declarative field constraints first (length / range /
13896    // choices, serializer-attr-or-model), then per-field validators, then
13897    // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13898    let checks = quote! {
13899        let mut __errors = #root::forms::FormErrors::default();
13900        #( #constraint_blocks )*
13901        #( #validator_calls )*
13902        #cross_validate_call
13903        if __errors.is_empty() {
13904            ::core::result::Result::Ok(())
13905        } else {
13906            ::core::result::Result::Err(__errors)
13907        }
13908    };
13909    let validate_body = quote! {
13910        let __sent = |_: &str| true;
13911        #checks
13912    };
13913    // PATCH: only the keys the body carries.
13914    let validate_patch_body = quote! {
13915        let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13916        #checks
13917    };
13918    // Inherent `validate(&self)` — kept for back-compat with direct
13919    // `serializer.validate()` calls that don't import `ModelSerializer`.
13920    // Emitted only when the serializer declares per-field/cross-field
13921    // validators (unchanged rule) so it never collides with a
13922    // hand-written inherent `validate`.
13923    let validate_method = if has_validators {
13924        quote! {
13925            impl #struct_name {
13926                /// Run the declarative field constraints, every
13927                /// `#[serializer(validate = "...")]` per-field validator,
13928                /// and (when declared) the container-level cross-field
13929                /// validator. Aggregates errors into `FormErrors` keyed by
13930                /// the field name. Returns `Ok(())` when all pass.
13931                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13932                    #validate_body
13933                }
13934            }
13935        }
13936    } else {
13937        quote! {}
13938    };
13939    // Trait-method override so the type-erased ViewSet write path can
13940    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13941    // there's anything to run (declarative constraints inherited from the
13942    // model count); otherwise the trait's default no-op is used.
13943    let trait_validate_override = if has_run_validations {
13944        quote! {
13945            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13946                #validate_body
13947            }
13948        }
13949    } else {
13950        quote! {}
13951    };
13952
13953    // For every `#[serializer(many = S)]` field, emit a
13954    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13955    // helper that maps the parents through `S::from_model`.
13956    let many_setters: Vec<_> = fields_info
13957        .iter()
13958        .filter_map(|fi| {
13959            let many_ty = fi.attrs.many.as_ref()?;
13960            let ident = &fi.ident;
13961            let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13962            Some(quote! {
13963                /// Populate this `many` field by mapping each parent model
13964                /// through the inner serializer's `from_model`. Call after
13965                /// fetching the M2M / one-to-many children since
13966                /// `from_model` itself can't await an SQL query.
13967                pub fn #setter(
13968                    &mut self,
13969                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13970                ) -> &mut Self {
13971                    self.#ident = models.iter()
13972                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13973                        .collect();
13974                    self
13975                }
13976            })
13977        })
13978        .collect();
13979    let many_setters_impl = if many_setters.is_empty() {
13980        quote! {}
13981    } else {
13982        quote! {
13983            impl #struct_name {
13984                #( #many_setters )*
13985            }
13986        }
13987    };
13988
13989    // Generate custom Serialize: skip write_only and serde-skipped fields
13990    let output_fields: Vec<_> = fields_info
13991        .iter()
13992        .filter(|fi| !fi.attrs.write_only && !fi.serde.skip_ser)
13993        .collect();
13994    // `skip_serializing_if` fields count only when rendered.
13995    let output_field_count = output_fields.iter().map(|fi| {
13996        let ident = &fi.ident;
13997        if let Some(pred) = &fi.serde.skip_ser_if {
13998            quote! { + if #pred(&self.#ident) { 0 } else { 1 } }
13999        } else {
14000            quote! { + 1 }
14001        }
14002    });
14003    let output_field_count = quote! { 0usize #( #output_field_count )* };
14004    let serialize_fields = output_fields.iter().map(|fi| {
14005        let ident = &fi.ident;
14006        let name_lit = &fi.ser_name;
14007        if let Some(pred) = &fi.serde.skip_ser_if {
14008            quote! {
14009                if #pred(&self.#ident) {
14010                    __state.skip_field(#name_lit)?;
14011                } else {
14012                    __state.serialize_field(#name_lit, &self.#ident)?;
14013                }
14014            }
14015        } else {
14016            quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
14017        }
14018    });
14019
14020    // writable_fields: normal + write_only.
14021    // Exclude:
14022    //   - `read_only` — server-computed.
14023    //   - `skip` — caller sets manually post-from_model.
14024    //   - `method` — computed from a Self::fn(&model) call; accepting
14025    //     it on write is meaningless.
14026    //   - `nested` / `many` — populated from related-model data, not
14027    //     from a field on the wire body.
14028    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
14029    // `many` in `writable_fields()`, which made the ViewSet write
14030    // path accept those fields from the JSON body and try to bind
14031    // them to the SQL UPDATE — a silent no-op at best, a type
14032    // mismatch at worst.
14033    // (`is_writable` is defined above, near the constraint codegen.)
14034    let writable_lits: Vec<_> = fields_info
14035        .iter()
14036        .filter(is_writable)
14037        .map(|fi| fi.de_name.clone())
14038        .collect();
14039
14040    // `writable_source_fields`: the MODEL field names of writable
14041    // serializer fields (source-resolved). The ViewSet write path skips
14042    // every model column NOT in this set, so `read_only` / `method` /
14043    // computed fields a client posts are ignored instead of written.
14044    let writable_source_lits: Vec<String> = fields_info
14045        .iter()
14046        .filter(is_writable)
14047        .map(|fi| {
14048            fi.attrs
14049                .source
14050                .clone()
14051                .unwrap_or_else(|| ident_name(&fi.ident))
14052        })
14053        .collect();
14054
14055    // `readable_source_fields`: model columns the JSON output copies
14056    // straight from the model, so `?ordering=` cannot sort on a column
14057    // the API never shows (#1845).
14058    let readable: Vec<&FieldInfo> = fields_info
14059        .iter()
14060        .filter(|fi| {
14061            !fi.attrs.write_only
14062                && !fi.serde.skip_ser
14063                && !fi.attrs.skip
14064                && fi.attrs.method.is_none()
14065                && !fi.attrs.nested
14066                && fi.attrs.many.is_none()
14067                && fi.attrs.slug.is_none()
14068        })
14069        .collect();
14070    let readable_source_lits: Vec<String> = readable
14071        .iter()
14072        .map(|fi| {
14073            fi.attrs
14074                .source
14075                .clone()
14076                .unwrap_or_else(|| ident_name(&fi.ident))
14077        })
14078        .collect();
14079    // Their JSON keys, so the ViewSet finds a renamed cursor column.
14080    let readable_lits: Vec<String> = readable.iter().map(|fi| fi.ser_name.clone()).collect();
14081
14082    // `from_writable_json`: build a partial instance for input
14083    // validation. Writable fields are parsed from the JSON body (keyed
14084    // by serializer field name); every other field defaults. Per-field
14085    // type errors collect into `FormErrors` keyed by the field name.
14086    // Construction is field-by-field (not `Self::default()`) so it works
14087    // for serializers regardless of a struct-level `Default`.
14088    let from_writable_json_inits: Vec<_> = fields_info
14089        .iter()
14090        .map(|fi| {
14091            let ident = &fi.ident;
14092            let fname = &fi.de_name;
14093            let ty = &fi.ty;
14094            if is_writable(&fi) {
14095                quote! {
14096                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
14097                        ::core::option::Option::Some(__v) => {
14098                            match #root::__serde_json::from_value::<#ty>(
14099                                ::core::clone::Clone::clone(__v),
14100                            ) {
14101                                ::core::result::Result::Ok(__x) => __x,
14102                                ::core::result::Result::Err(__e) => {
14103                                    __errors.add(#fname.to_owned(), __e.to_string());
14104                                    ::core::default::Default::default()
14105                                }
14106                            }
14107                        }
14108                        ::core::option::Option::None => ::core::default::Default::default(),
14109                    }
14110                }
14111            } else {
14112                quote! { #ident: ::core::default::Default::default() }
14113            }
14114        })
14115        .collect();
14116    // `validate_patch`: overlay each sent writable field on the stored row.
14117    let patch_overlays: Vec<_> = fields_info
14118        .iter()
14119        .filter(is_writable)
14120        .map(|fi| {
14121            let ident = &fi.ident;
14122            let fname = &fi.de_name;
14123            let ty = &fi.ty;
14124            quote! {
14125                if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
14126                    match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
14127                        ::core::result::Result::Ok(__x) => __merged.#ident = __x,
14128                        ::core::result::Result::Err(__e) => {
14129                            __errors.add(#fname.to_owned(), __e.to_string());
14130                        }
14131                    }
14132                }
14133            }
14134        })
14135        .collect();
14136    let patch_validate_call = if has_run_validations {
14137        quote! { __merged.__rustango_validate_patch(__obj) }
14138    } else {
14139        quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
14140    };
14141    let patch_validate_helper = if has_run_validations {
14142        quote! {
14143            impl #struct_name {
14144                #[doc(hidden)]
14145                fn __rustango_validate_patch(
14146                    &self,
14147                    __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
14148                ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14149                    #validate_patch_body
14150                }
14151            }
14152        }
14153    } else {
14154        quote! {}
14155    };
14156
14157    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
14158    // Only includes fields shown in JSON output (skips write_only). For each
14159    // `Option<T>` field, omit from `required` and add `.nullable()`.
14160    let openapi_impl = {
14161        #[cfg(feature = "openapi")]
14162        {
14163            let property_calls = output_fields.iter().map(|fi| {
14164                let name_lit = &fi.ser_name;
14165                let ty = &fi.ty;
14166                let nullable_call = if is_option(ty) {
14167                    quote! { .nullable() }
14168                } else {
14169                    quote! {}
14170                };
14171                // The bounds `validate()` enforces on a written field.
14172                let limits_call = is_writable(fi).then(|| {
14173                    let mname = fi
14174                        .attrs
14175                        .source
14176                        .clone()
14177                        .unwrap_or_else(|| ident_name(&fi.ident));
14178                    let (a, b, c, d) = (
14179                        opt_u64(fi.attrs.max_length),
14180                        opt_u64(fi.attrs.min_length),
14181                        opt_i64(fi.attrs.min),
14182                        opt_i64(fi.attrs.max),
14183                    );
14184                    quote! {
14185                        .field_limits(
14186                            <#model_path as #root::core::Model>::SCHEMA.field(#mname),
14187                            #a, #b, #c, #d,
14188                        )
14189                    }
14190                });
14191                quote! {
14192                    .property(
14193                        #name_lit,
14194                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
14195                            #limits_call
14196                            #nullable_call,
14197                    )
14198                }
14199            });
14200            let required_lits: Vec<_> = output_fields
14201                .iter()
14202                .filter(|fi| !is_option(&fi.ty) && fi.serde.skip_ser_if.is_none())
14203                .map(|fi| fi.ser_name.clone())
14204                .collect();
14205            quote! {
14206                impl #root::openapi::OpenApiSchema for #struct_name {
14207                    fn openapi_schema() -> #root::openapi::Schema {
14208                        #root::openapi::Schema::object()
14209                            #( #property_calls )*
14210                            .required([ #( #required_lits ),* ])
14211                    }
14212                }
14213            }
14214        }
14215        #[cfg(not(feature = "openapi"))]
14216        {
14217            quote! {}
14218        }
14219    };
14220
14221    // The other tables a `nested`, `many` or `slug` field renders rows of.
14222    let related_tables = fields_info.iter().filter_map(|fi| {
14223        let name = &fi.ser_name;
14224        if let Some(inner) = &fi.attrs.many {
14225            return Some(quote! {
14226                (#name, <<#inner as #root::serializer::ModelSerializer>::Model
14227                    as #root::core::Model>::SCHEMA.table)
14228            });
14229        }
14230        if !(fi.attrs.nested || fi.attrs.slug.is_some()) {
14231            return None;
14232        }
14233        let src_name = fi
14234            .attrs
14235            .source
14236            .as_deref()
14237            .unwrap_or(&ident_name(&fi.ident))
14238            .to_owned();
14239        let src_ident = ident_from_name(&src_name, fi.ident.span());
14240        Some(quote! {
14241            (#name, #root::serializer::relation_table(|__m: &#model_path| &__m.#src_ident))
14242        })
14243    });
14244
14245    Ok(quote! {
14246        impl #root::serializer::ModelSerializer for #struct_name {
14247            type Model = #model_path;
14248
14249            fn related_tables() -> ::std::vec::Vec<(&'static str, &'static str)> {
14250                ::std::vec![ #( #related_tables ),* ]
14251            }
14252
14253            fn from_model(model: &Self::Model) -> Self {
14254                Self {
14255                    #( #from_model_fields ),*
14256                }
14257            }
14258
14259            fn writable_fields() -> &'static [&'static str] {
14260                &[ #( #writable_lits ),* ]
14261            }
14262
14263            fn writable_source_fields() -> &'static [&'static str] {
14264                &[ #( #writable_source_lits ),* ]
14265            }
14266
14267            fn readable_source_fields() -> &'static [&'static str] {
14268                &[ #( #readable_source_lits ),* ]
14269            }
14270
14271            fn readable_fields() -> &'static [&'static str] {
14272                &[ #( #readable_lits ),* ]
14273            }
14274
14275            fn from_writable_json(
14276                __body: &#root::__serde_json::Value,
14277            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
14278                let mut __errors = #root::forms::FormErrors::default();
14279                let __obj = __body.as_object();
14280                let __out = Self {
14281                    #( #from_writable_json_inits ),*
14282                };
14283                if __errors.is_empty() {
14284                    ::core::result::Result::Ok(__out)
14285                } else {
14286                    ::core::result::Result::Err(__errors)
14287                }
14288            }
14289
14290            fn validate_patch(
14291                __stored: &Self::Model,
14292                __body: &#root::__serde_json::Value,
14293            ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14294                let mut __errors = #root::forms::FormErrors::default();
14295                let __obj = __body.as_object();
14296                #[allow(unused_mut)]
14297                let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
14298                #( #patch_overlays )*
14299                if !__errors.is_empty() {
14300                    return ::core::result::Result::Err(__errors);
14301                }
14302                #patch_validate_call
14303            }
14304
14305            #trait_validate_override
14306        }
14307
14308        #patch_validate_helper
14309
14310        impl #root::__serde::Serialize for #struct_name {
14311            fn serialize<S>(&self, serializer: S)
14312                -> ::core::result::Result<S::Ok, S::Error>
14313            where
14314                S: #root::__serde::Serializer,
14315            {
14316                use #root::__serde::ser::SerializeStruct;
14317                let mut __state = serializer.serialize_struct(
14318                    #struct_name_lit,
14319                    #output_field_count,
14320                )?;
14321                #( #serialize_fields )*
14322                __state.end()
14323            }
14324        }
14325
14326        #openapi_impl
14327
14328        #validate_method
14329
14330        #many_setters_impl
14331    })
14332}
14333
14334/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
14335/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
14336/// when the feature is off.
14337#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
14338fn is_option(ty: &syn::Type) -> bool {
14339    if let syn::Type::Path(p) = ty {
14340        if let Some(last) = p.path.segments.last() {
14341            return last.ident == "Option";
14342        }
14343    }
14344    false
14345}
14346
14347#[cfg(test)]
14348mod main_attr_tests {
14349    use super::*;
14350
14351    fn expand(args: &str) -> String {
14352        let body = quote! { async fn main() { run().await } };
14353        expand_main(args.parse().unwrap(), body)
14354            .expect("expansion failed")
14355            .to_string()
14356    }
14357
14358    /// Asserts on the expansion rather than on `parse_main_args`,
14359    /// because the defect was a subscriber reaching the output, not
14360    /// an argument being misread (#1465).
14361    #[test]
14362    fn logging_false_emits_no_subscriber() {
14363        let out = expand("logging = false");
14364        assert!(
14365            !out.contains("logging :: Setup"),
14366            "`logging = false` must not install a subscriber:\n{out}",
14367        );
14368        assert!(
14369            out.contains("block_on"),
14370            "the runtime must still be built:\n{out}",
14371        );
14372    }
14373
14374    #[test]
14375    fn default_still_installs_a_subscriber() {
14376        // The control. Without it, an `expand_main` that dropped the
14377        // prologue unconditionally would pass the test above while
14378        // leaving every default project with no logging at all.
14379        let out = expand("");
14380        assert!(
14381            out.contains("logging :: Setup"),
14382            "the default entrypoint must still install one:\n{out}",
14383        );
14384        // `./app serve` or `./app --port 8080` must keep logging to stdout:
14385        // only `Cli`, which knows its verbs, moves logs to stderr.
14386        assert!(
14387            !out.contains("args ()"),
14388            "the entrypoint must not route logs by argv:\n{out}",
14389        );
14390    }
14391
14392    #[test]
14393    fn logging_false_composes_with_flavor() {
14394        let out = expand("flavor = \"current_thread\", logging = false");
14395        assert!(!out.contains("logging :: Setup"), "{out}");
14396        assert!(out.contains("new_current_thread"), "{out}");
14397    }
14398
14399    /// The body runs on the big-stack thread; `stack_size` overrides it.
14400    #[test]
14401    fn body_runs_on_a_big_stack_thread() {
14402        let out = expand("");
14403        assert!(out.contains("run_on_main_thread"), "{out}");
14404        assert!(out.contains("MAIN_STACK_SIZE"), "{out}");
14405        let out = expand("stack_size = 16777216, logging = false");
14406        assert!(out.contains("16777216usize"), "{out}");
14407        assert!(!out.contains("MAIN_STACK_SIZE"), "{out}");
14408        assert!(
14409            out.contains("thread_stack_size"),
14410            "workers get it too:\n{out}"
14411        );
14412    }
14413
14414    /// A stack too small to run anything is a compile error.
14415    #[test]
14416    fn a_tiny_stack_size_is_refused() {
14417        let body = quote! { async fn main() {} };
14418        let err = expand_main("stack_size = 4096".parse().unwrap(), body)
14419            .expect_err("4 KiB")
14420            .to_string();
14421        assert!(err.contains("at least"), "{err}");
14422    }
14423
14424    /// Lint attributes reach the inner body fn; others do not.
14425    #[test]
14426    fn lint_attributes_reach_the_body() {
14427        let body = quote! {
14428            #[allow(unused_variables)]
14429            #[doc = "entry"]
14430            async fn main() { let x = 1; }
14431        };
14432        let out = expand_main(quote!(), body).unwrap().to_string();
14433        assert_eq!(out.matches("allow (unused_variables)").count(), 2, "{out}");
14434        assert_eq!(out.matches("doc = \"entry\"").count(), 1, "{out}");
14435    }
14436
14437    #[test]
14438    fn logging_true_is_the_default_shape() {
14439        assert!(
14440            parse_main_args(&"logging = true".parse().unwrap())
14441                .unwrap()
14442                .logging
14443        );
14444        assert!(
14445            !parse_main_args(&"logging=false".parse().unwrap())
14446                .unwrap()
14447                .logging
14448        );
14449    }
14450}
14451
14452#[cfg(test)]
14453mod main_arg_tests {
14454    use super::*;
14455
14456    fn expand(args: &str) -> syn::Result<TokenStream2> {
14457        let body = quote! { async fn main() { run().await } };
14458        expand_main(args.parse().unwrap(), body)
14459    }
14460
14461    /// A typo or unknown key is a compile error, not silently ignored.
14462    #[test]
14463    fn unknown_or_misspelt_args_are_errors() {
14464        for args in [
14465            "logging = flase",
14466            "multi_thread",
14467            "workers = 4",
14468            r#"flavor = "curent_thread""#,
14469            "worker_threads = 4",
14470        ] {
14471            let err = expand(args).expect_err(args).to_string();
14472            assert!(err.contains("#[rustango::main]"), "{args}: {err}");
14473        }
14474    }
14475
14476    #[test]
14477    fn supported_args_expand() {
14478        for args in [
14479            "",
14480            r#"flavor = "current_thread""#,
14481            r#"flavor = "multi_thread", logging = false"#,
14482            "logging = true",
14483        ] {
14484            assert!(expand(args).is_ok(), "{args}");
14485        }
14486    }
14487}
14488
14489#[cfg(test)]
14490mod embed_migrations_tests {
14491    use super::*;
14492
14493    fn expand(path: &str) -> String {
14494        expand_embed_migrations(quote! { #path })
14495            .expect("expansion failed")
14496            .to_string()
14497    }
14498
14499    #[test]
14500    fn missing_dir_warns() {
14501        let out = expand("./no_such_migrations_dir");
14502        assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
14503        assert!(out.contains("no_such_migrations_dir"), "{out}");
14504    }
14505
14506    #[test]
14507    fn existing_dir_does_not_warn() {
14508        let out = expand("./src");
14509        assert!(!out.contains("deprecated"), "{out}");
14510    }
14511}
14512
14513#[cfg(test)]
14514mod field_attr_tests {
14515    use super::*;
14516
14517    fn error_for(field: syn::Field) -> Option<String> {
14518        let attrs = parse_field_attrs(&field).unwrap();
14519        let kind = detect_type(&field.ty).unwrap().kind;
14520        check_bound_compatibility(&field, &attrs, kind)
14521            .err()
14522            .map(|e| e.to_string())
14523    }
14524
14525    /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
14526    #[test]
14527    fn type_specific_attrs_reject_other_types() {
14528        let f: syn::FieldsNamed = syn::parse_quote!({
14529            #[rustango(citext)] a: i64,
14530            #[rustango(vector(dims = 3))] b: String,
14531            #[rustango(geometry(srid = 4326))] c: String,
14532            #[rustango(citext)] d: String,
14533        });
14534        let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
14535        assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
14536        assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
14537        assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
14538        assert_eq!(errs[3], None);
14539    }
14540
14541    /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
14542    #[test]
14543    fn manager_ext_is_refused() {
14544        let input: DeriveInput = syn::parse_quote! {
14545            #[rustango(table = "p", manager(ext = "PostManagerExt"))]
14546            struct Post { id: i64 }
14547        };
14548        let err = parse_container_attrs(&input).err().expect("refused");
14549        assert!(err.to_string().contains("#2132"), "{err}");
14550    }
14551
14552    /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
14553    #[test]
14554    fn set_null_needs_an_optional_foreign_key() {
14555        let input: DeriveInput = syn::parse_quote! {
14556            #[rustango(table = "c")]
14557            struct Child {
14558                #[rustango(primary_key)] id: i64,
14559                #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
14560            }
14561        };
14562        let err = expand(&input).err().expect("refused");
14563        assert!(err.to_string().contains("set_null"), "{err}");
14564        let input: DeriveInput = syn::parse_quote! {
14565            #[rustango(table = "c")]
14566            struct Child {
14567                #[rustango(primary_key)] id: i64,
14568                #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
14569            }
14570        };
14571        assert!(expand(&input).is_ok());
14572    }
14573
14574    /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
14575    #[test]
14576    fn set_null_needs_a_nullable_fk_column() {
14577        let input: DeriveInput = syn::parse_quote! {
14578            #[rustango(table = "c")]
14579            struct Child {
14580                #[rustango(primary_key)] id: i64,
14581                #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
14582                #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
14583            }
14584        };
14585        let err = expand(&input).err().expect("refused");
14586        assert!(err.to_string().contains("set_null"), "{err}");
14587        let input: DeriveInput = syn::parse_quote! {
14588            #[rustango(table = "c")]
14589            struct Child {
14590                #[rustango(primary_key)] id: i64,
14591                #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
14592                #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
14593                #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
14594            }
14595        };
14596        assert!(expand(&input).is_ok());
14597    }
14598
14599    /// An index or `get_latest_by` naming no column is a compile error,
14600    /// one per name.
14601    #[test]
14602    fn unknown_index_columns_are_errors() {
14603        let input: DeriveInput = syn::parse_quote! {
14604            #[rustango(
14605                table = "w",
14606                unique_together = "a, nope",
14607                index_together = "a, b_typo",
14608                index("missing"),
14609                get_latest_by = "-when_typo"
14610            )]
14611            struct W { #[rustango(primary_key)] id: i64, a: i64, b: i64 }
14612        };
14613        let err = expand(&input).expect_err("must not compile");
14614        let all: Vec<String> = err.into_iter().map(|e| e.to_string()).collect();
14615        for name in ["nope", "b_typo", "missing", "when_typo"] {
14616            assert!(
14617                all.iter().any(|m| m.contains(&format!("`{name}`"))),
14618                "{name}: {all:?}"
14619            );
14620        }
14621    }
14622
14623    /// Real columns, field names for `get_latest_by` and expression
14624    /// indexes compile.
14625    #[test]
14626    fn known_index_columns_compile() {
14627        let input: DeriveInput = syn::parse_quote! {
14628            #[rustango(
14629                table = "w",
14630                unique_together = "a, b_col",
14631                index("lower(a)"),
14632                get_latest_by = "-b"
14633            )]
14634            struct W {
14635                #[rustango(primary_key)] id: i64,
14636                a: i64,
14637                #[rustango(column = "b_col")] b: i64,
14638            }
14639        };
14640        expand(&input).expect("compiles");
14641    }
14642
14643    /// Two FKs to one parent would emit the same reverse accessor twice.
14644    #[test]
14645    fn reverse_accessor_clash_names_related_name() {
14646        let input: DeriveInput = syn::parse_quote! {
14647            #[rustango(table = "m")]
14648            struct Message {
14649                #[rustango(primary_key)] id: i64,
14650                sender: ForeignKey<User>,
14651                recipient: ForeignKey<User>,
14652            }
14653        };
14654        let err = expand(&input).err().expect("refused");
14655        assert!(err.to_string().contains("related_name"), "{err}");
14656    }
14657
14658    /// `related_name = "+"` emits no reverse accessor, so a parent from
14659    /// another crate, which cannot take an inherent impl, still derives.
14660    #[test]
14661    fn related_name_plus_skips_the_reverse_accessor() {
14662        let input: DeriveInput = syn::parse_quote! {
14663            #[rustango(table = "m")]
14664            struct Message {
14665                #[rustango(primary_key)] id: i64,
14666                sender: ForeignKey<User>,
14667                #[rustango(related_name = "+")] recipient: ForeignKey<User>,
14668                #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
14669            }
14670        };
14671        let out = expand(&input).expect("derives").to_string();
14672        assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
14673        assert!(!out.contains("impl other :: Account {"), "{out}");
14674    }
14675
14676    fn field_error(src: &str) -> Option<String> {
14677        let f: syn::FieldsNamed = syn::parse_str(&format!("{{ {src} }}")).unwrap();
14678        let field = f.named.into_iter().next().unwrap();
14679        match parse_field_attrs(&field) {
14680            Err(e) => Some(e.to_string()),
14681            Ok(_) => error_for(field),
14682        }
14683    }
14684
14685    /// Flag attrs take `= true` like `blank`; `= false` is refused by name.
14686    #[test]
14687    fn flag_attrs_accept_eq_true() {
14688        for key in [
14689            "primary_key",
14690            "unique",
14691            "soft_delete",
14692            "index",
14693            "auto_now",
14694            "auto_now_add",
14695        ] {
14696            let f: syn::FieldsNamed =
14697                syn::parse_str(&format!("{{ #[rustango({key} = true)] a: i64 }}")).unwrap();
14698            let field = f.named.into_iter().next().unwrap();
14699            assert!(parse_field_attrs(&field).is_ok(), "{key} = true");
14700            let err = field_error(&format!("#[rustango({key} = false)] a: i64")).unwrap();
14701            assert!(err.contains(&format!("`{key} = false`")), "{err}");
14702        }
14703        let f: syn::Field =
14704            syn::parse_quote!(#[rustango(primary_key = true, unique = true)] a: i64);
14705        let attrs = parse_field_attrs(&f).unwrap();
14706        assert!(attrs.primary_key && attrs.unique);
14707    }
14708
14709    /// An unknown key is named, the nearest key suggested and the list shown.
14710    #[test]
14711    fn unknown_key_is_named_with_a_suggestion() {
14712        let err = field_error("#[rustango(max_lenght = 5)] a: String").unwrap();
14713        assert!(err.contains("`max_lenght`"), "{err}");
14714        assert!(err.contains("did you mean `max_length`"), "{err}");
14715        assert!(err.contains("Supported: column"), "{err}");
14716        let input: DeriveInput = syn::parse_quote! {
14717            #[rustango(tabel = "p")]
14718            struct P { id: i64 }
14719        };
14720        let err = parse_container_attrs(&input).err().unwrap().to_string();
14721        assert!(
14722            err.contains("`tabel`") && err.contains("did you mean `table`"),
14723            "{err}"
14724        );
14725        let err = field_error("#[rustango(zzzzzz)] a: i64").unwrap();
14726        assert!(!err.contains("did you mean"), "{err}");
14727    }
14728
14729    /// The listed keys are the parsed keys; a stale list would mislead.
14730    #[test]
14731    fn listed_keys_are_all_known() {
14732        for key in FIELD_ATTR_KEYS {
14733            let err = field_error(&format!("#[rustango({key})] a: i64")).unwrap_or_default();
14734            assert!(!err.contains("unknown rustango field"), "{key}: {err}");
14735        }
14736        for key in CONTAINER_ATTR_KEYS {
14737            let input: DeriveInput =
14738                syn::parse_str(&format!("#[rustango({key})] struct P {{ id: i64 }}")).unwrap();
14739            let err = parse_container_attrs(&input)
14740                .err()
14741                .map(|e| e.to_string())
14742                .unwrap_or_default();
14743            assert!(!err.contains("unknown rustango container"), "{key}: {err}");
14744        }
14745    }
14746
14747    /// `default` is raw SQL: a bare word or a non-number on a numeric field is refused.
14748    #[test]
14749    fn default_shapes_only_sqlite_takes_are_refused() {
14750        for bad in [
14751            r#"#[rustango(default = "draft")] s: String"#,
14752            r#"#[rustango(default = "draft")] s: Option<String>"#,
14753            r#"#[rustango(default = "abc")] n: i32"#,
14754            r#"#[rustango(default = "'abc'")] n: i64"#,
14755            r#"#[rustango(default = "true")] n: i32"#,
14756            r#"#[rustango(default = "")] n: i32"#,
14757            r#"#[rustango(default = "'x'")] n: f64"#,
14758        ] {
14759            assert!(field_error(bad).is_some(), "{bad} must be refused");
14760        }
14761        let err = field_error(r#"#[rustango(default = "draft")] s: String"#).unwrap();
14762        assert!(err.contains(r#"default = "'draft'""#), "{err}");
14763        // Valid on Postgres alone, so the message says that, not "a column".
14764        let err = field_error(r#"#[rustango(default = "current_user")] s: String"#).unwrap();
14765        assert!(
14766            err.contains("Postgres only") && !err.contains("column"),
14767            "{err}"
14768        );
14769        for good in [
14770            r#"#[rustango(default = "'draft'")] s: String"#,
14771            r#"#[rustango(default = "")] s: String"#,
14772            r#"#[rustango(default = "now()")] s: String"#,
14773            r#"#[rustango(default = "CURRENT_TIMESTAMP")] s: String"#,
14774            r#"#[rustango(default = "NULL")] s: Option<String>"#,
14775            r#"#[rustango(default = "0")] n: i32"#,
14776            r#"#[rustango(default = "-1")] n: i64"#,
14777            r#"#[rustango(default = "'5'")] n: i64"#,
14778            r#"#[rustango(default = "(1 + 2)")] n: i64"#,
14779            r#"#[rustango(default = "1.5")] n: f64"#,
14780            r#"#[rustango(default = "true")] b: bool"#,
14781        ] {
14782            assert_eq!(field_error(good), None, "{good}");
14783        }
14784    }
14785
14786    /// A raw ident derives names without `r#`; this used to panic (#1937).
14787    #[test]
14788    fn raw_idents_derive_plain_names() {
14789        let ident: syn::Ident = syn::parse_quote!(r#type);
14790        assert_eq!(column_type_ident(&ident).to_string(), "type_col");
14791        assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
14792    }
14793}
14794
14795#[cfg(test)]
14796mod model_attr_name_tests {
14797    use super::*;
14798
14799    fn container_error(item: proc_macro2::TokenStream) -> Option<String> {
14800        let input: DeriveInput = syn::parse2(item).unwrap();
14801        parse_container_attrs(&input).err().map(|e| e.to_string())
14802    }
14803
14804    fn field_error(field: proc_macro2::TokenStream) -> Option<String> {
14805        let f: syn::FieldsNamed = syn::parse2(quote!({ #field })).unwrap();
14806        let field = f.named.into_iter().next().unwrap();
14807        parse_field_attrs(&field).err().map(|e| e.to_string())
14808    }
14809
14810    /// `#[rgo(...)]` reads exactly as `#[rustango(...)]`.
14811    #[test]
14812    fn rgo_is_read_like_rustango() {
14813        let long: DeriveInput = syn::parse_quote! {
14814            #[rustango(table = "t", display = "name")] struct S;
14815        };
14816        let short: DeriveInput = syn::parse_quote! {
14817            #[rgo(table = "t", display = "name")] struct S;
14818        };
14819        let (long, short) = (
14820            parse_container_attrs(&long).unwrap(),
14821            parse_container_attrs(&short).unwrap(),
14822        );
14823        assert_eq!(long.table, short.table);
14824        assert_eq!(short.table.as_deref(), Some("t"));
14825        assert_eq!(long.display.map(|d| d.0), short.display.map(|d| d.0));
14826    }
14827
14828    #[test]
14829    fn mixing_names_on_one_item_or_field_is_refused() {
14830        let err = container_error(quote! {
14831            #[rustango(table = "t")] #[rgo(display = "name")] struct S;
14832        })
14833        .expect("a mix on an item must fail");
14834        assert!(
14835            err.contains("`#[rustango(...)]` and `#[rgo(...)]`"),
14836            "{err}"
14837        );
14838        let err = field_error(quote! { #[rgo(primary_key)] #[rustango(column = "c")] a: i64 })
14839            .expect("a mix on a field must fail");
14840        assert!(
14841            err.contains("`#[rgo(...)]` and `#[rustango(...)]`"),
14842            "{err}"
14843        );
14844        // Either name repeated alone is fine.
14845        assert_eq!(
14846            container_error(quote! { #[rgo(table = "t")] #[rgo(display = "name")] struct S; }),
14847            None
14848        );
14849        assert_eq!(
14850            field_error(quote! { #[rustango(primary_key)] #[rustango(column = "c")] a: i64 }),
14851            None
14852        );
14853    }
14854
14855    /// A hint quotes the attribute under the name the user wrote.
14856    #[test]
14857    fn hints_use_the_written_name() {
14858        let err = container_error(quote! { #[rgo(table = "t", unique_together = "a")] struct S; })
14859            .expect("one column must fail");
14860        assert!(err.contains("#[rgo(unique)]"), "{err}");
14861        let err =
14862            container_error(quote! { #[rustango(table = "t", index_together = "a")] struct S; })
14863                .expect("one column must fail");
14864        assert!(err.contains("#[rustango(index)]"), "{err}");
14865    }
14866}
14867
14868/// A migration file's text: a UTF-8 BOM dropped, UTF-16 LE / BE decoded by
14869/// its BOM. A copy of `rustango::text_file`'s decoder, which this crate
14870/// cannot depend on.
14871fn decode_text(bytes: Vec<u8>) -> Result<String, String> {
14872    fn utf16(bytes: &[u8], unit: fn([u8; 2]) -> u16) -> Result<String, String> {
14873        if bytes.len() % 2 != 0 {
14874            return Err("odd-length UTF-16".into());
14875        }
14876        let units = bytes.chunks_exact(2).map(|c| unit([c[0], c[1]]));
14877        char::decode_utf16(units)
14878            .collect::<Result<String, _>>()
14879            .map_err(|e| e.to_string())
14880    }
14881    match bytes.as_slice() {
14882        [0xFF, 0xFE, rest @ ..] => utf16(rest, u16::from_le_bytes),
14883        [0xFE, 0xFF, rest @ ..] => utf16(rest, u16::from_be_bytes),
14884        _ => {
14885            let mut bytes = bytes;
14886            if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
14887                bytes.drain(..3);
14888            }
14889            String::from_utf8(bytes).map_err(|e| e.to_string())
14890        }
14891    }
14892}
14893
14894#[cfg(test)]
14895mod decode_text_tests {
14896    use super::decode_text;
14897
14898    #[test]
14899    fn a_bom_or_utf16_migration_decodes() {
14900        let text = r#"{"name": "0001_é"}"#;
14901        let mut bom = vec![0xEF, 0xBB, 0xBF];
14902        bom.extend(text.bytes());
14903        let mut le = vec![0xFF, 0xFE];
14904        le.extend(text.encode_utf16().flat_map(u16::to_le_bytes));
14905        let mut be = vec![0xFE, 0xFF];
14906        be.extend(text.encode_utf16().flat_map(u16::to_be_bytes));
14907        for bytes in [text.as_bytes().to_vec(), bom, le, be] {
14908            assert_eq!(decode_text(bytes).unwrap(), text);
14909        }
14910        assert!(decode_text(vec![0xFF, 0xFE, 0x41]).is_err());
14911    }
14912}