Skip to main content

rustango_macros/
lib.rs

1//! Proc-macros for rustango.
2//!
3//! v0.1 ships `#[derive(Model)]`, which emits:
4//! * a `Model` impl carrying a static `ModelSchema`,
5//! * an `inventory::submit!` so the model is discoverable from the registry,
6//! * an inherent `objects()` returning a `QuerySet<Self>`,
7//! * a `sqlx::FromRow` impl so query results decode into the struct.
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13    parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14    PathArguments, Type, TypePath,
15};
16
17/// Resolve the consumer's local name for the `rustango` crate so the
18/// macro-emitted code keeps compiling when a downstream `Cargo.toml`
19/// renames the dep (`[dependencies] orm = { package = "rustango", … }`)
20/// or when the standalone `rustango-orm` crate ships in a future
21/// slice of the [orm-extract epic](https://github.com/ujeenet/rustango/issues/149).
22///
23/// Returns one of:
24/// - `quote!(::rustango)` — the consumer IS the `rustango` crate
25///   itself. We emit the absolute path `::rustango` (NOT `crate`)
26///   because examples and integration tests inside the rustango
27///   package compile as separate binaries — their own `crate::`
28///   namespace is the example/test file, not rustango's lib root.
29///   The absolute `::rustango` resolves correctly in both contexts
30///   (rustango lib code AND rustango/examples/*.rs).
31/// - `quote!(::ident)` — the consumer renamed the dep; emit the
32///   user's chosen ident.
33/// - `quote!(::rustango)` — fallback when `proc-macro-crate` can't
34///   read the manifest (rare; preserves today's behavior).
35///
36/// Issue [#142](https://github.com/ujeenet/rustango/issues/142).
37fn rustango_root() -> TokenStream2 {
38    use proc_macro_crate::{crate_name, FoundCrate};
39    match crate_name("rustango") {
40        Ok(FoundCrate::Itself) => quote!(::rustango),
41        Ok(FoundCrate::Name(name)) => {
42            let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
43            quote!(::#ident)
44        }
45        Err(_) => quote!(::rustango),
46    }
47}
48
49/// Derive a `Model` impl. See crate docs for the supported attributes.
50#[proc_macro_derive(Model, attributes(rustango, rgo))]
51pub fn derive_model(input: TokenStream) -> TokenStream {
52    let input = parse_macro_input!(input as DeriveInput);
53    expand(&input)
54        .unwrap_or_else(syn::Error::into_compile_error)
55        .into()
56}
57
58/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
59/// marker struct, wiring the full CRUD ViewSet in one annotation.
60///
61/// ```ignore
62/// #[derive(ViewSet)]
63/// #[viewset(
64///     model        = Post,
65///     fields       = "id, title, body, author_id",
66///     filter_fields = "author_id",
67///     search_fields = "title, body",
68///     ordering     = "-published_at",
69///     page_size    = 20,
70/// )]
71/// pub struct PostViewSet;
72///
73/// // Mount into your app:
74/// let app = Router::new()
75///     .merge(PostViewSet::router("/api/posts", pool.clone()));
76/// ```
77///
78/// Attributes:
79/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
80///   `SCHEMA` constant drives the endpoints.
81/// * `fields = "a, b, c"` — *required*. The only scalar fields list/retrieve
82///   JSON renders and create/update accept; other body keys are ignored.
83/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
84/// * `search_fields = "a, b"` — fields searched by `?search=...`.
85/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
86/// * `page_size = N` — default page size (default: 20; requests are capped
87///   at `ViewSet::max_page_size`, default 100).
88/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
89/// * `allow_anonymous` — flag; write actions without codenames are
90///   intended, so the mount warning is skipped.
91/// * `serializer = SomeSerializer` — render list / retrieve / create
92///   responses through a `#[derive(Serializer)]` type instead of the
93///   default field-level projection (`read_only` / `source` / `method`
94///   / `write_only` overrides apply). Tri-dialect. Requires the
95///   `serializer` feature.
96/// * `permissions(list = "...", retrieve = "...", create = "...",
97///   update = "...", destroy = "...")` — codenames required per action.
98#[proc_macro_derive(ViewSet, attributes(viewset))]
99pub fn derive_viewset(input: TokenStream) -> TokenStream {
100    let input = parse_macro_input!(input as DeriveInput);
101    expand_viewset(&input)
102        .unwrap_or_else(syn::Error::into_compile_error)
103        .into()
104}
105
106/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
107/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
108/// that walks every named field and:
109///
110/// * Parses the string value into the field's Rust type (`String`,
111///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
112///   nullable case).
113/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
114///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
115///   validators in declaration order, returning `FormError::Parse`
116///   on the first failure.
117///
118/// Example:
119///
120/// ```ignore
121/// #[derive(Form)]
122/// pub struct CreateItemForm {
123///     #[form(min_length = 1, max_length = 64)]
124///     pub name: String,
125///     #[form(min = 0, max = 150)]
126///     pub age: i32,
127///     pub active: bool,
128///     pub email: Option<String>,
129/// }
130///
131/// let parsed = CreateItemForm::parse(&form_map)?;
132/// ```
133#[proc_macro_derive(Form, attributes(form))]
134pub fn derive_form(input: TokenStream) -> TokenStream {
135    let input = parse_macro_input!(input as DeriveInput);
136    expand_form(&input)
137        .unwrap_or_else(syn::Error::into_compile_error)
138        .into()
139}
140
141/// Derive `rustango::serializer::ModelSerializer` for a struct.
142/// (intra-doc link disabled — the macro crate doesn't depend on
143/// `rustango` itself, so rustdoc can't resolve the path.)
144///
145/// # Container attribute (required)
146/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
147///
148/// # Field attributes
149/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
150/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
151/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
152/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
153/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
154/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
155/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
156/// - `#[serializer(slug = "name")]` — render a relation (`ForeignKey<T>` or `Option<ForeignKey<T>>`) as one of its fields: clones the loaded parent's `name`; excluded from `writable_fields()` (v0.44)
157/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
158/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
159///   write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
160///   no attr is given, override it when given (`min_length` is serializer-only)
161///
162/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
163/// It honours `#[serde(rename, rename_all, skip, skip_serializing, skip_serializing_if,
164/// skip_deserializing)]`; other `serde` keys are ignored.
165#[proc_macro_derive(Serializer, attributes(serializer, serde))]
166pub fn derive_serializer(input: TokenStream) -> TokenStream {
167    let input = parse_macro_input!(input as DeriveInput);
168    expand_serializer(&input)
169        .unwrap_or_else(syn::Error::into_compile_error)
170        .into()
171}
172
173/// Bake every `*.json` migration file in a directory into the binary
174/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
175/// of `(name, json_content)` pairs, lex-sorted by file stem.
176///
177/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
178/// behaviour as `migrate(pool, dir)` but with no filesystem access.
179/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
180/// (i.e. the crate that invokes the macro). Default is
181/// `"./migrations"` if no argument is supplied.
182///
183/// ```ignore
184/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
185/// // or:
186/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
187///
188/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
189/// ```
190///
191/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
192/// file's `name` field must equal its file stem, every `prev`
193/// reference must point to another migration in the same directory,
194/// and the JSON must parse. A broken chain — orphan `prev`, missing
195/// predecessor, malformed file — fails at macro-expansion time with
196/// a clear `compile_error!`. *No other Rust web framework
197/// validates migration chains at compile time*: Cot's migrations are
198/// imperative Rust code (no static chain), Loco's are SeaORM
199/// up/down (same), Rwf's are raw SQL (no chain at all).
200///
201/// Each migration is included via `include_str!` so cargo's rebuild
202/// detection picks up file *content* changes. Cargo does not watch
203/// directory listings, so a new migration file is missed until the
204/// crate rebuilds. Add a `build.rs` that tracks the directory:
205///
206/// ```ignore
207/// fn main() { println!("cargo:rerun-if-changed=migrations"); }
208/// ```
209///
210/// A missing directory embeds nothing and emits a deprecation warning.
211#[proc_macro]
212pub fn embed_migrations(input: TokenStream) -> TokenStream {
213    expand_embed_migrations(input.into())
214        .unwrap_or_else(syn::Error::into_compile_error)
215        .into()
216}
217
218/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
219/// compile time against typed columns. Issue #269 / T1.7.
220///
221/// Each invocation lowers to the equivalent typed-column method call:
222///
223/// ```ignore
224/// // These expand identically:
225/// Q!(User.email__icontains = "alice")
226/// User::email.ilike("%alice%")
227/// ```
228///
229/// Field-name typos fail the build (the macro emits `User::no_such_field`
230/// which doesn't exist) — the headline ergonomic win of this slice over
231/// filters keyed by a plain string.
232///
233/// # Supported lookup suffixes
234///
235/// * bare `=` / `__exact` → `.eq(value)`
236/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
237/// * `__ne` → `.ne(value)`
238/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
239/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
240/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
241/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
242/// * `__in` → `.is_in(iterable)`
243/// * `__not_in` → `.not_in(iterable)`
244/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
245/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
246/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
247///
248/// Unknown suffixes fail the build with a `compile_error!` pointing at
249/// the lookup token.
250///
251/// # Combine
252///
253/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
254/// `.and()` / `.or()` / `.not()` methods:
255///
256/// ```ignore
257/// User::objects()
258///     .where_(
259///         Q!(User.active = true)
260///             .and(Q!(User.email__icontains = "alice"))
261///     )
262///     .fetch(&pool).await?;
263/// ```
264///
265/// All emitted code routes through existing per-dialect writers — no new
266/// SQL emission machinery. Tri-dialect support is inherent.
267#[allow(non_snake_case)]
268#[proc_macro]
269pub fn Q(input: TokenStream) -> TokenStream {
270    expand_q(input.into())
271        .unwrap_or_else(syn::Error::into_compile_error)
272        .into()
273}
274
275/// `#[rustango::main]` — the runserver entrypoint. Wraps
276/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
277/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
278/// functions are zero-boilerplate. `RUST_LOG` and
279/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
280/// environment, else from `./.env` (only those keys are read):
281///
282/// ```ignore
283/// #[rustango::main]
284/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
285///     rustango::server::Builder::from_env().await?
286///         .migrate("migrations").await?
287///         .api(my_app::urls::api())
288///         .seed_with(my_app::seed::run).await?
289///         .serve("0.0.0.0:8080").await
290/// }
291/// ```
292///
293/// Optional `flavor = "current_thread"` passes through to
294/// `#[tokio::main]`; default is the multi-threaded runtime.
295///
296/// The body runs on a thread named `main` with an 8 MiB stack, since
297/// Windows gives the real main thread 1 MiB; `stack_size = <bytes>`
298/// overrides it. A panic there resumes on the real main thread.
299///
300/// `logging = false` suppresses the default subscriber, for apps
301/// that install their own. Needed whenever the body reaches
302/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
303/// macro installs gets there first, and `tracing` keeps the first
304/// one, so the `[logging]` section would otherwise be read and
305/// discarded (#1465).
306///
307/// ```ignore
308/// #[rustango::main(logging = false)]
309/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
310///     rustango::manage::Cli::new()
311///         .with_settings_from_env()
312///         .with_logging()          // now the first installer
313///         .run().await
314/// }
315/// ```
316///
317/// Pulls `tracing-subscriber` into the rustango crate behind the
318/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
319/// out get plain `#[tokio::main]` ergonomics without the dependency.
320#[proc_macro_attribute]
321pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
322    expand_main(args.into(), item.into())
323        .unwrap_or_else(syn::Error::into_compile_error)
324        .into()
325}
326
327fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
328    let mut input: syn::ItemFn = syn::parse2(item)?;
329    if input.sig.asyncness.is_none() {
330        return Err(syn::Error::new(
331            input.sig.ident.span(),
332            "`#[rustango::main]` must wrap an `async fn`",
333        ));
334    }
335
336    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
337    // to `#[tokio::main]`. Tokio's proc-macro internally emits
338    // `::tokio::*` paths that resolve against the user crate's deps,
339    // so calling it through the rustango re-export still requires the
340    // user to add tokio to their own Cargo.toml. Building the
341    // runtime ourselves keeps the dep transitive through the
342    // `runtime` feature on rustango.
343    //
344    // `flavor` and `logging`; anything else is a compile error.
345    let root = rustango_root();
346    let MainArgs {
347        flavor,
348        logging,
349        stack_size,
350    } = parse_main_args(&args)?;
351    let stack_size = match stack_size {
352        Some(n) => quote!(#n),
353        None => quote!(#root::__private_runtime::MAIN_STACK_SIZE),
354    };
355    let builder_call = match flavor {
356        Flavor::CurrentThread => quote! {
357            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
358        },
359        // Worker threads run handlers and `tokio::spawn` tasks: same stack.
360        Flavor::MultiThread => quote! {
361            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
362                .thread_stack_size(#stack_size)
363        },
364    };
365
366    // The user's fn becomes an inner `async fn` with its own return type;
367    // the outer sync fn runs it on a big-stack thread and reports its exit code.
368    let mut inner = input.clone();
369    inner.sig.ident = syn::Ident::new("__rustango_main_body", input.sig.ident.span());
370    // Lint attributes still apply to the body; the rest stay on the outer fn.
371    inner.attrs.retain(|a| {
372        ["allow", "expect", "deny", "warn", "forbid"]
373            .iter()
374            .any(|l| a.path().is_ident(l))
375    });
376    inner.vis = syn::Visibility::Inherited;
377    input.sig.asyncness = None;
378    input.sig.output = syn::parse2(quote!(-> ::std::process::ExitCode))?;
379
380    // `logging = false` drops the block entirely, so a later
381    // `Cli::with_logging()` is the first installer and its
382    // `[logging]` settings actually take effect (#1465).
383    let logging_prologue = if logging {
384        quote! {
385            // `Setup`'s defaults, not a copy of them: filter, format and
386            // the `Color::Auto` rule live in one place (#1493).
387            // `logging` and this macro share the `runtime` gate.
388            // The real `RUST_LOG` still wins over `./.env`'s (#2204).
389            let _ = #root::logging::Setup::new()
390                .with_default_env_filter(#root::__private_runtime::main_default_filter())
391                .with_format(#root::__private_runtime::main_format())
392                .install();
393        }
394    } else {
395        quote! {}
396    };
397
398    input.block = syn::parse2(quote! {{
399        { #logging_prologue }
400        #inner
401        #root::__private_runtime::run_on_main_thread(#stack_size, || {
402            let __rt = #builder_call
403                .enable_all()
404                .build()
405                .expect("failed to build tokio runtime");
406            ::std::process::Termination::report(__rt.block_on(__rustango_main_body()))
407        })
408    }})?;
409
410    Ok(quote! {
411        #input
412    })
413}
414
415enum Flavor {
416    MultiThread,
417    CurrentThread,
418}
419
420/// What `#[rustango::main]` was asked for.
421struct MainArgs {
422    flavor: Flavor,
423    logging: bool,
424    /// Bytes of stack for the main thread; `None` keeps the default.
425    stack_size: Option<usize>,
426}
427
428/// The smallest `stack_size` `#[rustango::main]` accepts: 64 KiB.
429const MIN_STACK_SIZE: usize = 64 * 1024;
430
431const MAIN_ARGS_HELP: &str =
432    "supported: `flavor = \"current_thread\" | \"multi_thread\"`, `logging = true | false`, `stack_size = <bytes>`";
433
434/// Parse `#[rustango::main(...)]`; an unknown or misspelt argument is an error.
435fn parse_main_args(args: &TokenStream2) -> syn::Result<MainArgs> {
436    use syn::parse::Parser as _;
437    let metas = syn::punctuated::Punctuated::<syn::MetaNameValue, syn::Token![,]>::parse_terminated
438        .parse2(args.clone())
439        .map_err(|e| {
440            syn::Error::new(
441                e.span(),
442                format!("#[rustango::main]: {e}; {MAIN_ARGS_HELP}"),
443            )
444        })?;
445    let mut out = MainArgs {
446        flavor: Flavor::MultiThread,
447        logging: true,
448        stack_size: None,
449    };
450    for meta in &metas {
451        let lit = match &meta.value {
452            syn::Expr::Lit(l) => Some(&l.lit),
453            _ => None,
454        };
455        let name = meta.path.get_ident().map(ToString::to_string);
456        match (name.as_deref(), lit) {
457            (Some("flavor"), Some(syn::Lit::Str(s))) if s.value() == "current_thread" => {
458                out.flavor = Flavor::CurrentThread;
459            }
460            (Some("flavor"), Some(syn::Lit::Str(s))) if s.value() == "multi_thread" => {
461                out.flavor = Flavor::MultiThread;
462            }
463            (Some("logging"), Some(syn::Lit::Bool(b))) => out.logging = b.value,
464            (Some("stack_size"), Some(syn::Lit::Int(n))) => {
465                let bytes = n.base10_parse::<usize>()?;
466                if bytes < MIN_STACK_SIZE {
467                    return Err(syn::Error::new(
468                        n.span(),
469                        format!("#[rustango::main]: `stack_size` must be at least {MIN_STACK_SIZE} bytes"),
470                    ));
471                }
472                out.stack_size = Some(bytes);
473            }
474            _ => {
475                return Err(syn::Error::new(
476                    meta.span(),
477                    format!("#[rustango::main]: unsupported argument; {MAIN_ARGS_HELP}"),
478                ))
479            }
480        }
481    }
482    Ok(out)
483}
484
485/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
486struct QInput {
487    base_path: syn::Path,
488    field: syn::Ident,
489    value: syn::Expr,
490}
491
492impl syn::parse::Parse for QInput {
493    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
494        let base_path: syn::Path = input.parse()?;
495        input.parse::<syn::Token![.]>()?;
496        let field: syn::Ident = input.parse()?;
497        input.parse::<syn::Token![=]>()?;
498        let value: syn::Expr = input.parse()?;
499        Ok(QInput {
500            base_path,
501            field,
502            value,
503        })
504    }
505}
506
507fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
508    let q: QInput = syn::parse2(input)?;
509    let root = rustango_root();
510    let field_str = ident_name(&q.field);
511    let field_span = q.field.span();
512    let (base, suffix) = match field_str.find("__") {
513        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
514        None => (field_str.as_str(), ""),
515    };
516    if base.is_empty() {
517        return Err(syn::Error::new(
518            field_span,
519            "Q!(): field name is empty before `__` suffix",
520        ));
521    }
522    let base_ident = ident_from_name(base, field_span);
523    let value = &q.value;
524    let path = &q.base_path;
525
526    // Most suffixes map directly to a Column method with the value
527    // forwarded unchanged. Some need value-shape massaging (wildcards
528    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
529    // ISNULL). Unknown suffixes fail the build.
530    let expanded = match suffix {
531        "" | "exact" => quote! {
532            #root::core::Column::eq(#path::#base_ident, #value)
533        },
534        "ne" => quote! {
535            #root::core::Column::ne(#path::#base_ident, #value)
536        },
537        "gt" => quote! {
538            #root::core::Column::gt(#path::#base_ident, #value)
539        },
540        "gte" => quote! {
541            #root::core::Column::gte(#path::#base_ident, #value)
542        },
543        "lt" => quote! {
544            #root::core::Column::lt(#path::#base_ident, #value)
545        },
546        "lte" => quote! {
547            #root::core::Column::lte(#path::#base_ident, #value)
548        },
549        // The escaped-LIKE lookups delegate to the typed Column
550        // methods, which escape the value and pair it with the
551        // *Escaped ops so `%` / `_` match literally on every dialect
552        // (#1257) — the macro no longer owns any pattern wrapping.
553        "iexact" => quote! {
554            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
555        },
556        "contains" => quote! {
557            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
558        },
559        "icontains" => quote! {
560            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
561        },
562        "startswith" => quote! {
563            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
564        },
565        "istartswith" => quote! {
566            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
567        },
568        "endswith" => quote! {
569            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
570        },
571        "iendswith" => quote! {
572            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
573        },
574        "in" => quote! {
575            #root::core::Column::is_in(#path::#base_ident, #value)
576        },
577        "not_in" => quote! {
578            #root::core::Column::not_in(#path::#base_ident, #value)
579        },
580        "isnull" => {
581            // Must be a bool literal at macro time so we can route to
582            // is_null vs is_not_null without a runtime branch.
583            let b = match value {
584                syn::Expr::Lit(syn::ExprLit {
585                    lit: syn::Lit::Bool(b),
586                    ..
587                }) => b.value(),
588                _ => {
589                    return Err(syn::Error::new_spanned(
590                        value,
591                        "Q!(): `__isnull` requires a `true` or `false` literal",
592                    ));
593                }
594            };
595            if b {
596                quote! { #root::core::Column::is_null(#path::#base_ident) }
597            } else {
598                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
599            }
600        }
601        "between" => {
602            // Accept a tuple literal `(lo, hi)`.
603            let tuple = match value {
604                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
605                _ => {
606                    return Err(syn::Error::new_spanned(
607                        value,
608                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
609                    ));
610                }
611            };
612            let lo = &tuple.elems[0];
613            let hi = &tuple.elems[1];
614            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
615        }
616        "regex" => quote! {
617            #root::core::Column::regex(#path::#base_ident, #value)
618        },
619        "iregex" => quote! {
620            #root::core::Column::iregex(#path::#base_ident, #value)
621        },
622        _ => {
623            return Err(syn::Error::new(
624                field_span,
625                format!(
626                    "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",
627                    suffix
628                ),
629            ));
630        }
631    };
632    Ok(expanded)
633}
634
635fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
636    // Default to "./migrations" if invoked without args.
637    let path_str = if input.is_empty() {
638        "./migrations".to_string()
639    } else {
640        let lit: LitStr = syn::parse2(input)?;
641        lit.value()
642    };
643
644    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
645        syn::Error::new(
646            proc_macro2::Span::call_site(),
647            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
648        )
649    })?;
650    let abs = std::path::Path::new(&manifest).join(&path_str);
651
652    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
653    if abs.is_dir() {
654        let read = std::fs::read_dir(&abs).map_err(|e| {
655            syn::Error::new(
656                proc_macro2::Span::call_site(),
657                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
658            )
659        })?;
660        for entry in read.flatten() {
661            let path = entry.path();
662            if !path.is_file() {
663                continue;
664            }
665            if path.extension().and_then(|s| s.to_str()) != Some("json") {
666                continue;
667            }
668            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
669                continue;
670            };
671            entries.push((stem.to_owned(), path));
672        }
673    }
674    entries.sort_by(|a, b| a.0.cmp(&b.0));
675
676    // Compile-time chain validation: read each migration's JSON,
677    // pull `name` and `prev` (file-stem-keyed for the chain check),
678    // and verify every `prev` points to another migration in the
679    // slice. Mismatches between the file stem and the embedded
680    // `name` field — and broken `prev` chains — fail at MACRO
681    // EXPANSION time so a misshapen migration set never compiles.
682    //
683    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
684    // migrations + a Rust proc-macro that reads them is a combo no
685    // other Rust web framework matches (Cot's are imperative Rust
686    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
687    // a static chain to validate).
688    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
689    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
690    let mut texts: Vec<String> = Vec::with_capacity(entries.len());
691    for (stem, path) in &entries {
692        let raw = std::fs::read(path)
693            .map_err(|e| e.to_string())
694            .and_then(decode_text)
695            .map_err(|e| {
696                syn::Error::new(
697                    proc_macro2::Span::call_site(),
698                    format!(
699                        "embed_migrations!: cannot read {} for chain validation: {e}",
700                        path.display()
701                    ),
702                )
703            })?;
704        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
705            syn::Error::new(
706                proc_macro2::Span::call_site(),
707                format!(
708                    "embed_migrations!: {} is not valid JSON: {e}",
709                    path.display()
710                ),
711            )
712        })?;
713        let name = json
714            .get("name")
715            .and_then(|v| v.as_str())
716            .ok_or_else(|| {
717                syn::Error::new(
718                    proc_macro2::Span::call_site(),
719                    format!(
720                        "embed_migrations!: {} is missing the `name` field",
721                        path.display()
722                    ),
723                )
724            })?
725            .to_owned();
726        if name != *stem {
727            return Err(syn::Error::new(
728                proc_macro2::Span::call_site(),
729                format!(
730                    "embed_migrations!: file stem `{stem}` does not match the migration's \
731                     `name` field `{name}` — rename the file or fix the JSON",
732                ),
733            ));
734        }
735        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
736        chain_names.push(name.clone());
737        prev_refs.push((name, prev));
738        texts.push(raw);
739    }
740
741    let name_set: std::collections::HashSet<&str> =
742        chain_names.iter().map(String::as_str).collect();
743    for (name, prev) in &prev_refs {
744        if let Some(p) = prev {
745            if !name_set.contains(p.as_str()) {
746                return Err(syn::Error::new(
747                    proc_macro2::Span::call_site(),
748                    format!(
749                        "embed_migrations!: broken migration chain — `{name}` declares \
750                         prev=`{p}` but no migration with that name exists in {}",
751                        abs.display()
752                    ),
753                ));
754            }
755        }
756    }
757
758    // The decoded text, so a BOM or UTF-16 file embeds as UTF-8;
759    // `include_bytes!` keeps the file a rebuild input.
760    let pairs: Vec<TokenStream2> = entries
761        .iter()
762        .zip(&texts)
763        .map(|((name, path), text)| {
764            let path_lit = path.display().to_string();
765            quote! { (#name, {
766                const _: &[u8] = ::core::include_bytes!(#path_lit);
767                #text
768            }) }
769        })
770        .collect();
771
772    // A wrong path used to embed nothing silently; a deprecation use is
773    // the only stable way for a proc macro to warn without breaking builds.
774    let missing_warning = if abs.is_dir() {
775        quote! {}
776    } else {
777        let note = format!(
778            "embed_migrations!: directory {} does not exist; no migrations are embedded",
779            abs.display()
780        );
781        quote! {
782            #[deprecated(note = #note)]
783            const __RUSTANGO_MIGRATIONS_DIR_MISSING: () = ();
784            #[allow(clippy::let_unit_value)]
785            let _ = __RUSTANGO_MIGRATIONS_DIR_MISSING;
786        }
787    };
788
789    Ok(quote! {
790        {
791            #missing_warning
792            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
793            __RUSTANGO_EMBEDDED
794        }
795    })
796}
797
798fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
799    let root = rustango_root();
800    let struct_name = &input.ident;
801
802    let Data::Struct(data) = &input.data else {
803        return Err(syn::Error::new_spanned(
804            struct_name,
805            "Model can only be derived on structs",
806        ));
807    };
808    let Fields::Named(named) = &data.fields else {
809        return Err(syn::Error::new_spanned(
810            struct_name,
811            "Model requires a struct with named fields",
812        ));
813    };
814
815    let mut container = parse_container_attrs(input)?;
816    let table = container
817        .table
818        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
819    let model_name = struct_name.to_string();
820
821    let collected = collect_fields(named, &table)?;
822    // An explicit `self_pk_column` must name a column of this model.
823    let explicit_pks = container
824        .reverse_has_relations
825        .iter()
826        .map(|r| (&r.self_pk_column, r.self_pk_span))
827        .chain(
828            container
829                .generic_has_relations
830                .iter()
831                .map(|r| (&r.self_pk_column, r.self_pk_span)),
832        );
833    for (column, span) in explicit_pks {
834        if let (Some(column), Some(span)) = (column, span) {
835            if !collected.column_entries.iter().any(|c| &c.column == column) {
836                return Err(syn::Error::new(
837                    span,
838                    format!("`self_pk_column = \"{column}\"` names no column of `{model_name}`"),
839                ));
840            }
841        }
842    }
843    // `reverse_has` / `generic_has` join on the PK column unless they name another.
844    if let Some((_, pk_column)) = &collected.primary_key {
845        for rel in &mut container.reverse_has_relations {
846            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
847        }
848        for rel in &mut container.generic_has_relations {
849            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
850        }
851    }
852
853    // Validate that #[rustango(display = "…")] names a real field.
854    if let Some((ref display, span)) = container.display {
855        if !collected.field_names.iter().any(|n| n == display) {
856            return Err(syn::Error::new(
857                span,
858                format!("`display = \"{display}\"` does not match any field on this struct"),
859            ));
860        }
861    }
862    let display = container.display.map(|(name, _)| name);
863    let app_label = container.app.clone();
864
865    // Validate admin field-name lists against declared field names.
866    // Note: `list_display` is intentionally NOT validated here. As of
867    // v0.32 it may also reference inventory-registered computed
868    // fields (via `register_admin_computed!`) whose existence the
869    // macro can't see at compile time — they're submitted from any
870    // crate that depends on rustango. The runtime list-view resolves
871    // unknown names against the inventory + silently drops the
872    // truly-bogus ones, which is the cheaper trade-off versus
873    // forcing a per-Model attr to opt out.
874    if let Some(admin) = &container.admin {
875        for (label, list) in [
876            ("search_fields", &admin.search_fields),
877            ("readonly_fields", &admin.readonly_fields),
878            ("list_filter", &admin.list_filter),
879        ] {
880            if let Some((names, span)) = list {
881                for name in names {
882                    if !collected.field_names.iter().any(|n| n == name) {
883                        return Err(syn::Error::new(
884                            *span,
885                            format!(
886                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
887                            ),
888                        ));
889                    }
890                }
891            }
892        }
893        if let Some((pairs, span)) = &admin.ordering {
894            for (name, _) in pairs {
895                if !collected.field_names.iter().any(|n| n == name) {
896                    return Err(syn::Error::new(
897                        *span,
898                        format!(
899                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
900                        ),
901                    ));
902                }
903            }
904        }
905        if let Some((groups, span)) = &admin.fieldsets {
906            for (_, fields) in groups {
907                for name in fields {
908                    if !collected.field_names.iter().any(|n| n == name) {
909                        return Err(syn::Error::new(
910                            *span,
911                            format!(
912                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
913                            ),
914                        ));
915                    }
916                }
917            }
918        }
919    }
920    if let Some(audit) = &container.audit {
921        if let Some((names, span)) = &audit.track {
922            for name in names {
923                if !collected.field_names.iter().any(|n| n == name) {
924                    return Err(syn::Error::new(
925                        *span,
926                        format!(
927                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
928                        ),
929                    ));
930                }
931            }
932        }
933    }
934
935    // Issue #291 / T2.5 — validate each `default_order` column name
936    // against the model's collected fields. Typos fail at macro-expand
937    // time, not at the database.
938    for (col, _desc, span) in &container.default_order {
939        if !collected.field_names.iter().any(|n| n == col) {
940            return Err(syn::Error::new(
941                *span,
942                format!(
943                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
944                ),
945            ));
946        }
947    }
948
949    check_known_columns(&container.indexes, &container.get_latest_by, &collected)?;
950
951    // Build the audit_track list for ModelSchema: None when no audit attr,
952    // Some(empty) when audit present without track, Some(names) when explicit.
953    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
954        audit
955            .track
956            .as_ref()
957            .map(|(names, _)| names.clone())
958            .unwrap_or_default()
959    });
960
961    // The upsert conflict target: a container-level composite unique,
962    // never a field `index(unique)` or a partial index PG can't target.
963    let upsert_unique: Option<Vec<String>> = container
964        .indexes
965        .iter()
966        .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
967        .map(|i| i.columns.clone());
968
969    // Merge field-level indexes into the container's index list.
970    let mut all_indexes: Vec<IndexAttr> = container.indexes;
971    for field in &named.named {
972        let ident = field.ident.as_ref().expect("named");
973        // The same default column as `process_field` (#1937).
974        let col = ident_name(ident);
975        // Re-parse field attrs to check for index flag
976        if let Ok(fa) = parse_field_attrs(field) {
977            if fa.index {
978                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
979                let auto_name = capped_index_name(if fa.index_unique {
980                    format!("{table}_{col_name}_uq_idx")
981                } else {
982                    format!("{table}_{col_name}_idx")
983                });
984                all_indexes.push(IndexAttr {
985                    span: field.span(),
986                    name: fa.index_name.or(Some(auto_name)),
987                    columns: vec![col_name],
988                    unique: fa.index_unique,
989                    method: fa.index_method,
990                    where_clause: None,
991                    include: Vec::new(),
992                });
993            }
994        }
995    }
996    // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
997    for field in &named.named {
998        let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
999            continue;
1000        };
1001        if det.kind != DetectedKind::String || fa.max_length.is_some() {
1002            continue;
1003        }
1004        let col = fa
1005            .column
1006            .clone()
1007            .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
1008        // MySQL indexes every foreign-key column too.
1009        let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
1010        if fa.primary_key
1011            || fa.unique
1012            || relation
1013            || all_indexes.iter().any(|i| i.columns.contains(&col))
1014        {
1015            return Err(syn::Error::new_spanned(
1016                field,
1017                "a primary-key, unique, indexed or foreign-key `String` needs \
1018                 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
1019            ));
1020        }
1021    }
1022
1023    let model_impl = model_impl_tokens(
1024        struct_name,
1025        &model_name,
1026        &table,
1027        display.as_deref(),
1028        app_label.as_deref(),
1029        container.admin.as_ref(),
1030        &container.default_order,
1031        &collected.field_schemas,
1032        collected.soft_delete_column.as_deref(),
1033        container.permissions,
1034        audit_track_names.as_deref(),
1035        &container.m2m,
1036        &all_indexes,
1037        &container.checks,
1038        &container.excludes,
1039        &container.composite_fks,
1040        &container.generic_fks,
1041        container.scope.as_deref(),
1042        container.is_view,
1043        container.verbose_name.as_deref(),
1044        container.verbose_name_plural.as_deref(),
1045        container.managed,
1046        container.base_manager_name.as_deref(),
1047        container.order_with_respect_to.as_deref(),
1048        container.proxy,
1049        &container.required_db_features,
1050        container.required_db_vendor.as_deref(),
1051        container.default_related_name.as_deref(),
1052        container.db_table_comment.as_deref(),
1053        container
1054            .get_latest_by
1055            .as_ref()
1056            .map(|(c, d, _)| (c.as_str(), *d)),
1057        &container.extra_permissions,
1058        &container.default_permissions,
1059        &container.global_scopes,
1060        &container.reverse_has_relations,
1061        &container.generic_has_relations,
1062    );
1063    let module_ident = column_module_ident(struct_name);
1064    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
1065    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
1066        let track_set: Option<std::collections::HashSet<&str>> = audit
1067            .track
1068            .as_ref()
1069            .map(|(names, _)| names.iter().map(String::as_str).collect());
1070        collected
1071            .column_entries
1072            .iter()
1073            .filter(|c| {
1074                track_set
1075                    .as_ref()
1076                    .map_or(true, |s| s.contains(c.name.as_str()))
1077            })
1078            .collect()
1079    });
1080    let inherent_impl = inherent_impl_tokens(
1081        struct_name,
1082        &collected,
1083        collected.primary_key.as_ref(),
1084        &column_consts,
1085        audited_fields.as_deref(),
1086        upsert_unique.as_deref(),
1087        &container.manager_fns,
1088    );
1089    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
1090    let from_row_impl = from_row_impl_tokens(
1091        struct_name,
1092        &collected.from_row_inits,
1093        &collected.from_row_inits_my,
1094    );
1095    let reverse_helpers = reverse_helper_tokens(
1096        struct_name,
1097        &collected.fk_relations,
1098        container.default_related_name.as_deref(),
1099    )?;
1100    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
1101    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
1102    // Issue #817 — `#[rustango(through(...))]` accessors.
1103    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
1104    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
1105    let reverse_has_accessors = reverse_has_accessor_tokens(
1106        struct_name,
1107        &container.reverse_has_relations,
1108        &collected.column_entries,
1109    );
1110    let generic_fk_accessors = generic_fk_accessor_tokens(
1111        struct_name,
1112        &container.generic_fks,
1113        &collected.column_entries,
1114    );
1115
1116    Ok(quote! {
1117        #model_impl
1118        #inherent_impl
1119        #from_row_impl
1120        #column_module
1121        #reverse_helpers
1122        #m2m_accessors
1123        #generic_m2m_accessors
1124        #through_accessors
1125        #reverse_has_accessors
1126        #generic_fk_accessors
1127
1128        #root::core::inventory::submit! {
1129            // `module_path!()` evaluates at the registration site,
1130            // so a Model declared in `crate::blog::models` records
1131            // `"<crate>::blog::models"` and `resolved_app_label()`
1132            // can infer "blog" without an explicit attribute.
1133            #root::core::ModelEntry::new(
1134                <#struct_name as #root::core::Model>::SCHEMA,
1135                ::core::module_path!(),
1136            )
1137            .with_audited(
1138                <#struct_name as #root::core::Model>::__rustango_audited_update,
1139                <#struct_name as #root::core::Model>::__rustango_audited_delete,
1140            )
1141            .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1142            .with_audited_update_record(
1143                <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1144            )
1145            .with_audited_cascade(<#struct_name as #root::core::Model>::__rustango_audited_cascade)
1146        }
1147    })
1148}
1149
1150/// An index or `get_latest_by` name that is no column of the model is a
1151/// compile error; an expression index such as `lower(email)` is not checked.
1152fn check_known_columns(
1153    indexes: &[IndexAttr],
1154    get_latest_by: &Option<(String, bool, proc_macro2::Span)>,
1155    collected: &CollectedFields,
1156) -> syn::Result<()> {
1157    let is_column = |c: &str| collected.column_entries.iter().any(|e| e.column == c);
1158    // Only plain names: an expression index such as `lower(email)` is legal.
1159    let plain = |c: &str| c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
1160    let mut found: Vec<(proc_macro2::Span, String)> = Vec::new();
1161    for idx in indexes {
1162        for col in idx.columns.iter().filter(|c| plain(c) && !is_column(c)) {
1163            found.push((
1164                idx.span,
1165                format!("index column `{col}` is not a column of this model; its DDL will fail"),
1166            ));
1167        }
1168    }
1169    if let Some((col, _, span)) = get_latest_by {
1170        if !is_column(col) && !collected.field_names.iter().any(|n| n == col) {
1171            found.push((
1172                *span,
1173                format!("`get_latest_by`: `{col}` is not a field of this model"),
1174            ));
1175        }
1176    }
1177    let mut errors = found
1178        .into_iter()
1179        .map(|(span, note)| syn::Error::new(span, note));
1180    let Some(mut first) = errors.next() else {
1181        return Ok(());
1182    };
1183    first.extend(errors);
1184    Err(first)
1185}
1186
1187/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1188/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1189fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1190    let root = rustango_root();
1191    let parent_ty = &rel.parent_type;
1192    let field_ident = &rel.field_ident;
1193    let null_fk = if rel.nullable {
1194        quote! {
1195            if #receiver.#field_ident.is_none() {
1196                return ::core::result::Result::Ok(false);
1197            }
1198        }
1199    } else {
1200        quote! {}
1201    };
1202    quote! {
1203        #null_fk
1204        #root::sql::__rustango_require_join(
1205            row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1206        )?;
1207    }
1208}
1209
1210/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1211/// reused, and the deeper chain stitched into it; re-decoding it would
1212/// drop what that chain loaded.
1213fn loaded_reuse_tokens(
1214    rel: &FkRelation,
1215    receiver: &TokenStream2,
1216    load: &TokenStream2,
1217) -> TokenStream2 {
1218    let root = rustango_root();
1219    let field_ident = &rel.field_ident;
1220    let pattern = if rel.nullable {
1221        quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1222    } else {
1223        quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1224    };
1225    quote! {
1226        if let #pattern = &mut #receiver.#field_ident {
1227            if let ::core::option::Option::Some(__r) = __rest {
1228                let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1229            }
1230            return ::core::result::Result::Ok(true);
1231        }
1232    }
1233}
1234
1235/// Store the decoded `_parent` on the FK field, keeping the key the row
1236/// holds: with `on = <unique column>` it is not the parent's PK.
1237fn loaded_assign_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1238    let root = rustango_root();
1239    let field_ident = &rel.field_ident;
1240    if rel.nullable {
1241        quote! {
1242            if let ::core::option::Option::Some(__fk) = &mut #receiver.#field_ident {
1243                let _pk = ::core::clone::Clone::clone(__fk.pk_ref());
1244                *__fk = #root::sql::ForeignKey::loaded(_pk, _parent);
1245            }
1246        }
1247    } else {
1248        quote! {
1249            let _pk = ::core::clone::Clone::clone(#receiver.#field_ident.pk_ref());
1250            #receiver.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1251        }
1252    }
1253}
1254
1255/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1256/// matches `field_name` against the model's FK fields and, for a
1257/// match, decodes the FK target via the parent's macro-generated
1258/// `__rustango_from_aliased_row`, and stores `ForeignKey::Loaded` on
1259/// `self` under the key the row already holds.
1260///
1261/// Always emitted (with empty arms for FK-less models, which
1262/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1263/// trait bound on `fetch_on` is universally satisfied — users
1264/// never have to think about implementing it.
1265fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1266    let root = rustango_root();
1267    let arms = fk_relations.iter().map(|rel| {
1268        let parent_ty = &rel.parent_type;
1269        let field_ident = &rel.field_ident;
1270        let fk_name = ident_name(field_ident);
1271        let fk_col = fk_name.as_str();
1272        let guard = join_guard_tokens(rel, &quote!(self));
1273        let reuse = loaded_reuse_tokens(
1274            rel,
1275            &quote!(self),
1276            &quote!(#root::sql::LoadRelated::__rustango_load_related),
1277        );
1278        let assign = loaded_assign_tokens(rel, &quote!(self));
1279        quote! {
1280            #fk_col => {
1281                #guard
1282                #reuse
1283                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1284                // Audit #451 — multi-hop `select_related("a__b__c")`:
1285                // stitch the deeper relation onto this parent first,
1286                // decoding it at the accumulated `__next_alias`. The
1287                // parent type also impls `LoadRelated`, so this recurses
1288                // the FK chain to arbitrary depth.
1289                if let ::core::option::Option::Some(__r) = __rest {
1290                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1291                        &mut _parent, row, __r, &__next_alias,
1292                    )?;
1293                }
1294                #assign
1295                ::core::result::Result::Ok(true)
1296            }
1297        }
1298    });
1299    quote! {
1300        #[cfg(feature = "postgres")]
1301        impl #root::sql::LoadRelated for #struct_name {
1302            #[allow(unused_variables)]
1303            fn __rustango_load_related(
1304                &mut self,
1305                row: &#root::sql::sqlx::postgres::PgRow,
1306                field_name: &str,
1307                alias: &str,
1308            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1309                // Audit #451 — split the multi-hop path: `base` is the FK
1310                // on THIS model, `__rest` (if any) is the remaining chain
1311                // to stitch onto the loaded parent. `__next_alias` is the
1312                // accumulated join alias the parent's columns live under
1313                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1314                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1315                    match field_name.split_once("__") {
1316                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1317                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1318                    };
1319                let __next_alias: ::std::string::String = match __rest {
1320                    ::core::option::Option::Some(__r) => {
1321                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1322                        ::std::format!("{}__{}", alias, __rb)
1323                    }
1324                    ::core::option::Option::None => ::std::string::String::new(),
1325                };
1326                match __base {
1327                    #( #arms )*
1328                    _ => ::core::result::Result::Ok(false),
1329                }
1330            }
1331        }
1332    }
1333}
1334
1335/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1336/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1337/// which expands to a `LoadRelatedMy` impl when rustango is built with
1338/// the `mysql` feature, and to nothing otherwise. The decoded parent
1339/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1340/// decoder, also batch8) so the dual emission is symmetric across
1341/// backends.
1342fn load_related_impl_my_tokens(
1343    struct_name: &syn::Ident,
1344    fk_relations: &[FkRelation],
1345) -> TokenStream2 {
1346    let root = rustango_root();
1347    let arms = fk_relations.iter().map(|rel| {
1348        let parent_ty = &rel.parent_type;
1349        let field_ident = &rel.field_ident;
1350        let fk_name = ident_name(field_ident);
1351        let fk_col = fk_name.as_str();
1352        let guard = join_guard_tokens(rel, &quote!(__self));
1353        let reuse = loaded_reuse_tokens(
1354            rel,
1355            &quote!(__self),
1356            &quote!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1357        );
1358        let assign = loaded_assign_tokens(rel, &quote!(__self));
1359        // `self` IS hygiene-tracked through macro_rules — emitted from
1360        // a different context than the `&mut self` parameter inside
1361        // the macro_rules-expanded fn. Pass it through as `__self`
1362        // and let the macro_rules rebind it to the receiver.
1363        quote! {
1364            #fk_col => {
1365                #guard
1366                #reuse
1367                let mut _parent: #parent_ty =
1368                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1369                // Audit #451 — multi-hop: stitch the deeper relation onto
1370                // the parent at the accumulated alias (see PG twin).
1371                if let ::core::option::Option::Some(__r) = __rest {
1372                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1373                        &mut _parent, row, __r, &__next_alias,
1374                    )?;
1375                }
1376                #assign
1377                ::core::result::Result::Ok(true)
1378            }
1379        }
1380    });
1381    quote! {
1382        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1383            #( #arms )*
1384        });
1385    }
1386}
1387
1388/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1389/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1390/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1391fn load_related_impl_sqlite_tokens(
1392    struct_name: &syn::Ident,
1393    fk_relations: &[FkRelation],
1394) -> TokenStream2 {
1395    let root = rustango_root();
1396    let arms = fk_relations.iter().map(|rel| {
1397        let parent_ty = &rel.parent_type;
1398        let field_ident = &rel.field_ident;
1399        let fk_name = ident_name(field_ident);
1400        let fk_col = fk_name.as_str();
1401        let guard = join_guard_tokens(rel, &quote!(__self));
1402        let reuse = loaded_reuse_tokens(
1403            rel,
1404            &quote!(__self),
1405            &quote!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1406        );
1407        let assign = loaded_assign_tokens(rel, &quote!(__self));
1408        quote! {
1409            #fk_col => {
1410                #guard
1411                #reuse
1412                let mut _parent: #parent_ty =
1413                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1414                // Audit #451 — multi-hop: stitch the deeper relation onto
1415                // the parent at the accumulated alias (see PG twin).
1416                if let ::core::option::Option::Some(__r) = __rest {
1417                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1418                        &mut _parent, row, __r, &__next_alias,
1419                    )?;
1420                }
1421                #assign
1422                ::core::result::Result::Ok(true)
1423            }
1424        }
1425    });
1426    quote! {
1427        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1428            #( #arms )*
1429        });
1430    }
1431}
1432
1433/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1434/// matches `field_name` against the model's FK fields and returns
1435/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1436/// group children by parent PK.
1437///
1438/// Always emitted (with `_ => None` for FK-less models) so the
1439/// trait bound on `fetch_with_prefetch` is universally satisfied.
1440fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1441    let root = rustango_root();
1442    let arms = fk_relations.iter().map(|rel| {
1443        let field_ident = &rel.field_ident;
1444        let fk_name = ident_name(field_ident);
1445        let fk_col = fk_name.as_str();
1446        if rel.pk_kind == DetectedKind::I64 {
1447            // i64 FK — return the stored PK so prefetch_related can
1448            // group children by it. Nullable variant unwraps via
1449            // `as_ref().map(...)`: an unset (NULL) FK column yields
1450            // `None` and that child sits out of the grouping (correct
1451            // semantics — it has no parent to attach to).
1452            if rel.nullable {
1453                quote! {
1454                    #fk_col => self.#field_ident
1455                        .as_ref()
1456                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1457                }
1458            } else {
1459                quote! {
1460                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1461                }
1462            }
1463        } else {
1464            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1465            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1466            // i64-keyed grouping path — the trait signature is
1467            // `Option<i64>` and a non-i64 PK can't lower into it.
1468            // The FK still works for everything else (CRUD, lazy
1469            // load via `.get()`, select_related JOINs); only the
1470            // bulk prefetch grouper needs the integer key.
1471            quote! {
1472                #fk_col => ::core::option::Option::None,
1473            }
1474        }
1475    });
1476    // PK-type-agnostic version: every FK arm emits an
1477    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1478    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1479    // opt OUT of the legacy i64 method (it returns None) but opt IN
1480    // here.
1481    let value_arms = fk_relations.iter().map(|rel| {
1482        let field_ident = &rel.field_ident;
1483        let fk_name = ident_name(field_ident);
1484        let fk_col = fk_name.as_str();
1485        if rel.nullable {
1486            quote! {
1487                #fk_col => self.#field_ident
1488                    .as_ref()
1489                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1490                        #root::sql::ForeignKey::pk(fk)
1491                    )),
1492            }
1493        } else {
1494            quote! {
1495                #fk_col => ::core::option::Option::Some(
1496                    ::core::convert::Into::<#root::core::SqlValue>::into(
1497                        self.#field_ident.pk()
1498                    )
1499                ),
1500            }
1501        }
1502    });
1503    quote! {
1504        impl #root::sql::FkPkAccess for #struct_name {
1505            #[allow(unused_variables)]
1506            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1507                match field_name {
1508                    #( #arms )*
1509                    _ => ::core::option::Option::None,
1510                }
1511            }
1512            #[allow(unused_variables)]
1513            fn __rustango_fk_pk_value(
1514                &self,
1515                field_name: &str,
1516            ) -> ::core::option::Option<#root::core::SqlValue> {
1517                match field_name {
1518                    #( #value_arms )*
1519                    _ => ::core::option::Option::None,
1520                }
1521            }
1522        }
1523    }
1524}
1525
1526/// `related_name = "+"`: the FK gets no reverse accessor on its parent.
1527const NO_REVERSE_ACCESSOR: &str = "+";
1528
1529/// For every `ForeignKey<Parent>` field on `Child`, emit
1530/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1531/// Reads the column the FK points at (`on`, else the PK) and runs one
1532/// `SELECT … WHERE <fk_column> = $1`: one round trip, no N+1.
1533///
1534/// The accessor takes any `SqlExecutor`; `_set_pool` is its deprecated
1535/// old name.
1536fn reverse_helper_tokens(
1537    child_ident: &syn::Ident,
1538    fk_relations: &[FkRelation],
1539    default_related_name: Option<&str>,
1540) -> syn::Result<TokenStream2> {
1541    let root = rustango_root();
1542    // `related_name = "+"`: no accessor, so a parent from another crate works.
1543    let fk_relations: Vec<&FkRelation> = fk_relations
1544        .iter()
1545        .filter(|rel| rel.related_name.as_deref() != Some(NO_REVERSE_ACCESSOR))
1546        .collect();
1547    if fk_relations.is_empty() {
1548        return Ok(TokenStream2::new());
1549    }
1550    // Method-name resolution per FK (issue #816 + follow-up):
1551    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1552    //      itself — wins over everything else.
1553    //   2. Container-level `default_related_name = "..."` on the
1554    //      child. Applies to every FK on this model that didn't
1555    //      override.
1556    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1557    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1558    //      edge cases.
1559    //
1560    // The PG-on-executor variant keeps the resolved name; the
1561    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1562    // framework's convention for the `&Pool` flavor of every helper).
1563    let default_pg_suffix = default_related_name
1564        .map(str::to_owned)
1565        .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1566    let name_of = |rel: &FkRelation| {
1567        rel.related_name
1568            .clone()
1569            .unwrap_or_else(|| default_pg_suffix.clone())
1570    };
1571    // Two FKs to one parent would define the same method twice (E0592).
1572    for (i, rel) in fk_relations.iter().enumerate() {
1573        let parent_ty = &rel.parent_type;
1574        let parent = quote!(#parent_ty).to_string();
1575        let name = name_of(rel);
1576        if let Some(first) = fk_relations[..i].iter().find(|r| {
1577            let other = &r.parent_type;
1578            quote!(#other).to_string() == parent && name_of(r) == name
1579        }) {
1580            return Err(syn::Error::new_spanned(
1581                &rel.field_ident,
1582                format!(
1583                    "`{}` and `{}` both point at `{parent}` and would both add `{parent}::{name}`; \
1584                     give one `#[rustango(related_name = \"...\")]`, or `related_name = \"+\"` for none",
1585                    first.field_ident, rel.field_ident
1586                ),
1587            ));
1588        }
1589    }
1590    let impls = fk_relations.iter().map(|rel| {
1591        let pg_suffix = name_of(rel);
1592        let pool_suffix = format!("{}_pool", pg_suffix);
1593        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1594        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1595        let parent_ty = &rel.parent_type;
1596        let fk_name = ident_name(&rel.field_ident);
1597        let fk_col = fk_name.as_str();
1598        let doc = format!(
1599            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1600             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1601             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1602             further `{child_ident}::objects()` filters via direct queryset use."
1603        );
1604        quote! {
1605            impl #parent_ty {
1606                #[doc = #doc]
1607                ///
1608                /// Runs on any [`#root::sql::SqlExecutor`]: a `&Pool`, a held
1609                /// `PoolConn`, a `PoolTx` or a Postgres sqlx executor.
1610                ///
1611                /// # Errors
1612                /// Returns [`#root::sql::ExecError`] for SQL-writing
1613                /// or driver failures.
1614                pub async fn #pg_method_ident<'_c, _E: #root::sql::SqlExecutor<'_c>>(
1615                    &self,
1616                    _executor: _E,
1617                ) -> ::core::result::Result<
1618                    ::std::vec::Vec<#child_ident>,
1619                    #root::sql::ExecError,
1620                > {
1621                    let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1622                    #root::query::QuerySet::<#child_ident>::new()
1623                        .filter_op(#fk_col, #root::core::Op::Eq, _key)
1624                        .fetch_on(_executor)
1625                        .await
1626                }
1627
1628                /// Old name of the reverse accessor without `_pool`.
1629                ///
1630                /// # Errors
1631                /// As the accessor.
1632                #[deprecated(since = "0.62.0", note = "drop the `_pool` suffix")]
1633                pub async fn #pool_method_ident(
1634                    &self,
1635                    pool: &#root::sql::Pool,
1636                ) -> ::core::result::Result<
1637                    ::std::vec::Vec<#child_ident>,
1638                    #root::sql::ExecError,
1639                > {
1640                    self.#pg_method_ident(pool).await
1641                }
1642            }
1643        }
1644    });
1645    Ok(quote! { #( #impls )* })
1646}
1647
1648/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1649/// relation declared on the model.
1650/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1651/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1652///
1653/// Closes #239 + #240 — `comment.content_object` /
1654/// `comment.content_object = post` ergonomics on top of the existing
1655/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1656///
1657/// `column_entries` is passed so we can resolve each `ct_column` /
1658/// `pk_column` SQL name back to its Rust field ident — the macro
1659/// only sees the column-side strings in the attribute, but the
1660/// emitted accessor needs to read the actual struct field.
1661fn generic_fk_accessor_tokens(
1662    struct_name: &syn::Ident,
1663    generic_fks: &[GenericFkAttr],
1664    column_entries: &[ColumnEntry],
1665) -> TokenStream2 {
1666    let root = rustango_root();
1667    if generic_fks.is_empty() {
1668        return TokenStream2::new();
1669    }
1670    let methods = generic_fks.iter().filter_map(|gfk| {
1671        // Resolve `ct_column` + `pk_column` to the struct's Rust
1672        // field idents. A typo (column name doesn't match any field)
1673        // emits no method for that registration — the user will see
1674        // the compiler reject the SCHEMA literal anyway, so there's
1675        // a clear error path without us double-reporting.
1676        let ct_ident = column_entries
1677            .iter()
1678            .find(|c| c.column == gfk.ct_column)
1679            .map(|c| c.ident.clone())?;
1680        let pk_ident = column_entries
1681            .iter()
1682            .find(|c| c.column == gfk.pk_column)
1683            .map(|c| c.ident.clone())?;
1684
1685        let accessor_ident =
1686            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1687        let setter_ident =
1688            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1689        let name_literal = gfk.name.as_str();
1690
1691        Some(quote! {
1692            #[doc = concat!(
1693                "Resolve the polymorphic `",
1694                #name_literal,
1695                "` relation. Reads `self.",
1696                stringify!(#ct_ident),
1697                "` + `self.",
1698                stringify!(#pk_ident),
1699                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1700                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1701                #name_literal,
1702                "\", ...))]`."
1703            )]
1704            pub async fn #accessor_ident(
1705                &self,
1706                pool: &#root::sql::Pool,
1707            ) -> ::core::result::Result<
1708                ::core::option::Option<#root::__serde_json::Value>,
1709                #root::sql::ExecError,
1710            > {
1711                let gfk = #root::contenttypes::GenericForeignKey::new(
1712                    self.#ct_ident as i64,
1713                    self.#pk_ident as i64,
1714                );
1715                gfk.get_object(pool).await
1716            }
1717
1718            #[doc = concat!(
1719                "Set the polymorphic `",
1720                #name_literal,
1721                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1722                stringify!(#ct_ident),
1723                "` and `self.",
1724                stringify!(#pk_ident),
1725                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1726                #name_literal,
1727                "\", ...))]`."
1728            )]
1729            pub async fn #setter_ident<T: #root::core::Model>(
1730                &mut self,
1731                pool: &#root::sql::Pool,
1732                target_pk: i64,
1733            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1734                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1735                    pool,
1736                    target_pk,
1737                ).await?;
1738                self.#ct_ident = gfk.content_type_id as _;
1739                self.#pk_ident = gfk.object_pk as _;
1740                ::core::result::Result::Ok(())
1741            }
1742        })
1743    });
1744    quote! {
1745        impl #struct_name {
1746            #( #methods )*
1747        }
1748    }
1749}
1750
1751fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1752    let root = rustango_root();
1753    if m2m_relations.is_empty() {
1754        return TokenStream2::new();
1755    }
1756    let methods = m2m_relations.iter().map(|rel| {
1757        let method_name = format!("{}_m2m", rel.name);
1758        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1759        let through = rel.through.as_str();
1760        let src_col = rel.src.as_str();
1761        let dst_col = rel.dst.as_str();
1762        quote! {
1763            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1764                #root::sql::M2MManager {
1765                    src_pk: self.__rustango_pk_value(),
1766                    through: #through,
1767                    src_col: #src_col,
1768                    dst_col: #dst_col,
1769                }
1770            }
1771        }
1772    });
1773    quote! {
1774        impl #struct_name {
1775            #( #methods )*
1776        }
1777    }
1778}
1779
1780/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1781/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1782fn generic_m2m_accessor_tokens(
1783    struct_name: &syn::Ident,
1784    relations: &[GenericM2MAttr],
1785) -> TokenStream2 {
1786    let root = rustango_root();
1787    if relations.is_empty() {
1788        return TokenStream2::new();
1789    }
1790    let methods = relations.iter().map(|rel| {
1791        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1792        let through = rel.through.as_str();
1793        let pk_col = rel.pk_column.as_str();
1794        let ct_col = rel.ct_column.as_str();
1795        let related_col = rel.related_column.as_str();
1796        quote! {
1797            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1798                #root::sql::GenericM2MManager {
1799                    src_pk: self.__rustango_pk_value(),
1800                    src_schema: <Self as #root::core::Model>::SCHEMA,
1801                    through: #through,
1802                    pk_col: #pk_col,
1803                    ct_col: #ct_col,
1804                    dst_col: #related_col,
1805                }
1806            }
1807        }
1808    });
1809    // Lets an owner delete find and clear its junction rows.
1810    let entries = relations.iter().map(|rel| {
1811        let through = rel.through.as_str();
1812        let pk_col = rel.pk_column.as_str();
1813        let ct_col = rel.ct_column.as_str();
1814        quote! {
1815            #root::core::inventory::submit! {
1816                #root::core::GenericM2MEntry::new(
1817                    <#struct_name as #root::core::Model>::SCHEMA,
1818                    #through,
1819                    #pk_col,
1820                    #ct_col,
1821                )
1822            }
1823        }
1824    });
1825    quote! {
1826        impl #struct_name {
1827            #( #methods )*
1828        }
1829        #( #entries )*
1830    }
1831}
1832
1833/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1834/// associated functions for each `#[rustango(reverse_has(...))]`
1835/// attribute. Issue #830.
1836///
1837/// The two emitted functions return ready-to-use `WhereExpr` nodes
1838/// that downstream callers drop into
1839/// `QuerySet::<Self>::where_raw(...)`:
1840///
1841/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1842///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1843///   `whereHas` parity (without the closure-style sub-predicate
1844///   refinement — that's a follow-up; users can layer additional
1845///   predicates by constructing the SelectQuery themselves and
1846///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1847/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1848///   `whereDoesntHave` parity.
1849///
1850/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1851/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1852/// machinery threads the outer-table reference through automatically
1853/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1854fn reverse_has_accessor_tokens(
1855    struct_name: &syn::Ident,
1856    reverse_has_relations: &[ReverseHasAttr],
1857    columns: &[ColumnEntry],
1858) -> TokenStream2 {
1859    let root = rustango_root();
1860    if reverse_has_relations.is_empty() {
1861        return TokenStream2::new();
1862    }
1863    let methods = reverse_has_relations.iter().map(|rel| {
1864        let exists_name = format!("{}_exists_expr", rel.name);
1865        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1866        let count_name = format!("{}_count", rel.name);
1867        let fetch_name = format!("{}_fetch", rel.name);
1868        let first_name = format!("{}_first", rel.name);
1869        let pluck_name = format!("{}_pluck", rel.name);
1870        let accessor_name = rel.name.as_str();
1871        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1872        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1873        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1874        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1875        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1876        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1877        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1878        let child = &rel.child;
1879        let child_fk_column = rel.child_fk_column.as_str();
1880        let self_pk_column = rel.self_pk_column();
1881        // The instance helpers filter on the value the EXISTS form joins on.
1882        let self_value = columns
1883            .iter()
1884            .find(|c| c.column == self_pk_column)
1885            .map_or_else(
1886                || quote! { self.__rustango_pk_value() },
1887                |c| {
1888                    let ident = &c.ident;
1889                    quote! {
1890                        ::core::convert::Into::<#root::core::SqlValue>::into(
1891                            ::core::clone::Clone::clone(&self.#ident)
1892                        )
1893                    }
1894                },
1895            );
1896        let exists_doc = format!(
1897            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1898             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1899             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1900             filter to {struct_name}s with at least one matching child.",
1901        );
1902        let not_exists_doc = format!(
1903            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1904             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1905             <outer>.{self_pk_column})`. Drop into \
1906             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1907             {struct_name}s with **no** matching child.",
1908        );
1909        let count_doc = format!(
1910            "Eloquent `$model->{name}->count()` analog — returns \
1911             the number of `{child}` rows whose `{child_fk_column}` \
1912             matches this `{struct_name}` instance's `{self_pk_column}`. \
1913             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1914             {child_fk_column} = <self.{self_pk_column}>`.",
1915            name = rel.name,
1916        );
1917        let accessor_doc = format!(
1918            "Eloquent `$model->{name}` accessor — returns a \
1919             `QuerySet<{child}>` filtered to rows whose \
1920             `{child_fk_column}` matches this `{struct_name}` \
1921             instance's `{self_pk_column}`. **Chainable**: compose `.filter()` \
1922             / `.order_by()` / `.limit()` etc. on top, then call \
1923             `.fetch(&pool)` (the QuerySet trait method) when \
1924             done. For the simple \"fetch all\" hot path with no \
1925             further composition, prefer the bare-name \
1926             `{name}_fetch(&pool)` companion.",
1927            name = rel.name,
1928        );
1929        let fetch_doc = format!(
1930            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1931             over `{name}(&self).fetch(&pool)`. Use this when \
1932             you don't need further `.filter()` / `.order_by()` \
1933             composition; falls back to the chainable accessor when \
1934             you do. Avoids the `_pool` suffix on the most common \
1935             call-site shape.",
1936            name = rel.name,
1937        );
1938        quote! {
1939            #[doc = #accessor_doc]
1940            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1941                #root::query::QuerySet::<#child>::new()
1942                    .filter(#child_fk_column, #self_value)
1943            }
1944
1945            #[doc = #fetch_doc]
1946            pub async fn #fetch_ident(
1947                &self,
1948                pool: &#root::sql::Pool,
1949            ) -> ::core::result::Result<
1950                ::std::vec::Vec<#child>,
1951                #root::sql::ExecError,
1952            > {
1953                use #root::sql::FetcherPool as _;
1954                self.#accessor_ident().fetch(pool).await
1955            }
1956
1957            /// Eloquent `$model->relation->first()` / `hasOne`
1958            /// semantics — bare-name shortcut over
1959            /// `self.<name>().first(&pool)`. Returns `None` when no
1960            /// child rows match. Useful when the relation is
1961            /// nominally many-to-one in shape but at most one row is
1962            /// expected (latest comment, primary tag, etc.).
1963            pub async fn #first_ident(
1964                &self,
1965                pool: &#root::sql::Pool,
1966            ) -> ::core::result::Result<
1967                ::core::option::Option<#child>,
1968                #root::sql::ExecError,
1969            > {
1970                self.#accessor_ident().first(pool).await
1971            }
1972
1973            /// Eloquent `$model->relation->pluck($col)` — project a
1974            /// single column from the child rows into a `Vec<U>`.
1975            /// Skips the typed `Child` decode, which is cheaper when
1976            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1977            pub async fn #pluck_ident<U>(
1978                &self,
1979                col: &'static str,
1980                pool: &#root::sql::Pool,
1981            ) -> ::core::result::Result<
1982                ::std::vec::Vec<U>,
1983                #root::sql::ExecError,
1984            >
1985            where
1986                U: #root::sql::FlatScalar,
1987            {
1988                self.#accessor_ident()
1989                    .values_list_flat(col)
1990                    .fetch::<U>(pool)
1991                    .await
1992            }
1993
1994            #[doc = #exists_doc]
1995            pub fn #exists_ident() -> #root::core::WhereExpr {
1996                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1997                let child_schema =
1998                    <#child as #root::core::Model>::SCHEMA;
1999                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
2000                    lhs: Expr::Column(#child_fk_column),
2001                    op: Op::Eq,
2002                    rhs: Expr::OuterRef(#self_pk_column),
2003                });
2004                WhereExpr::Exists(::std::boxed::Box::new(inner))
2005            }
2006
2007            #[doc = #not_exists_doc]
2008            pub fn #not_exists_ident() -> #root::core::WhereExpr {
2009                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
2010                let child_schema =
2011                    <#child as #root::core::Model>::SCHEMA;
2012                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
2013                    lhs: Expr::Column(#child_fk_column),
2014                    op: Op::Eq,
2015                    rhs: Expr::OuterRef(#self_pk_column),
2016                });
2017                WhereExpr::NotExists(::std::boxed::Box::new(inner))
2018            }
2019
2020            #[doc = #count_doc]
2021            pub async fn #count_ident(
2022                &self,
2023                pool: &#root::sql::Pool,
2024            ) -> ::core::result::Result<
2025                i64,
2026                #root::sql::ExecError,
2027            > {
2028                use #root::sql::CounterPool as _;
2029                self.#accessor_ident().count(pool).await
2030            }
2031        }
2032    });
2033    quote! {
2034        impl #struct_name {
2035            #( #methods )*
2036        }
2037    }
2038}
2039
2040/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
2041/// `#[rustango(through(...))]` attribute. Issue #817.
2042///
2043/// Each accessor builds a correlated subquery via
2044/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
2045/// intermediate table, filters on the FK column pointing at this
2046/// model, and projects the intermediate PK. The outer queryset
2047/// filters the far model by its FK-to-intermediate column being in
2048/// that set.
2049///
2050/// The returned `QuerySet<Far>` is **chainable** — the subquery
2051/// lives inside a `where_raw` clause so the user's later
2052/// `.filter()` / `.order_by()` / `.limit()` compositions don't
2053/// disturb it.
2054///
2055/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
2056/// SQLite — no LATERAL or backend-specific syntax involved.
2057fn through_accessor_tokens(
2058    struct_name: &syn::Ident,
2059    through_relations: &[ThroughAttr],
2060) -> TokenStream2 {
2061    let root = rustango_root();
2062    if through_relations.is_empty() {
2063        return TokenStream2::new();
2064    }
2065    let methods = through_relations.iter().map(|rel| {
2066        let method_name = format!("{}_through", rel.name);
2067        let count_name = format!("{}_through_count", rel.name);
2068        let fetch_name = format!("{}_through_fetch", rel.name);
2069        let first_name = format!("{}_through_first", rel.name);
2070        let pluck_name = format!("{}_through_pluck", rel.name);
2071        let method_ident = syn::Ident::new(&method_name, struct_name.span());
2072        let count_ident = syn::Ident::new(&count_name, struct_name.span());
2073        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
2074        let first_ident = syn::Ident::new(&first_name, struct_name.span());
2075        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
2076        let far = &rel.far;
2077        let intermediate = &rel.intermediate;
2078        let far_fk_column = rel.far_fk_column.as_str();
2079        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
2080        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
2081        let doc = format!(
2082            "Eloquent `hasManyThrough` accessor — returns a \
2083             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
2084             instance through the intermediate `{intermediate}` table. \
2085             Generated SQL shape: \
2086             `… WHERE {far_fk_column} IN (SELECT \
2087             {intermediate_pk_column} FROM <{intermediate}> WHERE \
2088             {intermediate_fk_column} = self.pk)`. Chainable like any \
2089             other QuerySet — compose `.filter()` / `.order_by()` / \
2090             `.limit()` etc. on top.",
2091        );
2092        let count_doc = format!(
2093            "Eloquent `$model->{name}->count()` analog for the \
2094             through-relation — returns the number of `{far}` rows \
2095             reachable through `{intermediate}`. Equivalent to \
2096             `self.{name}_through().count(pool)` but spelled \
2097             as a bare instance method for parity with the \
2098             `reverse_has` `<name>_count` shape.",
2099            name = rel.name,
2100        );
2101        quote! {
2102            #[doc = #doc]
2103            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
2104                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
2105                let intermediate_schema =
2106                    <#intermediate as #root::core::Model>::SCHEMA;
2107                let sub = SelectQuery::new(intermediate_schema)
2108                    .where_clause(WhereExpr::Predicate(Filter::new(
2109                        #intermediate_fk_column,
2110                        Op::Eq,
2111                        self.__rustango_pk_value(),
2112                    )))
2113                    .projection(::std::vec![#intermediate_pk_column]);
2114                #root::query::QuerySet::<#far>::new().where_raw(
2115                    WhereExpr::InSubquery {
2116                        column: #far_fk_column,
2117                        negated: false,
2118                        subquery: ::std::boxed::Box::new(sub),
2119                    },
2120                )
2121            }
2122
2123            #[doc = #count_doc]
2124            pub async fn #count_ident(
2125                &self,
2126                pool: &#root::sql::Pool,
2127            ) -> ::core::result::Result<
2128                i64,
2129                #root::sql::ExecError,
2130            > {
2131                use #root::sql::CounterPool as _;
2132                self.#method_ident().count(pool).await
2133            }
2134
2135            /// Eloquent `$model->relation->get()` for the
2136            /// through-relation — bare-name hot-path over
2137            /// `self.<name>_through().fetch(&pool)`. Use this
2138            /// when you don't need further `.filter()` /
2139            /// `.order_by()` composition; falls back to the
2140            /// chainable accessor when you do. Avoids the `_pool`
2141            /// suffix on the most common call-site shape.
2142            pub async fn #fetch_ident(
2143                &self,
2144                pool: &#root::sql::Pool,
2145            ) -> ::core::result::Result<
2146                ::std::vec::Vec<#far>,
2147                #root::sql::ExecError,
2148            > {
2149                use #root::sql::FetcherPool as _;
2150                self.#method_ident().fetch(pool).await
2151            }
2152
2153            /// Eloquent `hasOneThrough` analog — bare-name shortcut
2154            /// over `self.<name>_through().first(&pool)`. Returns
2155            /// `None` when no far rows are reachable through the
2156            /// intermediate. Useful when at most one row is
2157            /// expected (latest comment by country, primary tag,
2158            /// etc.).
2159            pub async fn #first_ident(
2160                &self,
2161                pool: &#root::sql::Pool,
2162            ) -> ::core::result::Result<
2163                ::core::option::Option<#far>,
2164                #root::sql::ExecError,
2165            > {
2166                self.#method_ident().first(pool).await
2167            }
2168
2169            /// Pluck a single column from the far rows into `Vec<U>`
2170            /// — cheaper than the typed `<Far>` decode when only one
2171            /// scalar column is needed.
2172            pub async fn #pluck_ident<U>(
2173                &self,
2174                col: &'static str,
2175                pool: &#root::sql::Pool,
2176            ) -> ::core::result::Result<
2177                ::std::vec::Vec<U>,
2178                #root::sql::ExecError,
2179            >
2180            where
2181                U: #root::sql::FlatScalar,
2182            {
2183                self.#method_ident()
2184                    .values_list_flat(col)
2185                    .fetch::<U>(pool)
2186                    .await
2187            }
2188        }
2189    });
2190    quote! {
2191        impl #struct_name {
2192            #( #methods )*
2193        }
2194    }
2195}
2196
2197struct ColumnEntry {
2198    /// The struct field ident, used both for the inherent const name on
2199    /// the model and for the inner column type's name.
2200    ident: syn::Ident,
2201    /// The struct's field type, used as `Column::Value`.
2202    value_ty: Type,
2203    /// Rust-side field name (e.g. `"id"`).
2204    name: String,
2205    /// SQL-side column name (e.g. `"user_id"`).
2206    column: String,
2207    /// `#root::core::FieldType::I64` etc.
2208    field_type_tokens: TokenStream2,
2209    /// A bare or optional `Uuid` field; MySQL decodes it from text.
2210    uuid_column: bool,
2211}
2212
2213struct CollectedFields {
2214    field_schemas: Vec<TokenStream2>,
2215    from_row_inits: Vec<TokenStream2>,
2216    /// Aliased counterparts of `from_row_inits` — read columns via
2217    /// `format!("{prefix}__{col}")` aliases so a Model can be
2218    /// decoded from a JOINed row's projected target columns.
2219    from_aliased_row_inits: Vec<TokenStream2>,
2220    /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2221    from_row_inits_my: Vec<TokenStream2>,
2222    from_aliased_row_inits_my: Vec<TokenStream2>,
2223    /// Static column-name list — used by the simple insert path
2224    /// (no `Auto<T>` fields). Aligned with `insert_values`.
2225    insert_columns: Vec<TokenStream2>,
2226    /// Static `Into<SqlValue>` expressions, one per field. Aligned
2227    /// with `insert_columns`. Used by the simple insert path only.
2228    insert_values: Vec<TokenStream2>,
2229    /// Per-field push expressions for the dynamic (Auto-aware)
2230    /// insert path. Each statement either unconditionally pushes
2231    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2232    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2233    insert_pushes: Vec<TokenStream2>,
2234    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2235    /// when `has_auto == false`.
2236    returning_cols: Vec<TokenStream2>,
2237    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2238    /// field. Run after `insert_returning` to populate the model.
2239    auto_assigns: Vec<TokenStream2>,
2240    /// `(ident, column_literal, is_uuid)` for every Auto field. Used by
2241    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2242    /// instead of `self`.
2243    auto_field_idents: Vec<(syn::Ident, String, bool)>,
2244    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2245    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2246    /// back into the struct after insert (MySQL has no RETURNING → the
2247    /// field stays at its placeholder until the next read).
2248    generated_field_idents: Vec<(syn::Ident, String)>,
2249    /// First `Auto<T>` field the database fills, with its inner `T`: the
2250    /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2251    first_db_auto: Option<(syn::Ident, Type)>,
2252    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2253    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2254    /// `columns`).
2255    bulk_pushes_no_auto: Vec<TokenStream2>,
2256    /// Bulk-insert per-row pushes for **all fields including Auto**.
2257    /// Used by the all-Auto-Set bulk path (Auto col included with the
2258    /// caller-supplied value).
2259    bulk_pushes_all: Vec<TokenStream2>,
2260    /// Column-name literals for non-Auto fields only (paired with
2261    /// `bulk_pushes_no_auto`).
2262    bulk_columns_no_auto: Vec<TokenStream2>,
2263    /// Column-name literals for every field including Auto (paired
2264    /// with `bulk_pushes_all`).
2265    bulk_columns_all: Vec<TokenStream2>,
2266    /// Fills each `default_uuid_v7` field of `_row` in place, so a
2267    /// mutable bulk insert hands the PKs back (#1934).
2268    bulk_fill_in_place: Vec<TokenStream2>,
2269    /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2270    rust_filled_idents: Vec<syn::Ident>,
2271    /// `auto_now` fields: `upsert` stamps them before binding.
2272    auto_now_idents: Vec<syn::Ident>,
2273    /// `default_uuid_v7` fields: an `Unset` one gets a fresh id, so the
2274    /// upsert marks it `generated` for the MySQL key guard.
2275    uuid_v7_fields: Vec<(syn::Ident, String)>,
2276    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2277    /// + the loop that asserts every row matches. One pair per Auto
2278    /// field. Empty when `has_auto == false`.
2279    bulk_auto_uniformity: Vec<TokenStream2>,
2280    /// Identifier of the first Auto field, used as the witness for
2281    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2282    first_auto_ident: Option<syn::Ident>,
2283    /// `true` if any field on the struct is `Auto<T>`.
2284    has_auto: bool,
2285    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2286    /// Gates `save()` codegen — only Auto PKs let us infer
2287    /// insert-vs-update from the in-memory value.
2288    pk_is_auto: bool,
2289    /// `Assignment` constructors for every non-PK column. Drives the
2290    /// UPDATE branch of `save()`.
2291    update_assignments: Vec<TokenStream2>,
2292    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2293    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2294    upsert_update_columns: Vec<TokenStream2>,
2295    primary_key: Option<(syn::Ident, String)>,
2296    column_entries: Vec<ColumnEntry>,
2297    /// Rust-side field names, in declaration order. Used to validate
2298    /// container attributes like `display = "…"`.
2299    field_names: Vec<String>,
2300    /// FK fields on this child model. Drives the reverse-relation
2301    /// helper emit — for each FK, the macro adds an inherent
2302    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2303    /// method on the parent type.
2304    fk_relations: Vec<FkRelation>,
2305    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2306    /// the model has one. Drives emission of the `soft_delete_on` /
2307    /// `restore_on` inherent methods. At most one such column per
2308    /// model is allowed; collect_fields rejects duplicates.
2309    soft_delete_column: Option<String>,
2310    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2311    /// companion to `soft_delete_column` for emitting predicates
2312    /// that need to read the field off `&self` (e.g. `trashed()`).
2313    soft_delete_field_ident: Option<syn::Ident>,
2314}
2315
2316#[derive(Clone)]
2317struct FkRelation {
2318    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2319    /// helper is emitted as `impl <ParentType> { … }`.
2320    parent_type: Type,
2321    /// Rust field ident of the FK. Its name is the key for select_related,
2322    /// prefetch and filters; the SQL column may differ (#1936).
2323    field_ident: syn::Ident,
2324    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2325    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2326    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2327    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2328    pk_kind: DetectedKind,
2329    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2330    /// FK column). Drives the `Some(...)` wrapping in load_related
2331    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2332    /// the codegen matches the field's declared shape.
2333    nullable: bool,
2334    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2335    /// override. When set, the reverse helper picks this name instead
2336    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2337    /// #816 (issue's "Related" note re: per-FK override).
2338    related_name: Option<String>,
2339}
2340
2341fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2342    let root = rustango_root();
2343    let cap = named.named.len();
2344    let mut out = CollectedFields {
2345        field_schemas: Vec::with_capacity(cap),
2346        from_row_inits: Vec::with_capacity(cap),
2347        from_aliased_row_inits: Vec::with_capacity(cap),
2348        from_row_inits_my: Vec::with_capacity(cap),
2349        from_aliased_row_inits_my: Vec::with_capacity(cap),
2350        insert_columns: Vec::with_capacity(cap),
2351        insert_values: Vec::with_capacity(cap),
2352        insert_pushes: Vec::with_capacity(cap),
2353        returning_cols: Vec::new(),
2354        auto_assigns: Vec::new(),
2355        auto_field_idents: Vec::new(),
2356        generated_field_idents: Vec::new(),
2357        first_db_auto: None,
2358        bulk_pushes_no_auto: Vec::with_capacity(cap),
2359        bulk_pushes_all: Vec::with_capacity(cap),
2360        bulk_columns_no_auto: Vec::with_capacity(cap),
2361        bulk_columns_all: Vec::with_capacity(cap),
2362        bulk_fill_in_place: Vec::new(),
2363        rust_filled_idents: Vec::new(),
2364        auto_now_idents: Vec::new(),
2365        uuid_v7_fields: Vec::new(),
2366        bulk_auto_uniformity: Vec::new(),
2367        first_auto_ident: None,
2368        has_auto: false,
2369        pk_is_auto: false,
2370        update_assignments: Vec::with_capacity(cap),
2371        upsert_update_columns: Vec::with_capacity(cap),
2372        primary_key: None,
2373        column_entries: Vec::with_capacity(cap),
2374        field_names: Vec::with_capacity(cap),
2375        fk_relations: Vec::new(),
2376        soft_delete_column: None,
2377        soft_delete_field_ident: None,
2378    };
2379
2380    for field in &named.named {
2381        let info = process_field(field, table)?;
2382        out.field_names.push(ident_name(info.ident));
2383        out.field_schemas.push(info.schema);
2384        out.from_row_inits.push(info.from_row_init);
2385        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2386        out.from_row_inits_my.push(info.from_row_init_my);
2387        out.from_aliased_row_inits_my
2388            .push(info.from_aliased_row_init_my);
2389        if let Some(parent_ty) = info.fk_inner.clone() {
2390            out.fk_relations.push(FkRelation {
2391                parent_type: parent_ty,
2392                field_ident: info.ident.clone(),
2393                pk_kind: info.fk_pk_kind,
2394                nullable: info.nullable,
2395                related_name: info.related_name.clone(),
2396            });
2397        }
2398        if info.soft_delete {
2399            if out.soft_delete_column.is_some() {
2400                return Err(syn::Error::new_spanned(
2401                    field,
2402                    "only one field may be marked `#[rustango(soft_delete)]`",
2403                ));
2404            }
2405            out.soft_delete_column = Some(info.column.clone());
2406            out.soft_delete_field_ident = Some(info.ident.clone());
2407        }
2408        let column = info.column.as_str();
2409        let ident = info.ident;
2410        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2411        // skip every write path — Postgres recomputes the value
2412        // from EXPR. Push only the column-entry record (so typed
2413        // column constants still exist for filtering / projection)
2414        // and the schema literal (already pushed above) and move
2415        // on. No insert_columns/values, no insert_pushes, no
2416        // bulk_*, no update_assignments, no upsert_update_columns,
2417        // no returning_cols.
2418        if info.generated_as.is_some() {
2419            out.column_entries.push(ColumnEntry {
2420                ident: ident.clone(),
2421                value_ty: info.value_ty.clone(),
2422                name: ident_name(ident),
2423                column: info.column.clone(),
2424                field_type_tokens: info.field_type_tokens,
2425                uuid_column: info.uuid_column,
2426            });
2427            // #1028 — refresh the DB-computed value after insert on
2428            // RETURNING-capable backends. The column joins `returning_cols`
2429            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2430            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2431            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2432            // it keeps the placeholder until the next read.
2433            out.returning_cols.push(quote!(#column));
2434            out.generated_field_idents
2435                .push((ident.clone(), info.column.clone()));
2436            continue;
2437        }
2438        out.insert_columns.push(quote!(#column));
2439        out.insert_values.push(quote! {
2440            ::core::convert::Into::<#root::core::SqlValue>::into(
2441                ::core::clone::Clone::clone(&self.#ident)
2442            )
2443        });
2444        if info.auto {
2445            out.has_auto = true;
2446            if out.first_auto_ident.is_none() {
2447                out.first_auto_ident = Some(ident.clone());
2448            }
2449            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2450            // before binding, so the value is already in
2451            // `self.#ident` after the insert_push — RETURNING is
2452            // redundant. Skip adding this column to returning_cols /
2453            // auto_assigns / auto_field_idents to avoid (a) an
2454            // unnecessary RETURNING column on every dialect, and (b)
2455            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2456            // integer PK.
2457            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2458            // for the same reason: they are filled Rust-side below and
2459            // bound, so the value is already in `self` and RETURNING
2460            // would be a redundant column on every dialect.
2461            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2462                if out.first_db_auto.is_none() {
2463                    out.first_db_auto = auto_inner_type(info.value_ty)
2464                        .cloned()
2465                        .map(|ty| (ident.clone(), ty));
2466                }
2467                out.returning_cols.push(quote!(#column));
2468                out.auto_field_idents.push((
2469                    ident.clone(),
2470                    info.column.clone(),
2471                    info.fk_pk_kind == DetectedKind::Uuid,
2472                ));
2473                out.auto_assigns.push(quote! {
2474                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2475                });
2476            }
2477            if info.default_uuid_v7 {
2478                // Rust-side UUIDv7 generation (issue #823, Eloquent
2479                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2480                // and bind; Auto::Set → bind the user's value. The
2481                // column is ALWAYS present in the INSERT statement —
2482                // no RETURNING / no DB DEFAULT needed.
2483                out.insert_pushes.push(quote! {
2484                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2485                        self.#ident = #root::sql::Auto::Set(
2486                            #root::__uuid::Uuid::now_v7(),
2487                        );
2488                    }
2489                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2490                        _columns.push(#column);
2491                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2492                            ::core::clone::Clone::clone(_v)
2493                        ));
2494                    }
2495                });
2496            } else if info.auto_now_add || info.auto_now {
2497                // Rust-side write timestamp (#1464), mirroring
2498                // `default_uuid_v7` above. `auto_now` has always bound
2499                // `Utc::now()` on UPDATE but not on INSERT, so the
2500                // first write of an `updated_at` took the DB default
2501                // while every later one took the clock — the same
2502                // column, filled two different ways.
2503                //
2504                // The default is the problem. `CREATE TABLE IF NOT
2505                // EXISTS` leaves an upgraded table's
2506                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2507                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2508                // statement that changes it — so every insert relying
2509                // on that default wrote the legacy shape forever, while
2510                // the migrate sweep converted the rows around it. The
2511                // column went permanently mixed and `ORDER BY` inverted
2512                // the admin audit log (#1616 rework review,
2513                // correctness-001 / dialects-005 / security-002).
2514                //
2515                // Binding it here makes the stale default unreachable
2516                // rather than trying to migrate it: the timestamp comes
2517                // from the application, not the database.
2518                out.insert_pushes.push(quote! {
2519                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2520                        self.#ident = #root::sql::Auto::Set(
2521                            #root::__chrono::Utc::now(),
2522                        );
2523                    }
2524                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2525                        _columns.push(#column);
2526                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2527                            ::core::clone::Clone::clone(_v)
2528                        ));
2529                    }
2530                });
2531            } else {
2532                out.insert_pushes.push(quote! {
2533                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2534                        _columns.push(#column);
2535                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2536                            ::core::clone::Clone::clone(_v)
2537                        ));
2538                    }
2539                });
2540            }
2541            // …except the timestamp columns, which must appear in BOTH
2542            // paths (#1464). The path is chosen by the *first* Auto
2543            // field — in practice the PK — so a bulk insert of rows
2544            // with an unset PK took the no-auto branch and dropped the
2545            // timestamp column, falling back to the DB default that
2546            // this change exists to stop depending on.
2547            //
2548            // `rows` is borrowed immutably here, so this fills per row
2549            // at push time rather than writing back into the struct. An
2550            // explicitly-Set value is still honoured; only `Unset`
2551            // takes the clock.
2552            // `default_uuid_v7` likewise: its column has no DB default (#1934).
2553            if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2554                out.rust_filled_idents.push(ident.clone());
2555            }
2556            if info.auto_now {
2557                out.auto_now_idents.push(ident.clone());
2558            }
2559            if info.default_uuid_v7 {
2560                out.uuid_v7_fields
2561                    .push((ident.clone(), info.column.clone()));
2562            }
2563            let rust_fill = if info.default_uuid_v7 {
2564                Some(quote!(#root::__uuid::Uuid::now_v7()))
2565            } else if info.auto_now_add || info.auto_now {
2566                Some(quote!(#root::__chrono::Utc::now()))
2567            } else {
2568                None
2569            };
2570            // Bulk: Auto fields appear in the all-Set path. An unset filled
2571            // field takes its fill there too, not NULL (#1950).
2572            out.bulk_columns_all.push(quote!(#column));
2573            if let Some(fill) = &rust_fill {
2574                let push = quote! {
2575                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2576                        match &_row.#ident {
2577                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2578                            #root::sql::Auto::Unset => #fill,
2579                        }
2580                    ));
2581                };
2582                out.bulk_columns_no_auto.push(quote!(#column));
2583                out.bulk_pushes_no_auto.push(push.clone());
2584                out.bulk_pushes_all.push(push);
2585            } else {
2586                out.bulk_pushes_all.push(quote! {
2587                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2588                        ::core::clone::Clone::clone(&_row.#ident)
2589                    ));
2590                });
2591            }
2592            if info.default_uuid_v7 {
2593                out.bulk_fill_in_place.push(quote! {
2594                    if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2595                        _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2596                    }
2597                });
2598            }
2599            // Uniformity check: every row's Auto state must match the
2600            // first row's. Mixed Set/Unset within one bulk_insert is
2601            // rejected here so the column list stays consistent.
2602            // A filled field may be unset on any row, so it is exempt.
2603            let ident_clone = ident.clone();
2604            if rust_fill.is_none() {
2605                out.bulk_auto_uniformity.push(quote! {
2606                    for _r in rows.iter().skip(1) {
2607                        if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2608                            return ::core::result::Result::Err(
2609                                #root::sql::ExecError::Sql(
2610                                    #root::sql::SqlError::BulkAutoMixed
2611                                )
2612                            );
2613                        }
2614                    }
2615                });
2616            }
2617        } else {
2618            out.insert_pushes.push(quote! {
2619                _columns.push(#column);
2620                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2621                    ::core::clone::Clone::clone(&self.#ident)
2622                ));
2623            });
2624            // Bulk: non-Auto fields appear in BOTH paths.
2625            out.bulk_columns_no_auto.push(quote!(#column));
2626            out.bulk_columns_all.push(quote!(#column));
2627            let push_expr = quote! {
2628                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2629                    ::core::clone::Clone::clone(&_row.#ident)
2630                ));
2631            };
2632            out.bulk_pushes_no_auto.push(push_expr.clone());
2633            out.bulk_pushes_all.push(push_expr);
2634        }
2635        if info.primary_key {
2636            if out.primary_key.is_some() {
2637                return Err(syn::Error::new_spanned(
2638                    field,
2639                    "only one field may be marked `#[rustango(primary_key)]`",
2640                ));
2641            }
2642            out.primary_key = Some((ident.clone(), info.column.clone()));
2643            if info.auto {
2644                out.pk_is_auto = true;
2645            }
2646        } else if info.auto_now_add {
2647            // Immutable post-insert: skip from UPDATE entirely.
2648        } else if info.auto_now {
2649            // `auto_now` columns: bind `chrono::Utc::now()` on every
2650            // UPDATE, and write it back so `self` matches the row.
2651            out.update_assignments.push(quote! {
2652                #root::core::Assignment::new(
2653                    #column,
2654                    {
2655                        let _now = #root::__chrono::Utc::now();
2656                        self.#ident = #root::sql::Auto::Set(_now);
2657                        ::core::convert::Into::<#root::core::SqlValue>::into(_now)
2658                    },
2659                )
2660            });
2661            out.upsert_update_columns.push(quote!(#column));
2662        } else {
2663            out.update_assignments.push(quote! {
2664                #root::core::Assignment::new(
2665                    #column,
2666                    ::core::convert::Into::<#root::core::SqlValue>::into(
2667                        ::core::clone::Clone::clone(&self.#ident)
2668                    ),
2669                )
2670            });
2671            out.upsert_update_columns.push(quote!(#column));
2672        }
2673        out.column_entries.push(ColumnEntry {
2674            ident: ident.clone(),
2675            value_ty: info.value_ty.clone(),
2676            name: ident_name(ident),
2677            column: info.column.clone(),
2678            field_type_tokens: info.field_type_tokens,
2679            uuid_column: info.uuid_column,
2680        });
2681    }
2682    Ok(out)
2683}
2684
2685/// A derived index name within PG's 63 bytes (MySQL takes 64); a longer one
2686/// keeps a prefix plus a hash of the whole, so two long names stay distinct.
2687fn capped_index_name(name: String) -> String {
2688    const MAX: usize = 63;
2689    if name.len() <= MAX {
2690        return name;
2691    }
2692    // FNV-1a: stable across builds, unlike `DefaultHasher`.
2693    let hash = name.bytes().fold(0x811c_9dc5_u32, |h, b| {
2694        (h ^ u32::from(b)).wrapping_mul(0x0100_0193)
2695    });
2696    let mut cut = MAX - 9;
2697    while !name.is_char_boundary(cut) {
2698        cut -= 1;
2699    }
2700    format!("{}_{hash:08x}", &name[..cut])
2701}
2702
2703fn model_impl_tokens(
2704    struct_name: &syn::Ident,
2705    model_name: &str,
2706    table: &str,
2707    display: Option<&str>,
2708    app_label: Option<&str>,
2709    admin: Option<&AdminAttrs>,
2710    default_order: &[(String, bool, proc_macro2::Span)],
2711    field_schemas: &[TokenStream2],
2712    soft_delete_column: Option<&str>,
2713    permissions: bool,
2714    audit_track: Option<&[String]>,
2715    m2m_relations: &[M2MAttr],
2716    indexes: &[IndexAttr],
2717    checks: &[CheckAttr],
2718    excludes: &[ExcludeAttr],
2719    composite_fks: &[CompositeFkAttr],
2720    generic_fks: &[GenericFkAttr],
2721    scope: Option<&str>,
2722    is_view: bool,
2723    verbose_name: Option<&str>,
2724    verbose_name_plural: Option<&str>,
2725    managed: bool,
2726    base_manager_name: Option<&str>,
2727    order_with_respect_to: Option<&str>,
2728    proxy: bool,
2729    required_db_features: &[String],
2730    required_db_vendor: Option<&str>,
2731    default_related_name: Option<&str>,
2732    db_table_comment: Option<&str>,
2733    get_latest_by: Option<(&str, bool)>,
2734    extra_permissions: &[(String, String)],
2735    default_permissions: &[String],
2736    global_scopes: &[GlobalScopeAttr],
2737    reverse_has_relations: &[ReverseHasAttr],
2738    generic_has_relations: &[GenericHasAttr],
2739) -> TokenStream2 {
2740    let root = rustango_root();
2741    let display_tokens = if let Some(name) = display {
2742        quote!(::core::option::Option::Some(#name))
2743    } else {
2744        quote!(::core::option::Option::None)
2745    };
2746    let app_label_tokens = if let Some(name) = app_label {
2747        quote!(::core::option::Option::Some(#name))
2748    } else {
2749        quote!(::core::option::Option::None)
2750    };
2751    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2752        quote!(::core::option::Option::Some(#col))
2753    } else {
2754        quote!(::core::option::Option::None)
2755    };
2756    let audit_track_tokens = match audit_track {
2757        None => quote!(::core::option::Option::None),
2758        Some(names) => {
2759            let lits = names.iter().map(|n| n.as_str());
2760            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2761        }
2762    };
2763    let admin_tokens = admin_config_tokens(admin);
2764    // Default `tenant` so single-tenant projects (no `scope` attr
2765    // anywhere) keep the v0.24.x behavior. Container-attr parser
2766    // already validated the value is "registry" or "tenant".
2767    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2768        Some("registry") => quote!(#root::core::ModelScope::Registry),
2769        _ => quote!(#root::core::ModelScope::Tenant),
2770    };
2771    let verbose_name_tokens = optional_str(verbose_name);
2772    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2773    let base_manager_name_tokens = optional_str(base_manager_name);
2774    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2775    let required_db_features_lits: Vec<&str> =
2776        required_db_features.iter().map(String::as_str).collect();
2777    let required_db_vendor_tokens = optional_str(required_db_vendor);
2778    let default_related_name_tokens = optional_str(default_related_name);
2779    let db_table_comment_tokens = optional_str(db_table_comment);
2780    let get_latest_by_tokens = match get_latest_by {
2781        Some((col, desc)) => {
2782            quote!(::core::option::Option::Some((#col, #desc)))
2783        }
2784        None => quote!(::core::option::Option::None),
2785    };
2786    let extra_permission_tokens: Vec<_> = extra_permissions
2787        .iter()
2788        .map(|(c, l)| quote!((#c, #l)))
2789        .collect();
2790    let default_permission_tokens: Vec<_> = default_permissions
2791        .iter()
2792        .map(|action| quote!(#action))
2793        .collect();
2794    let indexes_tokens = indexes.iter().map(|idx| {
2795        // When no explicit `name = "..."` was given, derive a stable,
2796        // collision-free name from the table + columns
2797        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2798        // would clash the moment a model declares two unnamed indexes.
2799        let derived_name = idx
2800            .name
2801            .clone()
2802            .unwrap_or_else(|| capped_index_name(format!("{table}_{}_idx", idx.columns.join("_"))));
2803        let name = derived_name.as_str();
2804        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2805        let unique = idx.unique;
2806        // Map the parsed method string onto the IndexMethod enum
2807        // variant — kept at the codegen layer so the IR doesn't
2808        // carry the string form.
2809        let method_variant = match idx.method.as_str() {
2810            "gin" => quote!(#root::core::IndexMethod::Gin),
2811            "gist" => quote!(#root::core::IndexMethod::Gist),
2812            "brin" => quote!(#root::core::IndexMethod::Brin),
2813            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2814            "hash" => quote!(#root::core::IndexMethod::Hash),
2815            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2816            _ => quote!(#root::core::IndexMethod::BTree),
2817        };
2818        let where_clause = match &idx.where_clause {
2819            Some(s) => quote!(::core::option::Option::Some(#s)),
2820            None => quote!(::core::option::Option::None),
2821        };
2822        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2823        quote! {{
2824            let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2825            i.unique = #unique;
2826            i.method = #method_variant;
2827            i.where_clause = #where_clause;
2828            i.include = &[ #(#include_lits),* ];
2829            i
2830        }}
2831    });
2832    let checks_tokens = checks.iter().map(|c| {
2833        let name = c.name.as_str();
2834        let expr = c.expr.as_str();
2835        quote!(#root::core::CheckConstraint::new(#name, #expr))
2836    });
2837    let excludes_tokens = excludes.iter().map(|e| {
2838        let name = e.name.as_str();
2839        let using = e.using.as_str();
2840        let element_tokens = e.elements.iter().map(|(col, op)| {
2841            let col_s = col.as_str();
2842            let op_s = op.as_str();
2843            quote!((#col_s, #op_s))
2844        });
2845        let where_tokens = match e.where_clause.as_deref() {
2846            Some(w) => quote!(::core::option::Option::Some(#w)),
2847            None => quote!(::core::option::Option::None),
2848        };
2849        quote! {{
2850            let mut e = #root::core::ExclusionConstraint::new(
2851                #name,
2852                #using,
2853                &[ #(#element_tokens),* ],
2854            );
2855            e.where_clause = #where_tokens;
2856            e
2857        }}
2858    });
2859    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2860        let name = rel.name.as_str();
2861        let to = rel.to.as_str();
2862        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2863        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2864        quote! {
2865            #root::core::CompositeFkRelation::new(
2866                #name,
2867                #to,
2868                &[ #(#from_cols),* ],
2869                &[ #(#on_cols),* ],
2870            )
2871        }
2872    });
2873    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2874        let name = rel.name.as_str();
2875        let ct_col = rel.ct_column.as_str();
2876        let pk_col = rel.pk_column.as_str();
2877        quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2878    });
2879    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2880    // no `#[rustango(default_order = "...")]` attribute was supplied.
2881    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2882        let col_lit = col.as_str();
2883        quote! { (#col_lit, #desc) }
2884    });
2885
2886    // Issue #820 — `global_scopes` slice literal. Empty when no
2887    // `#[rustango(global_scope(...))]` attribute was supplied. The
2888    // `apply` path is re-emitted verbatim so it resolves in the
2889    // consumer's scope; the name is stored as a string literal.
2890    let global_scope_tokens = global_scopes.iter().map(|s| {
2891        let name = s.name.as_str();
2892        // The slice sits in a nested `const`, where `Self` does not resolve.
2893        let mut apply = s.apply.clone();
2894        if let Some(first) = apply.segments.first_mut() {
2895            if first.ident == "Self" {
2896                first.ident = struct_name.clone();
2897            }
2898        }
2899        quote!(#root::core::GlobalScope::new(#name, #apply))
2900    });
2901
2902    let m2m_tokens = m2m_relations.iter().map(|rel| {
2903        let name = rel.name.as_str();
2904        let to = rel.to.as_str();
2905        let through = rel.through.as_str();
2906        let src = rel.src.as_str();
2907        let dst = rel.dst.as_str();
2908        let auto_create = rel.auto_create;
2909        quote! {{
2910            let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2911            m.auto_create = #auto_create;
2912            m
2913        }}
2914    });
2915    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2916    // when the model declares `#[rustango(reverse_has(...))]`. Each
2917    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2918    // a const expression. Models without reverse_has fall through to
2919    // the trait's empty default — no override emitted.
2920    let reverse_relations_override = if reverse_has_relations.is_empty() {
2921        quote!()
2922    } else {
2923        let entries = reverse_has_relations.iter().map(|rel| {
2924            let name = rel.name.as_str();
2925            let child = &rel.child;
2926            let child_fk_column = rel.child_fk_column.as_str();
2927            let self_pk_column = rel.self_pk_column();
2928            quote! {
2929                #root::core::ReverseRelation::new(
2930                    #name,
2931                    <#child as #root::core::Model>::SCHEMA,
2932                    #child_fk_column,
2933                    #self_pk_column,
2934                )
2935            }
2936        });
2937        quote! {
2938            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2939                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2940                RELS
2941            }
2942        }
2943    };
2944    // Issue #830 — `Model::generic_reverse_relations()` override for
2945    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2946    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2947        quote!()
2948    } else {
2949        let entries = generic_has_relations.iter().map(|rel| {
2950            let name = rel.name.as_str();
2951            let child = &rel.child;
2952            let ct_column = rel.ct_column.as_str();
2953            let pk_column = rel.pk_column.as_str();
2954            let self_pk_column = rel.self_pk_column();
2955            quote! {
2956                #root::core::GenericReverseRelation::new(
2957                    #name,
2958                    <#child as #root::core::Model>::SCHEMA,
2959                    #ct_column,
2960                    #pk_column,
2961                    #self_pk_column,
2962                )
2963            }
2964        });
2965        quote! {
2966            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2967                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2968                RELS
2969            }
2970        }
2971    };
2972    let fields_slice = const_slice(
2973        &quote!(#root::core::FieldSchema),
2974        field_schemas.iter().cloned(),
2975    );
2976    let m2m_slice = const_slice(&quote!(#root::core::M2MRelation), m2m_tokens);
2977    let indexes_slice = const_slice(&quote!(#root::core::IndexSchema), indexes_tokens);
2978    let checks_slice = const_slice(&quote!(#root::core::CheckConstraint), checks_tokens);
2979    let excludes_slice = const_slice(&quote!(#root::core::ExclusionConstraint), excludes_tokens);
2980    let composite_fk_slice = const_slice(
2981        &quote!(#root::core::CompositeFkRelation),
2982        composite_fk_tokens,
2983    );
2984    let generic_fk_slice = const_slice(&quote!(#root::core::GenericRelation), generic_fk_tokens);
2985    let audited_update_override = if audit_track.is_some() {
2986        quote! {
2987            fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2988                ::core::option::Option::Some(Self::__rustango_update_audited)
2989            }
2990            fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2991                ::core::option::Option::Some(Self::__rustango_delete_audited)
2992            }
2993            fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2994                ::core::option::Option::Some(Self::__rustango_create_audited)
2995            }
2996            fn __rustango_audited_update_record(
2997            ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
2998                ::core::option::Option::Some(Self::__rustango_update_record_audited)
2999            }
3000            fn __rustango_audited_cascade() -> ::core::option::Option<#root::audit::AuditedCascade> {
3001                ::core::option::Option::Some(Self::__rustango_cascade_audited)
3002            }
3003        }
3004    } else {
3005        quote!()
3006    };
3007    let global_scope_slice = const_slice(&quote!(#root::core::GlobalScope), global_scope_tokens);
3008    quote! {
3009        impl #root::core::Model for #struct_name {
3010            const SCHEMA: &'static #root::core::ModelSchema = &{
3011                #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
3012                    name: #model_name,
3013                    table: #table,
3014                    fields: #fields_slice,
3015                    display: #display_tokens,
3016                    app_label: #app_label_tokens,
3017                    admin: #admin_tokens,
3018                    soft_delete_column: #soft_delete_tokens,
3019                    permissions: #permissions,
3020                    audit_track: #audit_track_tokens,
3021                    m2m: #m2m_slice,
3022                    indexes: #indexes_slice,
3023                    check_constraints: #checks_slice,
3024                    exclusion_constraints: #excludes_slice,
3025                    composite_relations: #composite_fk_slice,
3026                    generic_relations: #generic_fk_slice,
3027                    scope: #scope_tokens,
3028                    default_order: &[ #(#default_order_tokens),* ],
3029                    is_view: #is_view,
3030                    verbose_name: #verbose_name_tokens,
3031                    verbose_name_plural: #verbose_name_plural_tokens,
3032                    managed: #managed,
3033                    base_manager_name: #base_manager_name_tokens,
3034                    order_with_respect_to: #order_with_respect_to_tokens,
3035                    proxy: #proxy,
3036                    required_db_features: &[ #(#required_db_features_lits),* ],
3037                    required_db_vendor: #required_db_vendor_tokens,
3038                    default_related_name: #default_related_name_tokens,
3039                    db_table_comment: #db_table_comment_tokens,
3040                    get_latest_by: #get_latest_by_tokens,
3041                    extra_permissions: &[ #(#extra_permission_tokens),* ],
3042                    default_permissions: &[ #(#default_permission_tokens),* ],
3043                    global_scopes: #global_scope_slice,
3044                })
3045            };
3046
3047            #reverse_relations_override
3048            #generic_reverse_relations_override
3049            #audited_update_override
3050        }
3051    }
3052}
3053
3054/// Emit the `admin: Option<&'static AdminConfig>` field for the model
3055/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
3056/// otherwise a static reference to a populated `AdminConfig`.
3057fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
3058    let root = rustango_root();
3059    let Some(admin) = admin else {
3060        return quote!(::core::option::Option::None);
3061    };
3062
3063    let list_display = admin
3064        .list_display
3065        .as_ref()
3066        .map(|(v, _)| v.as_slice())
3067        .unwrap_or(&[]);
3068    let list_display_lits = list_display.iter().map(|s| s.as_str());
3069
3070    let search_fields = admin
3071        .search_fields
3072        .as_ref()
3073        .map(|(v, _)| v.as_slice())
3074        .unwrap_or(&[]);
3075    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
3076
3077    let readonly_fields = admin
3078        .readonly_fields
3079        .as_ref()
3080        .map(|(v, _)| v.as_slice())
3081        .unwrap_or(&[]);
3082    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
3083
3084    let list_filter = admin
3085        .list_filter
3086        .as_ref()
3087        .map(|(v, _)| v.as_slice())
3088        .unwrap_or(&[]);
3089    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
3090
3091    let actions = admin
3092        .actions
3093        .as_ref()
3094        .map(|(v, _)| v.as_slice())
3095        .unwrap_or(&[]);
3096    let actions_lits = actions.iter().map(|s| s.as_str());
3097
3098    let fieldsets = admin
3099        .fieldsets
3100        .as_ref()
3101        .map(|(v, _)| v.as_slice())
3102        .unwrap_or(&[]);
3103    let fieldset_tokens = const_slice(
3104        &quote!(#root::core::Fieldset),
3105        fieldsets.iter().map(|(title, fields)| {
3106            let title = title.as_str();
3107            let field_lits = fields.iter().map(String::as_str);
3108            quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
3109        }),
3110    );
3111
3112    let list_display_links = admin
3113        .list_display_links
3114        .as_ref()
3115        .map(|(v, _)| v.as_slice())
3116        .unwrap_or(&[]);
3117    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
3118
3119    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
3120    let actions_on_top = admin.actions_on_top.unwrap_or(true);
3121    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
3122    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
3123
3124    let prepopulated = admin
3125        .prepopulated_fields
3126        .as_ref()
3127        .map(|(v, _)| v.as_slice())
3128        .unwrap_or(&[]);
3129    let prepopulated_tokens = const_slice(
3130        &quote!(#root::core::PrepopulatedField),
3131        prepopulated.iter().map(|(target, sources)| {
3132            let target = target.as_str();
3133            let source_lits = sources.iter().map(String::as_str);
3134            quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
3135        }),
3136    );
3137
3138    let raw_id_fields = admin
3139        .raw_id_fields
3140        .as_ref()
3141        .map(|(v, _)| v.as_slice())
3142        .unwrap_or(&[]);
3143    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
3144
3145    let autocomplete_fields = admin
3146        .autocomplete_fields
3147        .as_ref()
3148        .map(|(v, _)| v.as_slice())
3149        .unwrap_or(&[]);
3150    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
3151
3152    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
3153    let list_select_related_tokens = match admin.list_select_related.as_deref() {
3154        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
3155        Some("none") => quote!(#root::core::ListSelectRelated::None),
3156        Some(raw) => {
3157            let names: Vec<&str> = raw
3158                .split(',')
3159                .map(str::trim)
3160                .filter(|s| !s.is_empty())
3161                .collect();
3162            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
3163        }
3164    };
3165
3166    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
3167    // a Vec<(String, String)>. Empty / unset → no overrides.
3168    let formfield_pairs: Vec<(&str, &str)> = admin
3169        .formfield_overrides
3170        .as_ref()
3171        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
3172        .unwrap_or_default();
3173    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
3174        let field = *field;
3175        let widget = *widget;
3176        quote!((#field, #widget))
3177    });
3178
3179    let list_per_page = admin.list_per_page.unwrap_or(0);
3180
3181    let ordering_pairs = admin
3182        .ordering
3183        .as_ref()
3184        .map(|(v, _)| v.as_slice())
3185        .unwrap_or(&[]);
3186    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3187        let name = name.as_str();
3188        let desc = *desc;
3189        quote!((#name, #desc))
3190    });
3191
3192    quote! {
3193        ::core::option::Option::Some({
3194            const ADMIN: &#root::core::AdminConfig = &{
3195                #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3196                    list_display: &[ #( #list_display_lits ),* ],
3197                    search_fields: &[ #( #search_fields_lits ),* ],
3198                    list_per_page: #list_per_page,
3199                    ordering: &[ #( #ordering_tokens ),* ],
3200                    readonly_fields: &[ #( #readonly_fields_lits ),* ],
3201                    list_filter: &[ #( #list_filter_lits ),* ],
3202                    actions: &[ #( #actions_lits ),* ],
3203                    fieldsets: #fieldset_tokens,
3204                    list_display_links: &[ #( #list_display_links_lits ),* ],
3205                    search_help_text: #search_help_text,
3206                    actions_on_top: #actions_on_top,
3207                    actions_on_bottom: #actions_on_bottom,
3208                    date_hierarchy: #date_hierarchy,
3209                    prepopulated_fields: #prepopulated_tokens,
3210                    raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3211                    autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3212                    list_select_related: #list_select_related_tokens,
3213                    formfield_overrides: &[ #( #formfield_tokens ),* ],
3214                })
3215            };
3216            ADMIN
3217        })
3218    }
3219}
3220
3221fn inherent_impl_tokens(
3222    struct_name: &syn::Ident,
3223    fields: &CollectedFields,
3224    primary_key: Option<&(syn::Ident, String)>,
3225    column_consts: &TokenStream2,
3226    audited_fields: Option<&[&ColumnEntry]>,
3227    upsert_unique: Option<&[String]>,
3228    manager_fns: &[syn::Ident],
3229) -> TokenStream2 {
3230    let root = rustango_root();
3231
3232    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3233    // (audit-on-connection over &Pool needs a bi-dialect transaction
3234    // helper, deferred). Two body shapes:
3235    // - has_auto: build InsertQuery skipping Auto::Unset columns,
3236    //   request Auto cols in `returning`, dispatch via
3237    //   `insert_returning_pool`, then on the returned `PgRow` /
3238    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
3239    //   row OR drop the single i64 into the first Auto field on MySQL
3240    //   (multi-Auto models on MySQL error at runtime since
3241    //   `LAST_INSERT_ID()` only reports one)
3242    // - non-Auto: build InsertQuery with explicit columns/values and
3243    //   call `insert_pool` (no returning needed)
3244    // pool_insert_method body for the audited Auto-PK case is moved
3245    // to after audit_pair_tokens / audit_pk_to_string (they live
3246    // ~150 lines below). This block keeps the non-audited and
3247    // non-Auto branches in place — the audited Auto-PK arm is
3248    // computed below and merged via the dispatch helper variable.
3249    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3250        // Audited models with explicit (non-Auto) PKs go through
3251        // the non-Auto insert path below — the audit emit is one
3252        // round-trip after the INSERT inside the same tx via
3253        // audit::save_one_with_audit? No, INSERT semantics
3254        // differ. For non-Auto PK + audited, route through a
3255        // dedicated insert + audit emit on the same tx, but defer
3256        // the macro emission to the audit-bundle-aware block below
3257        // — this `quote!()` placeholder gets overwritten there.
3258        quote!()
3259    } else if audited_fields.is_some() && fields.has_auto {
3260        // Audited Auto-PK insert_pool — assembled after the audit
3261        // bundles. Placeholder; real emission below.
3262        quote!()
3263    } else if fields.has_auto {
3264        let pushes = &fields.insert_pushes;
3265        // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3266        let filled = &fields.rust_filled_idents;
3267        let was_unset: Vec<syn::Ident> = filled
3268            .iter()
3269            .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3270            .collect();
3271        let insert_or_ignore_call = quote! {
3272            #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3273            let mut _columns: ::std::vec::Vec<&'static str> =
3274                ::std::vec::Vec::new();
3275            let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3276                ::std::vec::Vec::new();
3277            #( #pushes )*
3278            let _query = #root::core::InsertQuery::new(
3279                <Self as #root::core::Model>::SCHEMA,
3280                _columns,
3281                _values,
3282            );
3283            let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3284            if !matches!(_result, ::core::result::Result::Ok(true)) {
3285                #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3286            }
3287            _result
3288        };
3289        // When every `Auto<T>` field is filled Rust-side
3290        // (`default_uuid_v7`, issue #823), there is no column to read
3291        // back from the database — `returning_cols` is empty. Route
3292        // through plain `insert_pool` instead of
3293        // `insert_returning_pool` to skip the redundant RETURNING /
3294        // LAST_INSERT_ID round-trip.
3295        if fields.returning_cols.is_empty() {
3296            quote! {
3297
3298
3299                /// Eloquent `Model::insertOrIgnore()` — same shape
3300                /// as the auto-PK branch above. Returns `Ok(true)`
3301                /// when inserted, `Ok(false)` when a conflict caused
3302                /// the INSERT to silently skip.
3303                ///
3304                /// # Errors
3305                /// As [`Self::insert`].
3306                pub async fn insert_or_ignore(
3307                    &mut self,
3308                    pool: &#root::sql::Pool,
3309                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3310                    #insert_or_ignore_call
3311                }
3312            }
3313        } else {
3314            quote! {
3315
3316
3317                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3318                /// row or silently skip on unique-constraint
3319                /// violation. Returns `Ok(true)` when a row was
3320                /// inserted, `Ok(false)` when a conflict caused the
3321                /// INSERT to silently skip.
3322                ///
3323                /// **Caveat on auto-PK models**: when the row is
3324                /// skipped (conflict), this instance's `Auto<T>`
3325                /// fields stay `Unset` — no PK is back-populated
3326                /// because the server didn't auto-assign one. For
3327                /// "insert then read back the PK or the existing
3328                /// row's PK", use the `upsert` family or
3329                /// `get_or_create`.
3330                ///
3331                /// # Errors
3332                /// As [`Self::insert`].
3333                pub async fn insert_or_ignore(
3334                    &mut self,
3335                    pool: &#root::sql::Pool,
3336                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3337                    #insert_or_ignore_call
3338                }
3339            }
3340        }
3341    } else {
3342        let insert_columns = &fields.insert_columns;
3343        let insert_values = &fields.insert_values;
3344        quote! {
3345
3346
3347            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3348            /// or silently skip on unique-constraint violation. Maps
3349            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3350            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3351            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3352            /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3353            ///
3354            /// Returns `Ok(true)` when a row was inserted,
3355            /// `Ok(false)` when a conflict caused the INSERT to
3356            /// silently skip.
3357            ///
3358            /// Use for "create-if-absent" patterns where you don't
3359            /// need the row back. For "find-or-create with the row
3360            /// returned", use the queryset-level
3361            /// `crate::sql::get_or_create` free function.
3362            ///
3363            /// # Errors
3364            /// As [`Self::insert`], plus any dialect-specific
3365            /// translation error from the ConflictClause writer.
3366            pub async fn insert_or_ignore(
3367                &self,
3368                pool: &#root::sql::Pool,
3369            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3370                let _query = #root::core::InsertQuery::new(
3371                    <Self as #root::core::Model>::SCHEMA,
3372                    ::std::vec![ #( #insert_columns ),* ],
3373                    ::std::vec![ #( #insert_values ),* ],
3374                );
3375                #root::sql::insert_or_ignore(pool, &_query).await
3376            }
3377        }
3378    };
3379
3380    // The tracked (column, JSON value) pairs, spliced only into
3381    // `__rustango_audit_pairs`. Empty when the model isn't audited.
3382    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3383        .map(|tracked| {
3384            tracked
3385                .iter()
3386                .map(|c| {
3387                    let column_lit = c.column.as_str();
3388                    let ident = &c.ident;
3389                    quote! {
3390                        (
3391                            #column_lit,
3392                            #root::core::__json_value(&self.#ident),
3393                        )
3394                    }
3395                })
3396                .collect()
3397        })
3398        .unwrap_or_default();
3399    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3400        if fields.pk_is_auto {
3401            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3402        } else {
3403            quote!(::std::format!("{}", &self.#pk_ident))
3404        }
3405    } else {
3406        quote!(::std::string::String::new())
3407    };
3408    // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3409    // is built, so every audited write path records the same shape.
3410    let delete_audited_body = if primary_key.is_some() {
3411        quote! {
3412            ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3413                pool,
3414                query,
3415                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3416            ))
3417        }
3418    } else {
3419        quote! {
3420            let _ = (pool, query);
3421            ::std::boxed::Box::pin(async {
3422                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3423                    table: <Self as #root::core::Model>::SCHEMA.table,
3424                })
3425            })
3426        }
3427    };
3428    let cascade_audited_body = if primary_key.is_some() {
3429        quote! {
3430            ::std::boxed::Box::pin(#root::audit::cascade_rows::<Self>(
3431                tx,
3432                <Self as #root::core::Model>::SCHEMA,
3433                rows,
3434                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3435            ))
3436        }
3437    } else {
3438        quote! {
3439            let _ = (tx, rows);
3440            ::std::boxed::Box::pin(async {
3441                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3442                    table: <Self as #root::core::Model>::SCHEMA.table,
3443                })
3444            })
3445        }
3446    };
3447    let update_audited_body = if primary_key.is_some() {
3448        quote! {
3449            ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3450                pool,
3451                query,
3452                move |_r: &Self| _r.__rustango_audit_entry(op),
3453            ))
3454        }
3455    } else {
3456        quote! {
3457            let _ = (pool, query, op);
3458            ::std::boxed::Box::pin(async {
3459                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3460                    table: <Self as #root::core::Model>::SCHEMA.table,
3461                })
3462            })
3463        }
3464    };
3465    let audit_entry_method = if audited_fields.is_some() {
3466        let pairs = audit_pair_tokens.iter();
3467        let pk_str = audit_pk_to_string.clone();
3468        quote! {
3469            /// This row's tracked `(column, value)` pairs.
3470            #[doc(hidden)]
3471            pub fn __rustango_audit_pairs(
3472                &self,
3473            ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3474                ::std::vec![ #( #pairs ),* ]
3475            }
3476
3477            /// This row's audit PK.
3478            #[doc(hidden)]
3479            pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3480                #pk_str
3481            }
3482
3483            /// Snapshot audit entry for `operation` on this row.
3484            #[doc(hidden)]
3485            pub fn __rustango_audit_entry(
3486                &self,
3487                operation: #root::audit::AuditOp,
3488            ) -> #root::audit::PendingEntry {
3489                #root::audit::PendingEntry {
3490                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3491                    entity_pk: self.__rustango_audit_pk(),
3492                    operation,
3493                    source: #root::audit::current_source(),
3494                    changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3495                }
3496            }
3497
3498            /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3499            #[doc(hidden)]
3500            pub fn __rustango_update_audited<'a>(
3501                pool: &'a #root::sql::Pool,
3502                query: &'a #root::core::UpdateQuery,
3503                op: #root::audit::AuditOp,
3504            ) -> ::std::pin::Pin<::std::boxed::Box<
3505                dyn ::core::future::Future<
3506                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3507                > + ::core::marker::Send + 'a,
3508            >> {
3509                #update_audited_body
3510            }
3511
3512            /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3513            #[doc(hidden)]
3514            pub fn __rustango_create_audited<'a, 't>(
3515                tx: &'a mut #root::sql::PoolTx<'t>,
3516                pk: #root::core::SqlValue,
3517            ) -> ::std::pin::Pin<::std::boxed::Box<
3518                dyn ::core::future::Future<
3519                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3520                > + ::core::marker::Send + 'a,
3521            >> {
3522                ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3523                    tx,
3524                    <Self as #root::core::Model>::SCHEMA,
3525                    pk,
3526                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3527                ))
3528            }
3529
3530            /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3531            #[doc(hidden)]
3532            pub fn __rustango_update_record_audited<'a, 't>(
3533                tx: &'a mut #root::sql::PoolTx<'t>,
3534                pk: #root::core::SqlValue,
3535            ) -> ::std::pin::Pin<::std::boxed::Box<
3536                dyn ::core::future::Future<
3537                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3538                > + ::core::marker::Send + 'a,
3539            >> {
3540                ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3541                    tx,
3542                    <Self as #root::core::Model>::SCHEMA,
3543                    pk,
3544                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3545                ))
3546            }
3547
3548            /// Rows an FK cascade will delete, behind `Model::__rustango_audited_cascade`.
3549            #[doc(hidden)]
3550            pub fn __rustango_cascade_audited<'a, 't>(
3551                tx: &'a mut #root::sql::PoolTx<'t>,
3552                rows: #root::core::WhereExpr,
3553            ) -> ::std::pin::Pin<::std::boxed::Box<
3554                dyn ::core::future::Future<
3555                    Output = ::core::result::Result<
3556                        ::std::vec::Vec<(#root::core::SqlValue, #root::audit::PendingEntry)>,
3557                        #root::sql::ExecError,
3558                    >,
3559                > + ::core::marker::Send + 'a,
3560            >> {
3561                #cascade_audited_body
3562            }
3563
3564            /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3565            #[doc(hidden)]
3566            pub fn __rustango_delete_audited<'a>(
3567                pool: &'a #root::sql::Pool,
3568                query: &'a #root::core::DeleteQuery,
3569            ) -> ::std::pin::Pin<::std::boxed::Box<
3570                dyn ::core::future::Future<
3571                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3572                > + ::core::marker::Send + 'a,
3573            >> {
3574                #delete_audited_body
3575            }
3576        }
3577    } else {
3578        quote!()
3579    };
3580    // Builds `_audit_entry`; `written` is the soft-delete column's new
3581    // value, recorded in place of the stale `self` field.
3582    let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3583        if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3584            quote! {
3585                let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3586                _audit_entry.set_tracked(#col, #v);
3587            }
3588        } else {
3589            quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3590        }
3591    };
3592    let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3593        .unwrap_or(#root::__serde_json::Value::Null));
3594    let restored_json = quote!(#root::__serde_json::Value::Null);
3595    // `&Pool` update runner for soft_delete / restore: one tx with the
3596    // audit row when audited, a plain UPDATE otherwise.
3597    let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3598        if audited_fields.is_some() {
3599            let entry = make_entry(op_path, Some(written));
3600            quote! {
3601                #entry
3602                #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3603            }
3604        } else {
3605            quote!(#root::sql::update_pool(pool, &_query).await)
3606        }
3607    };
3608    let pool_softdelete_run =
3609        make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3610    let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3611
3612    // `save_pool(&Pool)` — emitted for every model with a PK. The
3613    // audited `save_pool` is built below on the diff path; both share
3614    // `save_partial_methods`, which differ only in how they run `_query`.
3615    let save_ret = quote!(::core::result::Result<u64, #root::sql::ExecError>);
3616    let save_keep = quote!(matches!(_saved, ::core::result::Result::Ok(n) if n > 0));
3617    let save_partial_methods = |pk_ident: &syn::Ident,
3618                                pk_column_lit: &str,
3619                                run: TokenStream2|
3620     -> TokenStream2 {
3621        let assignments = &fields.update_assignments;
3622        // Pushed in step with `update_assignments`: one column each.
3623        let assignment_cols = &fields.upsert_update_columns;
3624        let write = restore_auto_now_unless(
3625            &fields.auto_now_idents,
3626            &save_ret,
3627            &save_keep,
3628            &quote! {
3629                // Build only the wanted assignments, so an unnamed
3630                // `auto_now` field is not stamped.
3631                let mut _filtered: ::std::vec::Vec<#root::core::Assignment> =
3632                    ::std::vec::Vec::with_capacity(_wanted_cols.len());
3633                #(
3634                    if _wanted_cols.contains(#assignment_cols) {
3635                        _filtered.push(#assignments);
3636                    }
3637                )*
3638                if _filtered.is_empty() {
3639                    // All field names valid, but they all map to
3640                    // non-assignable slots (PK column, computed/
3641                    // virtual fields, relations without an
3642                    // assignment). Same no-op as an empty list.
3643                    #root::__tracing::warn!(
3644                        target: "rustango::save_partial",
3645                        model = _schema.name,
3646                        "save_partial: every named field maps to a non-assignable column — no-op"
3647                    );
3648                    return ::core::result::Result::Ok(0);
3649                }
3650                let _query = #root::core::UpdateQuery::new(
3651                    _schema,
3652                    _filtered,
3653                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3654                        #pk_column_lit,
3655                        #root::core::Op::Eq,
3656                        ::core::convert::Into::<#root::core::SqlValue>::into(
3657                            ::core::clone::Clone::clone(&self.#pk_ident)
3658                        ),
3659                    )),
3660                );
3661                #run
3662            },
3663        );
3664        quote! {
3665            /// Save (UPDATE) only the listed Rust-side fields,
3666            /// leaving every other column untouched. Issue #66.
3667            ///
3668            /// `fields` are Rust-side struct field names; the macro
3669            /// resolves each to its SQL column. Unknown field
3670            /// names return [`#root::core::QueryError::UnknownField`]
3671            /// wrapped in `ExecError::Query`. An empty list is a
3672            /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3673            /// On an audited model the audit diff covers only these fields.
3674            ///
3675            /// Use this when:
3676            /// * you only mutated a couple of fields on a wide row
3677            ///   (avoid re-writing every column on every save), or
3678            /// * two writers diverged after their initial read and
3679            ///   you want to preserve the other writer's changes to
3680            ///   columns you didn't touch.
3681            ///
3682            /// Auto-PK models with an unset PK return
3683            /// [`#root::core::QueryError::UnknownField`] with
3684            /// field name `<pk>` — `save_partial` is an
3685            /// UPDATE-only path. Call [`Self::insert`]
3686            /// (or [`Self::save`] which dispatches based on
3687            /// PK state) for the INSERT case.
3688            ///
3689            /// # Errors
3690            /// As [`Self::save`], plus `UnknownField` for
3691            /// unknown / empty / Auto-Unset cases.
3692            pub async fn save_partial(
3693                &mut self,
3694                fields: &[&str],
3695                pool: &#root::sql::Pool,
3696            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3697                if fields.is_empty() {
3698                    #root::__tracing::warn!(
3699                        target: "rustango::save_partial",
3700                        model = <Self as #root::core::Model>::SCHEMA.name,
3701                        "save_partial called with empty field list — no-op"
3702                    );
3703                    return ::core::result::Result::Ok(0);
3704                }
3705                let _schema = <Self as #root::core::Model>::SCHEMA;
3706                // Validate field names against the schema.
3707                let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3708                    ::std::collections::HashSet::with_capacity(fields.len());
3709                for f in fields {
3710                    match _schema.field(f) {
3711                        ::core::option::Option::Some(fs) => {
3712                            _wanted_cols.insert(fs.column);
3713                        }
3714                        ::core::option::Option::None => {
3715                            return ::core::result::Result::Err(
3716                                #root::sql::ExecError::Query(
3717                                    #root::core::QueryError::UnknownField {
3718                                        model: _schema.name,
3719                                        field: (*f).to_owned(),
3720                                    }
3721                                )
3722                            );
3723                        }
3724                    }
3725                }
3726                #write
3727            }
3728
3729            /// Typed-column counterpart of [`Self::save_partial`] —
3730            /// issue #67. `fields` is a tuple of [`Column`]
3731            /// constants whose `Model` matches `Self`; typos and
3732            /// model mismatches surface at *compile time*
3733            /// (`Author::name` inside a `Post::save_partial_typed`
3734            /// call is a type error, no runtime check).
3735            ///
3736            /// ```ignore
3737            /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3738            /// ```
3739            ///
3740            /// Lowers to [`Self::save_partial`] under the hood — the
3741            /// tuple is reduced to a `&[&str]` slice of Rust-side
3742            /// field names and forwarded.
3743            ///
3744            /// [`Column`]: #root::core::Column
3745            ///
3746            /// # Errors
3747            /// As [`Self::save_partial`].
3748            pub async fn save_partial_typed<
3749                L: #root::core::TypedFieldList<Self>,
3750            >(
3751                &mut self,
3752                fields: L,
3753                pool: &#root::sql::Pool,
3754            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3755                let _names = fields.rust_field_names();
3756                let _refs: ::std::vec::Vec<&str> =
3757                    _names.iter().copied().collect();
3758                self.save_partial(&_refs, pool).await
3759            }
3760        }
3761    };
3762    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3763        let pk_column_lit = pk_col.as_str();
3764        if audited_fields.is_some() {
3765            quote!()
3766        } else {
3767            let partial = save_partial_methods(
3768                pk_ident,
3769                pk_column_lit,
3770                quote!(#root::sql::update_pool(pool, &_query).await),
3771            );
3772            quote! {
3773                /// Save this row to its table against either backend.
3774                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3775                /// `UPDATE` keyed on the PK; global scopes do not apply.
3776                /// A PK with no row is `RowNotFound`: use `insert` for a new row.
3777                ///
3778                /// # Errors
3779                /// As [`Self::save`].
3780                pub async fn save(
3781                    &mut self,
3782                    pool: &#root::sql::Pool,
3783                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3784                    self.__rustango_save_exec(pool).await
3785                }
3786
3787                #partial
3788            }
3789        }
3790    } else {
3791        quote!()
3792    };
3793
3794    // Audited `insert_pool` (overrides the placeholder set higher up
3795    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3796    // audited models get insert_pool routing through
3797    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3798    // + auto-PK readback + audit emit). Snapshot-style audit (the
3799    // PendingEntry's `changes` carries post-write field values).
3800    let pool_insert_method = if audited_fields.is_some() {
3801        if let Some(_) = primary_key {
3802            // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
3803            let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
3804            {
3805                fields.returning_cols.clone()
3806            } else {
3807                // Non-Auto-PK: still need RETURNING something for the
3808                // audit helper's contract (it errors on empty
3809                // returning). Return the PK column so the audit row
3810                // can carry the assigned PK back. Some non-Auto PKs
3811                // are server-side-default (e.g. UUIDv4 default), so
3812                // RETURNING is genuinely useful.
3813                primary_key
3814                    .map(|(_, col)| {
3815                        let lit = col.as_str();
3816                        vec![quote!(#lit)]
3817                    })
3818                    .unwrap_or_default()
3819            };
3820            // A failed COMMIT rolls the row back: put the read-back values back too.
3821            let assigned: Vec<syn::Ident> = fields
3822                .auto_field_idents
3823                .iter()
3824                .map(|(ident, _, _)| ident.clone())
3825                .collect();
3826            let body = restore_auto_now_unless(
3827                &assigned,
3828                &quote!(::core::result::Result<(), #root::sql::ExecError>),
3829                &quote!(_saved.is_ok()),
3830                &quote! {
3831                    let mut _query = self.__rustango_insert_query();
3832                    // The audit row needs the PK even when Rust filled it.
3833                    if _query.returning.is_empty() {
3834                        _query.returning = ::std::vec![ #( #returning_cols ),* ];
3835                    }
3836                    #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self, _op| {
3837                        _m.__rustango_audit_entry(_op)
3838                    })
3839                    .await
3840                },
3841            );
3842            quote! {
3843                /// Insert this row on any backend, with its audit row in the
3844                /// same transaction.
3845                ///
3846                /// Snapshot-style audit (post-write field values).
3847                ///
3848                /// # Errors
3849                /// As [`Self::insert`].
3850                pub async fn insert(
3851                    &mut self,
3852                    pool: &#root::sql::Pool,
3853                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3854                    #body
3855                }
3856            }
3857        } else {
3858            quote!()
3859        }
3860    } else {
3861        // Keep the non-audited pool_insert_method we built earlier.
3862        pool_insert_method
3863    };
3864
3865    // Update audited save_pool: now that insert_pool is wired for
3866    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3867    // insert_pool. Non-audited save_pool already does this.
3868    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3869    // Replaces the snapshot-only emission with a per-backend transaction
3870    // body that:
3871    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3872    //     column), capturing BEFORE values
3873    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3874    //  3. builds AFTER pairs from &self
3875    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3876    //     AuditOp::Update + diff_changes(...) on the same tx connection
3877    //  5. commits
3878    //
3879    // Per-backend arms inline the SQL string + placeholder shape, then
3880    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3881    // is polymorphic over Row type — the same tokens work against
3882    // PgRow and MySqlRow as long as the field's Rust type implements
3883    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3884    // primitives + chrono/uuid/serde_json::Value all do).
3885    let pool_save_method = if let Some(tracked) = audited_fields {
3886        if let Some((pk_ident, pk_col)) = primary_key {
3887            let pk_column_lit = pk_col.as_str();
3888            let partial = save_partial_methods(
3889                pk_ident,
3890                pk_column_lit,
3891                quote! {
3892                    self.__rustango_save_with_diff(
3893                        pool,
3894                        &_query,
3895                        ::core::option::Option::Some(&_wanted_cols),
3896                    )
3897                    .await
3898                },
3899            );
3900            // Per-tracked-column BEFORE-pair token list. Each entry
3901            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3902            // The Row alias resolves to PgRow / MySqlRow per call site,
3903            // so the same template generates both the PG and MySQL bodies.
3904            let mk_before_pairs =
3905                |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
3906                    tracked
3907                        .iter()
3908                        .map(|c| {
3909                            let getter = getter(c);
3910                            let column_lit = c.column.as_str();
3911                            let value_ty = &c.value_ty;
3912                            quote! {
3913                                (
3914                                    #column_lit,
3915                                    match #getter::<#value_ty>(
3916                                        _audit_before_row, #column_lit,
3917                                    ) {
3918                                        ::core::result::Result::Ok(v) => {
3919                                            #root::core::__json_value(&v)
3920                                        }
3921                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3922                                    },
3923                                )
3924                            }
3925                        })
3926                        .collect()
3927                };
3928            let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
3929            let before_pairs_my = mk_before_pairs(&|c| {
3930                if c.uuid_column {
3931                    quote!(#root::sql::try_get_flat_my)
3932                } else {
3933                    quote!(#root::sql::try_get_returning_my)
3934                }
3935            });
3936            let before_pairs_sqlite =
3937                mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
3938            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
3939            let pk_value_for_bind = if fields.pk_is_auto {
3940                quote!(self.#pk_ident.get().copied().unwrap_or_default())
3941            } else {
3942                quote!(::core::clone::Clone::clone(&self.#pk_ident))
3943            };
3944            let assignments = &fields.update_assignments;
3945            let unset_dispatch = if fields.has_auto {
3946                quote! {
3947                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3948                        return self.insert(pool).await.map(|()| 1u64);
3949                    }
3950                }
3951            } else {
3952                quote!()
3953            };
3954            let write = restore_auto_now_unless(
3955                &fields.auto_now_idents,
3956                &save_ret,
3957                &save_keep,
3958                &quote! {
3959                    let _query = #root::core::UpdateQuery::new(
3960                        <Self as #root::core::Model>::SCHEMA,
3961                        ::std::vec![ #( #assignments ),* ],
3962                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3963                            #pk_column_lit,
3964                            #root::core::Op::Eq,
3965                            ::core::convert::Into::<#root::core::SqlValue>::into(
3966                                ::core::clone::Clone::clone(&self.#pk_ident)
3967                            ),
3968                        )),
3969                    );
3970                    self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
3971                        .await
3972                        .and_then(#root::sql::__macro_internals::row_saved)
3973                },
3974            );
3975            quote! {
3976                /// Save this row on any backend, with a diff audit row
3977                /// (before and after) in the same transaction. An `Unset`
3978                /// `Auto` PK inserts instead; a PK with no row is `RowNotFound`.
3979                ///
3980                /// The audit row's `changes` JSON contains one
3981                /// `{ "field": { "before": …, "after": … } }` entry
3982                /// per tracked column whose value actually changed
3983                /// Keyed on the PK; global scopes do not apply.
3984                ///
3985                /// # Errors
3986                /// As [`Self::save`].
3987                pub async fn save(
3988                    &mut self,
3989                    pool: &#root::sql::Pool,
3990                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3991                    #unset_dispatch
3992                    #write
3993                }
3994
3995                /// Run `query` and write its diff entry, limited to `only` columns.
3996                #[doc(hidden)]
3997                async fn __rustango_save_with_diff(
3998                    &self,
3999                    pool: &#root::sql::Pool,
4000                    query: &#root::core::UpdateQuery,
4001                    only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
4002                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4003                    let mut _after_pairs = self.__rustango_audit_pairs();
4004                    if let ::core::option::Option::Some(only) = only {
4005                        _after_pairs.retain(|(col, _)| only.contains(col));
4006                    }
4007                    let _before_query = #root::audit::before_image_query(
4008                        <Self as #root::core::Model>::SCHEMA,
4009                        #pk_column_lit,
4010                        ::core::convert::Into::<#root::core::SqlValue>::into(
4011                            #pk_value_for_bind,
4012                        ),
4013                        &[ #( #tracked_cols ),* ],
4014                    );
4015                    #root::audit::save_one_with_diff(
4016                        pool,
4017                        query,
4018                        &_before_query,
4019                        <Self as #root::core::Model>::SCHEMA.table,
4020                        self.__rustango_audit_pk(),
4021                        _after_pairs,
4022                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4023                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4024                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4025                    ).await
4026                }
4027
4028                #partial
4029            }
4030        } else {
4031            quote!()
4032        }
4033    } else {
4034        pool_save_method
4035    };
4036
4037    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4038    // body shapes:
4039    // - non-audited: simple dispatch through `sql::delete_pool`
4040    // - audited: routes through `audit::delete_one_with_audit`,
4041    //   which opens a per-backend transaction wrapping DELETE +
4042    //   audit emit so the data write and audit row commit atomically.
4043    let pool_delete_method = {
4044        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4045        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4046        if let Some(pk_ident) = pk_ident_for_pool {
4047            if audited_fields.is_some() {
4048                quote! {
4049                    /// Delete this row on any backend, with its audit row in
4050                    /// the same transaction.
4051                    /// Keyed on the PK; global scopes do not apply.
4052                    ///
4053                    /// # Errors
4054                    /// As [`Self::delete`].
4055                    pub async fn delete(
4056                        &self,
4057                        pool: &#root::sql::Pool,
4058                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4059                        let _query = #root::core::DeleteQuery::by_pk(
4060                            <Self as #root::core::Model>::SCHEMA,
4061                            #pk_column_lit,
4062                            ::core::convert::Into::<#root::core::SqlValue>::into(
4063                                ::core::clone::Clone::clone(&self.#pk_ident)
4064                            ),
4065                        );
4066                        let _audit_entry =
4067                            self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4068                        #root::audit::delete_one_with_audit(
4069                            pool, &_query, &_audit_entry,
4070                        ).await
4071                    }
4072                }
4073            } else {
4074                quote! {
4075                    /// Delete the row identified by this instance's primary key
4076                    /// against either backend. Equivalent to [`Self::delete`] but
4077                    /// takes [`#root::sql::Pool`] and dispatches per backend.
4078                    /// Global scopes do not apply.
4079                    ///
4080                    /// # Errors
4081                    /// As [`Self::delete`].
4082                    pub async fn delete(
4083                        &self,
4084                        pool: &#root::sql::Pool,
4085                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4086                        self.__rustango_delete_exec(pool).await
4087                    }
4088                }
4089            }
4090        } else {
4091            quote!()
4092        }
4093    };
4094
4095    // Bulk writers on audited models write one audit row per affected
4096    // row, in the write's transaction (#1747).
4097    let (bulk_delete_run, destroy_run, bulk_update_run, truncate_run, truncate_cascade_run) =
4098        if audited_fields.is_some() {
4099            (
4100                quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4101                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4102            })
4103            .await),
4104                quote!(#root::audit::delete_each_with_audit::<Self>(pool, &_queries, |_r: &Self| {
4105                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4106            })
4107            .await),
4108                quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4109                _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4110            })
4111            .await),
4112                quote!(#root::audit::truncate_with_audit(
4113                pool,
4114                <Self as #root::core::Model>::SCHEMA.table,
4115                #root::sql::TruncateMode::Plain,
4116            )
4117            .await),
4118                quote!(#root::audit::truncate_with_audit(
4119                pool,
4120                <Self as #root::core::Model>::SCHEMA.table,
4121                #root::sql::TruncateMode::Cascade,
4122            )
4123            .await
4124            .map(drop)),
4125            )
4126        } else {
4127            (
4128                quote!(#root::sql::delete_pool(pool, &_query).await),
4129                quote!(#root::sql::__rustango_delete_all_or_none(pool, _queries).await),
4130                quote!(#root::sql::update_pool(pool, &_query).await),
4131                quote!(#root::sql::truncate(pool, <Self as #root::core::Model>::SCHEMA).await),
4132                quote!(#root::sql::truncate_cascade(pool, <Self as #root::core::Model>::SCHEMA).await),
4133            )
4134        };
4135
4136    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4137    // matching this instance's PK and overwrite the in-memory state
4138    // with the freshly-fetched columns.
4139    // Issue #825. Only emitted when the model declares a PK; non-PK
4140    // models can't address a specific row.
4141    //
4142    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4143    // every field from `self`; resets the PK to `Auto::Unset` when
4144    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4145    // a fresh autoincrement value. Non-Auto PKs preserve the source
4146    // PK — the caller must overwrite before insert. Pure-Rust, no
4147    // I/O, no dialect surface.
4148    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4149        let other_field_clones: Vec<TokenStream2> = fields
4150            .column_entries
4151            .iter()
4152            .filter(|c| &c.ident != pk_ident)
4153            .map(|c| {
4154                let ident = &c.ident;
4155                quote! {
4156                    #ident: ::core::clone::Clone::clone(&self.#ident)
4157                }
4158            })
4159            .collect();
4160        let pk_clone_token = if fields.pk_is_auto {
4161            quote! { #pk_ident: #root::sql::Auto::Unset }
4162        } else {
4163            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4164        };
4165        let replicate_doc = if fields.pk_is_auto {
4166            quote! {
4167                /// Eloquent-style `replicate()` — return a clone of this
4168                /// row with the primary key reset to [`Auto::Unset`] so
4169                /// the copy is ready for `insert_pool` / `save_pool` to
4170                /// allocate a fresh autoincrement value. Every other
4171                /// field is `Clone`d verbatim. Issue #825.
4172                ///
4173                /// `auto_now_add` / `auto_now` timestamp fields are
4174                /// **not** reset (Eloquent's `replicate` doesn't reset
4175                /// them either) — pass them through the normal insert
4176                /// path if you want fresh values, or assign them
4177                /// explicitly after the call.
4178            }
4179        } else {
4180            quote! {
4181                /// Eloquent-style `replicate()` — clone this row
4182                /// verbatim. Because the primary key is **not** an
4183                /// `Auto<T>`, the clone keeps the source PK; the
4184                /// caller must overwrite `copy.<pk>` before inserting
4185                /// to avoid a unique-key violation. Issue #825.
4186            }
4187        };
4188        // 2026-06-07 — field-name / shortcut collision guard.
4189        //
4190        // The macro emits both `pub const <field>: <field>_col = ...`
4191        // (per-field typed-column const, used by the typed-builder
4192        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4193        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4194        // model has a field named e.g. `count`, both items would land
4195        // in the same inherent impl with the same name, and the
4196        // compiler rejects the derive with "duplicate definitions".
4197        //
4198        // Drop the conflicting shortcut for that model. Callers can
4199        // still reach the same behavior via
4200        // `QuerySet::<Model>::default().count(&pool)`.
4201        let column_names: ::std::collections::HashSet<String> = fields
4202            .column_entries
4203            .iter()
4204            .map(|c| ident_name(&c.ident))
4205            .collect();
4206        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4207            if column_names.contains(name) {
4208                quote! {}
4209            } else {
4210                tokens
4211            }
4212        };
4213        let count_method = emit_if_no_field_collision(
4214            "count",
4215            quote! {
4216                /// Count rows of this model — `SELECT COUNT(*) FROM
4217                /// <table>`. Eloquent `Model::count()` parity.
4218                ///
4219                /// Skipped on models that already declare a field named
4220                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4221                /// in that case.
4222                ///
4223                /// # Errors
4224                /// As [`CounterPool::count`].
4225                ///
4226                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4227                pub async fn count(
4228                    pool: &#root::sql::Pool,
4229                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4230                    use #root::sql::CounterPool as _;
4231                    #root::query::QuerySet::<Self>::default()
4232                        .count(pool)
4233                        .await
4234                }
4235            },
4236        );
4237        let value_method = emit_if_no_field_collision(
4238            "value",
4239            quote! {
4240                /// Pluck a single scalar from the first row.
4241                /// Eloquent `Model::query()->value($col)` parity.
4242                ///
4243                /// Skipped on models that already declare a field named
4244                /// `value`. Drop into
4245                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4246                ///
4247                /// # Errors
4248                /// As `ValuesFlatQuerySet::first`.
4249                pub async fn value<U>(
4250                    col: &str,
4251                    pool: &#root::sql::Pool,
4252                ) -> ::core::result::Result<
4253                    ::core::option::Option<U>,
4254                    #root::sql::ExecError,
4255                >
4256                where
4257                    U: #root::sql::FlatScalar,
4258                {
4259                    let _col_static: &'static str = Self::__resolve_col(col)?;
4260                    #root::query::QuerySet::<Self>::default()
4261                        .values_list_flat(_col_static)
4262                        .first::<U>(pool)
4263                        .await
4264                }
4265            },
4266        );
4267        let sum_method = emit_if_no_field_collision(
4268            "sum",
4269            quote! {
4270                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4271                /// Skipped on models that already declare a field named
4272                /// `sum`.
4273                ///
4274                /// # Errors
4275                /// As [`#root::sql::fetch_aggregate_pool`].
4276                pub async fn sum<U>(
4277                    col: &str,
4278                    pool: &#root::sql::Pool,
4279                ) -> ::core::result::Result<
4280                    ::core::option::Option<U>,
4281                    #root::sql::ExecError,
4282                >
4283                where
4284                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4285                        + #root::sql::MaybeMyFromRow
4286                        + #root::sql::MaybeSqliteFromRow
4287                        + ::core::marker::Send
4288                        + ::core::marker::Unpin,
4289                {
4290                    #root::query::QuerySet::<Self>::default()
4291                        .sum::<U>(col, pool)
4292                        .await
4293                }
4294            },
4295        );
4296        let avg_method = emit_if_no_field_collision(
4297            "avg",
4298            quote! {
4299                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4300                /// models that already declare a field named `avg`.
4301                ///
4302                /// # Errors
4303                /// As [`#root::sql::fetch_aggregate_pool`].
4304                pub async fn avg<U>(
4305                    col: &str,
4306                    pool: &#root::sql::Pool,
4307                ) -> ::core::result::Result<
4308                    ::core::option::Option<U>,
4309                    #root::sql::ExecError,
4310                >
4311                where
4312                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4313                        + #root::sql::MaybeMyFromRow
4314                        + #root::sql::MaybeSqliteFromRow
4315                        + ::core::marker::Send
4316                        + ::core::marker::Unpin,
4317                {
4318                    #root::query::QuerySet::<Self>::default()
4319                        .avg::<U>(col, pool)
4320                        .await
4321                }
4322            },
4323        );
4324        let min_method = emit_if_no_field_collision(
4325            "min",
4326            quote! {
4327                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4328                /// models that already declare a field named `min`.
4329                ///
4330                /// # Errors
4331                /// As [`#root::sql::fetch_aggregate_pool`].
4332                pub async fn min<U>(
4333                    col: &str,
4334                    pool: &#root::sql::Pool,
4335                ) -> ::core::result::Result<
4336                    ::core::option::Option<U>,
4337                    #root::sql::ExecError,
4338                >
4339                where
4340                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4341                        + #root::sql::MaybeMyFromRow
4342                        + #root::sql::MaybeSqliteFromRow
4343                        + ::core::marker::Send
4344                        + ::core::marker::Unpin,
4345                {
4346                    #root::query::QuerySet::<Self>::default()
4347                        .min::<U>(col, pool)
4348                        .await
4349                }
4350            },
4351        );
4352        let max_method = emit_if_no_field_collision(
4353            "max",
4354            quote! {
4355                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4356                /// models that already declare a field named `max`.
4357                ///
4358                /// # Errors
4359                /// As [`#root::sql::fetch_aggregate_pool`].
4360                pub async fn max<U>(
4361                    col: &str,
4362                    pool: &#root::sql::Pool,
4363                ) -> ::core::result::Result<
4364                    ::core::option::Option<U>,
4365                    #root::sql::ExecError,
4366                >
4367                where
4368                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4369                        + #root::sql::MaybeMyFromRow
4370                        + #root::sql::MaybeSqliteFromRow
4371                        + ::core::marker::Send
4372                        + ::core::marker::Unpin,
4373                {
4374                    #root::query::QuerySet::<Self>::default()
4375                        .max::<U>(col, pool)
4376                        .await
4377                }
4378            },
4379        );
4380        let first_method = emit_if_no_field_collision(
4381            "first",
4382            quote! {
4383                /// First row of this model. Eloquent `Model::first()`.
4384                /// Skipped on models that already declare a field named
4385                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4386                ///
4387                /// # Errors
4388                /// As `QuerySet::first`.
4389                pub async fn first(
4390                    pool: &#root::sql::Pool,
4391                ) -> ::core::result::Result<
4392                    ::core::option::Option<Self>,
4393                    #root::sql::ExecError,
4394                > {
4395                    #root::query::QuerySet::<Self>::default()
4396                        .first(pool)
4397                        .await
4398                }
4399            },
4400        );
4401        let last_method = emit_if_no_field_collision(
4402            "last",
4403            quote! {
4404                /// Last row of this model by primary-key DESC.
4405                /// Eloquent `Model::query()->latest('id')->first()`
4406                /// parity — fetches the highest-PK row without
4407                /// requiring the caller to spell the PK column.
4408                /// Returns `None` on an empty table.
4409                ///
4410                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4411                /// Skipped on models that already declare a field
4412                /// named `last`.
4413                ///
4414                /// # Errors
4415                /// As `QuerySet::last`.
4416                pub async fn last(
4417                    pool: &#root::sql::Pool,
4418                ) -> ::core::result::Result<
4419                    ::core::option::Option<Self>,
4420                    #root::sql::ExecError,
4421                > {
4422                    #root::query::QuerySet::<Self>::default()
4423                        .last(pool)
4424                        .await
4425                }
4426            },
4427        );
4428        quote! {
4429            /// Re-SELECT this row by its primary key and overwrite
4430            /// every in-memory field with the freshly-fetched value.
4431            /// Issue #825.
4432            ///
4433            /// Use this when the row may have been modified by another
4434            /// process / connection / job since you read it — e.g. after
4435            /// a queued task callback, or to re-sync stale UI state
4436            /// before re-saving.
4437            ///
4438            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4439            /// when the primary key no longer matches any row (e.g.
4440            /// the row was deleted concurrently).
4441            ///
4442            /// # Errors
4443            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4444            /// the PK no longer exists.
4445            ///
4446            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4447            pub async fn refresh_from_db(
4448                &mut self,
4449                pool: &#root::sql::Pool,
4450            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4451                match self.fresh(pool).await? {
4452                    ::core::option::Option::Some(_fresh) => {
4453                        *self = _fresh;
4454                        ::core::result::Result::Ok(())
4455                    }
4456                    ::core::option::Option::None => ::core::result::Result::Err(
4457                        #root::sql::ExecError::Driver(
4458                            #root::sql::sqlx::Error::RowNotFound,
4459                        ),
4460                    ),
4461                }
4462            }
4463
4464            /// Atomically increment the integer column `col` by
4465            /// `by` for this row. Equivalent to
4466            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4467            ///
4468            /// **Doesn't mutate `self`** — the in-memory copy is now
4469            /// stale; call [`Self::refresh_from_db`] /
4470            /// [`Self::fresh`] to re-sync. Returns the rows-
4471            /// affected count (0 when the PK doesn't match any row,
4472            /// 1 on success).
4473            ///
4474            /// `col` is the Rust field name as a string; unknown
4475            /// fields surface as `UnknownField` at runtime. Negative
4476            /// `by` values atomically decrement (see also
4477            /// [`Self::decrement`]).
4478            ///
4479            /// # Errors
4480            /// As [`UpdaterPool::execute_pool`].
4481            ///
4482            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4483            pub async fn increment(
4484                &self,
4485                col: &str,
4486                by: i64,
4487                pool: &#root::sql::Pool,
4488            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4489                Self::__increment_one(self, col, by, pool).await
4490            }
4491
4492            /// Sibling of [`Self::increment`] — atomically
4493            /// decrement this row's `col` by `by`. Eloquent
4494            /// `$model->decrement($col, $by)` parity. Equivalent to
4495            /// `self.increment(col, -by, &pool)`; the separate name
4496            /// keeps call sites readable.
4497            ///
4498            /// # Errors
4499            /// As [`Self::increment`].
4500            pub async fn decrement(
4501                &self,
4502                col: &str,
4503                by: i64,
4504                pool: &#root::sql::Pool,
4505            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4506                Self::__increment_one(self, col, -by, pool).await
4507            }
4508
4509            /// Bulk-increment: add `by` to `col` on every row of the
4510            /// table. Eloquent `Model::query()->increment($col, $by)`
4511            /// parity. Use for counters, score adjustments, view
4512            /// rollups.
4513            ///
4514            /// # Errors
4515            /// As [`UpdaterPool::execute_pool`].
4516            ///
4517            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4518            pub async fn increment_each(
4519                col: &str,
4520                by: i64,
4521                pool: &#root::sql::Pool,
4522            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4523                Self::__increment_all(col, by, pool).await
4524            }
4525
4526            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4527            ///
4528            /// # Errors
4529            /// As [`Self::increment_each`].
4530            pub async fn decrement_each(
4531                col: &str,
4532                by: i64,
4533                pool: &#root::sql::Pool,
4534            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4535                Self::__increment_all(col, -by, pool).await
4536            }
4537
4538            /// Internal: forward to
4539            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4540            /// One-line wrapper kept as a per-Model method so the
4541            /// macro's emitted `increment` / `decrement` instance
4542            /// calls don't have to thread `Self` through manually.
4543            #[doc(hidden)]
4544            pub async fn __increment_one(
4545                this: &Self,
4546                col: &str,
4547                by: i64,
4548                pool: &#root::sql::Pool,
4549            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4550                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4551                    ::core::clone::Clone::clone(&this.#pk_ident),
4552                );
4553                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4554                    ::core::stringify!(#pk_ident),
4555                    _pk_val,
4556                    col,
4557                    by,
4558                    pool,
4559                )
4560                .await
4561            }
4562
4563            /// Internal: run
4564            /// [`#root::sql::model_shortcuts::increment_all_query`].
4565            #[doc(hidden)]
4566            pub async fn __increment_all(
4567                col: &str,
4568                by: i64,
4569                pool: &#root::sql::Pool,
4570            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4571                let _query =
4572                    #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4573                #bulk_update_run
4574            }
4575
4576            /// Internal: forward to
4577            /// [`#root::sql::model_shortcuts::resolve_col`].
4578            #[doc(hidden)]
4579            pub fn __resolve_col(
4580                col: &str,
4581            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4582                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4583            }
4584
4585            /// Internal: forward to
4586            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4587            #[doc(hidden)]
4588            #[must_use]
4589            pub fn __add_signed_expr(
4590                col_static: &'static str,
4591                signed_by: i64,
4592            ) -> #root::core::Expr {
4593                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4594            }
4595
4596            /// Re-SELECT this row by its primary key and return a
4597            /// **new** instance with the freshly-fetched fields.
4598            /// Eloquent `Model::fresh()` parity — non-mutating
4599            /// counterpart of [`Self::refresh_from_db`].
4600            ///
4601            /// Returns `Ok(None)` when the row was deleted
4602            /// concurrently — vs [`Self::refresh_from_db`]
4603            /// which surfaces that as `RowNotFound` because
4604            /// in-place mutation has nothing to write to.
4605            ///
4606            /// Useful when you want to compare the in-memory
4607            /// instance against the persisted state (audit-style
4608            /// diffs, conflict detection) without mutating the
4609            /// reference you already hold.
4610            ///
4611            /// # Errors
4612            /// As [`FetcherPool::fetch`].
4613            ///
4614            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4615            pub async fn fresh(
4616                &self,
4617                pool: &#root::sql::Pool,
4618            ) -> ::core::result::Result<
4619                ::core::option::Option<Self>,
4620                #root::sql::ExecError,
4621            > {
4622                use #root::sql::FetcherPool as _;
4623                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4624                    ::core::clone::Clone::clone(&self.#pk_ident),
4625                );
4626                // By key alone, as `save()` and `delete()` write it.
4627                let _rows: ::std::vec::Vec<Self> =
4628                    #root::query::QuerySet::<Self>::default()
4629                        .without_global_scopes()
4630                        .filter(::core::stringify!(#pk_ident), _pk_val)
4631                        .limit(1)
4632                        .fetch(pool)
4633                        .await?;
4634                ::core::result::Result::Ok(_rows.into_iter().next())
4635            }
4636
4637            #replicate_doc
4638            #[must_use]
4639            pub fn replicate(&self) -> Self {
4640                Self {
4641                    #pk_clone_token,
4642                    #( #other_field_clones, )*
4643                }
4644            }
4645
4646            #first_method
4647
4648            #last_method
4649
4650            /// Throwing counterpart of [`Self::first`] —
4651            /// errors with `RowNotFound` when the table is empty.
4652            /// Eloquent `Model::firstOrFail()` parity.
4653            ///
4654            /// # Errors
4655            /// As [`Self::first`]; additionally
4656            /// [`sqlx::Error::RowNotFound`] on empty tables.
4657            ///
4658            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4659            pub async fn first_or_fail(
4660                pool: &#root::sql::Pool,
4661            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4662                // Route through the queryset rather than `Self::first`,
4663                // which is suppressed on models with a field named
4664                // `first` (field/shortcut collision guard).
4665                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4666                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4667                    ::core::option::Option::None => ::core::result::Result::Err(
4668                        #root::sql::ExecError::Driver(
4669                            #root::sql::sqlx::Error::RowNotFound,
4670                        ),
4671                    ),
4672                }
4673            }
4674
4675            /// Single-column projection — `SELECT <col> FROM
4676            /// <table>`. Returns `Vec<U>` where each element is the
4677            /// decoded value of the column. Eloquent
4678            /// `Model::pluck($column)` parity.
4679            ///
4680            /// Thin wrapper over `QuerySet::<Self>::default()
4681            /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4682            /// `FlatScalar`; use `Option<_>` for a nullable column.
4683            ///
4684            /// # Errors
4685            /// As `ValuesFlatQuerySet::fetch`.
4686            pub async fn pluck<U>(
4687                col: &'static str,
4688                pool: &#root::sql::Pool,
4689            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4690            where
4691                U: #root::sql::FlatScalar,
4692            {
4693                #root::query::QuerySet::<Self>::default()
4694                    .values_list_flat(col)
4695                    .fetch::<U>(pool)
4696                    .await
4697            }
4698
4699            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4700            /// stream every row of this model in batches of `n`,
4701            /// invoking the callback once per batch. Stable PK-ASC
4702            /// ordering so the LIMIT/OFFSET pagination is
4703            /// deterministic across drivers.
4704            ///
4705            /// The callback is async — it can do further DB work
4706            /// (writes, related-row lookups, queue dispatch) per
4707            /// batch. Return `Err(...)` from the callback to abort
4708            /// the iteration early; the error bubbles up.
4709            ///
4710            /// **When to use**: bulk processing flows that can't
4711            /// fit the whole table in memory — sending newsletters,
4712            /// running data migrations, computing summary
4713            /// statistics. For small / known-bounded tables, plain
4714            /// `Self::all(&pool)` is simpler.
4715            ///
4716            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4717            /// large tables. For multi-million-row scans prefer
4718            /// keyset-by-PK (the standard "WHERE id > last_seen"
4719            /// shape) over `chunk(...)`.
4720            ///
4721            /// Skipped on models without a primary key — chunking
4722            /// needs a stable order to avoid skipping / repeating
4723            /// rows across batches.
4724            pub async fn chunk<F, Fut>(
4725                n: i64,
4726                pool: &#root::sql::Pool,
4727                mut cb: F,
4728            ) -> ::core::result::Result<(), #root::sql::ExecError>
4729            where
4730                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4731                Fut: ::core::future::Future<
4732                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4733                >,
4734            {
4735                use #root::sql::FetcherPool as _;
4736                // The field name: `filter` / `order_by` take fields, not columns.
4737                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4738                    ::core::option::Option::Some(f) => f.name,
4739                    ::core::option::Option::None => {
4740                        return ::core::result::Result::Ok(());
4741                    }
4742                };
4743                let mut offset: i64 = 0;
4744                loop {
4745                    let rows: ::std::vec::Vec<Self> =
4746                        #root::query::QuerySet::<Self>::default()
4747                            .order_by(&[(pk_col, false)])
4748                            .limit(n)
4749                            .offset(offset)
4750                            .fetch(pool)
4751                            .await?;
4752                    if rows.is_empty() {
4753                        return ::core::result::Result::Ok(());
4754                    }
4755                    let len = rows.len() as i64;
4756                    cb(rows).await?;
4757                    if len < n {
4758                        return ::core::result::Result::Ok(());
4759                    }
4760                    offset += n;
4761                }
4762            }
4763
4764            /// Eloquent `Model::chunkById($n, fn (...))` — same
4765            /// per-batch callback shape as [`Self::chunk`], but uses
4766            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4767            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4768            /// — the right choice for multi-million-row sweeps.
4769            ///
4770            /// The keyset compares the primary key in its own type,
4771            /// so any orderable PK works (`i32`, `i64`, UUID, string).
4772            ///
4773            /// Callback errors abort iteration; the error bubbles up
4774            /// unchanged. Empty table → callback invoked zero times.
4775            pub async fn chunk_by_id<F, Fut>(
4776                n: i64,
4777                pool: &#root::sql::Pool,
4778                mut cb: F,
4779            ) -> ::core::result::Result<(), #root::sql::ExecError>
4780            where
4781                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4782                Fut: ::core::future::Future<
4783                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4784                >,
4785            {
4786                use #root::sql::FetcherPool as _;
4787                // The field name: `filter` / `order_by` take fields, not columns.
4788                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4789                    ::core::option::Option::Some(f) => f.name,
4790                    ::core::option::Option::None => {
4791                        return ::core::result::Result::Ok(());
4792                    }
4793                };
4794                // The largest PK seen, in the PK's own type; none before
4795                // the first batch.
4796                let key = ::std::format!("{}__gt", pk_col);
4797                let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
4798                    ::core::option::Option::None;
4799                loop {
4800                    let mut qs = #root::query::QuerySet::<Self>::default();
4801                    if let ::core::option::Option::Some(v) = last_seen.take() {
4802                        qs = qs.filter(key.as_str(), v);
4803                    }
4804                    let rows: ::std::vec::Vec<Self> = qs
4805                        .order_by(&[(pk_col, false)])
4806                        .limit(n)
4807                        .fetch(pool)
4808                        .await?;
4809                    let len = rows.len() as i64;
4810                    // Capture the last row's PK BEFORE moving rows
4811                    // into the callback.
4812                    let ::core::option::Option::Some(max_pk) =
4813                        rows.last().map(|r| r.__rustango_pk_value())
4814                    else {
4815                        return ::core::result::Result::Ok(());
4816                    };
4817                    cb(rows).await?;
4818                    if len < n {
4819                        return ::core::result::Result::Ok(());
4820                    }
4821                    last_seen = ::core::option::Option::Some(max_pk);
4822                }
4823            }
4824
4825            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4826            /// per-row callback companion to [`Self::chunk`].
4827            /// Streams every row in keyset-paginated batches of
4828            /// `batch` size, calling `cb` once per row.
4829            ///
4830            /// Inherits the keyset-paginated scan of
4831            /// [`Self::chunk_by_id`] (O(N) total).
4832            ///
4833            /// ```ignore
4834            /// Post::each(500, &pool, |p| async move {
4835            ///     reindex(p).await?;
4836            ///     Ok(())
4837            /// }).await?;
4838            /// ```
4839            ///
4840            /// Return `Err(...)` from the callback to abort
4841            /// iteration; the error bubbles up unchanged.
4842            pub async fn each<F, Fut>(
4843                batch: i64,
4844                pool: &#root::sql::Pool,
4845                mut cb: F,
4846            ) -> ::core::result::Result<(), #root::sql::ExecError>
4847            where
4848                F: ::core::ops::FnMut(Self) -> Fut,
4849                Fut: ::core::future::Future<
4850                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4851                >,
4852            {
4853                use #root::sql::FetcherPool as _;
4854                // The field name: `filter` / `order_by` take fields, not columns.
4855                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4856                    ::core::option::Option::Some(f) => f.name,
4857                    ::core::option::Option::None => {
4858                        return ::core::result::Result::Ok(());
4859                    }
4860                };
4861                let key = ::std::format!("{}__gt", pk_col);
4862                let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
4863                    ::core::option::Option::None;
4864                loop {
4865                    let mut qs = #root::query::QuerySet::<Self>::default();
4866                    if let ::core::option::Option::Some(v) = last_seen.take() {
4867                        qs = qs.filter(key.as_str(), v);
4868                    }
4869                    let rows: ::std::vec::Vec<Self> = qs
4870                        .order_by(&[(pk_col, false)])
4871                        .limit(batch)
4872                        .fetch(pool)
4873                        .await?;
4874                    let len = rows.len() as i64;
4875                    let ::core::option::Option::Some(max_pk) =
4876                        rows.last().map(|r| r.__rustango_pk_value())
4877                    else {
4878                        return ::core::result::Result::Ok(());
4879                    };
4880                    for row in rows {
4881                        cb(row).await?;
4882                    }
4883                    if len < batch {
4884                        return ::core::result::Result::Ok(());
4885                    }
4886                    last_seen = ::core::option::Option::Some(max_pk);
4887                }
4888            }
4889
4890            /// Delete every row of this model with one `DELETE FROM
4891            /// <table>` on every backend; returns the rows deleted. Each
4892            /// FK's `ON DELETE` applies, so a `RESTRICT` / `NO ACTION`
4893            /// row still pointing here refuses it; see
4894            /// [`truncate_cascade`](Self::truncate_cascade).
4895            ///
4896            /// **Use only in tests / fixture-reset flows.** No per-row
4897            /// `pre_delete` / `post_delete` signals fire. An audited model
4898            /// gets one bulk audit entry, not one per row. Global scopes
4899            /// do not apply: every row goes, generic M2M links too.
4900            ///
4901            /// # Errors
4902            /// As [`truncate`].
4903            ///
4904            /// [`truncate`]: rustango::sql::truncate
4905            pub async fn truncate(
4906                pool: &#root::sql::Pool,
4907            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4908                #truncate_run
4909            }
4910
4911            /// [`truncate`](Self::truncate), also emptying every table
4912            /// that references this one, transitively and whatever its
4913            /// `ON DELETE`. The same on every backend.
4914            ///
4915            /// # Errors
4916            /// As [`truncate_cascade`].
4917            ///
4918            /// [`truncate_cascade`]: rustango::sql::truncate_cascade
4919            pub async fn truncate_cascade(
4920                pool: &#root::sql::Pool,
4921            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4922                #truncate_cascade_run
4923            }
4924
4925            /// Bulk-delete every row whose primary key is in
4926            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4927            /// Returns the affected row count.
4928            ///
4929            /// Eloquent `Model::destroy([1, 2, 3])` parity.
4930            /// Empty `pks` is a no-op (returns 0). All or none: past
4931            /// the bind cap the batches share one transaction.
4932            ///
4933            /// Accepts any iterable whose elements are
4934            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4935            /// `[i64; N]`, etc.
4936            ///
4937            /// # Errors
4938            /// As `delete_pool`.
4939            pub async fn destroy<V>(
4940                pks: impl ::core::iter::IntoIterator<Item = V>,
4941                pool: &#root::sql::Pool,
4942            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4943            where
4944                V: ::core::convert::Into<#root::core::SqlValue>,
4945            {
4946                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4947                    pks.into_iter().map(::core::convert::Into::into).collect();
4948                if _values.is_empty() {
4949                    return ::core::result::Result::Ok(0);
4950                }
4951                let _pk = <Self as #root::core::Model>::SCHEMA
4952                    .primary_key()
4953                    .ok_or_else(|| {
4954                        #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
4955                    })?;
4956                // One statement per `IN` list the dialect can bind, all or none.
4957                let mut _queries = ::std::vec::Vec::new();
4958                for _keys in #root::sql::__rustango_in_batches::<Self>(pool, _values)? {
4959                    _queries.push(
4960                        #root::query::QuerySet::<Self>::default()
4961                            .filter_op(
4962                                _pk.name,
4963                                #root::core::Op::In,
4964                                #root::core::SqlValue::List(_keys),
4965                            )
4966                            .compile_delete()?,
4967                    );
4968                }
4969                #destroy_run
4970            }
4971
4972            /// Fetch every row where `<col> = <val>`. Eloquent
4973            /// `Model::where($col, $val)->get()` parity.
4974            ///
4975            /// Thin wrapper over `QuerySet::<Self>::default()
4976            /// .filter(col, val).fetch(pool)`. For one row,
4977            /// use [`Self::first_where`]; for a chain that
4978            /// needs further `.filter()` / `.order_by()` /
4979            /// `.limit()`, drop down to `Self::query().filter(...)`
4980            /// directly.
4981            ///
4982            /// `val` accepts any value `Into<SqlValue>` so plain
4983            /// strings, ints, UUIDs, etc. all work.
4984            ///
4985            /// # Errors
4986            /// As [`FetcherPool::fetch`].
4987            ///
4988            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4989            pub async fn where_(
4990                col: &str,
4991                val: impl ::core::convert::Into<#root::core::SqlValue>,
4992                pool: &#root::sql::Pool,
4993            ) -> ::core::result::Result<
4994                ::std::vec::Vec<Self>,
4995                #root::sql::ExecError,
4996            > {
4997                use #root::sql::FetcherPool as _;
4998                #root::query::QuerySet::<Self>::default()
4999                    .filter(col, val)
5000                    .fetch(pool)
5001                    .await
5002            }
5003
5004            /// Fetch every row where `<col> IN (vals)`. Eloquent
5005            /// `Model::whereIn($col, $vals)->get()` parity. Empty
5006            /// `vals` returns no rows (matches SQL's empty-IN
5007            /// semantics).
5008            ///
5009            /// `vals` accepts any iterable whose items are
5010            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5011            /// etc.
5012            ///
5013            /// # Errors
5014            /// As [`FetcherPool::fetch`].
5015            ///
5016            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5017            pub async fn where_in<V>(
5018                col: &str,
5019                vals: impl ::core::iter::IntoIterator<Item = V>,
5020                pool: &#root::sql::Pool,
5021            ) -> ::core::result::Result<
5022                ::std::vec::Vec<Self>,
5023                #root::sql::ExecError,
5024            >
5025            where
5026                V: ::core::convert::Into<#root::core::SqlValue>,
5027            {
5028                use #root::sql::FetcherPool as _;
5029                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5030                    vals.into_iter().map(::core::convert::Into::into).collect();
5031                if _values.is_empty() {
5032                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5033                }
5034                let _key = ::std::format!("{}__in", col);
5035                let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
5036                let _split = _batches.len() > 1;
5037                let mut _rows = ::std::vec::Vec::new();
5038                for _keys in _batches {
5039                    _rows.extend(
5040                        #root::query::QuerySet::<Self>::default()
5041                            .filter(&_key, #root::core::SqlValue::List(_keys))
5042                            .fetch(pool)
5043                            .await?,
5044                    );
5045                }
5046                if _split {
5047                    _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
5048                }
5049                ::core::result::Result::Ok(_rows)
5050            }
5051
5052            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5053            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5054            /// `vals` returns every row (matches SQL's empty-NOT-IN
5055            /// semantics — vacuously true for every row).
5056            ///
5057            /// # Errors
5058            /// As [`FetcherPool::fetch`]; `ExecError::InListUnsplittable`
5059            /// when `vals` pass the backend's bind cap.
5060            ///
5061            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5062            pub async fn where_not_in<V>(
5063                col: &str,
5064                vals: impl ::core::iter::IntoIterator<Item = V>,
5065                pool: &#root::sql::Pool,
5066            ) -> ::core::result::Result<
5067                ::std::vec::Vec<Self>,
5068                #root::sql::ExecError,
5069            >
5070            where
5071                V: ::core::convert::Into<#root::core::SqlValue>,
5072            {
5073                use #root::sql::FetcherPool as _;
5074                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5075                    vals.into_iter().map(::core::convert::Into::into).collect();
5076                if _values.is_empty() {
5077                    return #root::query::QuerySet::<Self>::default()
5078                        .fetch(pool)
5079                        .await;
5080                }
5081                let _key = ::std::format!("{}__not_in", col);
5082                // A `NOT IN` cannot be split: past the bind cap it is refused.
5083                let _values = #root::sql::__rustango_one_in_list::<Self>(pool, _values)?;
5084                #root::query::QuerySet::<Self>::default()
5085                    .filter(&_key, #root::core::SqlValue::List(_values))
5086                    .fetch(pool)
5087                    .await
5088            }
5089
5090            /// Fetch every row where `<col> IS NULL`. Eloquent
5091            /// `Model::whereNull($col)->get()` parity.
5092            ///
5093            /// # Errors
5094            /// As [`FetcherPool::fetch`].
5095            ///
5096            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5097            pub async fn where_null(
5098                col: &str,
5099                pool: &#root::sql::Pool,
5100            ) -> ::core::result::Result<
5101                ::std::vec::Vec<Self>,
5102                #root::sql::ExecError,
5103            > {
5104                use #root::sql::FetcherPool as _;
5105                let _key = ::std::format!("{}__isnull", col);
5106                #root::query::QuerySet::<Self>::default()
5107                    .filter(&_key, #root::core::SqlValue::Bool(true))
5108                    .fetch(pool)
5109                    .await
5110            }
5111
5112            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5113            /// `Model::whereNotNull($col)->get()` parity.
5114            ///
5115            /// # Errors
5116            /// As [`FetcherPool::fetch`].
5117            ///
5118            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5119            pub async fn where_not_null(
5120                col: &str,
5121                pool: &#root::sql::Pool,
5122            ) -> ::core::result::Result<
5123                ::std::vec::Vec<Self>,
5124                #root::sql::ExecError,
5125            > {
5126                use #root::sql::FetcherPool as _;
5127                let _key = ::std::format!("{}__isnull", col);
5128                #root::query::QuerySet::<Self>::default()
5129                    .filter(&_key, #root::core::SqlValue::Bool(false))
5130                    .fetch(pool)
5131                    .await
5132            }
5133
5134            /// Fetch up to `n` rows in random order. Eloquent
5135            /// `Model::inRandomOrder()->limit($n)->get()` /
5136            /// `Model::query()->inRandomOrder()->get()->take($n)`
5137            /// parity. **Performance caveat**: random ordering
5138            /// forces a full table scan + per-row random key sort;
5139            /// the optimizer cannot use an index. Prefer a
5140            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5141            ///
5142            /// # Errors
5143            /// As [`FetcherPool::fetch`].
5144            ///
5145            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5146            pub async fn random_n(
5147                n: i64,
5148                pool: &#root::sql::Pool,
5149            ) -> ::core::result::Result<
5150                ::std::vec::Vec<Self>,
5151                #root::sql::ExecError,
5152            > {
5153                use #root::sql::FetcherPool as _;
5154                #root::query::QuerySet::<Self>::default()
5155                    .order_random()
5156                    .limit(n)
5157                    .fetch(pool)
5158                    .await
5159            }
5160
5161            /// Fetch one row in random order. Eloquent
5162            /// `Model::inRandomOrder()->first()` parity. Same
5163            /// performance caveat as [`Self::random_n`].
5164            ///
5165            /// # Errors
5166            /// As [`FetcherPool::fetch`].
5167            ///
5168            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5169            pub async fn random(
5170                pool: &#root::sql::Pool,
5171            ) -> ::core::result::Result<
5172                ::core::option::Option<Self>,
5173                #root::sql::ExecError,
5174            > {
5175                ::core::result::Result::Ok(
5176                    Self::random_n(1, pool).await?.into_iter().next(),
5177                )
5178            }
5179
5180            /// Fetch every row ordered ASC by `field`. Eloquent
5181            /// `Model::oldest($field)->get()` parity — the multi-row
5182            /// counterpart of [`Self::earliest`].
5183            ///
5184            /// # Errors
5185            /// As [`FetcherPool::fetch`].
5186            ///
5187            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5188            pub async fn oldest(
5189                field: &str,
5190                pool: &#root::sql::Pool,
5191            ) -> ::core::result::Result<
5192                ::std::vec::Vec<Self>,
5193                #root::sql::ExecError,
5194            > {
5195                use #root::sql::FetcherPool as _;
5196                #root::query::QuerySet::<Self>::default()
5197                    .order_by(&[(field, false)])
5198                    .fetch(pool)
5199                    .await
5200            }
5201
5202            /// Fetch every row ordered DESC by `field`. Eloquent
5203            /// `Model::latest($field)->get()` parity — the multi-row
5204            /// counterpart of [`Self::latest`].
5205            ///
5206            /// # Errors
5207            /// As [`FetcherPool::fetch`].
5208            ///
5209            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5210            pub async fn newest(
5211                field: &str,
5212                pool: &#root::sql::Pool,
5213            ) -> ::core::result::Result<
5214                ::std::vec::Vec<Self>,
5215                #root::sql::ExecError,
5216            > {
5217                use #root::sql::FetcherPool as _;
5218                #root::query::QuerySet::<Self>::default()
5219                    .order_by(&[(field, true)])
5220                    .fetch(pool)
5221                    .await
5222            }
5223
5224            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5225            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5226            /// Routes through the existing `__year` lookup suffix
5227            /// (issue #829).
5228            ///
5229            /// # Errors
5230            /// As [`FetcherPool::fetch`].
5231            ///
5232            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5233            pub async fn where_year(
5234                col: &str,
5235                year: i64,
5236                pool: &#root::sql::Pool,
5237            ) -> ::core::result::Result<
5238                ::std::vec::Vec<Self>,
5239                #root::sql::ExecError,
5240            > {
5241                use #root::sql::FetcherPool as _;
5242                let _key = ::std::format!("{}__year", col);
5243                #root::query::QuerySet::<Self>::default()
5244                    .filter(&_key, #root::core::SqlValue::I64(year))
5245                    .fetch(pool)
5246                    .await
5247            }
5248
5249            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5250            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5251            /// `month` is 1–12.
5252            ///
5253            /// # Errors
5254            /// As [`FetcherPool::fetch`].
5255            ///
5256            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5257            pub async fn where_month(
5258                col: &str,
5259                month: i64,
5260                pool: &#root::sql::Pool,
5261            ) -> ::core::result::Result<
5262                ::std::vec::Vec<Self>,
5263                #root::sql::ExecError,
5264            > {
5265                use #root::sql::FetcherPool as _;
5266                let _key = ::std::format!("{}__month", col);
5267                #root::query::QuerySet::<Self>::default()
5268                    .filter(&_key, #root::core::SqlValue::I64(month))
5269                    .fetch(pool)
5270                    .await
5271            }
5272
5273            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5274            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5275            /// `day` is 1–31.
5276            ///
5277            /// # Errors
5278            /// As [`FetcherPool::fetch`].
5279            ///
5280            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5281            pub async fn where_day(
5282                col: &str,
5283                day: i64,
5284                pool: &#root::sql::Pool,
5285            ) -> ::core::result::Result<
5286                ::std::vec::Vec<Self>,
5287                #root::sql::ExecError,
5288            > {
5289                use #root::sql::FetcherPool as _;
5290                let _key = ::std::format!("{}__day", col);
5291                #root::query::QuerySet::<Self>::default()
5292                    .filter(&_key, #root::core::SqlValue::I64(day))
5293                    .fetch(pool)
5294                    .await
5295            }
5296
5297            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5298            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5299            /// `hour` is 0–23.
5300            ///
5301            /// # Errors
5302            /// As [`FetcherPool::fetch`].
5303            ///
5304            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5305            pub async fn where_hour(
5306                col: &str,
5307                hour: i64,
5308                pool: &#root::sql::Pool,
5309            ) -> ::core::result::Result<
5310                ::std::vec::Vec<Self>,
5311                #root::sql::ExecError,
5312            > {
5313                use #root::sql::FetcherPool as _;
5314                let _key = ::std::format!("{}__hour", col);
5315                #root::query::QuerySet::<Self>::default()
5316                    .filter(&_key, #root::core::SqlValue::I64(hour))
5317                    .fetch(pool)
5318                    .await
5319            }
5320
5321            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5322            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5323            /// `minute` is 0–59.
5324            ///
5325            /// # Errors
5326            /// As [`FetcherPool::fetch`].
5327            ///
5328            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5329            pub async fn where_minute(
5330                col: &str,
5331                minute: i64,
5332                pool: &#root::sql::Pool,
5333            ) -> ::core::result::Result<
5334                ::std::vec::Vec<Self>,
5335                #root::sql::ExecError,
5336            > {
5337                use #root::sql::FetcherPool as _;
5338                let _key = ::std::format!("{}__minute", col);
5339                #root::query::QuerySet::<Self>::default()
5340                    .filter(&_key, #root::core::SqlValue::I64(minute))
5341                    .fetch(pool)
5342                    .await
5343            }
5344
5345            /// Fetch every row where `<col> LIKE <pattern>` —
5346            /// caller-supplied pattern (must include `%` / `_`
5347            /// wildcards manually). Eloquent `Model::whereLike`
5348            /// parity.
5349            ///
5350            /// # Errors
5351            /// As [`FetcherPool::fetch`].
5352            ///
5353            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5354            pub async fn where_like(
5355                col: &str,
5356                pattern: impl ::core::convert::Into<::std::string::String>,
5357                pool: &#root::sql::Pool,
5358            ) -> ::core::result::Result<
5359                ::std::vec::Vec<Self>,
5360                #root::sql::ExecError,
5361            > {
5362                use #root::sql::FetcherPool as _;
5363                let _key = ::std::format!("{}__like", col);
5364                #root::query::QuerySet::<Self>::default()
5365                    .filter(
5366                        &_key,
5367                        #root::core::SqlValue::String(pattern.into()),
5368                    )
5369                    .fetch(pool)
5370                    .await
5371            }
5372
5373            /// Fetch every row where `<col> ILIKE <pattern>` —
5374            /// case-insensitive LIKE (PG native, MySQL/SQLite
5375            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5376            ///
5377            /// # Errors
5378            /// As [`FetcherPool::fetch`].
5379            ///
5380            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5381            pub async fn where_ilike(
5382                col: &str,
5383                pattern: impl ::core::convert::Into<::std::string::String>,
5384                pool: &#root::sql::Pool,
5385            ) -> ::core::result::Result<
5386                ::std::vec::Vec<Self>,
5387                #root::sql::ExecError,
5388            > {
5389                use #root::sql::FetcherPool as _;
5390                let _key = ::std::format!("{}__ilike", col);
5391                #root::query::QuerySet::<Self>::default()
5392                    .filter(
5393                        &_key,
5394                        #root::core::SqlValue::String(pattern.into()),
5395                    )
5396                    .fetch(pool)
5397                    .await
5398            }
5399
5400            /// Fetch every row where `<col>` starts with `prefix`
5401            /// (auto-appends `%`). Eloquent
5402            /// `whereLike("col", "$prefix%")` parity.
5403            ///
5404            /// # Errors
5405            /// As [`FetcherPool::fetch`].
5406            ///
5407            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5408            pub async fn where_starts_with(
5409                col: &str,
5410                prefix: impl ::core::convert::Into<::std::string::String>,
5411                pool: &#root::sql::Pool,
5412            ) -> ::core::result::Result<
5413                ::std::vec::Vec<Self>,
5414                #root::sql::ExecError,
5415            > {
5416                use #root::sql::FetcherPool as _;
5417                let _key = ::std::format!("{}__startswith", col);
5418                #root::query::QuerySet::<Self>::default()
5419                    .filter(
5420                        &_key,
5421                        #root::core::SqlValue::String(prefix.into()),
5422                    )
5423                    .fetch(pool)
5424                    .await
5425            }
5426
5427            /// Fetch every row where `<col>` ends with `suffix`
5428            /// (auto-prepends `%`). Eloquent
5429            /// `whereLike("col", "%$suffix")` parity.
5430            ///
5431            /// # Errors
5432            /// As [`FetcherPool::fetch`].
5433            ///
5434            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5435            pub async fn where_ends_with(
5436                col: &str,
5437                suffix: impl ::core::convert::Into<::std::string::String>,
5438                pool: &#root::sql::Pool,
5439            ) -> ::core::result::Result<
5440                ::std::vec::Vec<Self>,
5441                #root::sql::ExecError,
5442            > {
5443                use #root::sql::FetcherPool as _;
5444                let _key = ::std::format!("{}__endswith", col);
5445                #root::query::QuerySet::<Self>::default()
5446                    .filter(
5447                        &_key,
5448                        #root::core::SqlValue::String(suffix.into()),
5449                    )
5450                    .fetch(pool)
5451                    .await
5452            }
5453
5454            /// Fetch every row where `<col>` contains `substr`
5455            /// (auto-wraps with `%`).
5456            /// Eloquent `whereLike("col", "%$substr%")` parity.
5457            ///
5458            /// # Errors
5459            /// As [`FetcherPool::fetch`].
5460            ///
5461            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5462            pub async fn where_contains(
5463                col: &str,
5464                substr: impl ::core::convert::Into<::std::string::String>,
5465                pool: &#root::sql::Pool,
5466            ) -> ::core::result::Result<
5467                ::std::vec::Vec<Self>,
5468                #root::sql::ExecError,
5469            > {
5470                use #root::sql::FetcherPool as _;
5471                let _key = ::std::format!("{}__contains", col);
5472                #root::query::QuerySet::<Self>::default()
5473                    .filter(
5474                        &_key,
5475                        #root::core::SqlValue::String(substr.into()),
5476                    )
5477                    .fetch(pool)
5478                    .await
5479            }
5480
5481            /// Fetch every row where `<col> > val`. Eloquent
5482            /// `Model::where($col, ">", $val)->get()` parity.
5483            ///
5484            /// # Errors
5485            /// As [`FetcherPool::fetch`].
5486            ///
5487            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5488            pub async fn where_gt(
5489                col: &str,
5490                val: impl ::core::convert::Into<#root::core::SqlValue>,
5491                pool: &#root::sql::Pool,
5492            ) -> ::core::result::Result<
5493                ::std::vec::Vec<Self>,
5494                #root::sql::ExecError,
5495            > {
5496                use #root::sql::FetcherPool as _;
5497                let _key = ::std::format!("{}__gt", col);
5498                #root::query::QuerySet::<Self>::default()
5499                    .filter(&_key, val)
5500                    .fetch(pool)
5501                    .await
5502            }
5503
5504            /// Fetch every row where `<col> >= val`. Eloquent
5505            /// `Model::where($col, ">=", $val)->get()` parity.
5506            ///
5507            /// # Errors
5508            /// As [`FetcherPool::fetch`].
5509            ///
5510            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5511            pub async fn where_gte(
5512                col: &str,
5513                val: impl ::core::convert::Into<#root::core::SqlValue>,
5514                pool: &#root::sql::Pool,
5515            ) -> ::core::result::Result<
5516                ::std::vec::Vec<Self>,
5517                #root::sql::ExecError,
5518            > {
5519                use #root::sql::FetcherPool as _;
5520                let _key = ::std::format!("{}__gte", col);
5521                #root::query::QuerySet::<Self>::default()
5522                    .filter(&_key, val)
5523                    .fetch(pool)
5524                    .await
5525            }
5526
5527            /// Fetch every row where `<col> < val`. Eloquent
5528            /// `Model::where($col, "<", $val)->get()` parity.
5529            ///
5530            /// # Errors
5531            /// As [`FetcherPool::fetch`].
5532            ///
5533            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5534            pub async fn where_lt(
5535                col: &str,
5536                val: impl ::core::convert::Into<#root::core::SqlValue>,
5537                pool: &#root::sql::Pool,
5538            ) -> ::core::result::Result<
5539                ::std::vec::Vec<Self>,
5540                #root::sql::ExecError,
5541            > {
5542                use #root::sql::FetcherPool as _;
5543                let _key = ::std::format!("{}__lt", col);
5544                #root::query::QuerySet::<Self>::default()
5545                    .filter(&_key, val)
5546                    .fetch(pool)
5547                    .await
5548            }
5549
5550            /// Fetch every row where `<col> <= val`. Eloquent
5551            /// `Model::where($col, "<=", $val)->get()` parity.
5552            ///
5553            /// # Errors
5554            /// As [`FetcherPool::fetch`].
5555            ///
5556            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5557            pub async fn where_lte(
5558                col: &str,
5559                val: impl ::core::convert::Into<#root::core::SqlValue>,
5560                pool: &#root::sql::Pool,
5561            ) -> ::core::result::Result<
5562                ::std::vec::Vec<Self>,
5563                #root::sql::ExecError,
5564            > {
5565                use #root::sql::FetcherPool as _;
5566                let _key = ::std::format!("{}__lte", col);
5567                #root::query::QuerySet::<Self>::default()
5568                    .filter(&_key, val)
5569                    .fetch(pool)
5570                    .await
5571            }
5572
5573            /// Fetch every row where `<col> <> val`. Eloquent
5574            /// `Model::where($col, "!=", $val)->get()` parity.
5575            ///
5576            /// # Errors
5577            /// As [`FetcherPool::fetch`].
5578            ///
5579            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5580            pub async fn where_ne(
5581                col: &str,
5582                val: impl ::core::convert::Into<#root::core::SqlValue>,
5583                pool: &#root::sql::Pool,
5584            ) -> ::core::result::Result<
5585                ::std::vec::Vec<Self>,
5586                #root::sql::ExecError,
5587            > {
5588                use #root::sql::FetcherPool as _;
5589                let _key = ::std::format!("{}__ne", col);
5590                #root::query::QuerySet::<Self>::default()
5591                    .filter(&_key, val)
5592                    .fetch(pool)
5593                    .await
5594            }
5595
5596            /// Fetch every row matching `<col> = val` for ANY of the
5597            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5598            /// parity. Empty `cols` returns no rows.
5599            ///
5600            /// Resolves each `&str` column to its SCHEMA-registered
5601            /// `&'static str` once and builds a single OR-composed
5602            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5603            ///
5604            /// # Errors
5605            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5606            /// when any column is not declared on the model.
5607            ///
5608            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5609            pub async fn where_any(
5610                cols: &[&str],
5611                val: impl ::core::convert::Into<#root::core::SqlValue>,
5612                pool: &#root::sql::Pool,
5613            ) -> ::core::result::Result<
5614                ::std::vec::Vec<Self>,
5615                #root::sql::ExecError,
5616            > {
5617                Self::__where_multi(cols, val, false, pool).await
5618            }
5619
5620            /// Fetch every row matching `<col> = val` for ALL listed
5621            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5622            /// Empty `cols` returns every row (vacuous AND).
5623            ///
5624            /// # Errors
5625            /// As [`Self::where_any`].
5626            pub async fn where_all(
5627                cols: &[&str],
5628                val: impl ::core::convert::Into<#root::core::SqlValue>,
5629                pool: &#root::sql::Pool,
5630            ) -> ::core::result::Result<
5631                ::std::vec::Vec<Self>,
5632                #root::sql::ExecError,
5633            > {
5634                Self::__where_multi(cols, val, true, pool).await
5635            }
5636
5637            /// Internal: build a Q expression composing `cols` via
5638            /// AND (`all`) or OR (`!all`), then fetch. Backs
5639            /// `where_any` / `where_all`.
5640            #[doc(hidden)]
5641            pub async fn __where_multi(
5642                cols: &[&str],
5643                val: impl ::core::convert::Into<#root::core::SqlValue>,
5644                all: bool,
5645                pool: &#root::sql::Pool,
5646            ) -> ::core::result::Result<
5647                ::std::vec::Vec<Self>,
5648                #root::sql::ExecError,
5649            > {
5650                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5651                    .await
5652            }
5653
5654            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5655            /// parity. PK-ordered
5656            /// is NOT guaranteed without an explicit `order_by` —
5657            /// drop into `Self::query()` for that.
5658            ///
5659            /// # Errors
5660            /// As [`FetcherPool::fetch`].
5661            ///
5662            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5663            pub async fn take(
5664                n: i64,
5665                pool: &#root::sql::Pool,
5666            ) -> ::core::result::Result<
5667                ::std::vec::Vec<Self>,
5668                #root::sql::ExecError,
5669            > {
5670                use #root::sql::FetcherPool as _;
5671                #root::query::QuerySet::<Self>::default()
5672                    .limit(n)
5673                    .fetch(pool)
5674                    .await
5675            }
5676
5677            /// Fetch the page-th window of `per_page` rows
5678            /// (1-indexed). Eloquent
5679            /// `Model::query()->forPage($page, $perPage)->get()`
5680            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5681            /// LIMIT per_page`; for large offsets this is O(N) —
5682            /// prefer keyset pagination via PK on hot paths.
5683            ///
5684            /// # Errors
5685            /// As [`FetcherPool::fetch`].
5686            ///
5687            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5688            pub async fn for_page(
5689                page: i64,
5690                per_page: i64,
5691                pool: &#root::sql::Pool,
5692            ) -> ::core::result::Result<
5693                ::std::vec::Vec<Self>,
5694                #root::sql::ExecError,
5695            > {
5696                use #root::sql::FetcherPool as _;
5697                let _offset = #root::list_params::page_offset(page, per_page);
5698                #root::query::QuerySet::<Self>::default()
5699                    .limit(per_page)
5700                    .offset(_offset)
5701                    .fetch(pool)
5702                    .await
5703            }
5704
5705            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5706            /// one page of rows AND the total row count in one
5707            /// call. Returns `(rows, total)`. Two queries: a
5708            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5709            /// for the total.
5710            ///
5711            /// Useful for paginated UIs that need both the visible
5712            /// rows AND a "Page X of Y" / total-count widget. For
5713            /// large tables prefer keyset pagination + cached count;
5714            /// every call to `paginate` re-counts the full table.
5715            ///
5716            /// Same 1-indexed `page` convention as [`Self::for_page`].
5717            ///
5718            /// # Errors
5719            /// As [`Self::for_page`] and [`Self::count`].
5720            pub async fn paginate(
5721                page: i64,
5722                per_page: i64,
5723                pool: &#root::sql::Pool,
5724            ) -> ::core::result::Result<
5725                (::std::vec::Vec<Self>, i64),
5726                #root::sql::ExecError,
5727            > {
5728                // Route through the queryset rather than `Self::count`:
5729                // the `count` inherent method is suppressed on models
5730                // that declare a field named `count` (field/shortcut
5731                // collision guard), and `paginate` must still compile
5732                // for those models.
5733                let total = {
5734                    use #root::sql::CounterPool as _;
5735                    #root::query::QuerySet::<Self>::default()
5736                        .count(pool)
5737                        .await?
5738                };
5739                let rows = Self::for_page(page, per_page, pool).await?;
5740                ::core::result::Result::Ok((rows, total))
5741            }
5742
5743            /// Bulk-update — set `set_col = set_val` on every row
5744            /// matching `where_col = where_val`. Returns affected row
5745            /// count. Eloquent
5746            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5747            /// parity.
5748            ///
5749            /// For multi-column updates drop into the queryset
5750            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5751            ///
5752            /// # Errors
5753            /// As [`UpdaterPool::execute_pool`].
5754            ///
5755            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5756            pub async fn update_where(
5757                where_col: &str,
5758                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5759                set_col: &str,
5760                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5761                pool: &#root::sql::Pool,
5762            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5763                let _query = #root::query::QuerySet::<Self>::default()
5764                    .filter(where_col, where_val)
5765                    .update()
5766                    .set(set_col, set_val)
5767                    .compile()?;
5768                #bulk_update_run
5769            }
5770
5771            /// Bulk-delete — remove every row matching
5772            /// `where_col = where_val`. Returns affected row count.
5773            /// Eloquent
5774            /// `Model::where($where_col, $where_val)->delete()` parity.
5775            ///
5776            /// # Errors
5777            /// As [`#root::sql::delete_pool`].
5778            pub async fn delete_where(
5779                where_col: &str,
5780                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5781                pool: &#root::sql::Pool,
5782            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5783                let _query = #root::query::QuerySet::<Self>::default()
5784                    .filter_op(where_col, #root::core::Op::Eq, where_val)
5785                    .compile_delete()?;
5786                #bulk_delete_run
5787            }
5788
5789            /// Bulk-update — set `set_col = set_val` on EVERY row of
5790            /// this model's table. **No WHERE clause** — use with
5791            /// care. Eloquent `Model::query()->update([$col => $val])`
5792            /// parity.
5793            ///
5794            /// Use for backfills, one-shot reset flows, etc. The
5795            /// returned count is rows affected.
5796            ///
5797            /// # Errors
5798            /// As [`UpdaterPool::execute_pool`].
5799            ///
5800            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5801            pub async fn update_all(
5802                set_col: &str,
5803                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5804                pool: &#root::sql::Pool,
5805            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5806                let _query = #root::query::QuerySet::<Self>::default()
5807                    .update()
5808                    .set(set_col, set_val)
5809                    .compile()?;
5810                #bulk_update_run
5811            }
5812
5813            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5814            /// Eloquent `Model::whereNotLike` parity. Pattern is
5815            /// passed verbatim — caller controls `%` / `_`.
5816            ///
5817            /// # Errors
5818            /// As [`FetcherPool::fetch`].
5819            ///
5820            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5821            pub async fn where_not_like(
5822                col: &str,
5823                pattern: impl ::core::convert::Into<::std::string::String>,
5824                pool: &#root::sql::Pool,
5825            ) -> ::core::result::Result<
5826                ::std::vec::Vec<Self>,
5827                #root::sql::ExecError,
5828            > {
5829                use #root::sql::FetcherPool as _;
5830                let _key = ::std::format!("{}__not_like", col);
5831                #root::query::QuerySet::<Self>::default()
5832                    .filter(
5833                        &_key,
5834                        #root::core::SqlValue::String(pattern.into()),
5835                    )
5836                    .fetch(pool)
5837                    .await
5838            }
5839
5840            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5841            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5842            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5843            ///
5844            /// # Errors
5845            /// As [`FetcherPool::fetch`].
5846            ///
5847            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5848            pub async fn where_not_ilike(
5849                col: &str,
5850                pattern: impl ::core::convert::Into<::std::string::String>,
5851                pool: &#root::sql::Pool,
5852            ) -> ::core::result::Result<
5853                ::std::vec::Vec<Self>,
5854                #root::sql::ExecError,
5855            > {
5856                use #root::sql::FetcherPool as _;
5857                let _key = ::std::format!("{}__not_ilike", col);
5858                #root::query::QuerySet::<Self>::default()
5859                    .filter(
5860                        &_key,
5861                        #root::core::SqlValue::String(pattern.into()),
5862                    )
5863                    .fetch(pool)
5864                    .await
5865            }
5866
5867            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5868            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5869            /// parity.
5870            ///
5871            /// # Errors
5872            /// As [`FetcherPool::fetch`].
5873            ///
5874            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5875            pub async fn where_not_between(
5876                col: &str,
5877                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5878                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5879                pool: &#root::sql::Pool,
5880            ) -> ::core::result::Result<
5881                ::std::vec::Vec<Self>,
5882                #root::sql::ExecError,
5883            > {
5884                use #root::sql::FetcherPool as _;
5885                let _key = ::std::format!("{}__not_between", col);
5886                let _vals = #root::core::SqlValue::List(::std::vec![
5887                    ::core::convert::Into::into(lo),
5888                    ::core::convert::Into::into(hi),
5889                ]);
5890                #root::query::QuerySet::<Self>::default()
5891                    .filter(&_key, _vals)
5892                    .fetch(pool)
5893                    .await
5894            }
5895
5896            /// Returns the SQL table name for this model. Eloquent
5897            /// `$model->getTable()` parity.
5898            #[must_use]
5899            pub fn table_name() -> &'static str {
5900                <Self as #root::core::Model>::SCHEMA.table
5901            }
5902
5903            /// Returns the SQL column name of this model's primary
5904            /// key, or `None` when the model has no
5905            /// `#[rustango(primary_key)]`. Eloquent
5906            /// `$model->getKeyName()` parity.
5907            #[must_use]
5908            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5909                <Self as #root::core::Model>::SCHEMA
5910                    .primary_key()
5911                    .map(|f| f.column)
5912            }
5913
5914            /// Fetch every row where `<col> BETWEEN lo AND hi`
5915            /// (inclusive on both ends — same as SQL). Eloquent
5916            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5917            ///
5918            /// # Errors
5919            /// As [`FetcherPool::fetch`].
5920            ///
5921            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5922            pub async fn where_between(
5923                col: &str,
5924                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5925                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5926                pool: &#root::sql::Pool,
5927            ) -> ::core::result::Result<
5928                ::std::vec::Vec<Self>,
5929                #root::sql::ExecError,
5930            > {
5931                use #root::sql::FetcherPool as _;
5932                let _key = ::std::format!("{}__between", col);
5933                let _vals = #root::core::SqlValue::List(::std::vec![
5934                    ::core::convert::Into::into(lo),
5935                    ::core::convert::Into::into(hi),
5936                ]);
5937                #root::query::QuerySet::<Self>::default()
5938                    .filter(&_key, _vals)
5939                    .fetch(pool)
5940                    .await
5941            }
5942
5943            /// Fetch the first row where `<col> = <val>`. Returns
5944            /// `Ok(None)` when no row matches. Eloquent
5945            /// `Model::firstWhere($col, $val)` parity.
5946            ///
5947            /// Thin wrapper over `QuerySet::<Self>::default()
5948            /// .filter(col, val).first(pool)`. Use this when you
5949            /// want one row identified by a non-PK column (e.g.
5950            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5951            ///
5952            /// `val` accepts any value `Into<SqlValue>` so plain
5953            /// strings, ints, UUIDs, etc. all work.
5954            ///
5955            /// # Errors
5956            /// As `QuerySet::first`.
5957            pub async fn first_where(
5958                col: &str,
5959                val: impl ::core::convert::Into<#root::core::SqlValue>,
5960                pool: &#root::sql::Pool,
5961            ) -> ::core::result::Result<
5962                ::core::option::Option<Self>,
5963                #root::sql::ExecError,
5964            > {
5965                #root::query::QuerySet::<Self>::default()
5966                    .filter(col, val)
5967                    .first(pool)
5968                    .await
5969            }
5970
5971            #value_method
5972
5973            /// Fetch the row with the largest non-NULL `field` value.
5974            /// Returns `Ok(None)` when the table is empty or every
5975            /// `field` is NULL. Thin wrapper over
5976            /// `QuerySet::<Self>::default().latest(field, pool)`.
5977            ///
5978            /// **Field name** is the Rust field ident as a string
5979            /// (not the SQL column). It is a runtime `&str`, so an
5980            /// unknown one compiles and surfaces as
5981            /// `ExecError::Query(QueryError::UnknownField)` when the
5982            /// query runs.
5983            ///
5984            /// # Errors
5985            /// As `QuerySet::latest`.
5986            pub async fn latest(
5987                field: &str,
5988                pool: &#root::sql::Pool,
5989            ) -> ::core::result::Result<
5990                ::core::option::Option<Self>,
5991                #root::sql::ExecError,
5992            > {
5993                #root::query::QuerySet::<Self>::default()
5994                    .latest(field, pool)
5995                    .await
5996            }
5997
5998            /// Sibling of [`Self::latest`]: fetches the row with the
5999            /// smallest non-NULL `field` value.
6000            ///
6001            /// # Errors
6002            /// As [`Self::latest`].
6003            pub async fn earliest(
6004                field: &str,
6005                pool: &#root::sql::Pool,
6006            ) -> ::core::result::Result<
6007                ::core::option::Option<Self>,
6008                #root::sql::ExecError,
6009            > {
6010                #root::query::QuerySet::<Self>::default()
6011                    .earliest(field, pool)
6012                    .await
6013            }
6014
6015            #count_method
6016
6017            /// `true` when the table contains at least one row.
6018            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6019            /// over `QuerySet::<Self>::default().exists(pool)`.
6020            ///
6021            /// # Errors
6022            /// As [`ExistsPool::exists`].
6023            ///
6024            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6025            pub async fn exists(
6026                pool: &#root::sql::Pool,
6027            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6028                use #root::sql::ExistsPool as _;
6029                #root::query::QuerySet::<Self>::default()
6030                    .exists(pool)
6031                    .await
6032            }
6033
6034            /// Inverse of [`Self::exists`] — returns `true` when
6035            /// the table has zero rows. Eloquent
6036            /// `Model::doesntExist()` parity.
6037            ///
6038            /// # Errors
6039            /// As [`Self::exists`].
6040            pub async fn doesnt_exist(
6041                pool: &#root::sql::Pool,
6042            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6043                Self::exists(pool).await.map(|e| !e)
6044            }
6045
6046            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6047            /// `true` when a row with primary key `pk` exists in
6048            /// the table. Sugar over
6049            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6050            ///
6051            /// Differs from [`Self::exists`] (which checks "any row
6052            /// in the table") by checking a specific PK existence.
6053            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6054            /// because the row is never materialized — the SQL is
6055            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6056            ///
6057            /// # Errors
6058            /// As [`#root::sql::ExistsPool::contains_pk`].
6059            pub async fn contains_pk(
6060                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6061                pool: &#root::sql::Pool,
6062            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6063                use #root::sql::ExistsPool as _;
6064                #root::query::QuerySet::<Self>::default()
6065                    .contains_pk(pool, pk)
6066                    .await
6067            }
6068
6069            #sum_method
6070            #avg_method
6071            #min_method
6072            #max_method
6073
6074            /// Fetch every row of this model from `pool`. Eloquent
6075            /// `Model::all()` parity — a thin wrapper over
6076            /// `QuerySet::<Self>::default().fetch(pool)`.
6077            ///
6078            /// **Use with care on large tables** — there's no
6079            /// pagination or limit; the entire table is materialized
6080            /// into memory. For anything beyond fixture / lookup
6081            /// tables, page through `QuerySet::<Self>::default()
6082            /// .order_by(...).limit(N).offset(M).fetch(pool)`
6083            /// or stream via `.iterator(chunk_size)`.
6084            ///
6085            /// # Errors
6086            /// As [`FetcherPool::fetch`].
6087            ///
6088            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6089            pub async fn all(
6090                pool: &#root::sql::Pool,
6091            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6092            {
6093                use #root::sql::FetcherPool as _;
6094                #root::query::QuerySet::<Self>::default()
6095                    .fetch(pool)
6096                    .await
6097            }
6098
6099            /// Look up every row whose primary key is in `pks`.
6100            /// Returns the matching rows in **inventory** order — NOT
6101            /// the order of `pks`. Empty `pks` returns an empty
6102            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6103            /// with a list) parity.
6104            ///
6105            /// Thin wrapper over `QuerySet::<Self>::default()
6106            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6107            /// Caller-supplied PKs that don't match a row are
6108            /// silently skipped (the returned vec is shorter than
6109            /// the input list). For an order-preserving / "fail
6110            /// when any missing" variant, build the queryset
6111            /// explicitly with `in_bulk` instead.
6112            ///
6113            /// Accepts any iterable whose elements are
6114            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6115            /// `Vec<Uuid>`, etc.
6116            ///
6117            /// # Errors
6118            /// As [`FetcherPool::fetch`].
6119            ///
6120            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6121            pub async fn find_many<V>(
6122                pks: impl ::core::iter::IntoIterator<Item = V>,
6123                pool: &#root::sql::Pool,
6124            ) -> ::core::result::Result<
6125                ::std::vec::Vec<Self>,
6126                #root::sql::ExecError,
6127            >
6128            where
6129                V: ::core::convert::Into<#root::core::SqlValue>,
6130            {
6131                use #root::sql::FetcherPool as _;
6132                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6133                    pks.into_iter().map(::core::convert::Into::into).collect();
6134                if _values.is_empty() {
6135                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6136                }
6137                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6138                let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
6139                let _split = _batches.len() > 1;
6140                let mut _rows = ::std::vec::Vec::new();
6141                for _keys in _batches {
6142                    _rows.extend(
6143                        #root::query::QuerySet::<Self>::default()
6144                            .filter(&_key, #root::core::SqlValue::List(_keys))
6145                            .fetch(pool)
6146                            .await?,
6147                    );
6148                }
6149                if _split {
6150                    _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
6151                }
6152                ::core::result::Result::Ok(_rows)
6153            }
6154
6155            /// Look up the row whose primary key equals `pk`. Returns
6156            /// `Ok(None)` when no row matches, rather than erroring.
6157            /// Eloquent `Model::find`
6158            /// shape — accepts any value `Into<SqlValue>`.
6159            ///
6160            /// One-liner shortcut for the common
6161            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6162            /// .limit(1).fetch(pool).await?.into_iter().next()`
6163            /// dance.
6164            ///
6165            /// # Errors
6166            /// As [`FetcherPool::fetch`].
6167            ///
6168            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6169            pub async fn find(
6170                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6171                pool: &#root::sql::Pool,
6172            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6173            {
6174                use #root::sql::FetcherPool as _;
6175                let _pk_val: #root::core::SqlValue = pk.into();
6176                let mut _rows: ::std::vec::Vec<Self> =
6177                    #root::query::QuerySet::<Self>::default()
6178                        .filter(::core::stringify!(#pk_ident), _pk_val)
6179                        .limit(1)
6180                        .fetch(pool)
6181                        .await?;
6182                ::core::result::Result::Ok(_rows.into_iter().next())
6183            }
6184
6185            /// Look up the row whose primary key equals `pk`. Errors
6186            /// when no row matches — the throwing counterpart of
6187            /// [`Self::find`]. Eloquent `Model::findOrFail`
6188            /// parity.
6189            ///
6190            /// Translates the miss into
6191            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6192            /// so callers can `?`-bubble straight through the typical
6193            /// `ExecError` error chain.
6194            ///
6195            /// # Errors
6196            /// As [`Self::find`]; additionally
6197            /// [`sqlx::Error::RowNotFound`] when no row matches.
6198            ///
6199            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6200            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6201            pub async fn find_or_fail(
6202                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6203                pool: &#root::sql::Pool,
6204            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6205                match Self::find(pk, pool).await? {
6206                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6207                    ::core::option::Option::None => ::core::result::Result::Err(
6208                        #root::sql::ExecError::Driver(
6209                            #root::sql::sqlx::Error::RowNotFound,
6210                        ),
6211                    ),
6212                }
6213            }
6214
6215            /// Look up multiple rows by primary key, **failing** if
6216            /// any requested PK is missing. Eloquent
6217            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6218            /// inventory order (NOT request order — same as
6219            /// [`Self::find_many`]).
6220            ///
6221            /// Empty `pks` returns an empty `Vec` (no rows requested
6222            /// → nothing to fail on).
6223            ///
6224            /// Differs from [`Self::find_many`] in that this method
6225            /// requires every PK to resolve. If even one is missing,
6226            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6227            /// in `ExecError::Driver`). A key counts as found when the
6228            /// database matches it to a row, so repeats and keys its
6229            /// collation folds together (`abc` / `ABC` on MySQL) pass.
6230            ///
6231            /// # Errors
6232            /// As [`Self::find_many`], plus `RowNotFound` when a
6233            /// requested PK matches no row.
6234            pub async fn find_many_or_fail<V>(
6235                pks: impl ::core::iter::IntoIterator<Item = V>,
6236                pool: &#root::sql::Pool,
6237            ) -> ::core::result::Result<
6238                ::std::vec::Vec<Self>,
6239                #root::sql::ExecError,
6240            >
6241            where
6242                V: ::core::convert::Into<#root::core::SqlValue>,
6243            {
6244                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6245                    pks.into_iter().map(::core::convert::Into::into).collect();
6246                if _values.is_empty() {
6247                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6248                }
6249                use #root::sql::ExistsPool as _;
6250                let mut _seen: ::std::collections::HashSet<
6251                    ::std::string::String,
6252                > = ::std::collections::HashSet::new();
6253                let mut _keys: ::std::vec::Vec<#root::core::SqlValue> =
6254                    ::std::vec::Vec::new();
6255                for v in &_values {
6256                    if _seen.insert(v.to_display_string()) {
6257                        _keys.push(::core::clone::Clone::clone(v));
6258                    }
6259                }
6260                let _rows = Self::find_many(_values, pool).await?;
6261                let _found: ::std::collections::HashSet<::std::string::String> = _rows
6262                    .iter()
6263                    .map(|r| r.__rustango_pk_value().to_display_string())
6264                    .collect();
6265                // A key with no exact twin among the rows may still match one
6266                // under the column's collation: the database decides.
6267                for _key in _keys {
6268                    if _found.contains(&_key.to_display_string()) {
6269                        continue;
6270                    }
6271                    let _hit = #root::query::QuerySet::<Self>::default()
6272                        .filter(::core::stringify!(#pk_ident), _key)
6273                        .exists(pool)
6274                        .await?;
6275                    if !_hit {
6276                        return ::core::result::Result::Err(
6277                            #root::sql::ExecError::Driver(
6278                                #root::sql::sqlx::Error::RowNotFound,
6279                            ),
6280                        );
6281                    }
6282                }
6283                ::core::result::Result::Ok(_rows)
6284            }
6285
6286            /// Find by primary key or run `fallback` to produce a
6287            /// default row to return. Eloquent
6288            /// `Model::findOr($pk, fn() => …)` parity.
6289            ///
6290            /// Unlike [`Self::find_or_fail`] (which raises on
6291            /// miss), this is the "give me something sensible"
6292            /// branch: typical use is "fetch the user's row, else
6293            /// fall back to an anonymous/guest stub".
6294            ///
6295            /// `fallback` runs only when no row matches — the DB
6296            /// round-trip happens unconditionally.
6297            ///
6298            /// # Errors
6299            /// As [`Self::find`].
6300            pub async fn find_or<F>(
6301                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6302                pool: &#root::sql::Pool,
6303                fallback: F,
6304            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6305            where
6306                F: ::core::ops::FnOnce() -> Self,
6307            {
6308                ::core::result::Result::Ok(
6309                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6310                )
6311            }
6312
6313            /// Same as [`Self::find_or`] but also returns a `bool`
6314            /// indicating whether the row was found in the DB
6315            /// (`true`) or freshly built from `fallback` (`false`).
6316            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6317            /// in PHP the returned model also exposes `->exists`;
6318            /// here that's surfaced as the second tuple element.
6319            ///
6320            /// Useful for edit-or-create form handlers where the
6321            /// caller needs to know whether to PATCH or POST when
6322            /// the user submits the form.
6323            ///
6324            /// # Errors
6325            /// As [`Self::find`].
6326            pub async fn find_or_new<F>(
6327                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6328                pool: &#root::sql::Pool,
6329                fallback: F,
6330            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6331            where
6332                F: ::core::ops::FnOnce() -> Self,
6333            {
6334                match Self::find(pk, pool).await? {
6335                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6336                    ::core::option::Option::None => {
6337                        ::core::result::Result::Ok((fallback(), false))
6338                    }
6339                }
6340            }
6341
6342            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6343            /// [`Self::find_or_new`] but **persists** the new row
6344            /// when the PK isn't found. Returns `(row, exists: bool)`
6345            /// — `exists=true` when the PK was found,
6346            /// `false` when a fresh row was inserted.
6347            ///
6348            /// ```ignore
6349            /// let (post, found) = Post::find_or_insert(
6350            ///     pk,
6351            ///     &pool,
6352            ///     || Post { id: Auto::default(), title: "new".into() },
6353            /// ).await?;
6354            /// // `found` true → returned existing row;
6355            /// // `found` false → fallback was inserted, `post.id` populated.
6356            /// ```
6357            ///
6358            /// **Caveat**: two concurrent calls that both miss the
6359            /// find can both INSERT, violating uniqueness. Wrap in a
6360            /// transaction or rely on a UNIQUE constraint + handle
6361            /// the conflict error if you need race-free semantics.
6362            ///
6363            /// # Errors
6364            /// As [`Self::find`] and [`Self::save`].
6365            pub async fn find_or_insert<F>(
6366                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6367                pool: &#root::sql::Pool,
6368                fallback: F,
6369            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6370            where
6371                F: ::core::ops::FnOnce() -> Self,
6372            {
6373                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6374                    return ::core::result::Result::Ok((_row, true));
6375                }
6376                let mut _new = fallback();
6377                _new.save(pool).await?;
6378                ::core::result::Result::Ok((_new, false))
6379            }
6380
6381            /// Fetch the first row of the table, or run `fallback`
6382            /// when the table is empty. Eloquent
6383            /// `Model::firstOr(fn() => …)` parity.
6384            ///
6385            /// # Errors
6386            /// As [`Self::first`].
6387            pub async fn first_or<F>(
6388                pool: &#root::sql::Pool,
6389                fallback: F,
6390            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6391            where
6392                F: ::core::ops::FnOnce() -> Self,
6393            {
6394                // Route through the queryset rather than `Self::first`,
6395                // which is suppressed on models with a field named
6396                // `first` (field/shortcut collision guard).
6397                ::core::result::Result::Ok(
6398                    #root::query::QuerySet::<Self>::default()
6399                        .first(pool)
6400                        .await?
6401                        .unwrap_or_else(fallback),
6402                )
6403            }
6404
6405            /// Fetch exactly one row matching `<col> = <val>`. Errors
6406            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6407            /// or more than one matches
6408            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6409            /// Eloquent `Model::sole($col, $val)` parity.
6410            ///
6411            /// # Errors
6412            /// As [`Self::where_`] plus the explicit
6413            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6414            pub async fn sole(
6415                col: &str,
6416                val: impl ::core::convert::Into<#root::core::SqlValue>,
6417                pool: &#root::sql::Pool,
6418            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6419                let mut _rows = Self::where_(col, val, pool).await?;
6420                match _rows.len() {
6421                    0 => ::core::result::Result::Err(
6422                        #root::sql::ExecError::Driver(
6423                            #root::sql::sqlx::Error::RowNotFound,
6424                        ),
6425                    ),
6426                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6427                    n => ::core::result::Result::Err(
6428                        #root::sql::ExecError::MultipleRowsReturned {
6429                            op: "sole",
6430                            table: <Self as #root::core::Model>::SCHEMA.name,
6431                            count: n,
6432                        },
6433                    ),
6434                }
6435            }
6436        }
6437    } else {
6438        quote!()
6439    };
6440
6441    // `_tx` family: the shared `__rustango_*_exec` bodies on an open
6442    // `PoolTx`; an audited model writes its snapshot entry in the same tx.
6443    let tx_emit = |op: TokenStream2| -> TokenStream2 {
6444        if audited_fields.is_some() && primary_key.is_some() {
6445            quote! {
6446                #root::audit::__emit_many_tx(
6447                    tx,
6448                    &[self.__rustango_audit_entry(#root::audit::AuditOp::#op)],
6449                )
6450                .await?;
6451            }
6452        } else {
6453            quote!()
6454        }
6455    };
6456    let tx_insert_method = {
6457        let self_ref = if fields.has_auto {
6458            quote!(&mut self)
6459        } else {
6460            quote!(&self)
6461        };
6462        let emit = tx_emit(quote!(Create));
6463        let refuse_pkless = if audited_fields.is_some() && primary_key.is_none() {
6464            quote! {
6465                return ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
6466                    table: <Self as #root::core::Model>::SCHEMA.table,
6467                });
6468            }
6469        } else {
6470            quote!()
6471        };
6472        quote! {
6473            /// Insert this row inside an open transaction; an audited model
6474            /// writes its audit row there too.
6475            ///
6476            /// # Errors
6477            /// As [`Self::insert`].
6478            #[allow(unreachable_code)]
6479            pub async fn insert_tx(
6480                #self_ref,
6481                tx: &mut #root::sql::PoolTx<'_>,
6482            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6483                #refuse_pkless
6484                self.__rustango_insert_exec(&mut *tx).await?;
6485                #emit
6486                ::core::result::Result::Ok(())
6487            }
6488        }
6489    };
6490    let tx_save_method = if let Some((pk_ident, _)) = primary_key {
6491        let emit_create = tx_emit(quote!(Create));
6492        let emit_update = tx_emit(quote!(Update));
6493        let was_unset = if fields.pk_is_auto {
6494            quote!(matches!(self.#pk_ident, #root::sql::Auto::Unset))
6495        } else {
6496            quote!(false)
6497        };
6498        quote! {
6499            /// Save this row inside an open transaction; an audited model
6500            /// writes its audit row there too.
6501            ///
6502            /// # Errors
6503            /// As [`Self::save`].
6504            pub async fn save_tx(
6505                &mut self,
6506                tx: &mut #root::sql::PoolTx<'_>,
6507            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6508                let _was_unset = #was_unset;
6509                let _n = self.__rustango_save_exec(&mut *tx).await?;
6510                if _n > 0 {
6511                    if _was_unset {
6512                        #emit_create
6513                    } else {
6514                        #emit_update
6515                    }
6516                }
6517                ::core::result::Result::Ok(_n)
6518            }
6519        }
6520    } else {
6521        quote!()
6522    };
6523    let tx_delete_method = if primary_key.is_some() {
6524        let emit = tx_emit(quote!(Delete));
6525        quote! {
6526            /// Delete this row inside an open transaction; an audited model
6527            /// writes its audit row there too.
6528            ///
6529            /// # Errors
6530            /// As [`Self::delete`].
6531            pub async fn delete_tx(
6532                &self,
6533                tx: &mut #root::sql::PoolTx<'_>,
6534            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6535                let _n = self.__rustango_delete_exec(&mut *tx).await?;
6536                if _n > 0 {
6537                    #emit
6538                }
6539                ::core::result::Result::Ok(_n)
6540            }
6541        }
6542    } else {
6543        quote!()
6544    };
6545
6546    // The one statement-level write path: `_on` (refusing audited models)
6547    // and `_tx` share these `__rustango_*_exec` bodies; non-audited `&Pool`
6548    // methods forward to them too.
6549    let refuse = |method: &str| -> TokenStream2 {
6550        let reason = format!("`{method}` cannot audit; use the `&Pool` method");
6551        quote!(#root::sql::__macro_internals::refuse_audited::<Self>(#reason)?;)
6552    };
6553    let refuse_save = refuse("save_on");
6554    let save_method = if let Some((pk_ident, pk_column)) = primary_key {
6555        let pk_column_lit = pk_column.as_str();
6556        let assignments = &fields.update_assignments;
6557        let dispatch_unset = if fields.pk_is_auto {
6558            quote! {
6559                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6560                    return self.__rustango_insert_exec(_executor).await.map(|()| 1u64);
6561                }
6562            }
6563        } else {
6564            quote!()
6565        };
6566        let write = restore_auto_now_unless(
6567            &fields.auto_now_idents,
6568            &save_ret,
6569            &save_keep,
6570            &quote! {
6571                let _query = #root::core::UpdateQuery::new(
6572                    <Self as #root::core::Model>::SCHEMA,
6573                    ::std::vec![ #( #assignments ),* ],
6574                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6575                        #pk_column_lit,
6576                        #root::core::Op::Eq,
6577                        ::core::convert::Into::<#root::core::SqlValue>::into(
6578                            ::core::clone::Clone::clone(&self.#pk_ident)
6579                        ),
6580                    )),
6581                );
6582                #root::sql::__macro_internals::update_on(_executor, &_query)
6583                    .await
6584                    .and_then(#root::sql::__macro_internals::row_saved)
6585            },
6586        );
6587        let upsert = if fields.pk_is_auto {
6588            let upsert_cols = &fields.upsert_update_columns;
6589            let upsert_pushes = &fields.insert_pushes;
6590            let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6591                fields.uuid_v7_fields.iter().cloned().unzip();
6592            let upsert_returning = &fields.returning_cols;
6593            let auto_now_idents = &fields.auto_now_idents;
6594            // Prefer the first `unique_together`: a server-assigned PK never
6595            // collides, so a PK-only target would always insert.
6596            let upsert_target_columns: Vec<String> =
6597                upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6598            let upsert_target_lits = upsert_target_columns
6599                .iter()
6600                .map(String::as_str)
6601                .collect::<Vec<_>>();
6602            let conflict_clause = if fields.upsert_update_columns.is_empty() {
6603                quote!(#root::core::ConflictClause::DoNothing)
6604            } else {
6605                quote!(#root::core::ConflictClause::DoUpdate {
6606                    target: ::std::vec![ #( #upsert_target_lits ),* ],
6607                    update_columns: ::std::vec![ #( #upsert_cols ),* ],
6608                })
6609            };
6610            let unit_ret = quote!(::core::result::Result<(), #root::sql::ExecError>);
6611            let unit_keep = quote!(_saved.is_ok());
6612            let exec_body = restore_auto_now_unless(
6613                auto_now_idents,
6614                &unit_ret,
6615                &unit_keep,
6616                &quote! {
6617                    let _query = self.__rustango_upsert_query();
6618                    let _row = #root::sql::__macro_internals::insert_returning_on(
6619                        _executor, &_query,
6620                    )
6621                    .await?;
6622                    #root::sql::apply_auto_pk(_row, self)
6623                },
6624            );
6625            let pool_body = if audited_fields.is_some() {
6626                restore_auto_now_unless(
6627                    auto_now_idents,
6628                    &unit_ret,
6629                    &unit_keep,
6630                    &quote! {
6631                        let _query = self.__rustango_upsert_query();
6632                        #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self, _op| {
6633                            _m.__rustango_audit_entry(_op)
6634                        })
6635                        .await
6636                    },
6637                )
6638            } else {
6639                quote!(self.__rustango_upsert_exec(pool).await)
6640            };
6641            let refuse_upsert = refuse("upsert_on");
6642            quote! {
6643                /// Insert this row, or update the row it conflicts with: one
6644                /// `INSERT … ON CONFLICT` (MySQL: `ON DUPLICATE KEY UPDATE`).
6645                /// The target is the first `unique_together`, else the PK.
6646                /// Fills the `Auto<T>` fields from the written row.
6647                ///
6648                /// # Errors
6649                /// As [`Self::insert`].
6650                pub async fn upsert(
6651                    &mut self,
6652                    pool: &#root::sql::Pool,
6653                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6654                    #pool_body
6655                }
6656
6657                /// [`Self::upsert`] on any [`#root::sql::SqlExecutor`]. An
6658                /// audited model refuses it: use [`Self::upsert`].
6659                ///
6660                /// # Errors
6661                /// As [`Self::upsert`].
6662                pub async fn upsert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6663                    &mut self,
6664                    _executor: _E,
6665                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6666                    #refuse_upsert
6667                    self.__rustango_upsert_exec(_executor).await
6668                }
6669
6670                #[doc(hidden)]
6671                pub async fn __rustango_upsert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6672                    &mut self,
6673                    _executor: _E,
6674                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6675                    #exec_body
6676                }
6677
6678                /// The upsert statement; stamps `auto_now` like `save`.
6679                #[doc(hidden)]
6680                pub fn __rustango_upsert_query(&mut self) -> #root::core::InsertQuery {
6681                    let mut _columns: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
6682                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6683                        ::std::vec::Vec::new();
6684                    #[allow(unused_mut)]
6685                    let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
6686                    #(
6687                        if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
6688                            _generated.push(#gen_cols);
6689                        }
6690                    )*
6691                    #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
6692                    #( #upsert_pushes )*
6693                    #root::core::InsertQuery::new(
6694                        <Self as #root::core::Model>::SCHEMA,
6695                        _columns,
6696                        _values,
6697                    )
6698                    .returning(::std::vec![ #( #upsert_returning ),* ])
6699                    .on_conflict(#conflict_clause)
6700                    .generated(_generated)
6701                }
6702            }
6703        } else {
6704            quote!()
6705        };
6706        Some(quote! {
6707            /// [`Self::save`] on any [`#root::sql::SqlExecutor`]: a held
6708            /// `PoolConn`, a `PoolTx`, a tenant connection or a Postgres sqlx
6709            /// executor. An audited model refuses it: use [`Self::save`].
6710            ///
6711            /// # Errors
6712            /// As [`Self::save`].
6713            pub async fn save_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6714                &mut self,
6715                _executor: _E,
6716            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6717                #refuse_save
6718                self.__rustango_save_exec(_executor).await
6719            }
6720
6721            #[doc(hidden)]
6722            pub async fn __rustango_save_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6723                &mut self,
6724                _executor: _E,
6725            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6726                #dispatch_unset
6727                #write
6728            }
6729
6730            /// Old name of [`Self::save`].
6731            ///
6732            /// # Errors
6733            /// As [`Self::save`].
6734            #[deprecated(since = "0.62.0", note = "use `save`")]
6735            pub async fn save_pool(
6736                &mut self,
6737                pool: &#root::sql::Pool,
6738            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6739                self.save(pool).await
6740            }
6741
6742            #upsert
6743        })
6744    } else {
6745        None
6746    };
6747
6748    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6749        let pk_column_lit = pk_column.as_str();
6750        let refuse_delete = refuse("delete_on");
6751        // Optional `soft_delete_on` / `restore_on` companions when the
6752        // model has a `#[rustango(soft_delete)]` column. They land
6753        // alongside the regular `delete_on` so callers have both
6754        // options — a hard delete (audit-tracked as a real DELETE) and
6755        // a logical delete (audit-tracked as an UPDATE setting the
6756        // deleted_at column to NOW()).
6757        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6758            let col_lit = col;
6759            let sd_field_ident = fields
6760                .soft_delete_field_ident
6761                .clone()
6762                .expect("soft_delete_column without ident");
6763            let refuse_soft_delete = refuse("soft_delete_on");
6764            let refuse_restore = refuse("restore_on");
6765            // `self` takes the written value, so a later `save` keeps it.
6766            let mark_deleted = quote! {
6767                if _n > 0 {
6768                    self.#sd_field_ident = ::core::option::Option::Some(_now);
6769                }
6770            };
6771            let mark_restored = quote! {
6772                if _n > 0 {
6773                    self.#sd_field_ident = ::core::option::Option::None;
6774                }
6775            };
6776            quote! {
6777                /// Soft-delete this row on any [`#root::sql::SqlExecutor`]: set
6778                /// its `#[rustango(soft_delete)]` column to now. An audited
6779                /// model refuses it: use [`Self::soft_delete`].
6780                ///
6781                /// # Errors
6782                /// As [`Self::delete`].
6783                pub async fn soft_delete_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6784                    &mut self,
6785                    _executor: _E,
6786                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6787                    #refuse_soft_delete
6788                    let _now = #root::__chrono::Utc::now();
6789                    let _query = #root::soft_delete::__mark_query(
6790                        <Self as #root::core::Model>::SCHEMA,
6791                        #col_lit,
6792                        #pk_column_lit,
6793                        ::core::convert::Into::<#root::core::SqlValue>::into(
6794                            ::core::clone::Clone::clone(&self.#pk_ident)
6795                        ),
6796                        ::core::option::Option::Some(_now),
6797                    );
6798                    let _n = #root::sql::__macro_internals::update_on(_executor, &_query).await?;
6799                    #mark_deleted
6800                    ::core::result::Result::Ok(_n)
6801                }
6802
6803                /// Inverse of [`Self::soft_delete_on`]: clear the soft-delete
6804                /// column. An audited model refuses it: use [`Self::restore`].
6805                ///
6806                /// # Errors
6807                /// As [`Self::delete`].
6808                pub async fn restore_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6809                    &mut self,
6810                    _executor: _E,
6811                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6812                    #refuse_restore
6813                    let _query = #root::soft_delete::__mark_query(
6814                        <Self as #root::core::Model>::SCHEMA,
6815                        #col_lit,
6816                        #pk_column_lit,
6817                        ::core::convert::Into::<#root::core::SqlValue>::into(
6818                            ::core::clone::Clone::clone(&self.#pk_ident)
6819                        ),
6820                        ::core::option::Option::None,
6821                    );
6822                    let _n = #root::sql::__macro_internals::update_on(_executor, &_query).await?;
6823                    #mark_restored
6824                    ::core::result::Result::Ok(_n)
6825                }
6826
6827                /// Soft-delete this row on any backend. Eloquent `Model::delete()` semantics on
6828                /// soft-delete-enabled models (closes #821 partial).
6829                ///
6830                /// Sets the `#[rustango(soft_delete)]` column to
6831                /// `NOW()` on every backend. Query helpers
6832                /// (`QuerySet::active()` / `only_trashed()`,
6833                /// `soft_delete::active_filter` /
6834                /// `compose_with_active`) filter trashed rows out by
6835                /// reading `IS NULL` on the same column. Keyed on the PK;
6836                /// global scopes do not apply. When a row changed, `self`
6837                /// takes the stamp too, so a later `save` keeps it.
6838                ///
6839                /// # Errors
6840                /// As [`#root::sql::update_pool`].
6841                pub async fn soft_delete(
6842                    &mut self,
6843                    pool: &#root::sql::Pool,
6844                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6845                    let _now = #root::__chrono::Utc::now();
6846                    let _query = #root::soft_delete::__mark_query(
6847                        <Self as #root::core::Model>::SCHEMA,
6848                        #col_lit,
6849                        #pk_column_lit,
6850                        ::core::convert::Into::<#root::core::SqlValue>::into(
6851                            ::core::clone::Clone::clone(&self.#pk_ident)
6852                        ),
6853                        ::core::option::Option::Some(_now),
6854                    );
6855                    let _n = { #pool_softdelete_run }?;
6856                    #mark_deleted
6857                    ::core::result::Result::Ok(_n)
6858                }
6859
6860                /// Tri-dialect counterpart of [`Self::restore_on`].
6861                /// Clears the `#[rustango(soft_delete)]` column back
6862                /// to `NULL`, marking the row live again. Eloquent
6863                /// `Model::restore()` parity. Keyed on the PK; global
6864                /// scopes do not apply. When a row changed, `self` is
6865                /// cleared too.
6866                ///
6867                /// # Errors
6868                /// As [`#root::sql::update_pool`].
6869                pub async fn restore(
6870                    &mut self,
6871                    pool: &#root::sql::Pool,
6872                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6873                    let _query = #root::soft_delete::__mark_query(
6874                        <Self as #root::core::Model>::SCHEMA,
6875                        #col_lit,
6876                        #pk_column_lit,
6877                        ::core::convert::Into::<#root::core::SqlValue>::into(
6878                            ::core::clone::Clone::clone(&self.#pk_ident)
6879                        ),
6880                        ::core::option::Option::None,
6881                    );
6882                    let _n = { #pool_restore_run }?;
6883                    #mark_restored
6884                    ::core::result::Result::Ok(_n)
6885                }
6886
6887                /// Hard-delete this row, ignoring the soft-delete
6888                /// column. Eloquent `Model::forceDelete()` parity —
6889                /// the escape hatch when you need to actually purge
6890                /// data (GDPR, fixture cleanup, etc.).
6891                ///
6892                /// Equivalent to [`Self::delete_pool`] (the framework's
6893                /// non-soft delete) — exposed under the Eloquent name
6894                /// for muscle-memory + so soft-delete-enabled models
6895                /// have all three operations (soft / restore / force)
6896                /// in one place.
6897                ///
6898                /// # Errors
6899                /// As [`Self::delete_pool`].
6900                pub async fn force_delete(
6901                    &self,
6902                    pool: &#root::sql::Pool,
6903                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6904                    Self::delete(self, pool).await
6905                }
6906
6907                /// Returns `true` when this row is soft-deleted (its
6908                /// `#[rustango(soft_delete)]` column is currently
6909                /// set — Eloquent `$model->trashed()` parity).
6910                ///
6911                /// Pure in-memory predicate over `&self`; does not
6912                /// hit the database. Useful in admin/template code
6913                /// like `{% if post.trashed() %}…{% endif %}` and in
6914                /// guard clauses on restore/force-delete flows.
6915                pub fn trashed(&self) -> bool {
6916                    ::core::option::Option::is_some(&self.#sd_field_ident)
6917                }
6918
6919                /// Fetch every row whose `#[rustango(soft_delete)]`
6920                /// column is `NULL` (a.k.a. the live, non-trashed
6921                /// rows). Eloquent's default `Model::all()` behavior on
6922                /// a soft-delete model (Eloquent auto-scopes trashed
6923                /// rows out; rustango doesn't have auto-scoping yet —
6924                /// see issue #820 — so this is the explicit shortcut).
6925                ///
6926                /// One-liner over `QuerySet::<Self>::default()
6927                /// .active().fetch(pool)`. Closes #821 partial.
6928                ///
6929                /// # Errors
6930                /// As [`#root::sql::FetcherPool::fetch`].
6931                pub async fn active(
6932                    pool: &#root::sql::Pool,
6933                ) -> ::core::result::Result<
6934                    ::std::vec::Vec<Self>,
6935                    #root::sql::ExecError,
6936                > {
6937                    use #root::sql::FetcherPool as _;
6938                    #root::query::QuerySet::<Self>::default()
6939                        .active()
6940                        .fetch(pool)
6941                        .await
6942                }
6943
6944                /// Fetch ONLY soft-deleted rows. Eloquent
6945                /// `Model::onlyTrashed()->get()` parity — drives the
6946                /// admin "Trash" page, restore flows, GDPR purge
6947                /// scans, etc.
6948                ///
6949                /// One-liner over `QuerySet::<Self>::default()
6950                /// .only_trashed().fetch(pool)`. Closes #821
6951                /// partial.
6952                ///
6953                /// # Errors
6954                /// As [`#root::sql::FetcherPool::fetch`].
6955                pub async fn only_trashed(
6956                    pool: &#root::sql::Pool,
6957                ) -> ::core::result::Result<
6958                    ::std::vec::Vec<Self>,
6959                    #root::sql::ExecError,
6960                > {
6961                    use #root::sql::FetcherPool as _;
6962                    #root::query::QuerySet::<Self>::default()
6963                        .only_trashed()
6964                        .fetch(pool)
6965                        .await
6966                }
6967
6968                /// Fetch every row, both live and soft-deleted.
6969                /// Eloquent `Model::withTrashed()->get()` parity.
6970                ///
6971                /// Today every queryset already includes trashed rows
6972                /// (rustango has no global-scope tracking yet — issue
6973                /// #820), so this is functionally equivalent to
6974                /// [`Self::all`]. Exposed as a named shortcut so
6975                /// soft-delete-aware code reads `Model::with_trashed_pool`
6976                /// rather than `Model::all_pool` — keeps intent visible
6977                /// in callers and stays correct when auto-scoping lands.
6978                ///
6979                /// Closes #821 partial.
6980                ///
6981                /// # Errors
6982                /// As [`#root::sql::FetcherPool::fetch`].
6983                pub async fn with_trashed(
6984                    pool: &#root::sql::Pool,
6985                ) -> ::core::result::Result<
6986                    ::std::vec::Vec<Self>,
6987                    #root::sql::ExecError,
6988                > {
6989                    use #root::sql::FetcherPool as _;
6990                    #root::query::QuerySet::<Self>::default()
6991                        .with_trashed()
6992                        .fetch(pool)
6993                        .await
6994                }
6995
6996            }
6997        } else {
6998            quote!()
6999        };
7000        quote! {
7001
7002
7003            /// [`Self::delete`] on any [`#root::sql::SqlExecutor`]. An audited
7004            /// model refuses it: use [`Self::delete`].
7005            ///
7006            /// # Errors
7007            /// As [`Self::delete`].
7008            pub async fn delete_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7009                &self,
7010                _executor: _E,
7011            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7012                #refuse_delete
7013                self.__rustango_delete_exec(_executor).await
7014            }
7015
7016            #[doc(hidden)]
7017            pub async fn __rustango_delete_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7018                &self,
7019                _executor: _E,
7020            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7021                let _query = #root::core::DeleteQuery::by_pk(
7022                    <Self as #root::core::Model>::SCHEMA,
7023                    #pk_column_lit,
7024                    ::core::convert::Into::<#root::core::SqlValue>::into(
7025                        ::core::clone::Clone::clone(&self.#pk_ident)
7026                    ),
7027                );
7028                #root::sql::__macro_internals::delete_on(_executor, &_query).await
7029            }
7030
7031            /// Old name of [`Self::delete`].
7032            ///
7033            /// # Errors
7034            /// As [`Self::delete`].
7035            #[deprecated(since = "0.62.0", note = "use `delete`")]
7036            pub async fn delete_pool(
7037                &self,
7038                pool: &#root::sql::Pool,
7039            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7040                self.delete(pool).await
7041            }
7042
7043
7044            #pool_delete_method
7045            #pool_insert_method
7046            #audit_entry_method
7047            #pool_save_method
7048            #refresh_replicate_methods
7049            #tx_delete_method
7050            #tx_insert_method
7051            #tx_save_method
7052            #soft_delete_methods
7053
7054            /// Returns `true` when `other` represents the same DB
7055            /// row as `self` — i.e. their primary keys compare
7056            /// equal. Eloquent `$model->is($other)` parity.
7057            ///
7058            /// Because both arguments are typed `&Self`, the
7059            /// model/table check is automatic — `Post::is` cannot
7060            /// be invoked against a `Comment` at compile time. Only
7061            /// the PK has to be compared at runtime.
7062            pub fn is(&self, other: &Self) -> bool {
7063                self.#pk_ident == other.#pk_ident
7064            }
7065
7066            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7067            /// parity.
7068            pub fn is_not(&self, other: &Self) -> bool {
7069                self.#pk_ident != other.#pk_ident
7070            }
7071
7072            /// Returns this row's primary-key value as an
7073            /// [`#root::core::SqlValue`]. Eloquent
7074            /// `$model->getKey()` parity.
7075            ///
7076            /// Useful when you need to thread the PK through a
7077            /// generic `Into<SqlValue>`-bound API without knowing the
7078            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7079            #[must_use]
7080            pub fn get_key(&self) -> #root::core::SqlValue {
7081                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7082            }
7083
7084        }
7085    });
7086
7087    let refuse_insert = refuse("insert_on");
7088    // The audited `insert` is built with the PK methods; an audited model
7089    // without a PK has no row to name, so its insert refuses.
7090    let audited_insert = audited_fields.is_some() && primary_key.is_some();
7091    let insert_pool_body = if audited_fields.is_some() {
7092        quote! {
7093            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7094                table: <Self as #root::core::Model>::SCHEMA.table,
7095            })
7096        }
7097    } else {
7098        quote!(self.__rustango_insert_exec(pool).await)
7099    };
7100    let plain_insert_auto = if audited_insert {
7101        quote!()
7102    } else {
7103        quote! {
7104            /// Insert this row on any backend, reading each `Auto` field
7105            /// back from the written row.
7106            ///
7107            /// # Errors
7108            /// Returns [`#root::sql::ExecError`] for SQL-writing or driver failures.
7109            pub async fn insert(
7110                &mut self,
7111                pool: &#root::sql::Pool,
7112            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7113                #insert_pool_body
7114            }
7115        }
7116    };
7117    let plain_insert_plain = if audited_insert {
7118        quote!()
7119    } else {
7120        quote! {
7121            /// Insert this row on any backend.
7122            ///
7123            /// # Errors
7124            /// Returns [`#root::sql::ExecError`] for SQL-writing or driver failures.
7125            pub async fn insert(
7126                &self,
7127                pool: &#root::sql::Pool,
7128            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7129                #insert_pool_body
7130            }
7131        }
7132    };
7133    let insert_method = if fields.has_auto {
7134        let pushes = &fields.insert_pushes;
7135        let returning_cols = &fields.returning_cols;
7136        quote! {
7137            #plain_insert_auto
7138            /// [`Self::insert`] on any [`#root::sql::SqlExecutor`]. An audited
7139            /// model refuses it: use [`Self::insert`].
7140            ///
7141            /// # Errors
7142            /// As [`Self::insert`].
7143            pub async fn insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7144                &mut self,
7145                _executor: _E,
7146            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7147                #refuse_insert
7148                self.__rustango_insert_exec(_executor).await
7149            }
7150
7151            #[doc(hidden)]
7152            pub async fn __rustango_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7153                &mut self,
7154                _executor: _E,
7155            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7156                let _query = self.__rustango_insert_query();
7157                // Every `Auto` filled Rust-side: nothing to read back.
7158                if _query.returning.is_empty() {
7159                    return #root::sql::__macro_internals::insert_on(_executor, &_query).await;
7160                }
7161                let _row = #root::sql::__macro_internals::insert_returning_on(
7162                    _executor, &_query,
7163                )
7164                .await?;
7165                #root::sql::apply_auto_pk(_row, self)
7166            }
7167
7168            /// The INSERT for this row; fills Rust-side `Auto` values first.
7169            #[doc(hidden)]
7170            pub fn __rustango_insert_query(&mut self) -> #root::core::InsertQuery {
7171                let mut _columns: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7172                let mut _values: ::std::vec::Vec<#root::core::SqlValue> = ::std::vec::Vec::new();
7173                #( #pushes )*
7174                #root::core::InsertQuery::new(
7175                    <Self as #root::core::Model>::SCHEMA,
7176                    _columns,
7177                    _values,
7178                )
7179                .returning(::std::vec![ #( #returning_cols ),* ])
7180            }
7181
7182            /// Old name of [`Self::insert`].
7183            ///
7184            /// # Errors
7185            /// As [`Self::insert`].
7186            #[deprecated(since = "0.62.0", note = "use `insert`")]
7187            pub async fn insert_pool(
7188                &mut self,
7189                pool: &#root::sql::Pool,
7190            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7191                self.insert(pool).await
7192            }
7193        }
7194    } else {
7195        let insert_columns = &fields.insert_columns;
7196        let insert_values = &fields.insert_values;
7197        // The audited `insert` takes `&mut self` for its audit helper.
7198        let pool_self = if audited_fields.is_some() {
7199            quote!(&mut self)
7200        } else {
7201            quote!(&self)
7202        };
7203        quote! {
7204            #plain_insert_plain
7205            /// [`Self::insert`] on any [`#root::sql::SqlExecutor`]. An audited
7206            /// model refuses it: use [`Self::insert`].
7207            ///
7208            /// # Errors
7209            /// As [`Self::insert`].
7210            pub async fn insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7211                &self,
7212                _executor: _E,
7213            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7214                #refuse_insert
7215                self.__rustango_insert_exec(_executor).await
7216            }
7217
7218            #[doc(hidden)]
7219            pub async fn __rustango_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7220                &self,
7221                _executor: _E,
7222            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7223                #root::sql::__macro_internals::insert_on(_executor, &self.__rustango_insert_query())
7224                    .await
7225            }
7226
7227            /// The INSERT for this row.
7228            #[doc(hidden)]
7229            pub fn __rustango_insert_query(&self) -> #root::core::InsertQuery {
7230                #root::core::InsertQuery::new(
7231                    <Self as #root::core::Model>::SCHEMA,
7232                    ::std::vec![ #( #insert_columns ),* ],
7233                    ::std::vec![ #( #insert_values ),* ],
7234                )
7235            }
7236
7237            /// Old name of [`Self::insert`].
7238            ///
7239            /// # Errors
7240            /// As [`Self::insert`].
7241            #[deprecated(since = "0.62.0", note = "use `insert`")]
7242            pub async fn insert_pool(
7243                #pool_self,
7244                pool: &#root::sql::Pool,
7245            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7246                self.insert(pool).await
7247            }
7248        }
7249    };
7250
7251    // `bulk_insert(&Pool)`: audited models write their rows and one
7252    // `Create` entry each in one transaction.
7253    let bulk_pool_body = if audited_fields.is_some() {
7254        quote! {
7255            let mut _tx = #root::sql::__macro_internals::write_tx(pool).await?;
7256            Self::__rustango_bulk_insert_exec(rows, &mut *_tx).await?;
7257            // MySQL has no RETURNING: a database-filled PK stays unset.
7258            if rows.iter().any(|_row| _row.__rustango_audit_pk().is_empty()) {
7259                return ::core::result::Result::Err(#root::sql::ExecError::AuditUnsupported {
7260                    table: <Self as #root::core::Model>::SCHEMA.table,
7261                    reason: "MySQL has no RETURNING to read the written rows",
7262                });
7263            }
7264            let _entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
7265                .iter()
7266                .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
7267                .collect();
7268            #root::audit::__emit_many_tx(&mut _tx, &_entries).await?;
7269            _tx.commit().await?;
7270            ::core::result::Result::Ok(())
7271        }
7272    } else {
7273        quote!(Self::__rustango_bulk_insert_exec(rows, pool).await)
7274    };
7275    let refuse_bulk = refuse("bulk_insert_on");
7276    let bulk_insert_method = if fields.has_auto {
7277        let cols_no_auto = &fields.bulk_columns_no_auto;
7278        let cols_all = &fields.bulk_columns_all;
7279        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7280        let pushes_all = &fields.bulk_pushes_all;
7281        let returning_cols = &fields.returning_cols;
7282        let uniformity = &fields.bulk_auto_uniformity;
7283        let fill_in_place = &fields.bulk_fill_in_place;
7284        // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7285        let first_auto_ident = fields
7286            .first_db_auto
7287            .as_ref()
7288            .map(|(ident, _)| ident)
7289            .or(fields.first_auto_ident.as_ref())
7290            .expect("has_auto implies first_auto_ident is Some");
7291        quote! {
7292            /// Bulk-insert `rows`: one statement, or one per chunk in one
7293            /// transaction past the bind cap. Every row's `Auto<T>` PK must
7294            /// be `Auto::Unset` (the database fills it) or every one `Set`.
7295            /// Each row's `Auto` fields are read back from `RETURNING`; MySQL
7296            /// has none, so it leaves them unset. An empty slice is a no-op.
7297            ///
7298            /// # Errors
7299            /// Returns [`#root::sql::ExecError`] for validation,
7300            /// SQL-writing, mixed-Auto rejection, or driver failures.
7301            pub async fn bulk_insert(
7302                rows: &mut [Self],
7303                pool: &#root::sql::Pool,
7304            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7305                #bulk_pool_body
7306            }
7307
7308            /// [`Self::bulk_insert`] on any [`#root::sql::SqlExecutor`]. An
7309            /// audited model refuses it: use [`Self::bulk_insert`].
7310            ///
7311            /// # Errors
7312            /// As [`Self::bulk_insert`].
7313            pub async fn bulk_insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7314                rows: &mut [Self],
7315                _executor: _E,
7316            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7317                #refuse_bulk
7318                Self::__rustango_bulk_insert_exec(rows, _executor).await
7319            }
7320
7321            #[doc(hidden)]
7322            pub async fn __rustango_bulk_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7323                rows: &mut [Self],
7324                _executor: _E,
7325            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7326                if rows.is_empty() {
7327                    return ::core::result::Result::Ok(());
7328                }
7329                for _row in rows.iter_mut() {
7330                    #( #fill_in_place )*
7331                }
7332                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7333                let _query = #root::core::BulkInsertQuery::new(
7334                    <Self as #root::core::Model>::SCHEMA,
7335                    _columns,
7336                    _all_rows,
7337                )
7338                .returning(::std::vec![ #( #returning_cols ),* ]);
7339                let _returned =
7340                    #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7341                if _returned.is_empty() {
7342                    return ::core::result::Result::Ok(());
7343                }
7344                if _returned.len() != rows.len() {
7345                    return ::core::result::Result::Err(#root::sql::ExecError::Sql(
7346                        #root::sql::SqlError::BulkInsertReturningMismatch {
7347                            expected: rows.len(),
7348                            actual: _returned.len(),
7349                        },
7350                    ));
7351                }
7352                for (_row, _row_mut) in _returned.into_iter().zip(rows.iter_mut()) {
7353                    #root::sql::apply_auto_pk(_row, _row_mut)?;
7354                }
7355                ::core::result::Result::Ok(())
7356            }
7357
7358            /// Columns and per-row values for every bulk write. A database-filled
7359            /// `Auto` is left out when every row leaves it `Unset`, written when
7360            /// every row sets it; a mix is `BulkAutoMixed`.
7361            #[doc(hidden)]
7362            pub fn __rustango_bulk_rows(
7363                rows: &[Self],
7364            ) -> ::core::result::Result<
7365                (
7366                    ::std::vec::Vec<&'static str>,
7367                    ::std::vec::Vec<::std::vec::Vec<#root::core::SqlValue>>,
7368                ),
7369                #root::sql::ExecError,
7370            > {
7371                let _first_unset = match rows.first() {
7372                    ::core::option::Option::Some(_r) => {
7373                        matches!(_r.#first_auto_ident, #root::sql::Auto::Unset)
7374                    }
7375                    ::core::option::Option::None => true,
7376                };
7377                #( #uniformity )*
7378                let mut _all_rows: ::std::vec::Vec<
7379                    ::std::vec::Vec<#root::core::SqlValue>,
7380                > = ::std::vec::Vec::with_capacity(rows.len());
7381                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7382                    for _row in rows.iter() {
7383                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7384                            ::std::vec::Vec::new();
7385                        #( #pushes_no_auto )*
7386                        _all_rows.push(_row_vals);
7387                    }
7388                    ::std::vec![ #( #cols_no_auto ),* ]
7389                } else {
7390                    for _row in rows.iter() {
7391                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7392                            ::std::vec::Vec::new();
7393                        #( #pushes_all )*
7394                        _all_rows.push(_row_vals);
7395                    }
7396                    ::std::vec![ #( #cols_all ),* ]
7397                };
7398                ::core::result::Result::Ok((_columns, _all_rows))
7399            }
7400        }
7401    } else {
7402        let cols_all = &fields.bulk_columns_all;
7403        let pushes_all = &fields.bulk_pushes_all;
7404        quote! {
7405            /// Bulk-insert `rows`: one statement, or one per chunk in one
7406            /// transaction past the bind cap. An empty slice is a no-op.
7407            ///
7408            /// # Errors
7409            /// Returns [`#root::sql::ExecError`] for validation,
7410            /// SQL-writing, or driver failures.
7411            pub async fn bulk_insert(
7412                rows: &[Self],
7413                pool: &#root::sql::Pool,
7414            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7415                #bulk_pool_body
7416            }
7417
7418            /// [`Self::bulk_insert`] on any [`#root::sql::SqlExecutor`]. An
7419            /// audited model refuses it: use [`Self::bulk_insert`].
7420            ///
7421            /// # Errors
7422            /// As [`Self::bulk_insert`].
7423            pub async fn bulk_insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7424                rows: &[Self],
7425                _executor: _E,
7426            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7427                #refuse_bulk
7428                Self::__rustango_bulk_insert_exec(rows, _executor).await
7429            }
7430
7431            #[doc(hidden)]
7432            pub async fn __rustango_bulk_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7433                rows: &[Self],
7434                _executor: _E,
7435            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7436                if rows.is_empty() {
7437                    return ::core::result::Result::Ok(());
7438                }
7439                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7440                let _query = #root::core::BulkInsertQuery::new(
7441                    <Self as #root::core::Model>::SCHEMA,
7442                    _columns,
7443                    _all_rows,
7444                );
7445                #root::sql::__macro_internals::bulk_insert_on(_executor, &_query)
7446                    .await
7447                    .map(drop)
7448            }
7449
7450            /// Columns and per-row values for every bulk write.
7451            #[doc(hidden)]
7452            pub fn __rustango_bulk_rows(
7453                rows: &[Self],
7454            ) -> ::core::result::Result<
7455                (
7456                    ::std::vec::Vec<&'static str>,
7457                    ::std::vec::Vec<::std::vec::Vec<#root::core::SqlValue>>,
7458                ),
7459                #root::sql::ExecError,
7460            > {
7461                let mut _all_rows: ::std::vec::Vec<
7462                    ::std::vec::Vec<#root::core::SqlValue>,
7463                > = ::std::vec::Vec::with_capacity(rows.len());
7464                for _row in rows.iter() {
7465                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7466                        ::std::vec::Vec::new();
7467                    #( #pushes_all )*
7468                    _all_rows.push(_row_vals);
7469                }
7470                ::core::result::Result::Ok((::std::vec![ #( #cols_all ),* ], _all_rows))
7471            }
7472        }
7473    };
7474
7475    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7476    // (no postgres-feature gate); routes through the existing
7477    // `bulk_insert_pool` + per-dialect conflict writer. Its columns come
7478    // from `__rustango_bulk_rows`, as `bulk_insert`'s do.
7479    // Audited models audit each written row; MySQL, with no RETURNING,
7480    // is refused there (#1747, #1795).
7481    let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7482        (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7483        (true, true) => quote! {
7484            #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7485                _r.__rustango_audit_entry(_op)
7486            })
7487            .await
7488        },
7489        (true, false) => quote! {
7490            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7491                table: <Self as #root::core::Model>::SCHEMA.table,
7492            })
7493        },
7494    };
7495    let bulk_upsert_pool_method = {
7496        let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7497            fields.uuid_v7_fields.iter().cloned().unzip();
7498        quote! {
7499            /// Tri-dialect bulk insert that updates on conflict — the
7500            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7501            ///
7502            /// Per-row values are extracted and lowered into a
7503            /// [`#root::core::BulkInsertQuery`] with
7504            /// `on_conflict = DoUpdate { target, update_columns }`. The
7505            /// writer dispatches per-dialect:
7506            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7507            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7508            ///   `target` is not the only unique key, since MySQL matches every UNIQUE index
7509            ///
7510            /// `target` names the column(s) whose unique constraint
7511            /// defines the conflict (typically a `unique` or
7512            /// `unique_together` natural-key column, NOT the `Auto<T>`
7513            /// PK). `update_cols` names the columns to overwrite on
7514            /// conflict — every other column is left untouched on the
7515            /// existing row.
7516            ///
7517            /// A database-filled `Auto` PK follows [`Self::bulk_insert`]: all
7518            /// `Unset` leaves it to the database, all `Set` writes it (so
7519            /// `target = ["id"]` updates by id), a mix is `BulkAutoMixed`.
7520            /// Rows repeating a `target` key: the last one wins.
7521            /// Empty slice is a no-op. On an audited model each written
7522            /// row gets a `create` or `update` audit row; MySQL refuses it.
7523            ///
7524            /// # Errors
7525            /// Returns [`#root::sql::ExecError`] for validation,
7526            /// SQL-writing, or driver failures.
7527            pub async fn bulk_upsert_pool(
7528                rows: &[Self],
7529                target: &[&'static str],
7530                update_cols: &[&'static str],
7531                pool: &#root::sql::Pool,
7532            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7533                if rows.is_empty() {
7534                    return ::core::result::Result::Ok(());
7535                }
7536                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7537                #[allow(unused_mut)]
7538                let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7539                #(
7540                    if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7541                        _generated.push(#gen_cols);
7542                    }
7543                )*
7544                let _query = #root::core::BulkInsertQuery::new(
7545                    <Self as #root::core::Model>::SCHEMA,
7546                    _columns,
7547                    _all_rows,
7548                )
7549                .on_conflict_do_update(target, update_cols)
7550                .generated(_generated);
7551                #run_conflict_bulk_insert
7552            }
7553
7554            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7555            /// skip rows that would violate a unique constraint. Issue
7556            /// #267 / T1.5. Same per-dialect dispatch as
7557            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7558            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7559            /// <pivot> = <pivot>` (MySQL no-op write).
7560            ///
7561            /// # Errors
7562            /// As [`Self::bulk_upsert_pool`].
7563            pub async fn bulk_insert_or_ignore_pool(
7564                rows: &[Self],
7565                pool: &#root::sql::Pool,
7566            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7567                if rows.is_empty() {
7568                    return ::core::result::Result::Ok(());
7569                }
7570                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7571                let _query = #root::core::BulkInsertQuery::new(
7572                    <Self as #root::core::Model>::SCHEMA,
7573                    _columns,
7574                    _all_rows,
7575                )
7576                .on_conflict_do_nothing();
7577                #run_conflict_bulk_insert
7578            }
7579        }
7580    };
7581
7582    // Ergonomic `Model::bulk_update(objs, fields)`.
7583    // The SQL/IR/executor stack
7584    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7585    // `write_bulk_update_*` writers) already existed; what was missing
7586    // was the per-model constructor that maps `&[Self]` + a runtime
7587    // column list into rows of `[pk, col_vals…]` so callers don't
7588    // hand-build the IR. Emitted only when the model has a primary key
7589    // (the PK is the join key and can't itself be updated).
7590    let bulk_update_rows_run = if audited_fields.is_some() {
7591        quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7592            _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7593        })
7594        .await)
7595    } else {
7596        quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7597    };
7598    let bulk_update_method = match &fields.primary_key {
7599        None => quote! {},
7600        Some((pk_ident, pk_col)) => {
7601            // One pair of match arms per non-PK column: a validation arm
7602            // resolving the runtime name to its `&'static str` column,
7603            // and a value arm pushing that field off the row.
7604            let mut col_arms: Vec<TokenStream2> = Vec::new();
7605            let mut val_arms: Vec<TokenStream2> = Vec::new();
7606            for entry in &fields.column_entries {
7607                if &entry.column == pk_col {
7608                    continue;
7609                }
7610                let col = &entry.column;
7611                let ident = &entry.ident;
7612                col_arms.push(quote! { #col => #col, });
7613                val_arms.push(quote! {
7614                    #col => _row_vals.push(
7615                        ::core::convert::Into::<#root::core::SqlValue>::into(
7616                            ::core::clone::Clone::clone(&_o.#ident)
7617                        )
7618                    ),
7619                });
7620            }
7621            quote! {
7622                /// Write per-row-different values for the named `fields`
7623                /// across every object in `objs`, matched by primary key.
7624                /// Past the backend's bind cap the rows go in batches inside
7625                /// one transaction, so the update is all or none.
7626                ///
7627                /// `fields` names the **columns** to update. The primary
7628                /// key identifies each row and cannot itself be updated
7629                /// (pass it and you get
7630                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7631                /// Empty `objs` or `fields` is a no-op returning `0`.
7632                /// Objects whose PK matches no row are simply not updated.
7633                /// Returns the number of rows affected.
7634                ///
7635                /// Tri-dialect: lowers to a
7636                /// [`#root::core::BulkUpdateQuery`] and dispatches
7637                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7638                /// a CTE + correlated subquery on SQLite, an inner
7639                /// `JOIN (VALUES …)` on MySQL.
7640                ///
7641                /// # Errors
7642                /// [`#root::core::QueryError::UnknownField`] for a name
7643                /// that isn't a column on this model,
7644                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7645                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7646                /// SQL-writing / driver failures.
7647                pub async fn bulk_update(
7648                    objs: &[Self],
7649                    fields: &[&str],
7650                    pool: &#root::sql::Pool,
7651                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7652                    if objs.is_empty() || fields.is_empty() {
7653                        return ::core::result::Result::Ok(0);
7654                    }
7655                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7656                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7657                        ::std::vec::Vec::with_capacity(fields.len());
7658                    for &_f in fields {
7659                        let _col: &'static str = match _f {
7660                            #pk_col => {
7661                                return ::core::result::Result::Err(
7662                                    ::core::convert::Into::into(
7663                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7664                                            model: _model_name,
7665                                            field: ::std::string::ToString::to_string(_f),
7666                                        }
7667                                    )
7668                                );
7669                            }
7670                            #( #col_arms )*
7671                            _ => {
7672                                return ::core::result::Result::Err(
7673                                    ::core::convert::Into::into(
7674                                        #root::core::QueryError::UnknownField {
7675                                            model: _model_name,
7676                                            field: ::std::string::ToString::to_string(_f),
7677                                        }
7678                                    )
7679                                );
7680                            }
7681                        };
7682                        // A model whose only field is the primary key emits no
7683                        // non-PK arms, so every arm above diverges and this push
7684                        // is genuinely unreachable — correct, since such a model
7685                        // has nothing to bulk-update. Scoped to the statement
7686                        // rather than the fn so a real unreachability elsewhere
7687                        // in this body still warns. Without it every PK-only
7688                        // model warns in the *user's* crate, which they cannot
7689                        // act on (#1290).
7690                        #[allow(unreachable_code)]
7691                        _update_columns.push(_col);
7692                    }
7693                    let mut _rows: ::std::vec::Vec<
7694                        ::std::vec::Vec<#root::core::SqlValue>,
7695                    > = ::std::vec::Vec::with_capacity(objs.len());
7696                    for _o in objs.iter() {
7697                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7698                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7699                        // PK first — the writers expect `[pk, …update cols]`.
7700                        _row_vals.push(
7701                            ::core::convert::Into::<#root::core::SqlValue>::into(
7702                                ::core::clone::Clone::clone(&_o.#pk_ident)
7703                            )
7704                        );
7705                        for &_f in fields {
7706                            match _f {
7707                                #( #val_arms )*
7708                                // Unreachable: every name was validated
7709                                // against the same arm set above.
7710                                _ => {}
7711                            }
7712                        }
7713                        _rows.push(_row_vals);
7714                    }
7715                    let _query = #root::core::BulkUpdateQuery::new(
7716                        <Self as #root::core::Model>::SCHEMA,
7717                        _update_columns,
7718                        _rows,
7719                    );
7720                    #bulk_update_rows_run
7721                }
7722            }
7723        }
7724    };
7725
7726    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7727        quote! {
7728            /// Hidden runtime accessor for the primary-key value as a
7729            /// [`SqlValue`]. Used by reverse-relation helpers
7730            /// (`<parent>::<child>_set`) emitted from sibling models'
7731            /// FK fields. Not part of the public API.
7732            #[doc(hidden)]
7733            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7734                ::core::convert::Into::<#root::core::SqlValue>::into(
7735                    ::core::clone::Clone::clone(&self.#pk_ident)
7736                )
7737            }
7738        }
7739    });
7740
7741    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7742        let column_arms = fields.column_entries.iter().map(|entry| {
7743            let col = &entry.column;
7744            let ident = &entry.ident;
7745            quote! {
7746                #col => ::core::option::Option::Some(
7747                    ::core::convert::Into::<#root::core::SqlValue>::into(
7748                        ::core::clone::Clone::clone(&self.#ident)
7749                    )
7750                ),
7751            }
7752        });
7753        quote! {
7754            impl #root::sql::HasPkValue for #struct_name {
7755                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7756                    ::core::convert::Into::<#root::core::SqlValue>::into(
7757                        ::core::clone::Clone::clone(&self.#pk_ident)
7758                    )
7759                }
7760                fn __rustango_column_value(
7761                    &self,
7762                    column: &str,
7763                ) -> ::core::option::Option<#root::core::SqlValue> {
7764                    match column {
7765                        #( #column_arms )*
7766                        _ => ::core::option::Option::None,
7767                    }
7768                }
7769            }
7770        }
7771    });
7772
7773    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7774
7775    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7776    // dispatch to the right per-backend body without the macro emitting
7777    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7778    // so audited models with non-Auto PKs (which still go through
7779    // `insert_one_with_audit` → `apply_auto_pk`) link.
7780    let assign_auto_pk_pool_impl = {
7781        let auto_assigns = &fields.auto_assigns;
7782        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7783        // so the body is structurally identical to `auto_assigns` —
7784        // only the helper name swaps from `try_get_returning` to
7785        // `try_get_returning_sqlite` so the closure typechecks against
7786        // a `SqliteRow` instead of a `PgRow`.
7787        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7788            .auto_field_idents
7789            .iter()
7790            .map(|(ident, column, uuid)| {
7791                // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
7792                if *uuid {
7793                    return quote! {
7794                        self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
7795                            _returning_row, #column
7796                        )?);
7797                    };
7798                }
7799                quote! {
7800                    self.#ident = #root::sql::try_get_returning_sqlite(
7801                        _returning_row, #column
7802                    )?;
7803                }
7804            })
7805            .collect();
7806        // #1028 — decode each `generated_as` column from the same
7807        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7808        // own type drives the decode (no `Auto<T>` wrapper).
7809        let generated_assigns: Vec<TokenStream2> = fields
7810            .generated_field_idents
7811            .iter()
7812            .map(|(ident, column)| {
7813                quote! {
7814                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7815                }
7816            })
7817            .collect();
7818        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7819            .generated_field_idents
7820            .iter()
7821            .map(|(ident, column)| {
7822                quote! {
7823                    self.#ident = #root::sql::try_get_returning_sqlite(
7824                        _returning_row, #column
7825                    )?;
7826                }
7827            })
7828            .collect();
7829        let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
7830            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7831            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7832            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7833            // those models can't use AUTO_INCREMENT). The trait is only
7834            // touched on the MySQL arm at runtime, so PG-only consumers
7835            // never see the bound failure.
7836            //
7837            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7838            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7839            // runtime with "multi-column RETURNING". MySQL has no
7840            // multi-column RETURNING semantic and a follow-up SELECT
7841            // would need cross-trait plumbing. Pragmatic shape: succeed
7842            // with the FIRST Auto field populated from LAST_INSERT_ID();
7843            // any other Auto fields stay `Auto::Unset`. Callers that
7844            // need the DB-defaulted timestamp / UUID can re-fetch the
7845            // row by PK after `save_pool`. Fixes the cookbook chapter
7846            // 12 dialect divergence.
7847            quote! {
7848                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7849                    ::rustango_from_mysql_auto_id(_id)?;
7850                self.#first = #root::sql::Auto::Set(_converted);
7851                ::core::result::Result::Ok(())
7852            }
7853        } else {
7854            quote! {
7855                let _ = _id;
7856                ::core::result::Result::Ok(())
7857            }
7858        };
7859        quote! {
7860            impl #root::sql::AssignAutoPkPool for #struct_name {
7861                fn __rustango_assign_from_pg_row(
7862                    &mut self,
7863                    _returning_row: &#root::sql::PgReturningRow,
7864                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7865                    #( #auto_assigns )*
7866                    #( #generated_assigns )*
7867                    ::core::result::Result::Ok(())
7868                }
7869                fn __rustango_assign_from_mysql_id(
7870                    &mut self,
7871                    _id: i64,
7872                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7873                    #mysql_body
7874                }
7875                fn __rustango_assign_from_sqlite_row(
7876                    &mut self,
7877                    _returning_row: &#root::sql::SqliteReturningRow,
7878                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7879                    #( #auto_assigns_sqlite )*
7880                    #( #generated_assigns_sqlite )*
7881                    ::core::result::Result::Ok(())
7882                }
7883            }
7884        }
7885    };
7886
7887    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7888    let aliased_row_helper = quote! {
7889        /// Decode a row's aliased target columns (produced by
7890        /// `select_related`'s LEFT JOIN) into a fresh instance of
7891        /// this model. Reads each column via
7892        /// `format!("{prefix}__{col}")`, matching the alias the
7893        /// SELECT writer emitted. Slice 9.0d.
7894        #[doc(hidden)]
7895        #[cfg(feature = "postgres")]
7896        pub fn __rustango_from_aliased_row(
7897            row: &#root::sql::sqlx::postgres::PgRow,
7898            prefix: &str,
7899        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7900            ::core::result::Result::Ok(Self {
7901                #( #from_aliased_row_inits ),*
7902            })
7903        }
7904    };
7905    // v0.23.0-batch8 — MySQL counterpart, gated through the
7906    // cfg-aware macro_rules so PG-only builds expand to nothing.
7907    let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
7908    let aliased_row_helper_my = quote! {
7909        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7910            #( #from_aliased_row_inits_my ),*
7911        });
7912    };
7913
7914    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7915    // pattern + cfg gate on the `sqlite` feature.
7916    let aliased_row_helper_sqlite = quote! {
7917        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7918            #( #from_aliased_row_inits ),*
7919        });
7920    };
7921
7922    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7923    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7924    let load_related_impl_sqlite =
7925        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7926
7927    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7928    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7929    // default `Self::objects()`. Each accessor returns a fresh
7930    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7931    // methods the user defined.
7932    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7933        .iter()
7934        .map(|fn_ident| {
7935            let model_name_str = struct_name.to_string();
7936            let fn_name_str = fn_ident.to_string();
7937            let doc = format!(
7938                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7939                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7940                 equivalent to `Self::objects()`. Chains with any \
7941                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7942                 extension methods."
7943            );
7944            quote! {
7945                #[doc = #doc]
7946                #[must_use]
7947                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7948                    #root::query::QuerySet::new()
7949                }
7950            }
7951        })
7952        .collect();
7953
7954    quote! {
7955        impl #struct_name {
7956            /// Start a new `QuerySet` over this model.
7957            #[must_use]
7958            pub fn objects() -> #root::query::QuerySet<#struct_name> {
7959                #root::query::QuerySet::new()
7960            }
7961
7962            /// Eloquent-shape alias of [`Self::objects`]. Returns
7963            /// a fresh `QuerySet<Self>` ready for `.filter()` /
7964            /// `.where_()` / etc. Matches Laravel muscle-memory:
7965            ///
7966            /// ```ignore
7967            /// // Eloquent:    Post::query()->where('published', true)
7968            /// // rustango:    Post::query().filter("published", true)
7969            /// //         or:  Post::objects().filter("published", true)
7970            /// ```
7971            ///
7972            /// Both names point at the same underlying constructor;
7973            /// neither is preferred.
7974            #[must_use]
7975            pub fn query() -> #root::query::QuerySet<#struct_name> {
7976                #root::query::QuerySet::new()
7977            }
7978
7979            #( #extra_manager_fns )*
7980
7981            #insert_method
7982
7983            #bulk_insert_method
7984
7985            #bulk_upsert_pool_method
7986
7987            #bulk_update_method
7988
7989            #save_method
7990
7991            #pk_methods
7992
7993            #pk_value_helper
7994
7995            #aliased_row_helper
7996
7997            #column_consts
7998        }
7999
8000        #aliased_row_helper_my
8001
8002        #aliased_row_helper_sqlite
8003
8004        #load_related_impl
8005
8006        #load_related_impl_my
8007
8008        #load_related_impl_sqlite
8009
8010        #has_pk_value_impl
8011
8012        #fk_pk_access_impl
8013
8014        #assign_auto_pk_pool_impl
8015    }
8016}
8017
8018/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8019fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8020    let lines = entries.iter().map(|e| {
8021        let ident = &e.ident;
8022        let col_ty = column_type_ident(ident);
8023        quote! {
8024            #[allow(non_upper_case_globals)]
8025            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8026        }
8027    });
8028    quote! { #(#lines)* }
8029}
8030
8031/// Emit a hidden per-model module carrying one zero-sized type per field,
8032/// each with a `Column` impl pointing back at the model.
8033fn column_module_tokens(
8034    module_ident: &syn::Ident,
8035    struct_name: &syn::Ident,
8036    entries: &[ColumnEntry],
8037) -> TokenStream2 {
8038    let root = rustango_root();
8039    let items = entries.iter().map(|e| {
8040        let col_ty = column_type_ident(&e.ident);
8041        let value_ty = &e.value_ty;
8042        let name = &e.name;
8043        let column = &e.column;
8044        let field_type_tokens = &e.field_type_tokens;
8045        quote! {
8046            #[derive(::core::clone::Clone, ::core::marker::Copy)]
8047            pub struct #col_ty;
8048
8049            impl #root::core::Column for #col_ty {
8050                type Model = super::#struct_name;
8051                type Value = #value_ty;
8052                const NAME: &'static str = #name;
8053                const COLUMN: &'static str = #column;
8054                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8055            }
8056        }
8057    });
8058    quote! {
8059        #[doc(hidden)]
8060        #[allow(non_camel_case_types, non_snake_case)]
8061        pub mod #module_ident {
8062            // Re-import the parent scope so field types referencing
8063            // sibling models (e.g. `ForeignKey<Author>`) resolve
8064            // inside this submodule. Without this we'd hit
8065            // `proc_macro_derive_resolution_fallback` warnings.
8066            #[allow(unused_imports)]
8067            use super::*;
8068            #(#items)*
8069        }
8070    }
8071}
8072
8073fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8074    let name = ident_name(field_ident);
8075    syn::Ident::new(&format!("{name}_col"), field_ident.span())
8076}
8077
8078/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8079/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8080fn restore_auto_now_unless(
8081    idents: &[syn::Ident],
8082    ret: &TokenStream2,
8083    keep: &TokenStream2,
8084    body: &TokenStream2,
8085) -> TokenStream2 {
8086    if idents.is_empty() {
8087        return body.clone();
8088    }
8089    let prev: Vec<syn::Ident> = (0..idents.len())
8090        .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8091        .collect();
8092    quote! {
8093        #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8094        let _saved: #ret = async { #body }.await;
8095        if !(#keep) {
8096            #( self.#idents = #prev; )*
8097        }
8098        _saved
8099    }
8100}
8101
8102/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8103fn ident_name(ident: &syn::Ident) -> String {
8104    syn::ext::IdentExt::unraw(ident).to_string()
8105}
8106
8107/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8108fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8109    match syn::parse_str::<syn::Ident>(name) {
8110        Ok(mut ident) => {
8111            ident.set_span(span);
8112            ident
8113        }
8114        Err(_) => syn::Ident::new_raw(name, span),
8115    }
8116}
8117
8118fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8119    syn::Ident::new(
8120        &format!("__rustango_cols_{struct_name}"),
8121        struct_name.span(),
8122    )
8123}
8124
8125fn from_row_impl_tokens(
8126    struct_name: &syn::Ident,
8127    from_row_inits: &[TokenStream2],
8128    from_row_inits_my: &[TokenStream2],
8129) -> TokenStream2 {
8130    let root = rustango_root();
8131    // The Postgres impl is always emitted — every rustango build pulls in
8132    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8133    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8134    // macro_rules whose body collapses to nothing when rustango is built
8135    // without the `mysql` feature. No user-facing feature shim required.
8136    //
8137    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8138    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8139    // back into a comma-separated list inside square brackets. Since each
8140    // entry is already in `field: expr` shape, the existing tokens slot in.
8141    quote! {
8142        #[cfg(feature = "postgres")]
8143        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8144            for #struct_name
8145        {
8146            fn from_row(
8147                row: &'r #root::sql::sqlx::postgres::PgRow,
8148            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8149                ::core::result::Result::Ok(Self {
8150                    #( #from_row_inits ),*
8151                })
8152            }
8153        }
8154
8155        #root::__impl_my_from_row!(#struct_name, |row| {
8156            #( #from_row_inits_my ),*
8157        });
8158
8159        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8160            #( #from_row_inits ),*
8161        });
8162    }
8163}
8164
8165struct ContainerAttrs {
8166    table: Option<String>,
8167    display: Option<(String, proc_macro2::Span)>,
8168    /// Explicit app label from `#[rustango(app = "blog")]`.
8169    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8170    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8171    /// at runtime — this attribute is the override for cases where
8172    /// the inference is wrong (e.g. a model that conceptually belongs
8173    /// to one app but is physically in another module).
8174    app: Option<String>,
8175    /// Per-model admin knobs from
8176    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8177    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8178    /// admin code falls back to `AdminConfig::DEFAULT`.
8179    admin: Option<AdminAttrs>,
8180    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8181    /// `None` when the model isn't audited — write paths emit no
8182    /// audit entries. When present, single-row writes capture
8183    /// before/after for the listed fields and bulk writes batch
8184    /// snapshots into one INSERT into `rustango_audit_log`.
8185    audit: Option<AuditAttrs>,
8186    /// `true` when `#[rustango(permissions)]` is present. Signals that
8187    /// `auto_create_permissions` should seed the four CRUD codenames for
8188    /// this model.
8189    permissions: bool,
8190    /// Many-to-many relations declared via
8191    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8192    ///                 src = "post_id", dst = "tag_id"))]`.
8193    m2m: Vec<M2MAttr>,
8194    /// Polymorphic M2M relations declared via
8195    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8196    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8197    ///   related_column = "tag_id"))]` (issue #818).
8198    generic_m2m: Vec<GenericM2MAttr>,
8199    /// Composite indexes declared via
8200    /// `#[rustango(index("col1, col2"))]` or
8201    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8202    /// Single-column indexes from `#[rustango(index)]` on fields are
8203    /// accumulated here during field collection.
8204    indexes: Vec<IndexAttr>,
8205    /// Table-level CHECK constraints declared via
8206    /// `#[rustango(check(name = "…", expr = "…"))]`.
8207    checks: Vec<CheckAttr>,
8208    /// Table-level PG `EXCLUDE` constraints declared via
8209    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8210    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8211    /// migration writer renders them on Postgres and skips with a
8212    /// warning on MySQL/SQLite. Issue #319.
8213    excludes: Vec<ExcludeAttr>,
8214    /// Composite (multi-column) FKs declared via
8215    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8216    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8217    composite_fks: Vec<CompositeFkAttr>,
8218    /// Generic ("any model") FKs declared via
8219    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8220    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8221    generic_fks: Vec<GenericFkAttr>,
8222    /// Where this model lives in a tenancy deployment, declared via
8223    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8224    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8225    /// to partition diff output between registry-scoped and
8226    /// tenant-scoped migration files.
8227    scope: Option<String>,
8228    /// Extra QuerySet accessor names from
8229    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8230    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8231    /// the default `Self::objects()`. Multiple attributes allowed.
8232    manager_fns: Vec<syn::Ident>,
8233    /// Default ordering declared via `#[rustango(default_order =
8234    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8235    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8236    /// `-` prefix means descending; the `+` prefix or no prefix means
8237    /// ascending.
8238    default_order: Vec<(String, bool, proc_macro2::Span)>,
8239    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8240    /// Routes the emitted schema's `is_view = true` so the migration
8241    /// snapshot skips this model (its underlying SQL view is operator-
8242    /// managed, not rustango-managed).
8243    is_view: bool,
8244    /// `#[rustango(managed = false)]` — who owns the table.
8245    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8246    /// the migration snapshot skips this model so `makemigrations` /
8247    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8248    /// TABLE` against it (operator-managed schema).
8249    managed: bool,
8250    /// Name of the model's base manager, from
8251    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8252    /// `ModelSchema::base_manager_name`. Declarative-only today.
8253    base_manager_name: Option<String>,
8254    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8255    /// field this model's instances are ordered relative to.
8256    /// Declarative-only today; threaded onto
8257    /// `ModelSchema::order_with_respect_to`.
8258    order_with_respect_to: Option<String>,
8259    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8260    /// Marks the model as a proxy that
8261    /// shares its DB table with another struct. Threaded into
8262    /// `ModelSchema::proxy` so future codegen can skip table-owning
8263    /// behavior for proxies.
8264    proxy: bool,
8265    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8266    /// Each comma-separated capability token surfaces on
8267    /// `ModelSchema::required_db_features` so `manage check --deploy`
8268    /// can warn when the active dialect lacks one.
8269    required_db_features: Vec<String>,
8270    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8271    /// Normalized to the dialect name `manage check --deploy`
8272    /// compares against `Settings.database.backend`. Aliases
8273    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8274    /// stored under the canonical name.
8275    required_db_vendor: Option<String>,
8276    /// `#[rustango(default_related_name = "...")]`. Threaded into
8277    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8278    /// name to use when an FK / M2M field doesn't override it.
8279    /// Today rustango doesn't auto-emit reverse managers; the
8280    /// metadata is the foundation for that work.
8281    default_related_name: Option<String>,
8282    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8283    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8284    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8285    /// inline `COMMENT='...'`, SQLite: no-op).
8286    db_table_comment: Option<String>,
8287    /// `#[rustango(get_latest_by = "created_at")]` /
8288    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8289    /// `(column, descending)` where `descending = true` when the
8290    /// attribute value starts with `-`. Threaded into
8291    /// `ModelSchema::get_latest_by`.
8292    get_latest_by: Option<(String, bool, proc_macro2::Span)>,
8293    /// Extra `(codename, label)` permissions
8294    /// from `#[rustango(extra_permissions = "approve:Can approve,
8295    /// archive:Can archive")]`. Comma-separated `codename:label`
8296    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8297    extra_permissions: Vec<(String, String)>,
8298    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8299    /// `"view"`) the framework
8300    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8301    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8302    /// Validated at parse time; unknown actions fail with a span-pointing
8303    /// error.
8304    default_permissions: Vec<String>,
8305    /// `#[rustango(verbose_name = "blog post")]` — the
8306    /// human-readable singular label for the model. Threaded into
8307    /// `ModelSchema::verbose_name` so admin section headers /
8308    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8309    /// over the Rust struct identifier.
8310    verbose_name: Option<String>,
8311    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8312    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8313    /// When unset, `display_label_plural()` auto-suffixes `s`.
8314    verbose_name_plural: Option<String>,
8315    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8316    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8317    /// a name (used by `QuerySet::without_global_scope`) with a
8318    /// `fn() -> WhereExpr` path that the macro emits into
8319    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8320    global_scopes: Vec<GlobalScopeAttr>,
8321    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8322    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8323    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8324    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8325    /// entry emits an inherent `<name>_through(&self)` accessor that
8326    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8327    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8328    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8329    through_relations: Vec<ThroughAttr>,
8330    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8331    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8332    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8333    /// Each entry emits two associated functions on the parent —
8334    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8335    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8336    /// over a correlated subquery against the child table. Users
8337    /// drop the result into `QuerySet::where_raw(...)`.
8338    reverse_has_relations: Vec<ReverseHasAttr>,
8339    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8340    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8341    /// entry so the relation-existence family resolves polymorphic
8342    /// children by name.
8343    generic_has_relations: Vec<GenericHasAttr>,
8344}
8345
8346/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8347/// declaration. Each entry becomes one `core::GlobalScope` in the
8348/// emitted schema literal; `apply` resolves at macro-expand time
8349/// against the consumer's scope so the function must be in scope at
8350/// the use site. Issue #820.
8351struct GlobalScopeAttr {
8352    name: String,
8353    apply: syn::Path,
8354}
8355
8356/// Parsed `#[rustango(through(...))]` declaration. Issue
8357/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8358/// `hasManyThrough` / `hasOneThrough` parity.
8359///
8360/// `Country hasManyThrough Post via User` declares as:
8361///
8362/// ```ignore
8363/// #[rustango(through(
8364///     name                   = "posts",
8365///     far                    = "Post",
8366///     far_fk_column          = "author_id",
8367///     intermediate           = "User",
8368///     intermediate_fk_column = "country_id",
8369/// ))]
8370/// struct Country { ... }
8371/// ```
8372///
8373/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8374/// which returns a queryset filtered via a correlated subquery —
8375/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8376/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8377/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8378/// because the subquery lives inside a `WhereExpr::InSubquery` node
8379/// and the outer queryset's pending list stays empty.
8380///
8381/// All four required arguments use **SQL column / table names**
8382/// (not Rust field names) to sidestep the multi-hop-filter substrate
8383/// gap. Once that substrate lands, a higher-level Rust-field-name
8384/// shorthand can be added without breaking this surface.
8385struct ThroughAttr {
8386    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8387    name: String,
8388    /// Far model type identifier. `far = "Post"` → returns
8389    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8390    /// the derive expands.
8391    far: syn::Ident,
8392    /// SQL column on the far model's table that references the
8393    /// intermediate model's primary key. For `Post`'s
8394    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8395    /// default `<field>_id` convention) or whatever the user
8396    /// declared via `#[rustango(db_column = "...")]`.
8397    far_fk_column: String,
8398    /// Intermediate model type identifier. Needed to look up its
8399    /// `SCHEMA` so the subquery's `FROM` clause points at the
8400    /// intermediate table. `intermediate = "User"`.
8401    intermediate: syn::Ident,
8402    /// SQL column on the intermediate's table that references the
8403    /// source (this) model's primary key. For `User`'s
8404    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8405    intermediate_fk_column: String,
8406    /// SQL primary-key column on the intermediate's table — the
8407    /// column the subquery projects. Optional; defaults to `"id"`
8408    /// (rustango's default PK column name). Override when the
8409    /// intermediate declares a custom PK column.
8410    intermediate_pk_column: String,
8411}
8412
8413/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8414/// child_fk_column = "..."))]` declaration. Issue
8415/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8416/// `whereHas` / `whereDoesntHave` parity.
8417///
8418/// `Post hasMany Comment` declares as:
8419///
8420/// ```ignore
8421/// #[rustango(reverse_has(
8422///     name             = "comments",
8423///     child            = "Comment",
8424///     child_fk_column  = "post_id",
8425/// ))]
8426/// struct Post { ... }
8427/// ```
8428///
8429/// The macro emits two associated functions on `Post`:
8430///
8431/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8432///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8433///   <outer>.<self_pk_column>)`.
8434/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8435///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8436///
8437/// User code:
8438///
8439/// ```ignore
8440/// // Posts with at least one comment:
8441/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8442/// // Posts with no comments:
8443/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8444/// ```
8445///
8446/// As with #817, all column / table identifiers are **SQL names**
8447/// (not Rust field names) so the substrate is independent of the
8448/// outstanding multi-hop filter resolver gap. The emitted
8449/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8450/// SQL-emit time via the writer's scope stack.
8451struct ReverseHasAttr {
8452    /// Accessor name. `name = "comments"` → emits
8453    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8454    name: String,
8455    /// Child model type identifier. `child = "Comment"` — needed to
8456    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8457    /// points at the child table.
8458    child: syn::Ident,
8459    /// SQL column on the child's table that references this model's
8460    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8461    /// column is `"post_id"`.
8462    child_fk_column: String,
8463    /// Column on **this** model's table the child FK references — the
8464    /// `OuterRef` target. Defaults to the PK column (`"id"` without one).
8465    self_pk_column: Option<String>,
8466    /// Where an explicit `self_pk_column` was written, for its error.
8467    self_pk_span: Option<proc_macro2::Span>,
8468}
8469
8470impl ReverseHasAttr {
8471    fn self_pk_column(&self) -> &str {
8472        self.self_pk_column.as_deref().unwrap_or("id")
8473    }
8474}
8475
8476/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8477/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8478/// relation-existence family (issue #830). The child is a polymorphic,
8479/// content-type-discriminated model; emits a `Model::
8480/// generic_reverse_relations()` entry the queryset resolves by name.
8481struct GenericHasAttr {
8482    /// Accessor name, e.g. `name = "tags"`.
8483    name: String,
8484    /// Child model type identifier (`child = "Tag"`) — looked up for its
8485    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8486    child: syn::Ident,
8487    /// Column on the child table holding the parent's content-type id.
8488    /// Optional; defaults to `"content_type_id"`.
8489    ct_column: String,
8490    /// Column on the child table holding the parent's PK value.
8491    /// Optional; defaults to `"object_pk"`.
8492    pk_column: String,
8493    /// SQL primary-key column on **this** (parent) model's table.
8494    /// Defaults to the PK column (`"id"` without one).
8495    self_pk_column: Option<String>,
8496    /// Where an explicit `self_pk_column` was written, for its error.
8497    self_pk_span: Option<proc_macro2::Span>,
8498}
8499
8500impl GenericHasAttr {
8501    fn self_pk_column(&self) -> &str {
8502        self.self_pk_column.as_deref().unwrap_or("id")
8503    }
8504}
8505
8506/// Parsed form of one index declaration (field-level or container-level).
8507struct IndexAttr {
8508    /// The declaring attribute, for diagnostics.
8509    span: proc_macro2::Span,
8510    /// Index name; auto-derived when `None` at parse time.
8511    name: Option<String>,
8512    /// Column names in the index.
8513    columns: Vec<String>,
8514    /// `true` for `CREATE UNIQUE INDEX`.
8515    unique: bool,
8516    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8517    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8518    /// `"btree"` when the attribute is absent — the DDL writer omits
8519    /// the `USING` clause and the backend uses its own default
8520    /// (btree on every supported dialect).
8521    method: String,
8522    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8523    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8524    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8525    where_clause: Option<String>,
8526    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8527    /// Empty `Vec` (the default) means "no covering columns".
8528    include: Vec<String>,
8529}
8530
8531/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8532struct CheckAttr {
8533    name: String,
8534    expr: String,
8535}
8536
8537/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8538/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8539/// PG-only — surfaced on every backend in the macro emit; the migration
8540/// writer skips the op on MySQL/SQLite. Issue #319.
8541struct ExcludeAttr {
8542    /// Constraint name (free-form Rust identifier).
8543    name: String,
8544    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8545    using: String,
8546    /// Comma-separated `(column, operator)` pairs, in declaration
8547    /// order. Parsed from `"col WITH op, col WITH op"`.
8548    elements: Vec<(String, String)>,
8549    /// Optional `WHERE` predicate (raw SQL).
8550    where_clause: Option<String>,
8551}
8552
8553/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8554/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8555/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8556/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8557/// the model, not the field.
8558struct CompositeFkAttr {
8559    /// Logical relation name (free-form Rust identifier).
8560    name: String,
8561    /// SQL table name of the target.
8562    to: String,
8563    /// Source-side column names, in declaration order.
8564    from: Vec<String>,
8565    /// Target-side column names, same length / order as `from`.
8566    on: Vec<String>,
8567}
8568
8569/// Parsed form of one `#[rustango(generic_fk(name = "target",
8570/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8571/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8572/// generic ("any model") FKs live on the model, not the field.
8573struct GenericFkAttr {
8574    /// Logical relation name (free-form Rust identifier).
8575    name: String,
8576    /// Source-side column carrying the `content_type_id` value.
8577    ct_column: String,
8578    /// Source-side column carrying the target row's primary key.
8579    pk_column: String,
8580}
8581
8582/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8583struct M2MAttr {
8584    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8585    name: String,
8586    /// Target table (e.g. `"app_tags"`).
8587    to: String,
8588    /// Junction table (e.g. `"post_tags"`).
8589    through: String,
8590    /// Source FK column in the junction table (e.g. `"post_id"`).
8591    src: String,
8592    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8593    dst: String,
8594    /// Whether the migration writer should auto-create the junction
8595    /// table. Default `true`. Set `auto_create = false` (#324) when
8596    /// the operator declares the through table as its own
8597    /// `#[derive(Model)]` struct with extra columns.
8598    auto_create: bool,
8599}
8600
8601/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8602/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8603/// junction carries a ContentType discriminator so unrelated models
8604/// share one pivot + related set.
8605struct GenericM2MAttr {
8606    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8607    name: String,
8608    /// Polymorphic junction table (e.g. `"taggables"`).
8609    through: String,
8610    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8611    pk_column: String,
8612    /// Junction column holding the owning model's `content_type_id`
8613    /// discriminator (e.g. `"taggable_type"`).
8614    ct_column: String,
8615    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8616    related_column: String,
8617}
8618
8619/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8620/// "user"))]`. `track` is a comma-separated list of field names whose
8621/// before/after values land in the JSONB `changes` column. `source`
8622/// is informational only — it pins a default source when the model
8623/// is written outside any `audit::with_source(...)` scope (rare).
8624#[derive(Default)]
8625struct AuditAttrs {
8626    /// Field names to capture in the `changes` JSONB. Validated
8627    /// against declared scalar fields at compile time. Empty means
8628    /// "track every scalar field".
8629    track: Option<(Vec<String>, proc_macro2::Span)>,
8630}
8631
8632/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8633/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8634/// comma-separated strings; we validate each ident against the model's
8635/// declared fields at compile time.
8636#[derive(Default)]
8637struct AdminAttrs {
8638    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8639    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8640    list_per_page: Option<usize>,
8641    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8642    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8643    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8644    /// Bulk action names. No field-validation against model fields —
8645    /// these are action handlers, not column references.
8646    actions: Option<(Vec<String>, proc_macro2::Span)>,
8647    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8648    /// sections, comma-separated fields per section, optional
8649    /// `Title:` prefix. Empty title omits the `<legend>`.
8650    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8651    /// `admin(list_display_links = "title")`. Names
8652    /// from `list_display` whose cells should link to detail/edit.
8653    /// Issue #350.
8654    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8655    /// `admin(search_help_text = "...")`. Short
8656    /// caption rendered beside the admin list view's search box.
8657    /// Issue #353.
8658    search_help_text: Option<String>,
8659    /// `admin(actions_on_top = false)`. Hides the
8660    /// action-bar above the table. Default `true`. Issue #354.
8661    actions_on_top: Option<bool>,
8662    /// `admin(actions_on_bottom = true)`. Renders an
8663    /// additional action-bar below the table. Default `false`.
8664    /// Issue #354.
8665    actions_on_bottom: Option<bool>,
8666    /// `admin(date_hierarchy = "created_at")`. Name of
8667    /// a date / datetime field whose values render as a clickable
8668    /// year / month / day drill-down strip above the list table.
8669    /// Empty / unset disables the strip. Issue #355.
8670    date_hierarchy: Option<String>,
8671    /// `admin(prepopulated_fields = "slug:title")`.
8672    /// Each entry is `target:source[+source2]`; multiple entries are
8673    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8674    /// The admin change-form emits JS that slugifies the source values
8675    /// into the target field on every keystroke. Issue #356.
8676    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8677    /// `admin(raw_id_fields = "parent, owner")`. Names
8678    /// of FK fields whose change-form widget renders a lookup link
8679    /// next to the input. Issue #357.
8680    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8681    /// `admin(autocomplete_fields = "author_id")`.
8682    /// Names of FK fields whose change-form widget renders an
8683    /// Ajax-driven typeahead populated from a `__autocomplete`
8684    /// endpoint on the target model. Issue #358.
8685    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8686    /// `admin(list_select_related = "all" | "none" | "author, …")`
8687    /// — tunes the admin list view's FK auto-JOIN
8688    /// policy. Default `"all"` matches rustango's join-everything
8689    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8690    list_select_related: Option<String>,
8691    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8692    /// Each entry is `field_name:widget_name`; multiple
8693    /// entries comma-separated. Empty / unset → no overrides. The
8694    /// list of widget names supported is documented on
8695    /// `AdminConfig::formfield_overrides`. Issue #359.
8696    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8697}
8698
8699/// The names `#[derive(Model)]` reads its attributes under: `rgo` is a
8700/// short alias of `rustango` with the same meaning.
8701#[derive(Clone, Copy, PartialEq, Eq)]
8702enum ModelAttrName {
8703    Rustango,
8704    Rgo,
8705}
8706
8707impl ModelAttrName {
8708    fn of(attr: &syn::Attribute) -> Option<Self> {
8709        let path = attr.path();
8710        if path.is_ident("rustango") {
8711            Some(Self::Rustango)
8712        } else if path.is_ident("rgo") {
8713            Some(Self::Rgo)
8714        } else {
8715            None
8716        }
8717    }
8718
8719    fn as_str(self) -> &'static str {
8720        match self {
8721            Self::Rustango => "rustango",
8722            Self::Rgo => "rgo",
8723        }
8724    }
8725}
8726
8727/// The model attributes in `attrs`, under either name. One item or field
8728/// uses one name, so a mix reads as a mistake rather than a merge.
8729fn model_attrs(attrs: &[syn::Attribute]) -> syn::Result<Vec<&syn::Attribute>> {
8730    let mut first: Option<ModelAttrName> = None;
8731    let mut out = Vec::new();
8732    for attr in attrs {
8733        let Some(name) = ModelAttrName::of(attr) else {
8734            continue;
8735        };
8736        match first {
8737            Some(seen) if seen != name => {
8738                return Err(syn::Error::new_spanned(
8739                    attr.path(),
8740                    format!(
8741                        "`#[{}(...)]` and `#[{}(...)]` are the same attribute; \
8742                         use one name on an item or field",
8743                        seen.as_str(),
8744                        name.as_str(),
8745                    ),
8746                ));
8747            }
8748            _ => first = Some(name),
8749        }
8750        out.push(attr);
8751    }
8752    Ok(out)
8753}
8754
8755fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8756    let mut out = ContainerAttrs {
8757        table: None,
8758        display: None,
8759        app: None,
8760        admin: None,
8761        audit: None,
8762        // Default `permissions = true` so every `#[derive(Model)]`
8763        // gets the four CRUD codenames seeded by `auto_create_permissions`
8764        // and is visible to non-superusers in the tenant admin without
8765        // manual per-model annotation. Models that intentionally don't
8766        // want permission rows (registry-internal types, framework
8767        // tables operators shouldn't manage directly) opt out via
8768        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8769        // out-of-the-box admin invisibility regression (#62).
8770        permissions: true,
8771        m2m: Vec::new(),
8772        generic_m2m: Vec::new(),
8773        indexes: Vec::new(),
8774        checks: Vec::new(),
8775        excludes: Vec::new(),
8776        composite_fks: Vec::new(),
8777        generic_fks: Vec::new(),
8778        scope: None,
8779        manager_fns: Vec::new(),
8780        default_order: Vec::new(),
8781        is_view: false,
8782        managed: true,
8783        verbose_name: None,
8784        verbose_name_plural: None,
8785        base_manager_name: None,
8786        order_with_respect_to: None,
8787        proxy: false,
8788        required_db_features: Vec::new(),
8789        required_db_vendor: None,
8790        default_related_name: None,
8791        db_table_comment: None,
8792        get_latest_by: None,
8793        extra_permissions: Vec::new(),
8794        default_permissions: Vec::new(),
8795        global_scopes: Vec::new(),
8796        through_relations: Vec::new(),
8797        reverse_has_relations: Vec::new(),
8798        generic_has_relations: Vec::new(),
8799    };
8800    for attr in model_attrs(&input.attrs)? {
8801        // The name the user wrote, for hints that quote the attribute.
8802        let written = ModelAttrName::of(attr).map_or("rustango", ModelAttrName::as_str);
8803        attr.parse_nested_meta(|meta| {
8804            if meta.path.is_ident("table") {
8805                let s: LitStr = meta.value()?.parse()?;
8806                let name = s.value();
8807                // v0.27.3 (#65) — macro-time guard against table names
8808                // that compile but break SQL downstream. Hyphens are
8809                // the common footgun: PostgreSQL accepts a quoted
8810                // `"intermediate-region"` in CREATE TABLE, but the
8811                // FK / index name derivation in `migrate::ddl`
8812                // emits `intermediate-region_field_fkey` unquoted,
8813                // which then fails the SQL parser. Same shape rule
8814                // as Postgres regular identifiers so the safe path
8815                // is the only path.
8816                validate_table_name(&name, s.span())?;
8817                out.table = Some(name);
8818                return Ok(());
8819            }
8820            if meta.path.is_ident("display") {
8821                let s: LitStr = meta.value()?.parse()?;
8822                out.display = Some((s.value(), s.span()));
8823                return Ok(());
8824            }
8825            if meta.path.is_ident("app") {
8826                let s: LitStr = meta.value()?.parse()?;
8827                out.app = Some(s.value());
8828                return Ok(());
8829            }
8830            if meta.path.is_ident("scope") {
8831                let s: LitStr = meta.value()?.parse()?;
8832                let val = s.value();
8833                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8834                    return Err(meta.error(format!(
8835                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8836                    )));
8837                }
8838                out.scope = Some(val);
8839                return Ok(());
8840            }
8841            if meta.path.is_ident("admin") {
8842                let mut admin = AdminAttrs::default();
8843                meta.parse_nested_meta(|inner| {
8844                    if inner.path.is_ident("list_display") {
8845                        let s: LitStr = inner.value()?.parse()?;
8846                        admin.list_display =
8847                            Some((split_field_list(&s.value()), s.span()));
8848                        return Ok(());
8849                    }
8850                    if inner.path.is_ident("search_fields") {
8851                        let s: LitStr = inner.value()?.parse()?;
8852                        admin.search_fields =
8853                            Some((split_field_list(&s.value()), s.span()));
8854                        return Ok(());
8855                    }
8856                    if inner.path.is_ident("readonly_fields") {
8857                        let s: LitStr = inner.value()?.parse()?;
8858                        admin.readonly_fields =
8859                            Some((split_field_list(&s.value()), s.span()));
8860                        return Ok(());
8861                    }
8862                    if inner.path.is_ident("list_per_page") {
8863                        let lit: syn::LitInt = inner.value()?.parse()?;
8864                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8865                        return Ok(());
8866                    }
8867                    if inner.path.is_ident("ordering") {
8868                        let s: LitStr = inner.value()?.parse()?;
8869                        admin.ordering = Some((
8870                            parse_ordering_list(&s.value()),
8871                            s.span(),
8872                        ));
8873                        return Ok(());
8874                    }
8875                    if inner.path.is_ident("list_filter") {
8876                        let s: LitStr = inner.value()?.parse()?;
8877                        admin.list_filter =
8878                            Some((split_field_list(&s.value()), s.span()));
8879                        return Ok(());
8880                    }
8881                    if inner.path.is_ident("actions") {
8882                        let s: LitStr = inner.value()?.parse()?;
8883                        admin.actions =
8884                            Some((split_field_list(&s.value()), s.span()));
8885                        return Ok(());
8886                    }
8887                    if inner.path.is_ident("fieldsets") {
8888                        let s: LitStr = inner.value()?.parse()?;
8889                        admin.fieldsets =
8890                            Some((parse_fieldset_list(&s.value()), s.span()));
8891                        return Ok(());
8892                    }
8893                    if inner.path.is_ident("list_display_links") {
8894                        let s: LitStr = inner.value()?.parse()?;
8895                        admin.list_display_links =
8896                            Some((split_field_list(&s.value()), s.span()));
8897                        return Ok(());
8898                    }
8899                    if inner.path.is_ident("search_help_text") {
8900                        let s: LitStr = inner.value()?.parse()?;
8901                        admin.search_help_text = Some(s.value());
8902                        return Ok(());
8903                    }
8904                    if inner.path.is_ident("actions_on_top") {
8905                        let lit: syn::LitBool = inner.value()?.parse()?;
8906                        admin.actions_on_top = Some(lit.value);
8907                        return Ok(());
8908                    }
8909                    if inner.path.is_ident("actions_on_bottom") {
8910                        let lit: syn::LitBool = inner.value()?.parse()?;
8911                        admin.actions_on_bottom = Some(lit.value);
8912                        return Ok(());
8913                    }
8914                    if inner.path.is_ident("date_hierarchy") {
8915                        let s: LitStr = inner.value()?.parse()?;
8916                        admin.date_hierarchy = Some(s.value());
8917                        return Ok(());
8918                    }
8919                    if inner.path.is_ident("prepopulated_fields") {
8920                        let s: LitStr = inner.value()?.parse()?;
8921                        admin.prepopulated_fields =
8922                            Some((parse_prepopulated_list(&s.value()), s.span()));
8923                        return Ok(());
8924                    }
8925                    if inner.path.is_ident("raw_id_fields") {
8926                        let s: LitStr = inner.value()?.parse()?;
8927                        admin.raw_id_fields =
8928                            Some((split_field_list(&s.value()), s.span()));
8929                        return Ok(());
8930                    }
8931                    if inner.path.is_ident("autocomplete_fields") {
8932                        let s: LitStr = inner.value()?.parse()?;
8933                        admin.autocomplete_fields =
8934                            Some((split_field_list(&s.value()), s.span()));
8935                        return Ok(());
8936                    }
8937                    if inner.path.is_ident("list_select_related") {
8938                        let s: LitStr = inner.value()?.parse()?;
8939                        admin.list_select_related = Some(s.value());
8940                        return Ok(());
8941                    }
8942                    if inner.path.is_ident("formfield_overrides") {
8943                        let s: LitStr = inner.value()?.parse()?;
8944                        admin.formfield_overrides =
8945                            Some((parse_formfield_overrides(&s.value()), s.span()));
8946                        return Ok(());
8947                    }
8948                    Err(inner.error(
8949                        "unknown admin attribute (supported: \
8950                         `list_display`, `list_display_links`, \
8951                         `search_fields`, `search_help_text`, \
8952                         `readonly_fields`, \
8953                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8954                         `actions_on_top`, `actions_on_bottom`, \
8955                         `date_hierarchy`, \
8956                         `prepopulated_fields`, \
8957                         `raw_id_fields`, \
8958                         `autocomplete_fields`, \
8959                         `list_select_related`, \
8960                         `formfield_overrides`, \
8961                         `fieldsets`)",
8962                    ))
8963                })?;
8964                out.admin = Some(admin);
8965                return Ok(());
8966            }
8967            if meta.path.is_ident("manager") {
8968                // Its empty trait could not take methods (#2132).
8969                return Err(meta.error(
8970                    "`manager(ext = ...)` was removed (#2132): declare your own \
8971                     `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
8972                ));
8973            }
8974            if meta.path.is_ident("manager_fn") {
8975                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8976                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8977                // next to the default `Self::objects()`. Multiple
8978                // attributes accumulate.
8979                let s: LitStr = meta.value()?.parse()?;
8980                let name = s.value();
8981                if name.is_empty() {
8982                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8983                }
8984                if name == "objects" {
8985                    return Err(meta.error(
8986                        "`manager_fn = \"objects\"` collides with the default \
8987                         accessor — pick a different name",
8988                    ));
8989                }
8990                let ident = syn::Ident::new(&name, s.span());
8991                if out.manager_fns.iter().any(|prev| *prev == ident) {
8992                    return Err(meta.error(format!(
8993                        "duplicate `manager_fn = \"{name}\"`"
8994                    )));
8995                }
8996                out.manager_fns.push(ident);
8997                return Ok(());
8998            }
8999            if meta.path.is_ident("default_order") {
9000                // `#[rustango(default_order = "-created_at, status")]`
9001                // — issue #291 / T2.5. Comma-separated list; `-prefix`
9002                // means descending, `+prefix` or bare name means ascending.
9003                // Per-query opt-in via `QuerySet::with_default_order()`.
9004                let s: LitStr = meta.value()?.parse()?;
9005                let raw = s.value();
9006                let span = s.span();
9007                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9008                    Vec::new();
9009                for entry in raw.split(',') {
9010                    let trimmed = entry.trim();
9011                    if trimmed.is_empty() {
9012                        return Err(syn::Error::new(
9013                            span,
9014                            "`default_order = \"...\"` has an empty entry — \
9015                             check for a stray comma",
9016                        ));
9017                    }
9018                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9019                        (true, rest.trim().to_owned())
9020                    } else if let Some(rest) = trimmed.strip_prefix('+') {
9021                        (false, rest.trim().to_owned())
9022                    } else {
9023                        (false, trimmed.to_owned())
9024                    };
9025                    if name.is_empty() {
9026                        return Err(syn::Error::new(
9027                            span,
9028                            "`default_order` entry has no column name after the prefix",
9029                        ));
9030                    }
9031                    if parsed.iter().any(|(n, _, _)| *n == name) {
9032                        return Err(syn::Error::new(
9033                            span,
9034                            format!("duplicate column `{name}` in `default_order`"),
9035                        ));
9036                    }
9037                    parsed.push((name, desc, span));
9038                }
9039                if parsed.is_empty() {
9040                    return Err(syn::Error::new(
9041                        span,
9042                        "`default_order = \"...\"` cannot be empty",
9043                    ));
9044                }
9045                out.default_order = parsed;
9046                return Ok(());
9047            }
9048            if meta.path.is_ident("global_scope") {
9049                // `#[rustango(global_scope(name = "active", apply =
9050                //  path::to::fn))]` — issue #820. The apply function
9051                // path resolves at macro-expand time in the consumer's
9052                // scope; the macro re-emits it verbatim into the
9053                // `ModelSchema::global_scopes` slice literal.
9054                let span = meta.path.span();
9055                let mut scope_name: Option<String> = None;
9056                let mut apply_path: Option<syn::Path> = None;
9057                meta.parse_nested_meta(|inner| {
9058                    if inner.path.is_ident("name") {
9059                        let s: LitStr = inner.value()?.parse()?;
9060                        let raw = s.value();
9061                        if raw.trim().is_empty() {
9062                            return Err(syn::Error::new(
9063                                s.span(),
9064                                "`global_scope(name = \"...\")` must not be empty",
9065                            ));
9066                        }
9067                        scope_name = Some(raw);
9068                        return Ok(());
9069                    }
9070                    if inner.path.is_ident("apply") {
9071                        let p: syn::Path = inner.value()?.parse()?;
9072                        apply_path = Some(p);
9073                        return Ok(());
9074                    }
9075                    Err(inner.error(
9076                        "unknown `global_scope` attribute (supported: \
9077                         `name`, `apply`)",
9078                    ))
9079                })?;
9080                let Some(name) = scope_name else {
9081                    return Err(syn::Error::new(
9082                        span,
9083                        "`global_scope` requires `name = \"...\"`",
9084                    ));
9085                };
9086                let Some(apply) = apply_path else {
9087                    return Err(syn::Error::new(
9088                        span,
9089                        "`global_scope` requires `apply = fn_path`",
9090                    ));
9091                };
9092                if out.global_scopes.iter().any(|s| s.name == name) {
9093                    return Err(syn::Error::new(
9094                        span,
9095                        format!(
9096                            "duplicate global scope name `{name}` — \
9097                             pick a unique identifier so \
9098                             `QuerySet::without_global_scope(\"{name}\")` \
9099                             is unambiguous"
9100                        ),
9101                    ));
9102                }
9103                out.global_scopes.push(GlobalScopeAttr { name, apply });
9104                return Ok(());
9105            }
9106            if meta.path.is_ident("through") {
9107                // `#[rustango(through(name = "posts", far = "Post",
9108                //  far_fk_column = "author_id", intermediate = "User",
9109                //  intermediate_fk_column = "country_id"
9110                //  [, intermediate_pk_column = "id"]))]` — issue #817.
9111                let span = meta.path.span();
9112                let mut accessor_name: Option<String> = None;
9113                let mut far_ident: Option<syn::Ident> = None;
9114                let mut far_fk_column: Option<String> = None;
9115                let mut intermediate_ident: Option<syn::Ident> = None;
9116                let mut intermediate_fk_column: Option<String> = None;
9117                let mut intermediate_pk_column: Option<String> = None;
9118                fn parse_nonempty_string(
9119                    inner: &syn::meta::ParseNestedMeta<'_>,
9120                    field: &str,
9121                ) -> syn::Result<String> {
9122                    let s: LitStr = inner.value()?.parse()?;
9123                    let raw = s.value();
9124                    let trimmed = raw.trim();
9125                    if trimmed.is_empty() {
9126                        return Err(syn::Error::new(
9127                            s.span(),
9128                            format!("`through({field} = \"...\")` must not be empty"),
9129                        ));
9130                    }
9131                    Ok(trimmed.to_owned())
9132                }
9133                meta.parse_nested_meta(|inner| {
9134                    if inner.path.is_ident("name") {
9135                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9136                        return Ok(());
9137                    }
9138                    if inner.path.is_ident("far") {
9139                        // Far model name accepted as a string literal
9140                        // so the attribute fits inside the existing
9141                        // `parse_nested_meta` shape; parsed back into a
9142                        // `syn::Ident` so the emitted accessor can
9143                        // reference the type directly.
9144                        let s: LitStr = inner.value()?.parse()?;
9145                        let raw = s.value();
9146                        let trimmed = raw.trim();
9147                        if trimmed.is_empty() {
9148                            return Err(syn::Error::new(
9149                                s.span(),
9150                                "`through(far = \"...\")` must not be empty",
9151                            ));
9152                        }
9153                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9154                        return Ok(());
9155                    }
9156                    if inner.path.is_ident("far_fk_column") {
9157                        far_fk_column =
9158                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9159                        return Ok(());
9160                    }
9161                    if inner.path.is_ident("intermediate") {
9162                        let s: LitStr = inner.value()?.parse()?;
9163                        let raw = s.value();
9164                        let trimmed = raw.trim();
9165                        if trimmed.is_empty() {
9166                            return Err(syn::Error::new(
9167                                s.span(),
9168                                "`through(intermediate = \"...\")` must not be empty",
9169                            ));
9170                        }
9171                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9172                        return Ok(());
9173                    }
9174                    if inner.path.is_ident("intermediate_fk_column") {
9175                        intermediate_fk_column =
9176                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9177                        return Ok(());
9178                    }
9179                    if inner.path.is_ident("intermediate_pk_column") {
9180                        intermediate_pk_column =
9181                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9182                        return Ok(());
9183                    }
9184                    Err(inner.error(
9185                        "unknown `through` attribute (supported: \
9186                         `name`, `far`, `far_fk_column`, \
9187                         `intermediate`, `intermediate_fk_column`, \
9188                         `intermediate_pk_column`)",
9189                    ))
9190                })?;
9191                let Some(name) = accessor_name else {
9192                    return Err(syn::Error::new(
9193                        span,
9194                        "`through` requires `name = \"...\"`",
9195                    ));
9196                };
9197                let Some(far) = far_ident else {
9198                    return Err(syn::Error::new(
9199                        span,
9200                        "`through` requires `far = \"FarModelType\"`",
9201                    ));
9202                };
9203                let Some(far_fk_column) = far_fk_column else {
9204                    return Err(syn::Error::new(
9205                        span,
9206                        "`through` requires `far_fk_column = \"<column>\"`",
9207                    ));
9208                };
9209                let Some(intermediate) = intermediate_ident else {
9210                    return Err(syn::Error::new(
9211                        span,
9212                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9213                    ));
9214                };
9215                let Some(intermediate_fk_column) = intermediate_fk_column else {
9216                    return Err(syn::Error::new(
9217                        span,
9218                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9219                    ));
9220                };
9221                let intermediate_pk_column =
9222                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9223                if out.through_relations.iter().any(|t| t.name == name) {
9224                    return Err(syn::Error::new(
9225                        span,
9226                        format!(
9227                            "duplicate `through(name = \"{name}\")` — \
9228                             pick a unique accessor name"
9229                        ),
9230                    ));
9231                }
9232                out.through_relations.push(ThroughAttr {
9233                    name,
9234                    far,
9235                    far_fk_column,
9236                    intermediate,
9237                    intermediate_fk_column,
9238                    intermediate_pk_column,
9239                });
9240                return Ok(());
9241            }
9242            if meta.path.is_ident("reverse_has") {
9243                // `#[rustango(reverse_has(name = "comments",
9244                //  child = "Comment", child_fk_column = "post_id"
9245                //  [, self_pk_column = "id"]))]` — issue #830.
9246                let span = meta.path.span();
9247                let mut accessor_name: Option<String> = None;
9248                let mut child_ident: Option<syn::Ident> = None;
9249                let mut child_fk_column: Option<String> = None;
9250                let mut self_pk_column: Option<String> = None;
9251                let mut self_pk_span: Option<proc_macro2::Span> = None;
9252                meta.parse_nested_meta(|inner| {
9253                    if inner.path.is_ident("name") {
9254                        let s: LitStr = inner.value()?.parse()?;
9255                        let raw = s.value();
9256                        if raw.trim().is_empty() {
9257                            return Err(syn::Error::new(
9258                                s.span(),
9259                                "`reverse_has(name = \"...\")` must not be empty",
9260                            ));
9261                        }
9262                        accessor_name = Some(raw);
9263                        return Ok(());
9264                    }
9265                    if inner.path.is_ident("child") {
9266                        let s: LitStr = inner.value()?.parse()?;
9267                        let raw = s.value();
9268                        let trimmed = raw.trim();
9269                        if trimmed.is_empty() {
9270                            return Err(syn::Error::new(
9271                                s.span(),
9272                                "`reverse_has(child = \"...\")` must not be empty",
9273                            ));
9274                        }
9275                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9276                        return Ok(());
9277                    }
9278                    if inner.path.is_ident("child_fk_column") {
9279                        let s: LitStr = inner.value()?.parse()?;
9280                        let raw = s.value();
9281                        let trimmed = raw.trim();
9282                        if trimmed.is_empty() {
9283                            return Err(syn::Error::new(
9284                                s.span(),
9285                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9286                            ));
9287                        }
9288                        child_fk_column = Some(trimmed.to_owned());
9289                        return Ok(());
9290                    }
9291                    if inner.path.is_ident("self_pk_column") {
9292                        let s: LitStr = inner.value()?.parse()?;
9293                        let raw = s.value();
9294                        let trimmed = raw.trim();
9295                        if trimmed.is_empty() {
9296                            return Err(syn::Error::new(
9297                                s.span(),
9298                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9299                            ));
9300                        }
9301                        self_pk_column = Some(trimmed.to_owned());
9302                        self_pk_span = Some(s.span());
9303                        return Ok(());
9304                    }
9305                    Err(inner.error(
9306                        "unknown `reverse_has` attribute (supported: \
9307                         `name`, `child`, `child_fk_column`, \
9308                         `self_pk_column`)",
9309                    ))
9310                })?;
9311                let Some(name) = accessor_name else {
9312                    return Err(syn::Error::new(
9313                        span,
9314                        "`reverse_has` requires `name = \"...\"`",
9315                    ));
9316                };
9317                let Some(child) = child_ident else {
9318                    return Err(syn::Error::new(
9319                        span,
9320                        "`reverse_has` requires `child = \"ChildModelType\"`",
9321                    ));
9322                };
9323                let Some(child_fk_column) = child_fk_column else {
9324                    return Err(syn::Error::new(
9325                        span,
9326                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9327                    ));
9328                };
9329                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9330                    return Err(syn::Error::new(
9331                        span,
9332                        format!(
9333                            "duplicate `reverse_has(name = \"{name}\")` — \
9334                             pick a unique accessor name"
9335                        ),
9336                    ));
9337                }
9338                out.reverse_has_relations.push(ReverseHasAttr {
9339                    name,
9340                    child,
9341                    child_fk_column,
9342                    self_pk_column,
9343                    self_pk_span,
9344                });
9345                return Ok(());
9346            }
9347            if meta.path.is_ident("generic_has") {
9348                // `#[rustango(generic_has(name = "tags",
9349                //  child = "Tag", ct_column = "content_type_id",
9350                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9351                //  — issue #830, the reverse generic-FK (GFK) arm of the
9352                //  relation-existence family.
9353                let span = meta.path.span();
9354                let mut accessor_name: Option<String> = None;
9355                let mut child_ident: Option<syn::Ident> = None;
9356                let mut ct_column: Option<String> = None;
9357                let mut pk_column: Option<String> = None;
9358                let mut self_pk_column: Option<String> = None;
9359                let mut self_pk_span: Option<proc_macro2::Span> = None;
9360                meta.parse_nested_meta(|inner| {
9361                    if inner.path.is_ident("name") {
9362                        let s: LitStr = inner.value()?.parse()?;
9363                        let raw = s.value();
9364                        if raw.trim().is_empty() {
9365                            return Err(syn::Error::new(
9366                                s.span(),
9367                                "`generic_has(name = \"...\")` must not be empty",
9368                            ));
9369                        }
9370                        accessor_name = Some(raw);
9371                        return Ok(());
9372                    }
9373                    if inner.path.is_ident("child") {
9374                        let s: LitStr = inner.value()?.parse()?;
9375                        let trimmed = s.value().trim().to_owned();
9376                        if trimmed.is_empty() {
9377                            return Err(syn::Error::new(
9378                                s.span(),
9379                                "`generic_has(child = \"...\")` must not be empty",
9380                            ));
9381                        }
9382                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9383                        return Ok(());
9384                    }
9385                    if inner.path.is_ident("ct_column") {
9386                        let s: LitStr = inner.value()?.parse()?;
9387                        let trimmed = s.value().trim().to_owned();
9388                        if trimmed.is_empty() {
9389                            return Err(syn::Error::new(
9390                                s.span(),
9391                                "`generic_has(ct_column = \"...\")` must not be empty",
9392                            ));
9393                        }
9394                        ct_column = Some(trimmed);
9395                        return Ok(());
9396                    }
9397                    if inner.path.is_ident("pk_column") {
9398                        let s: LitStr = inner.value()?.parse()?;
9399                        let trimmed = s.value().trim().to_owned();
9400                        if trimmed.is_empty() {
9401                            return Err(syn::Error::new(
9402                                s.span(),
9403                                "`generic_has(pk_column = \"...\")` must not be empty",
9404                            ));
9405                        }
9406                        pk_column = Some(trimmed);
9407                        return Ok(());
9408                    }
9409                    if inner.path.is_ident("self_pk_column") {
9410                        let s: LitStr = inner.value()?.parse()?;
9411                        let trimmed = s.value().trim().to_owned();
9412                        if trimmed.is_empty() {
9413                            return Err(syn::Error::new(
9414                                s.span(),
9415                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9416                            ));
9417                        }
9418                        self_pk_column = Some(trimmed);
9419                        self_pk_span = Some(s.span());
9420                        return Ok(());
9421                    }
9422                    Err(inner.error(
9423                        "unknown `generic_has` attribute (supported: \
9424                         `name`, `child`, `ct_column`, `pk_column`, \
9425                         `self_pk_column`)",
9426                    ))
9427                })?;
9428                let Some(name) = accessor_name else {
9429                    return Err(syn::Error::new(
9430                        span,
9431                        "`generic_has` requires `name = \"...\"`",
9432                    ));
9433                };
9434                let Some(child) = child_ident else {
9435                    return Err(syn::Error::new(
9436                        span,
9437                        "`generic_has` requires `child = \"ChildModelType\"`",
9438                    ));
9439                };
9440                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9441                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9442                if out.generic_has_relations.iter().any(|r| r.name == name) {
9443                    return Err(syn::Error::new(
9444                        span,
9445                        format!(
9446                            "duplicate `generic_has(name = \"{name}\")` — \
9447                             pick a unique accessor name"
9448                        ),
9449                    ));
9450                }
9451                out.generic_has_relations.push(GenericHasAttr {
9452                    name,
9453                    child,
9454                    ct_column,
9455                    pk_column,
9456                    self_pk_column,
9457                    self_pk_span,
9458                });
9459                return Ok(());
9460            }
9461            if meta.path.is_ident("audit") {
9462                let mut audit = AuditAttrs::default();
9463                meta.parse_nested_meta(|inner| {
9464                    if inner.path.is_ident("track") {
9465                        let s: LitStr = inner.value()?.parse()?;
9466                        audit.track =
9467                            Some((split_field_list(&s.value()), s.span()));
9468                        return Ok(());
9469                    }
9470                    Err(inner.error(
9471                        "unknown audit attribute (supported: `track`)",
9472                    ))
9473                })?;
9474                out.audit = Some(audit);
9475                return Ok(());
9476            }
9477            if meta.path.is_ident("permissions") {
9478                // Two forms accepted:
9479                //   #[rustango(permissions)]          — flag form, true
9480                //   #[rustango(permissions = false)]  — explicit opt-out
9481                //   #[rustango(permissions = true)]   — explicit opt-in
9482                if let Ok(v) = meta.value() {
9483                    let lit: syn::LitBool = v.parse()?;
9484                    out.permissions = lit.value;
9485                } else {
9486                    out.permissions = true;
9487                }
9488                return Ok(());
9489            }
9490            if meta.path.is_ident("view") {
9491                // Issue #293 / T2.10. Two forms accepted, matching
9492                // the `permissions` flag pattern:
9493                //   #[rustango(view)]          — flag form, true
9494                //   #[rustango(view = false)]  — explicit opt-out
9495                //   #[rustango(view = true)]   — explicit opt-in
9496                if let Ok(v) = meta.value() {
9497                    let lit: syn::LitBool = v.parse()?;
9498                    out.is_view = lit.value;
9499                } else {
9500                    out.is_view = true;
9501                }
9502                return Ok(());
9503            }
9504            if meta.path.is_ident("managed") {
9505                // Who owns the table. Issue #321.
9506                //   #[rustango(managed = false)]  — operator-managed table
9507                //   #[rustango(managed = true)]   — rustango-managed (the default)
9508                // Bare-flag form is intentionally not accepted: writing
9509                // `#[rustango(managed)]` reads as "yes please manage it"
9510                // which is already the default. The opt-out is the only
9511                // useful state, so it must be explicit.
9512                let v = meta.value()?;
9513                let lit: syn::LitBool = v.parse()?;
9514                out.managed = lit.value;
9515                return Ok(());
9516            }
9517            if meta.path.is_ident("verbose_name") {
9518                let s: LitStr = meta.value()?.parse()?;
9519                out.verbose_name = Some(s.value());
9520                return Ok(());
9521            }
9522            if meta.path.is_ident("verbose_name_plural") {
9523                let s: LitStr = meta.value()?.parse()?;
9524                out.verbose_name_plural = Some(s.value());
9525                return Ok(());
9526            }
9527            if meta.path.is_ident("db_table_comment") {
9528                // Free-form table-level comment attached to the DB
9529                // catalog.
9530                let s: LitStr = meta.value()?.parse()?;
9531                out.db_table_comment = Some(s.value());
9532                return Ok(());
9533            }
9534            if meta.path.is_ident("proxy") {
9535                // Declarative flag
9536                // marking the struct as a proxy of another model that
9537                // shares its DB table. Stored on `ModelSchema::proxy`
9538                // so future codegen can skip `CreateTable` emission
9539                // for proxies (parent owns the table) and route
9540                // per-instance method resolution to the proxy class.
9541                //
9542                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9543                let value = if meta.input.peek(syn::Token![=]) {
9544                    meta.value()?.parse::<syn::LitBool>()?.value
9545                } else {
9546                    true
9547                };
9548                out.proxy = value;
9549                return Ok(());
9550            }
9551            if meta.path.is_ident("order_with_respect_to") {
9552                // `order_with_respect_to = "parent_fk"` — the model's
9553                // instances are intrinsically ordered relative to
9554                // their parent FK.
9555                //
9556                // rustango stores the FK field name on
9557                // `ModelSchema::order_with_respect_to`. Declarative-only
9558                // today: the migration writer + admin surfaces still
9559                // treat every model identically. Future codegen will
9560                // key off the metadata to auto-emit the `_order`
9561                // column and reorder helpers.
9562                //
9563                // Validated as a Rust-shape identifier so the macro
9564                // can reject typos at derive time.
9565                let s: LitStr = meta.value()?.parse()?;
9566                let raw = s.value();
9567                if raw.is_empty() {
9568                    return Err(syn::Error::new(
9569                        s.span(),
9570                        "`order_with_respect_to` must be a non-empty FK field name",
9571                    ));
9572                }
9573                let valid = raw
9574                    .chars()
9575                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9576                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9577                if !valid {
9578                    return Err(syn::Error::new(
9579                        s.span(),
9580                        format!(
9581                            "`order_with_respect_to` must be a valid Rust \
9582                             identifier (letters / digits / underscores, \
9583                             not starting with a digit); got `{raw}`"
9584                        ),
9585                    ));
9586                }
9587                out.order_with_respect_to = Some(raw);
9588                return Ok(());
9589            }
9590            if meta.path.is_ident("required_db_features") {
9591                // Capability tokens the model needs (e.g.
9592                // `"json_extract"`, `"window_functions"`,
9593                // `"row_security"`). Comma-separated.
9594                // `manage check --deploy` walks every model and warns
9595                // when the active backend doesn't advertise the
9596                // capability.
9597                //
9598                // rustango ships a small registry of capability tokens
9599                // each dialect supports — see `Dialect::supports`.
9600                // Unknown tokens still parse (they end up on the
9601                // schema and show up in the warning) so projects can
9602                // declare aspirational capabilities and the check
9603                // verb will keep nagging until the dialect implements
9604                // them.
9605                let s: LitStr = meta.value()?.parse()?;
9606                let raw = s.value();
9607                let features: Vec<String> = raw
9608                    .split(',')
9609                    .map(str::trim)
9610                    .filter(|s| !s.is_empty())
9611                    .map(str::to_owned)
9612                    .collect();
9613                if features.is_empty() {
9614                    return Err(syn::Error::new(
9615                        s.span(),
9616                        "`required_db_features` must list at least one \
9617                         comma-separated capability token",
9618                    ));
9619                }
9620                out.required_db_features = features;
9621                return Ok(());
9622            }
9623            if meta.path.is_ident("required_db_vendor") {
9624                // The model is only meant to run against the named DB
9625                // backend. `manage check --deploy` flags a mismatch so
9626                // ops catches "I forgot to switch DATABASE_URL" at
9627                // deploy time rather than runtime.
9628                //
9629                // Restricted to the three backends rustango ships
9630                // dialects for so the check verb can compare reliably.
9631                let s: LitStr = meta.value()?.parse()?;
9632                let raw = s.value().to_ascii_lowercase();
9633                match raw.as_str() {
9634                    "postgresql" | "postgres" | "pg" => {
9635                        out.required_db_vendor = Some("postgres".to_owned());
9636                    }
9637                    "mysql" | "mariadb" => {
9638                        out.required_db_vendor = Some("mysql".to_owned());
9639                    }
9640                    "sqlite" | "sqlite3" => {
9641                        out.required_db_vendor = Some("sqlite".to_owned());
9642                    }
9643                    _ => {
9644                        return Err(syn::Error::new(
9645                            s.span(),
9646                            format!(
9647                                "unknown required_db_vendor `{raw}` — \
9648                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9649                                 `mysql` (alias: `mariadb`), or `sqlite` \
9650                                 (alias: `sqlite3`)"
9651                            ),
9652                        ));
9653                    }
9654                }
9655                return Ok(());
9656            }
9657            if meta.path.is_ident("base_manager_name") {
9658                // Name of the manager that `<instance>.<relation>_set`
9659                // uses when resolving reverse-relation managers.
9660                // Distinct from `default_manager_name` (what
9661                // `Self::objects()` returns). Stored on
9662                // `ModelSchema::base_manager_name`.
9663                //
9664                // Validated as a Rust identifier so it stays safe to
9665                // re-emit as code in future reverse-manager codegen.
9666                let s: LitStr = meta.value()?.parse()?;
9667                let raw = s.value();
9668                if raw.is_empty() {
9669                    return Err(syn::Error::new(
9670                        s.span(),
9671                        "`base_manager_name` must be a non-empty string",
9672                    ));
9673                }
9674                let valid = raw
9675                    .chars()
9676                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9677                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9678                if !valid {
9679                    return Err(syn::Error::new(
9680                        s.span(),
9681                        format!(
9682                            "`base_manager_name` must be a valid Rust \
9683                             identifier (letters / digits / underscores, \
9684                             not starting with a digit); got `{raw}`"
9685                        ),
9686                    ));
9687                }
9688                out.base_manager_name = Some(raw);
9689                return Ok(());
9690            }
9691            if meta.path.is_ident("default_related_name") {
9692                // The name reverse-relation accessors use when callers
9693                // don't override `related_name = "..."` on the FK / M2M
9694                // field. Stored on `ModelSchema::default_related_name`
9695                // so future reverse-manager codegen / schema emit /
9696                // admin templates can pick the right accessor name
9697                // (today rustango doesn't auto-emit reverse managers;
9698                // the metadata is the foundation for that work).
9699                //
9700                // Enforced non-empty + ASCII identifier-shape so the
9701                // string is safe to use as a Rust ident later.
9702                let s: LitStr = meta.value()?.parse()?;
9703                let raw = s.value();
9704                if raw.is_empty() {
9705                    return Err(syn::Error::new(
9706                        s.span(),
9707                        "`default_related_name` must be a non-empty string",
9708                    ));
9709                }
9710                let valid = raw
9711                    .chars()
9712                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9713                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9714                if !valid {
9715                    return Err(syn::Error::new(
9716                        s.span(),
9717                        format!(
9718                            "`default_related_name` must be snake_case ASCII \
9719                             (lowercase letters / digits / underscores, not \
9720                             starting with a digit); got `{raw}`"
9721                        ),
9722                    ));
9723                }
9724                out.default_related_name = Some(raw);
9725                return Ok(());
9726            }
9727            if meta.path.is_ident("extra_permissions") {
9728                // Comma-separated `codename:label` pairs.
9729                let s: LitStr = meta.value()?.parse()?;
9730                let raw = s.value();
9731                let mut pairs = Vec::new();
9732                for entry in raw.split(',') {
9733                    let entry = entry.trim();
9734                    if entry.is_empty() {
9735                        continue;
9736                    }
9737                    let (codename, label) = match entry.split_once(':') {
9738                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9739                        None => (entry.to_owned(), entry.to_owned()),
9740                    };
9741                    if codename.is_empty() {
9742                        return Err(meta.error(
9743                            "`extra_permissions` entries must be `codename:label` pairs",
9744                        ));
9745                    }
9746                    pairs.push((codename, label));
9747                }
9748                if pairs.is_empty() {
9749                    return Err(meta
9750                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9751                }
9752                out.extra_permissions = pairs;
9753                return Ok(());
9754            }
9755            if meta.path.is_ident("default_permissions") {
9756                // Comma-separated subset of the CRUD action set.
9757                // Empty means all four (the framework default).
9758                let s: LitStr = meta.value()?.parse()?;
9759                let raw = s.value();
9760                let mut actions: Vec<String> = Vec::new();
9761                for entry in raw.split(',') {
9762                    let action = entry.trim().to_ascii_lowercase();
9763                    if action.is_empty() {
9764                        continue;
9765                    }
9766                    match action.as_str() {
9767                        "add" | "change" | "delete" | "view" => {}
9768                        other => {
9769                            return Err(syn::Error::new(
9770                                s.span(),
9771                                format!(
9772                                    "unknown default_permissions action `{other}` — \
9773                                     expected one of `add`, `change`, `delete`, `view`"
9774                                ),
9775                            ));
9776                        }
9777                    }
9778                    if !actions.contains(&action) {
9779                        actions.push(action);
9780                    }
9781                }
9782                if actions.is_empty() {
9783                    return Err(syn::Error::new(
9784                        s.span(),
9785                        "`default_permissions = \"…\"` must list at least one action; \
9786                         use `permissions = false` on the container if you want NO \
9787                         permissions seeded for this model.",
9788                    ));
9789                }
9790                out.default_permissions = actions;
9791                return Ok(());
9792            }
9793            if meta.path.is_ident("get_latest_by") {
9794                // The `-` prefix selects descending order.
9795                let s: LitStr = meta.value()?.parse()?;
9796                let raw = s.value();
9797                let trimmed = raw.trim();
9798                if trimmed.is_empty() {
9799                    return Err(meta.error("`get_latest_by` must name a column"));
9800                }
9801                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9802                    (stripped.to_owned(), true)
9803                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9804                    (stripped.to_owned(), false)
9805                } else {
9806                    (trimmed.to_owned(), false)
9807                };
9808                if col.is_empty() {
9809                    return Err(meta.error("`get_latest_by` must name a column"));
9810                }
9811                out.get_latest_by = Some((col, desc, s.span()));
9812                return Ok(());
9813            }
9814            if meta.path.is_ident("unique_together") {
9815                // Composite UNIQUE index. Two syntaxes:
9816                //
9817                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9818                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9819                //
9820                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9821                // (col1, col2)`, where <name> defaults to
9822                // `<table>_<col1>_<col2>_uq` when not supplied.
9823                let (columns, name) = parse_together_attr(&meta, "unique_together", written)?;
9824                out.indexes.push(IndexAttr {
9825                    span: meta.path.span(),
9826                    name,
9827                    columns,
9828                    unique: true,
9829                    method: "btree".to_owned(),
9830                    where_clause: None,
9831                include: Vec::new(),
9832                });
9833                return Ok(());
9834            }
9835            if meta.path.is_ident("index_together") {
9836                // Composite (non-unique) index. Two syntaxes
9837                // mirroring `unique_together`.
9838                //
9839                //   #[rustango(index_together = "created_at, status")]
9840                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9841                let (columns, name) = parse_together_attr(&meta, "index_together", written)?;
9842                out.indexes.push(IndexAttr {
9843                    span: meta.path.span(),
9844                    name,
9845                    columns,
9846                    unique: false,
9847                    method: "btree".to_owned(),
9848                    where_clause: None,
9849                include: Vec::new(),
9850                });
9851                return Ok(());
9852            }
9853            if meta.path.is_ident("unique_when") {
9854                // Partial unique index. Issue #265 / T1.3.
9855                //
9856                //   #[rustango(unique_when(
9857                //       columns   = "email",
9858                //       condition = "deleted_at IS NULL",
9859                //       name      = "unique_active_email"
9860                //   ))]
9861                //
9862                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9863                // on PG / SQLite (both ship partial indexes natively).
9864                // MySQL falls back to a plain UNIQUE index — the
9865                // condition is lost; document the limitation in the
9866                // generated migration.
9867                let mut columns: Option<Vec<String>> = None;
9868                let mut condition: Option<String> = None;
9869                let mut name: Option<String> = None;
9870                let mut include: Vec<String> = Vec::new();
9871                meta.parse_nested_meta(|inner| {
9872                    if inner.path.is_ident("columns") {
9873                        let s: LitStr = inner.value()?.parse()?;
9874                        columns = Some(split_field_list(&s.value()));
9875                        return Ok(());
9876                    }
9877                    if inner.path.is_ident("condition") {
9878                        let s: LitStr = inner.value()?.parse()?;
9879                        condition = Some(s.value());
9880                        return Ok(());
9881                    }
9882                    if inner.path.is_ident("name") {
9883                        let s: LitStr = inner.value()?.parse()?;
9884                        name = Some(s.value());
9885                        return Ok(());
9886                    }
9887                    if inner.path.is_ident("include") {
9888                        // PG 11+ covering-index columns. Non-key columns
9889                        // travel with the index leaf for index-only
9890                        // scans. Dropped on MySQL/SQLite by the writer.
9891                        let s: LitStr = inner.value()?.parse()?;
9892                        include = split_field_list(&s.value());
9893                        return Ok(());
9894                    }
9895                    Err(inner.error(
9896                        "unknown unique_when attribute (supported: \
9897                         `columns = \"...\"`, `condition = \"...\"`, \
9898                         `name = \"...\"`, `include = \"...\"`)",
9899                    ))
9900                })?;
9901                let columns = columns.ok_or_else(|| {
9902                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9903                })?;
9904                let condition = condition.ok_or_else(|| {
9905                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9906                })?;
9907                if columns.is_empty() {
9908                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9909                }
9910                out.indexes.push(IndexAttr {
9911                    span: meta.path.span(),
9912                    name,
9913                    columns,
9914                    unique: true,
9915                    method: "btree".to_owned(),
9916                    where_clause: Some(condition),
9917                    include,
9918                });
9919                return Ok(());
9920            }
9921            if meta.path.is_ident("index_when") {
9922                // Non-unique partial index. Sibling of `unique_when`
9923                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9924                //
9925                //   #[rustango(index_when(
9926                //       columns   = "status, created_at",
9927                //       condition = "deleted_at IS NULL",
9928                //       name      = "active_status_created_idx"
9929                //   ))]
9930                //
9931                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9932                // on PG / SQLite (both ship partial indexes natively).
9933                // MySQL has no native partial-index support — the writer
9934                // emits a plain CREATE INDEX and the condition is lost;
9935                // operators wanting that selectivity on MySQL should
9936                // declare a covering index plus an application-level
9937                // filter.
9938                let mut columns: Option<Vec<String>> = None;
9939                let mut condition: Option<String> = None;
9940                let mut name: Option<String> = None;
9941                let mut method: String = "btree".to_owned();
9942                let mut include: Vec<String> = Vec::new();
9943                meta.parse_nested_meta(|inner| {
9944                    if inner.path.is_ident("columns") {
9945                        let s: LitStr = inner.value()?.parse()?;
9946                        columns = Some(split_field_list(&s.value()));
9947                        return Ok(());
9948                    }
9949                    if inner.path.is_ident("condition") {
9950                        let s: LitStr = inner.value()?.parse()?;
9951                        condition = Some(s.value());
9952                        return Ok(());
9953                    }
9954                    if inner.path.is_ident("name") {
9955                        let s: LitStr = inner.value()?.parse()?;
9956                        name = Some(s.value());
9957                        return Ok(());
9958                    }
9959                    if inner.path.is_ident("method") {
9960                        let s: LitStr = inner.value()?.parse()?;
9961                        method = s.value();
9962                        return Ok(());
9963                    }
9964                    if inner.path.is_ident("include") {
9965                        // PG 11+ covering-index columns; non-key
9966                        // columns travel with the index leaf. Dropped
9967                        // on MySQL/SQLite.
9968                        let s: LitStr = inner.value()?.parse()?;
9969                        include = split_field_list(&s.value());
9970                        return Ok(());
9971                    }
9972                    Err(inner.error(
9973                        "unknown index_when attribute (supported: \
9974                         `columns = \"...\"`, `condition = \"...\"`, \
9975                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9976                         `include = \"...\"`)",
9977                    ))
9978                })?;
9979                let columns = columns
9980                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9981                let condition = condition.ok_or_else(|| {
9982                    meta.error("`index_when(...)` requires `condition = \"...\"`")
9983                })?;
9984                if columns.is_empty() {
9985                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
9986                }
9987                out.indexes.push(IndexAttr {
9988                    span: meta.path.span(),
9989                    name,
9990                    columns,
9991                    unique: false,
9992                    method,
9993                    where_clause: Some(condition),
9994                    include,
9995                });
9996                return Ok(());
9997            }
9998            if meta.path.is_ident("index") {
9999                // Container-level composite index. Two syntaxes:
10000                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
10001                //   #[rustango(index("col1, col2"))]    — call form (same result)
10002                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
10003                // The call form takes the column list as a leading string
10004                // literal, then optional `unique` / `name = "..."` /
10005                // `method = "..."` flags (a leading literal can't compose
10006                // under `parse_nested_meta`, so the paren body is parsed by
10007                // hand). `unique_together` / `index_together` remain
10008                // aliases for the same feature.
10009                let cols_lit: LitStr;
10010                let mut unique = false;
10011                let mut name: Option<String> = None;
10012                let mut method = "btree".to_owned();
10013                if meta.input.peek(syn::token::Paren) {
10014                    let content;
10015                    syn::parenthesized!(content in meta.input);
10016                    cols_lit = content.parse()?;
10017                    while content.peek(syn::Token![,]) {
10018                        content.parse::<syn::Token![,]>()?;
10019                        if content.is_empty() {
10020                            break;
10021                        }
10022                        let flag: syn::Ident = content.parse()?;
10023                        if flag == "unique" {
10024                            unique = true;
10025                        } else if flag == "name" {
10026                            content.parse::<syn::Token![=]>()?;
10027                            let s: LitStr = content.parse()?;
10028                            name = Some(s.value());
10029                        } else if flag == "method" {
10030                            content.parse::<syn::Token![=]>()?;
10031                            let s: LitStr = content.parse()?;
10032                            let v = s.value();
10033                            match v.as_str() {
10034                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10035                                | "bloom" => method = v,
10036                                other => {
10037                                    return Err(syn::Error::new(
10038                                        s.span(),
10039                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10040                                    ));
10041                                }
10042                            }
10043                        } else {
10044                            return Err(syn::Error::new(
10045                                flag.span(),
10046                                "unknown index flag (supported: `unique`, `name`, `method`)",
10047                            ));
10048                        }
10049                    }
10050                } else {
10051                    cols_lit = meta.value()?.parse()?;
10052                }
10053                let columns = split_field_list(&cols_lit.value());
10054                out.indexes.push(IndexAttr {
10055                    span: meta.path.span(),
10056                    name,
10057                    columns,
10058                    unique,
10059                    method,
10060                    where_clause: None,
10061                    include: Vec::new(),
10062                });
10063                return Ok(());
10064            }
10065            if meta.path.is_ident("check") {
10066                // #[rustango(check(name = "…", expr = "…"))]
10067                let mut name: Option<String> = None;
10068                let mut expr: Option<String> = None;
10069                meta.parse_nested_meta(|inner| {
10070                    if inner.path.is_ident("name") {
10071                        let s: LitStr = inner.value()?.parse()?;
10072                        name = Some(s.value());
10073                        return Ok(());
10074                    }
10075                    if inner.path.is_ident("expr") {
10076                        let s: LitStr = inner.value()?.parse()?;
10077                        expr = Some(s.value());
10078                        return Ok(());
10079                    }
10080                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10081                })?;
10082                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10083                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10084                out.checks.push(CheckAttr { name, expr });
10085                return Ok(());
10086            }
10087            if meta.path.is_ident("exclude") {
10088                // #[rustango(exclude(name = "…", using = "gist",
10089                //                    elements = "col WITH op, col WITH op",
10090                //                    where = "…"))]
10091                let mut name: Option<String> = None;
10092                let mut using: Option<String> = None;
10093                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10094                let mut where_clause: Option<String> = None;
10095                meta.parse_nested_meta(|inner| {
10096                    if inner.path.is_ident("name") {
10097                        let s: LitStr = inner.value()?.parse()?;
10098                        name = Some(s.value());
10099                        return Ok(());
10100                    }
10101                    if inner.path.is_ident("using") {
10102                        let s: LitStr = inner.value()?.parse()?;
10103                        using = Some(s.value());
10104                        return Ok(());
10105                    }
10106                    if inner.path.is_ident("elements") {
10107                        let s: LitStr = inner.value()?.parse()?;
10108                        elements_raw = Some((s.value(), s.span()));
10109                        return Ok(());
10110                    }
10111                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10112                        let s: LitStr = inner.value()?.parse()?;
10113                        where_clause = Some(s.value());
10114                        return Ok(());
10115                    }
10116                    Err(inner.error(
10117                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10118                    ))
10119                })?;
10120                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10121                let using = using.unwrap_or_else(|| "gist".to_owned());
10122                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10123                    meta.error(
10124                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
10125                    )
10126                })?;
10127                // Parse `col WITH op` pairs separated by commas.
10128                let mut elements: Vec<(String, String)> = Vec::new();
10129                for pair in elements_str.split(',') {
10130                    let pair = pair.trim();
10131                    if pair.is_empty() {
10132                        continue;
10133                    }
10134                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10135                    let col = split.next().unwrap_or("").trim();
10136                    let rest = split.next().unwrap_or("").trim();
10137                    // `WITH op` — case-insensitive on `WITH`, then op.
10138                    let rest_lc = rest.to_ascii_lowercase();
10139                    let op = rest_lc
10140                        .strip_prefix("with")
10141                        .map(|r| r.trim_start())
10142                        .filter(|r| !r.is_empty())
10143                        .map(|_| {
10144                            // Pull the original-case op from `rest` after the
10145                            // `WITH ` token (5 chars).
10146                            rest[4..].trim_start().to_owned()
10147                        });
10148                    let Some(op) = op else {
10149                        return Err(syn::Error::new(
10150                            elements_span,
10151                            format!(
10152                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10153                                 (e.g. `room_id WITH =` or `during WITH &&`)"
10154                            ),
10155                        ));
10156                    };
10157                    if col.is_empty() || op.is_empty() {
10158                        return Err(syn::Error::new(
10159                            elements_span,
10160                            format!(
10161                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10162                                 (both sides non-empty)"
10163                            ),
10164                        ));
10165                    }
10166                    elements.push((col.to_owned(), op));
10167                }
10168                if elements.is_empty() {
10169                    return Err(syn::Error::new(
10170                        elements_span,
10171                        "exclude requires at least one `col WITH op` element",
10172                    ));
10173                }
10174                out.excludes.push(ExcludeAttr {
10175                    name,
10176                    using,
10177                    elements,
10178                    where_clause,
10179                });
10180                return Ok(());
10181            }
10182            if meta.path.is_ident("generic_fk") {
10183                let mut gfk = GenericFkAttr {
10184                    name: String::new(),
10185                    ct_column: String::new(),
10186                    pk_column: String::new(),
10187                };
10188                meta.parse_nested_meta(|inner| {
10189                    if inner.path.is_ident("name") {
10190                        let s: LitStr = inner.value()?.parse()?;
10191                        gfk.name = s.value();
10192                        return Ok(());
10193                    }
10194                    if inner.path.is_ident("ct_column") {
10195                        let s: LitStr = inner.value()?.parse()?;
10196                        gfk.ct_column = s.value();
10197                        return Ok(());
10198                    }
10199                    if inner.path.is_ident("pk_column") {
10200                        let s: LitStr = inner.value()?.parse()?;
10201                        gfk.pk_column = s.value();
10202                        return Ok(());
10203                    }
10204                    Err(inner.error(
10205                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10206                    ))
10207                })?;
10208                if gfk.name.is_empty() {
10209                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10210                }
10211                if gfk.ct_column.is_empty() {
10212                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10213                }
10214                if gfk.pk_column.is_empty() {
10215                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10216                }
10217                out.generic_fks.push(gfk);
10218                return Ok(());
10219            }
10220            if meta.path.is_ident("fk_composite") {
10221                let mut fk = CompositeFkAttr {
10222                    name: String::new(),
10223                    to: String::new(),
10224                    from: Vec::new(),
10225                    on: Vec::new(),
10226                };
10227                meta.parse_nested_meta(|inner| {
10228                    if inner.path.is_ident("name") {
10229                        let s: LitStr = inner.value()?.parse()?;
10230                        fk.name = s.value();
10231                        return Ok(());
10232                    }
10233                    if inner.path.is_ident("to") {
10234                        let s: LitStr = inner.value()?.parse()?;
10235                        fk.to = s.value();
10236                        return Ok(());
10237                    }
10238                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10239                    // comma-list of string literals.
10240                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10241                        let value = inner.value()?;
10242                        let content;
10243                        syn::parenthesized!(content in value);
10244                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10245                            content.parse_terminated(
10246                                |p| p.parse::<syn::LitStr>(),
10247                                syn::Token![,],
10248                            )?;
10249                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10250                        if inner.path.is_ident("on") {
10251                            fk.on = cols;
10252                        } else {
10253                            fk.from = cols;
10254                        }
10255                        return Ok(());
10256                    }
10257                    Err(inner.error(
10258                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10259                    ))
10260                })?;
10261                if fk.name.is_empty() {
10262                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10263                }
10264                if fk.to.is_empty() {
10265                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10266                }
10267                if fk.from.is_empty() || fk.on.is_empty() {
10268                    return Err(meta.error(
10269                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10270                    ));
10271                }
10272                if fk.from.len() != fk.on.len() {
10273                    return Err(meta.error(format!(
10274                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10275                        fk.from.len(),
10276                        fk.on.len(),
10277                    )));
10278                }
10279                out.composite_fks.push(fk);
10280                return Ok(());
10281            }
10282            if meta.path.is_ident("m2m") {
10283                let mut m2m = M2MAttr {
10284                    name: String::new(),
10285                    to: String::new(),
10286                    through: String::new(),
10287                    src: String::new(),
10288                    dst: String::new(),
10289                    auto_create: true,
10290                };
10291                meta.parse_nested_meta(|inner| {
10292                    if inner.path.is_ident("name") {
10293                        let s: LitStr = inner.value()?.parse()?;
10294                        m2m.name = s.value();
10295                        return Ok(());
10296                    }
10297                    if inner.path.is_ident("to") {
10298                        let s: LitStr = inner.value()?.parse()?;
10299                        m2m.to = s.value();
10300                        return Ok(());
10301                    }
10302                    if inner.path.is_ident("through") {
10303                        let s: LitStr = inner.value()?.parse()?;
10304                        m2m.through = s.value();
10305                        return Ok(());
10306                    }
10307                    if inner.path.is_ident("src") {
10308                        let s: LitStr = inner.value()?.parse()?;
10309                        m2m.src = s.value();
10310                        return Ok(());
10311                    }
10312                    if inner.path.is_ident("dst") {
10313                        let s: LitStr = inner.value()?.parse()?;
10314                        m2m.dst = s.value();
10315                        return Ok(());
10316                    }
10317                    if inner.path.is_ident("auto_create") {
10318                        let lit: syn::LitBool = inner.value()?.parse()?;
10319                        m2m.auto_create = lit.value;
10320                        return Ok(());
10321                    }
10322                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10323                })?;
10324                if m2m.name.is_empty() {
10325                    return Err(meta.error("m2m requires `name = \"...\"`"));
10326                }
10327                if m2m.to.is_empty() {
10328                    return Err(meta.error("m2m requires `to = \"...\"`"));
10329                }
10330                if m2m.through.is_empty() {
10331                    return Err(meta.error("m2m requires `through = \"...\"`"));
10332                }
10333                if m2m.src.is_empty() {
10334                    return Err(meta.error("m2m requires `src = \"...\"`"));
10335                }
10336                if m2m.dst.is_empty() {
10337                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10338                }
10339                out.m2m.push(m2m);
10340                return Ok(());
10341            }
10342            if meta.path.is_ident("generic_m2m") {
10343                let mut gm = GenericM2MAttr {
10344                    name: String::new(),
10345                    through: String::new(),
10346                    pk_column: String::new(),
10347                    ct_column: String::new(),
10348                    related_column: String::new(),
10349                };
10350                meta.parse_nested_meta(|inner| {
10351                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10352                        let s: LitStr = inner.value()?.parse()?;
10353                        Ok(s.value())
10354                    };
10355                    if inner.path.is_ident("name") {
10356                        gm.name = field(&inner)?;
10357                        return Ok(());
10358                    }
10359                    if inner.path.is_ident("through") {
10360                        gm.through = field(&inner)?;
10361                        return Ok(());
10362                    }
10363                    if inner.path.is_ident("pk_column") {
10364                        gm.pk_column = field(&inner)?;
10365                        return Ok(());
10366                    }
10367                    if inner.path.is_ident("ct_column") {
10368                        gm.ct_column = field(&inner)?;
10369                        return Ok(());
10370                    }
10371                    if inner.path.is_ident("related_column") {
10372                        gm.related_column = field(&inner)?;
10373                        return Ok(());
10374                    }
10375                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10376                })?;
10377                for (val, label) in [
10378                    (&gm.name, "name"),
10379                    (&gm.through, "through"),
10380                    (&gm.pk_column, "pk_column"),
10381                    (&gm.ct_column, "ct_column"),
10382                    (&gm.related_column, "related_column"),
10383                ] {
10384                    if val.is_empty() {
10385                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10386                    }
10387                }
10388                out.generic_m2m.push(gm);
10389                return Ok(());
10390            }
10391            Err(unknown_key_error(&meta, "container", CONTAINER_ATTR_KEYS))
10392        })?;
10393    }
10394    Ok(out)
10395}
10396
10397/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10398/// owned field names, trimming whitespace and skipping empty entries.
10399/// Field-name validation against the model is done by the caller.
10400fn split_field_list(raw: &str) -> Vec<String> {
10401    raw.split(',')
10402        .map(str::trim)
10403        .filter(|s| !s.is_empty())
10404        .map(str::to_owned)
10405        .collect()
10406}
10407
10408/// Shared parser for `unique_together` and `index_together` container
10409/// attrs. Accepts both shapes:
10410///
10411///   * `attr = "col1, col2"`              — auto-derived index name.
10412///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10413///
10414/// Returns `(columns, name)`.
10415fn parse_together_attr(
10416    meta: &syn::meta::ParseNestedMeta<'_>,
10417    attr: &str,
10418    written: &str,
10419) -> syn::Result<(Vec<String>, Option<String>)> {
10420    // Disambiguate by whether the next token is `=` (key-value) or
10421    // `(` (parenthesized).
10422    if meta.input.peek(syn::Token![=]) {
10423        let cols_lit: LitStr = meta.value()?.parse()?;
10424        let columns = split_field_list(&cols_lit.value());
10425        check_together_columns(meta, attr, written, &columns)?;
10426        return Ok((columns, None));
10427    }
10428    let mut columns: Option<Vec<String>> = None;
10429    let mut name: Option<String> = None;
10430    meta.parse_nested_meta(|inner| {
10431        if inner.path.is_ident("columns") {
10432            let s: LitStr = inner.value()?.parse()?;
10433            columns = Some(split_field_list(&s.value()));
10434            return Ok(());
10435        }
10436        if inner.path.is_ident("name") {
10437            let s: LitStr = inner.value()?.parse()?;
10438            name = Some(s.value());
10439            return Ok(());
10440        }
10441        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10442    })?;
10443    let columns = columns.ok_or_else(|| {
10444        meta.error(format!(
10445            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10446        ))
10447    })?;
10448    check_together_columns(meta, attr, written, &columns)?;
10449    Ok((columns, name))
10450}
10451
10452fn check_together_columns(
10453    meta: &syn::meta::ParseNestedMeta<'_>,
10454    attr: &str,
10455    written: &str,
10456    columns: &[String],
10457) -> syn::Result<()> {
10458    if columns.len() < 2 {
10459        let flag = if attr == "unique_together" {
10460            "unique"
10461        } else {
10462            "index"
10463        };
10464        let single = format!("#[{written}({flag})] on the field");
10465        return Err(meta.error(format!(
10466            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10467        )));
10468    }
10469    Ok(())
10470}
10471
10472/// Parse the fieldsets DSL: pipe-separated sections, optional
10473/// `"Title:"` prefix on each, comma-separated field names after.
10474/// Examples:
10475/// * `"name, office"` → one untitled section with two fields
10476/// * `"Identity: name, office | Metadata: created_at"` → two titled
10477///   sections
10478///
10479/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10480fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10481    raw.split('|')
10482        .map(str::trim)
10483        .filter(|s| !s.is_empty())
10484        .map(|section| {
10485            // Split off an optional `Title:` prefix (first colon).
10486            let (title, rest) = match section.split_once(':') {
10487                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10488                _ => (String::new(), section),
10489            };
10490            let fields = split_field_list(rest);
10491            (title, fields)
10492        })
10493        .collect()
10494}
10495
10496/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10497/// comma-separated entry maps a target field to one or more source
10498/// fields joined with `+`. Whitespace around tokens is trimmed.
10499/// Entries missing `:` or with empty target/source lists are dropped.
10500fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10501    raw.split(',')
10502        .map(str::trim)
10503        .filter(|s| !s.is_empty())
10504        .filter_map(|entry| {
10505            let (target, sources_raw) = entry.split_once(':')?;
10506            let target = target.trim().to_owned();
10507            if target.is_empty() {
10508                return None;
10509            }
10510            let sources: Vec<String> = sources_raw
10511                .split('+')
10512                .map(|s| s.trim().to_owned())
10513                .filter(|s| !s.is_empty())
10514                .collect();
10515            if sources.is_empty() {
10516                return None;
10517            }
10518            Some((target, sources))
10519        })
10520        .collect()
10521}
10522
10523/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10524/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10525/// and empty halves drop silently — the macro layer only enforces shape,
10526/// not field-name vs. widget-name validity (those checks happen at
10527/// `AdminConfig` consumption time). Issue #359.
10528fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10529    raw.split(',')
10530        .map(str::trim)
10531        .filter(|s| !s.is_empty())
10532        .filter_map(|entry| {
10533            let (field, widget) = entry.split_once(':')?;
10534            let field = field.trim().to_owned();
10535            let widget = widget.trim().to_owned();
10536            if field.is_empty() || widget.is_empty() {
10537                return None;
10538            }
10539            Some((field, widget))
10540        })
10541        .collect()
10542}
10543
10544/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10545/// Returns `(field_name, desc)` pairs in the same order as the input.
10546fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10547    raw.split(',')
10548        .map(str::trim)
10549        .filter(|s| !s.is_empty())
10550        .map(|spec| {
10551            spec.strip_prefix('-')
10552                .map_or((spec.to_owned(), false), |rest| {
10553                    (rest.trim().to_owned(), true)
10554                })
10555        })
10556        .collect()
10557}
10558
10559struct FieldAttrs {
10560    column: Option<String>,
10561    primary_key: bool,
10562    fk: Option<String>,
10563    o2o: Option<String>,
10564    on: Option<String>,
10565    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10566    /// "set_default" | "no_action")]` — what happens to this row when
10567    /// the row it points at is deleted.
10568    /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10569    /// set; the macro errors at compile time if applied to a
10570    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10571    /// DDL writer renders `ON DELETE <action>` after the constraint
10572    /// clause when this is `Some`; `None` falls back to the database
10573    /// default (NO ACTION on every backend rustango supports).
10574    on_delete: Option<String>,
10575    /// `#[rustango(related_name = "...")]` — per-FK
10576    /// reverse-accessor override. When set, the derive emits
10577    /// `Parent::<related_name>[_pool]` instead of the container-level
10578    /// `default_related_name` or the `<child_snake>_set[_pool]`
10579    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10580    /// silently ignored on non-FK fields. Follow-up to #816.
10581    /// `"+"` emits none, for a parent defined in another crate.
10582    related_name: Option<String>,
10583    max_length: Option<u32>,
10584    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10585    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10586    /// unconstrained `vector` column.
10587    vector_dims: Option<u32>,
10588    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10589    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10590    /// unconstrained `geometry(Point)` column (SRID 0).
10591    geometry_srid: Option<u32>,
10592    min: Option<i64>,
10593    max: Option<i64>,
10594    default: Option<String>,
10595    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10596    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10597    /// "gen_random_uuid()"`. The Rust field type must be
10598    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10599    /// INSERTs so the DB DEFAULT fires.
10600    auto_uuid: bool,
10601    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10602    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10603    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10604    /// the field is `Auto::Unset`, then bound as a normal parameter
10605    /// rather than relying on a per-dialect DB function. Issue #823
10606    /// (Eloquent `HasUuids`).
10607    ///
10608    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10609    /// Composes with every backend (PG / MySQL / SQLite) — no
10610    /// `gen_random_uuid()` requirement on the database.
10611    default_uuid_v7: bool,
10612    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10613    /// Server-set on insert, immutable from app code afterwards.
10614    /// Implies `auto + default = "now()"`. Field type must be
10615    /// `DateTime<Utc>`.
10616    auto_now_add: bool,
10617    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10618    /// every insert AND every update. Implies `auto + default =
10619    /// "now()"`; the macro additionally rewrites `update_on` /
10620    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10621    /// field value.
10622    auto_now: bool,
10623    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10624    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10625    /// `soft_delete_on(executor)` and `restore_on(executor)`
10626    /// methods on the model.
10627    soft_delete: bool,
10628    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10629    /// the column in the generated DDL.
10630    unique: bool,
10631    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10632    /// generates a `CREATE INDEX` for this column. `unique` here means
10633    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10634    index: bool,
10635    index_unique: bool,
10636    index_name: Option<String>,
10637    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10638    /// `"btree"`. Issue #34.
10639    index_method: String,
10640    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10641    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10642    /// the macro skips this column from every INSERT and UPDATE
10643    /// path, so the database always recomputes the value from
10644    /// `EXPR`. Backlog item #35.
10645    generated_as: Option<String>,
10646    /// `#[rustango(help_text = "…")]` — help text
10647    /// rendered below the admin form's input. Threaded into
10648    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10649    /// layers can read it.
10650    help_text: Option<String>,
10651    /// `#[rustango(choices = "value:Label, value:Label")]` — the
10652    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10653    /// as a `&'static [(&'static str, &'static str)]` slice. When
10654    /// present, the admin form renders a `<select>` instead of `<input>`
10655    /// and the validator rejects values not in the list. Only meaningful
10656    /// for `FieldType::String`; the macro errors at compile time if
10657    /// applied to a non-string field.
10658    choices: Option<Vec<(String, String)>>,
10659    /// `#[rustango(db_comment = "…")]` — DB-side column
10660    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10661    /// the comment in CREATE TABLE; Postgres emits a separate
10662    /// `COMMENT ON COLUMN` statement after the table is created;
10663    /// SQLite silently drops the value (no native column comments).
10664    db_comment: Option<String>,
10665    /// `#[rustango(verbose_name = "…")]` — human-readable
10666    /// label for the field. Threaded into `FieldSchema::verbose_name`
10667    /// so admin column headers, form labels, and other display
10668    /// surfaces can prefer the friendly caption over the Rust
10669    /// identifier. `None` means renderers fall back to the field name.
10670    verbose_name: Option<String>,
10671    /// `#[rustango(editable = false)]` — opt out of
10672    /// auto-generated form rendering. Defaults to `true` so existing
10673    /// fields keep their current admin / form behavior; setting
10674    /// `false` removes the field from the admin change-form entirely
10675    /// (the value is still visible on detail / list views, just not
10676    /// editable).
10677    editable: bool,
10678    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10679    /// "form may submit empty even when DB is NOT NULL". Threaded into
10680    /// `FieldSchema::blank`. Defaults to `false`.
10681    blank: bool,
10682    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10683    /// case-insensitive text column. Threaded into
10684    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10685    /// fields; the macro errors at derive time if applied elsewhere.
10686    case_insensitive: bool,
10687    /// `#[rustango(validators = "email,url")]` — the
10688    /// model-level validator chain. Comma-separated names that
10689    /// dispatch to the `validators::*` family in `validate_value`.
10690    /// Empty by default; fires on every typed INSERT/UPDATE. Each is
10691    /// spanned on the attribute string, so an unknown name errors there.
10692    validators: Vec<syn::Ident>,
10693    /// `#[rustango(secret)]` / `secret = false`; `None` defers to the name rules.
10694    secret: Option<bool>,
10695}
10696
10697/// Keys `parse_field_attrs` accepts, listed in its unknown-key error.
10698const FIELD_ATTR_KEYS: &[&str] = &[
10699    "column",
10700    "primary_key",
10701    "fk",
10702    "o2o",
10703    "on",
10704    "on_delete",
10705    "related_name",
10706    "max_length",
10707    "vector",
10708    "geometry",
10709    "min",
10710    "max",
10711    "default",
10712    "generated_as",
10713    "help_text",
10714    "choices",
10715    "db_comment",
10716    "verbose_name",
10717    "editable",
10718    "secret",
10719    "blank",
10720    "citext",
10721    "validators",
10722    "auto_uuid",
10723    "default_uuid_v7",
10724    "auto_now_add",
10725    "auto_now",
10726    "soft_delete",
10727    "unique",
10728    "index",
10729];
10730
10731/// Keys `parse_container_attrs` accepts, listed in its unknown-key error.
10732const CONTAINER_ATTR_KEYS: &[&str] = &[
10733    "table",
10734    "display",
10735    "app",
10736    "scope",
10737    "admin",
10738    "manager",
10739    "manager_fn",
10740    "default_order",
10741    "global_scope",
10742    "through",
10743    "reverse_has",
10744    "generic_has",
10745    "audit",
10746    "permissions",
10747    "view",
10748    "managed",
10749    "verbose_name",
10750    "verbose_name_plural",
10751    "db_table_comment",
10752    "proxy",
10753    "order_with_respect_to",
10754    "required_db_features",
10755    "required_db_vendor",
10756    "base_manager_name",
10757    "default_related_name",
10758    "extra_permissions",
10759    "default_permissions",
10760    "get_latest_by",
10761    "unique_together",
10762    "index_together",
10763    "unique_when",
10764    "index_when",
10765    "index",
10766    "check",
10767    "exclude",
10768    "generic_fk",
10769    "fk_composite",
10770    "m2m",
10771    "generic_m2m",
10772];
10773
10774/// A flag attribute: `#[rustango(key)]` or `#[rustango(key = true)]`.
10775/// `= false` is refused: off is the default, so the attribute is a no-op.
10776fn parse_flag(meta: &syn::meta::ParseNestedMeta<'_>, key: &str) -> syn::Result<()> {
10777    if meta.input.peek(syn::Token![=]) {
10778        let lit: syn::LitBool = meta.value()?.parse()?;
10779        if !lit.value {
10780            return Err(syn::Error::new(
10781                lit.span(),
10782                format!("`{key} = false` is the default; remove the attribute"),
10783            ));
10784        }
10785    }
10786    Ok(())
10787}
10788
10789/// "unknown rustango <what> attribute `key`", with the nearest key and the full list.
10790fn unknown_key_error(
10791    meta: &syn::meta::ParseNestedMeta<'_>,
10792    what: &str,
10793    keys: &[&str],
10794) -> syn::Error {
10795    let path = &meta.path;
10796    let got = path
10797        .get_ident()
10798        .map_or_else(|| quote!(#path).to_string(), ToString::to_string);
10799    let mut msg = format!("unknown rustango {what} attribute `{got}`");
10800    if let Some(near) = keys
10801        .iter()
10802        .map(|k| (edit_distance(&got, k), *k))
10803        .filter(|(d, k)| *d <= 2.max(k.len() / 4))
10804        .min_by_key(|(d, _)| *d)
10805        .map(|(_, k)| k)
10806    {
10807        msg.push_str(&format!("; did you mean `{near}`?"));
10808    }
10809    msg.push_str(&format!(" Supported: {}", keys.join(", ")));
10810    meta.error(msg)
10811}
10812
10813/// Levenshtein distance, for "did you mean" hints.
10814fn edit_distance(a: &str, b: &str) -> usize {
10815    let b: Vec<char> = b.chars().collect();
10816    let mut prev: Vec<usize> = (0..=b.len()).collect();
10817    for (i, ca) in a.chars().enumerate() {
10818        let mut cur = vec![i + 1];
10819        for (j, cb) in b.iter().enumerate() {
10820            let sub = prev[j] + usize::from(ca != *cb);
10821            cur.push(sub.min(prev[j + 1] + 1).min(cur[j] + 1));
10822        }
10823        prev = cur;
10824    }
10825    prev[b.len()]
10826}
10827
10828fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10829    let mut out = FieldAttrs {
10830        column: None,
10831        primary_key: false,
10832        fk: None,
10833        o2o: None,
10834        on: None,
10835        on_delete: None,
10836        related_name: None,
10837        max_length: None,
10838        vector_dims: None,
10839        geometry_srid: None,
10840        min: None,
10841        max: None,
10842        default: None,
10843        auto_uuid: false,
10844        default_uuid_v7: false,
10845        auto_now_add: false,
10846        auto_now: false,
10847        soft_delete: false,
10848        unique: false,
10849        index: false,
10850        index_unique: false,
10851        index_name: None,
10852        index_method: "btree".to_owned(),
10853        generated_as: None,
10854        help_text: None,
10855        choices: None,
10856        db_comment: None,
10857        verbose_name: None,
10858        editable: true,
10859        blank: false,
10860        case_insensitive: false,
10861        validators: Vec::new(),
10862        secret: None,
10863    };
10864    for attr in model_attrs(&field.attrs)? {
10865        attr.parse_nested_meta(|meta| {
10866            if meta.path.is_ident("column") {
10867                let s: LitStr = meta.value()?.parse()?;
10868                let name = s.value();
10869                validate_sql_identifier(&name, "column", s.span())?;
10870                out.column = Some(name);
10871                return Ok(());
10872            }
10873            if meta.path.is_ident("primary_key") {
10874                parse_flag(&meta, "primary_key")?;
10875                out.primary_key = true;
10876                return Ok(());
10877            }
10878            if meta.path.is_ident("fk") {
10879                let s: LitStr = meta.value()?.parse()?;
10880                out.fk = Some(s.value());
10881                return Ok(());
10882            }
10883            if meta.path.is_ident("o2o") {
10884                let s: LitStr = meta.value()?.parse()?;
10885                out.o2o = Some(s.value());
10886                return Ok(());
10887            }
10888            if meta.path.is_ident("on") {
10889                let s: LitStr = meta.value()?.parse()?;
10890                out.on = Some(s.value());
10891                return Ok(());
10892            }
10893            if meta.path.is_ident("on_delete") {
10894                let s: LitStr = meta.value()?.parse()?;
10895                let raw = s.value();
10896                let normalized = raw.trim().to_ascii_lowercase();
10897                // Validate at parse time so the user gets a clear span
10898                // rather than a downstream compile error in the emit.
10899                match normalized.as_str() {
10900                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10901                    _ => {
10902                        return Err(syn::Error::new(
10903                            s.span(),
10904                            format!(
10905                                "unknown on_delete action `{raw}`; expected one of \
10906                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10907                            ),
10908                        ));
10909                    }
10910                }
10911                out.on_delete = Some(normalized);
10912                return Ok(());
10913            }
10914            if meta.path.is_ident("related_name") {
10915                let s: LitStr = meta.value()?.parse()?;
10916                let raw = s.value();
10917                if raw.trim().is_empty() {
10918                    return Err(syn::Error::new(
10919                        s.span(),
10920                        "`related_name` must be a non-empty identifier",
10921                    ));
10922                }
10923                // Validate as a Rust-ident shape — the value becomes
10924                // a method name on the parent type. Same rule as
10925                // `default_related_name` so the two surfaces match.
10926                // `"+"` emits no accessor at all.
10927                let snake = raw
10928                    .chars()
10929                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10930                    && !raw.starts_with(char::is_numeric);
10931                if !snake && raw != NO_REVERSE_ACCESSOR {
10932                    return Err(syn::Error::new(
10933                        s.span(),
10934                        "`related_name` must be snake_case ASCII (lowercase letters, \
10935                         digits, underscores; no leading digit), or \"+\" for none",
10936                    ));
10937                }
10938                out.related_name = Some(raw);
10939                return Ok(());
10940            }
10941            if meta.path.is_ident("max_length") {
10942                let lit: syn::LitInt = meta.value()?.parse()?;
10943                out.max_length = Some(lit.base10_parse::<u32>()?);
10944                return Ok(());
10945            }
10946            // `#[rustango(vector(dims = N))]` — pgvector column
10947            // dimension (#824). Nested-meta form so it reads like the
10948            // other typed-column attrs.
10949            if meta.path.is_ident("vector") {
10950                meta.parse_nested_meta(|inner| {
10951                    if inner.path.is_ident("dims") {
10952                        let lit: syn::LitInt = inner.value()?.parse()?;
10953                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10954                        return Ok(());
10955                    }
10956                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10957                })?;
10958                return Ok(());
10959            }
10960            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10961            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10962            if meta.path.is_ident("geometry") {
10963                meta.parse_nested_meta(|inner| {
10964                    if inner.path.is_ident("srid") {
10965                        let lit: syn::LitInt = inner.value()?.parse()?;
10966                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10967                        return Ok(());
10968                    }
10969                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10970                })?;
10971                return Ok(());
10972            }
10973            if meta.path.is_ident("min") {
10974                out.min = Some(parse_signed_i64(&meta)?);
10975                return Ok(());
10976            }
10977            if meta.path.is_ident("max") {
10978                out.max = Some(parse_signed_i64(&meta)?);
10979                return Ok(());
10980            }
10981            if meta.path.is_ident("default") {
10982                let s: LitStr = meta.value()?.parse()?;
10983                out.default = Some(s.value());
10984                return Ok(());
10985            }
10986            if meta.path.is_ident("generated_as") {
10987                let s: LitStr = meta.value()?.parse()?;
10988                out.generated_as = Some(s.value());
10989                return Ok(());
10990            }
10991            if meta.path.is_ident("help_text") {
10992                let s: LitStr = meta.value()?.parse()?;
10993                out.help_text = Some(s.value());
10994                return Ok(());
10995            }
10996            if meta.path.is_ident("choices") {
10997                let s: LitStr = meta.value()?.parse()?;
10998                let raw = s.value();
10999                let mut pairs: Vec<(String, String)> = Vec::new();
11000                for chunk in raw.split(',') {
11001                    let chunk = chunk.trim();
11002                    if chunk.is_empty() {
11003                        continue;
11004                    }
11005                    let (value, label) = match chunk.split_once(':') {
11006                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
11007                        None => (chunk.to_owned(), chunk.to_owned()),
11008                    };
11009                    if value.is_empty() {
11010                        return Err(syn::Error::new(
11011                            s.span(),
11012                            "`choices` entry has empty value before `:`",
11013                        ));
11014                    }
11015                    pairs.push((value, label));
11016                }
11017                if pairs.is_empty() {
11018                    return Err(syn::Error::new(
11019                        s.span(),
11020                        "`choices = \"…\"` must contain at least one value",
11021                    ));
11022                }
11023                out.choices = Some(pairs);
11024                return Ok(());
11025            }
11026            if meta.path.is_ident("db_comment") {
11027                let s: LitStr = meta.value()?.parse()?;
11028                out.db_comment = Some(s.value());
11029                return Ok(());
11030            }
11031            if meta.path.is_ident("verbose_name") {
11032                let s: LitStr = meta.value()?.parse()?;
11033                out.verbose_name = Some(s.value());
11034                return Ok(());
11035            }
11036            if meta.path.is_ident("secret") {
11037                // `#[rustango(secret)]` or `#[rustango(secret = false)]`.
11038                out.secret = Some(if let Ok(v) = meta.value() {
11039                    v.parse::<syn::LitBool>()?.value
11040                } else {
11041                    true
11042                });
11043                return Ok(());
11044            }
11045            if meta.path.is_ident("editable") {
11046                // Two forms accepted:
11047                //   #[rustango(editable = false)] / true — explicit
11048                //   #[rustango(editable)] — flag form (= true, the
11049                //   default, so harmless; included for symmetry)
11050                if let Ok(v) = meta.value() {
11051                    let lit: syn::LitBool = v.parse()?;
11052                    out.editable = lit.value;
11053                } else {
11054                    out.editable = true;
11055                }
11056                return Ok(());
11057            }
11058            if meta.path.is_ident("blank") {
11059                // Two forms accepted:
11060                //   #[rustango(blank)] — flag form, true
11061                //   #[rustango(blank = false)] / true — explicit
11062                if let Ok(v) = meta.value() {
11063                    let lit: syn::LitBool = v.parse()?;
11064                    out.blank = lit.value;
11065                } else {
11066                    out.blank = true;
11067                }
11068                return Ok(());
11069            }
11070            if meta.path.is_ident("citext") {
11071                // Case-insensitive text column (#344). Two forms:
11072                //   #[rustango(citext)]          — flag form, true
11073                //   #[rustango(citext = true)]   — explicit
11074                //   #[rustango(citext = false)]  — explicit opt-out
11075                // String-only validation lives in the field-type
11076                // emitter (the FieldType discriminant is computed in
11077                // `detect_type`); the macro records the flag and the
11078                // DDL writer emits dialect-specific COLLATE / CITEXT.
11079                if let Ok(v) = meta.value() {
11080                    let lit: syn::LitBool = v.parse()?;
11081                    out.case_insensitive = lit.value;
11082                } else {
11083                    out.case_insensitive = true;
11084                }
11085                return Ok(());
11086            }
11087            if meta.path.is_ident("validators") {
11088                let s: LitStr = meta.value()?.parse()?;
11089                let raw = s.value();
11090                out.validators = raw
11091                    .split(',')
11092                    .map(str::trim)
11093                    .filter(|s| !s.is_empty())
11094                    .map(|name| {
11095                        let mut ident = syn::parse_str::<syn::Ident>(name).map_err(|_| {
11096                            syn::Error::new(s.span(), format!("unknown validator `{name}`"))
11097                        })?;
11098                        ident.set_span(s.span());
11099                        Ok(ident)
11100                    })
11101                    .collect::<syn::Result<_>>()?;
11102                if out.validators.is_empty() {
11103                    return Err(syn::Error::new(
11104                        s.span(),
11105                        "`validators = \"…\"` must list at least one name",
11106                    ));
11107                }
11108                return Ok(());
11109            }
11110            if meta.path.is_ident("auto_uuid") {
11111                parse_flag(&meta, "auto_uuid")?;
11112                out.auto_uuid = true;
11113                // Implied: PK + auto + DEFAULT gen_random_uuid().
11114                // Each is also explicitly settable; the explicit
11115                // value wins if conflicting.
11116                out.primary_key = true;
11117                if out.default.is_none() {
11118                    out.default = Some("gen_random_uuid()".into());
11119                }
11120                return Ok(());
11121            }
11122            if meta.path.is_ident("default_uuid_v7") {
11123                parse_flag(&meta, "default_uuid_v7")?;
11124                // Backend-neutral counterpart of `auto_uuid` — issue #823.
11125                // No SQL DEFAULT (the macro fills the value Rust-side
11126                // before binding); just mark the field as PK + Auto
11127                // so the insert path is the `Auto::Unset → generate`
11128                // branch.
11129                out.default_uuid_v7 = true;
11130                out.primary_key = true;
11131                return Ok(());
11132            }
11133            if meta.path.is_ident("auto_now_add") {
11134                parse_flag(&meta, "auto_now_add")?;
11135                out.auto_now_add = true;
11136                if out.default.is_none() {
11137                    out.default = Some("now()".into());
11138                }
11139                return Ok(());
11140            }
11141            if meta.path.is_ident("auto_now") {
11142                parse_flag(&meta, "auto_now")?;
11143                out.auto_now = true;
11144                if out.default.is_none() {
11145                    out.default = Some("now()".into());
11146                }
11147                return Ok(());
11148            }
11149            if meta.path.is_ident("soft_delete") {
11150                parse_flag(&meta, "soft_delete")?;
11151                out.soft_delete = true;
11152                return Ok(());
11153            }
11154            if meta.path.is_ident("unique") {
11155                parse_flag(&meta, "unique")?;
11156                out.unique = true;
11157                return Ok(());
11158            }
11159            if meta.path.is_ident("index") {
11160                parse_flag(&meta, "index")?;
11161                out.index = true;
11162                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11163                if meta.input.peek(syn::token::Paren) {
11164                    meta.parse_nested_meta(|inner| {
11165                        if inner.path.is_ident("unique") {
11166                            out.index_unique = true;
11167                            return Ok(());
11168                        }
11169                        if inner.path.is_ident("name") {
11170                            let s: LitStr = inner.value()?.parse()?;
11171                            out.index_name = Some(s.value());
11172                            return Ok(());
11173                        }
11174                        if inner.path.is_ident("method") {
11175                            let s: LitStr = inner.value()?.parse()?;
11176                            let v = s.value();
11177                            match v.as_str() {
11178                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11179                                    out.index_method = v;
11180                                }
11181                                other => {
11182                                    return Err(inner.error(format!(
11183                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11184                                    )));
11185                                }
11186                            }
11187                            return Ok(());
11188                        }
11189                        Err(inner.error(
11190                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11191                        ))
11192                    })?;
11193                }
11194                return Ok(());
11195            }
11196            Err(unknown_key_error(&meta, "field", FIELD_ATTR_KEYS))
11197        })?;
11198    }
11199    Ok(out)
11200}
11201
11202/// Parse a signed integer literal, accepting optional leading `-`.
11203fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11204    let expr: syn::Expr = meta.value()?.parse()?;
11205    match expr {
11206        syn::Expr::Lit(syn::ExprLit {
11207            lit: syn::Lit::Int(lit),
11208            ..
11209        }) => lit.base10_parse::<i64>(),
11210        syn::Expr::Unary(syn::ExprUnary {
11211            op: syn::UnOp::Neg(_),
11212            expr,
11213            ..
11214        }) => {
11215            if let syn::Expr::Lit(syn::ExprLit {
11216                lit: syn::Lit::Int(lit),
11217                ..
11218            }) = *expr
11219            {
11220                let v: i64 = lit.base10_parse()?;
11221                Ok(-v)
11222            } else {
11223                Err(syn::Error::new_spanned(expr, "expected integer literal"))
11224            }
11225        }
11226        other => Err(syn::Error::new_spanned(
11227            other,
11228            "expected integer literal (signed)",
11229        )),
11230    }
11231}
11232
11233struct FieldInfo<'a> {
11234    ident: &'a syn::Ident,
11235    column: String,
11236    primary_key: bool,
11237    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11238    /// skip this column when `Auto::Unset` and emit it under
11239    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11240    auto: bool,
11241    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11242    /// the `Column::Value` associated type for typed-column tokens.
11243    value_ty: &'a Type,
11244    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11245    field_type_tokens: TokenStream2,
11246    schema: TokenStream2,
11247    from_row_init: TokenStream2,
11248    /// Variant of [`Self::from_row_init`] that reads the column via
11249    /// `format!("{prefix}__{col}")` so a model can be decoded out of
11250    /// the aliased columns of a JOINed row. Drives slice 9.0d's
11251    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11252    /// helper that `select_related` calls when stitching loaded FKs.
11253    from_aliased_row_init: TokenStream2,
11254    /// MySQL twins of the two inits above. They differ only for a UUID
11255    /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11256    from_row_init_my: TokenStream2,
11257    from_aliased_row_init_my: TokenStream2,
11258    /// A bare or optional `Uuid` field.
11259    uuid_column: bool,
11260    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11261    /// relation helper emit (`Author::<child>_set`) needs to know `T`
11262    /// to point the generated method at the right child model.
11263    fk_inner: Option<Type>,
11264    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11265    /// `kind` (since ForeignKey detection sets `kind` to K's
11266    /// underlying type) but stored separately for clarity at the
11267    /// `FkRelation` construction site, which only sees the FK's
11268    /// surface fields.
11269    fk_pk_kind: DetectedKind,
11270    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11271    /// the bare `ForeignKey<T, K>`. Routes the load_related and
11272    /// fk_pk_access emitters to wrap assignments / accessors in
11273    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11274    /// FK column compiles end-to-end. The DDL writer reads this off
11275    /// the field schema (`nullable` flag); the macro just needs to
11276    /// keep the Rust-side codegen consistent.
11277    nullable: bool,
11278    /// `true` when this column was marked `#[rustango(auto_now)]` —
11279    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11280    /// column instead of the user-supplied value, so `updated_at`
11281    /// always reflects the latest write without the caller having
11282    /// to remember to set it.
11283    auto_now: bool,
11284    /// `true` when this column was marked `#[rustango(auto_now_add)]`
11285    /// — the column is server-set on INSERT (DB DEFAULT) and
11286    /// **immutable** afterwards. `update_on` / `save_on` skip the
11287    /// column entirely so a stale `created_at` value in memory never
11288    /// rewrites the persisted timestamp.
11289    auto_now_add: bool,
11290    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11291    /// Triggers emission of `soft_delete_on(executor)` and
11292    /// `restore_on(executor)` on the model's inherent impl. There is
11293    /// at most one such column per model — emission asserts this.
11294    soft_delete: bool,
11295    /// `Some` when this column was marked
11296    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11297    /// every INSERT and UPDATE path; the database recomputes the
11298    /// value from `EXPR`. Backlog item #35.
11299    generated_as: Option<String>,
11300    /// `true` when this column was marked
11301    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11302    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11303    /// with `Uuid::now_v7()` before binding, so the PK is generated
11304    /// Rust-side and the column is always present in the INSERT
11305    /// statement (no DB DEFAULT requirement). Issue #823.
11306    default_uuid_v7: bool,
11307    /// `Some` when this FK field carried `#[rustango(related_name =
11308    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11309    /// consumed by [`reverse_helper_tokens`] to override the default
11310    /// `<child_snake>_set` accessor name. Follow-up to #816.
11311    related_name: Option<String>,
11312}
11313
11314/// Reject table names that won't survive SQL identifier
11315/// derivation downstream. Postgres' regular-identifier rule
11316/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11317/// through the framework's unquoted FK / index / constraint name
11318/// emission without surprises. We also disallow leading-digit and
11319/// the empty string for clarity.
11320///
11321/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11322/// here — those produce a runtime error from the SQL parser,
11323/// which is loud enough; statically enumerating reserved words
11324/// across the three supported dialects is more friction than help.
11325///
11326/// Backlog item #65.
11327fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11328    validate_sql_identifier(name, "table", span)
11329}
11330
11331/// Reject SQL identifiers that compile but break downstream SQL
11332/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11333/// `kind` is "table" / "column" — used for the error message so users
11334/// see which attribute caused the failure.
11335fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11336    if name.is_empty() {
11337        return Err(syn::Error::new(
11338            span,
11339            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11340        ));
11341    }
11342    let mut chars = name.chars();
11343    let first = chars.next().unwrap();
11344    if !(first.is_ascii_alphabetic() || first == '_') {
11345        return Err(syn::Error::new(
11346            span,
11347            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11348        ));
11349    }
11350    for c in chars {
11351        if !(c.is_ascii_alphanumeric() || c == '_') {
11352            return Err(syn::Error::new(
11353                span,
11354                format!(
11355                    "{kind} name `{name}` contains invalid character {c:?} — \
11356                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11357                     Hyphens in particular break FK / index name derivation \
11358                     downstream; use underscores instead (e.g. `{}`)",
11359                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11360                ),
11361            ));
11362        }
11363    }
11364    Ok(())
11365}
11366
11367fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11368    let root = rustango_root();
11369    let attrs = parse_field_attrs(field)?;
11370    let ident = field
11371        .ident
11372        .as_ref()
11373        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11374    let name = ident_name(ident);
11375    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11376    let primary_key = attrs.primary_key;
11377    let DetectedType {
11378        kind,
11379        nullable,
11380        auto: detected_auto,
11381        fk_inner,
11382    } = detect_type(&field.ty)?;
11383    check_bound_compatibility(field, &attrs, kind)?;
11384    let auto = detected_auto;
11385    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11386    // INSERT path: the user must wrap the field in `Auto<T>`, which
11387    // marks the column as DB-default-supplied. The mixin attrs then
11388    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11389    // for `auto_now`, force the value on UPDATE too.
11390    if attrs.auto_uuid {
11391        if kind != DetectedKind::Uuid {
11392            return Err(syn::Error::new_spanned(
11393                field,
11394                "`#[rustango(auto_uuid)]` requires the field type to be \
11395                 `Auto<uuid::Uuid>`",
11396            ));
11397        }
11398        if !detected_auto {
11399            return Err(syn::Error::new_spanned(
11400                field,
11401                "`#[rustango(auto_uuid)]` requires the field type to be \
11402                 wrapped in `Auto<...>` so the macro skips the column on \
11403                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11404            ));
11405        }
11406    }
11407    if attrs.default_uuid_v7 {
11408        if kind != DetectedKind::Uuid {
11409            return Err(syn::Error::new_spanned(
11410                field,
11411                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11412                 `Auto<uuid::Uuid>`",
11413            ));
11414        }
11415        if !detected_auto {
11416            return Err(syn::Error::new_spanned(
11417                field,
11418                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11419                 wrapped in `Auto<...>` so the macro can detect the \
11420                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11421            ));
11422        }
11423        if attrs.auto_uuid {
11424            return Err(syn::Error::new_spanned(
11425                field,
11426                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11427                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11428                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11429                 Pick one.",
11430            ));
11431        }
11432    }
11433    if attrs.auto_now_add || attrs.auto_now {
11434        if kind != DetectedKind::DateTime {
11435            return Err(syn::Error::new_spanned(
11436                field,
11437                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11438                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11439            ));
11440        }
11441        if !detected_auto {
11442            return Err(syn::Error::new_spanned(
11443                field,
11444                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11445                 the field type to be wrapped in `Auto<...>` so the macro skips \
11446                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11447            ));
11448        }
11449    }
11450    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11451        return Err(syn::Error::new_spanned(
11452            field,
11453            "`#[rustango(soft_delete)]` requires the field type to be \
11454             `Option<chrono::DateTime<chrono::Utc>>`",
11455        ));
11456    }
11457    let is_mixin_auto =
11458        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11459    if detected_auto && !primary_key && !is_mixin_auto {
11460        return Err(syn::Error::new_spanned(
11461            field,
11462            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11463             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11464             `auto_now`",
11465        ));
11466    }
11467    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11468        return Err(syn::Error::new_spanned(
11469            field,
11470            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11471             SERIAL / BIGSERIAL already supplies a default sequence.",
11472        ));
11473    }
11474    if fk_inner.is_some() && primary_key {
11475        return Err(syn::Error::new_spanned(
11476            field,
11477            "`ForeignKey<T>` is not allowed on a primary-key field — \
11478             a row's PK is its own identity, not a reference to a parent.",
11479        ));
11480    }
11481    if attrs.generated_as.is_some() {
11482        if primary_key {
11483            return Err(syn::Error::new_spanned(
11484                field,
11485                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11486                 primary-key field — a PK must be writable so the row \
11487                 has an identity at INSERT time.",
11488            ));
11489        }
11490        if attrs.default.is_some() {
11491            return Err(syn::Error::new_spanned(
11492                field,
11493                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11494                 `default = \"…\"` — Postgres rejects DEFAULT on \
11495                 generated columns. The expression IS the default.",
11496            ));
11497        }
11498        if detected_auto {
11499            return Err(syn::Error::new_spanned(
11500                field,
11501                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11502                 an `Auto<T>` field — generated columns are computed \
11503                 by the DB, not server-assigned via a sequence. Use a \
11504                 plain Rust type (e.g. `f64`).",
11505            ));
11506        }
11507        if fk_inner.is_some() {
11508            return Err(syn::Error::new_spanned(
11509                field,
11510                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11511                 ForeignKey field.",
11512            ));
11513        }
11514    }
11515    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11516    let column_lit = column.as_str();
11517    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11518    // dimension that the bare `Vector` Rust type can't carry, so emit
11519    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11520    // fallback of `Vector(0)`.
11521    let field_type_tokens = if kind == DetectedKind::Vector {
11522        let root = rustango_root();
11523        let dims = attrs.vector_dims.unwrap_or(0);
11524        quote!(#root::core::FieldType::Vector(#dims))
11525    } else if kind == DetectedKind::Geometry {
11526        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11527        // SRID that the bare `Point` Rust type can't carry, so emit
11528        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11529        let root = rustango_root();
11530        let srid = attrs.geometry_srid.unwrap_or(0);
11531        quote!(#root::core::FieldType::Geometry(#srid))
11532    } else {
11533        kind.variant_tokens()
11534    };
11535    let max_length = optional_u32(attrs.max_length);
11536    let min = optional_i64(attrs.min);
11537    let max = optional_i64(attrs.max);
11538    let default = optional_str(attrs.default.as_deref());
11539
11540    let unique = attrs.unique;
11541    let generated_as = optional_str(attrs.generated_as.as_deref());
11542    let help_text = optional_str(attrs.help_text.as_deref());
11543    let choices = optional_choices(attrs.choices.as_deref());
11544    let db_comment = optional_str(attrs.db_comment.as_deref());
11545    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11546    let editable = attrs.editable;
11547    let secret = match attrs.secret {
11548        Some(v) => quote!(::core::option::Option::Some(#v)),
11549        None => quote!(::core::option::Option::None),
11550    };
11551    let blank = attrs.blank;
11552    let case_insensitive = attrs.case_insensitive;
11553    // A path per name: one the runtime does not know fails to resolve.
11554    let validators = &attrs.validators;
11555    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11556    {
11557        return Err(syn::Error::new_spanned(
11558            field,
11559            "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11560             or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11561             has no meaning on a non-FK column.",
11562        ));
11563    }
11564    if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11565        return Err(syn::Error::new_spanned(
11566            field,
11567            "`on_delete = \"set_null\"` needs a nullable column: use \
11568             `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11569             alias for `Option` is not seen).",
11570        ));
11571    }
11572    let fk_on_delete = match attrs.on_delete.as_deref() {
11573        None => quote!(::core::option::Option::None),
11574        Some(action) => {
11575            let variant = match action {
11576                "cascade" => quote!(Cascade),
11577                "restrict" => quote!(Restrict),
11578                "set_null" => quote!(SetNull),
11579                "set_default" => quote!(SetDefault),
11580                "no_action" => quote!(NoAction),
11581                // parse_field_attrs already validated this — guard against future drift.
11582                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11583            };
11584            quote!(::core::option::Option::Some(
11585                #root::core::OnDeleteAction::#variant
11586            ))
11587        }
11588    };
11589    let auto_now_flag = attrs.auto_now;
11590    let cast = match cast_type(&field.ty) {
11591        Some(ty) => quote!(::core::option::Option::Some(<#ty>::FIELD_CAST)),
11592        None => quote!(::core::option::Option::None),
11593    };
11594    // A sealed cast is always a credential: `secret = false` cannot opt it out.
11595    let sealed_guard = match (attrs.secret, cast_type(&field.ty)) {
11596        (Some(false), Some(ty)) => quote! {
11597            const _: () = ::core::assert!(
11598                !::core::matches!(<#ty>::FIELD_CAST, #root::core::FieldCast::Sealed),
11599                "`#[rustango(secret = false)]` on a sealed cast: it is always a credential",
11600            );
11601        },
11602        _ => quote!(),
11603    };
11604    let schema = quote! {{
11605        #sealed_guard
11606        #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11607            name: #name,
11608            column: #column_lit,
11609            ty: #field_type_tokens,
11610            nullable: #nullable,
11611            primary_key: #primary_key,
11612            relation: #relation,
11613            max_length: #max_length,
11614            min: #min,
11615            max: #max,
11616            default: #default,
11617            auto: #auto,
11618            auto_now: #auto_now_flag,
11619            unique: #unique,
11620            generated_as: #generated_as,
11621            help_text: #help_text,
11622            choices: #choices,
11623            db_comment: #db_comment,
11624            verbose_name: #verbose_name,
11625            editable: #editable,
11626            blank: #blank,
11627            case_insensitive: #case_insensitive,
11628            fk_on_delete: #fk_on_delete,
11629            validators: &[ #( #root::core::__validator_names::#validators ),* ],
11630            cast: #cast,
11631            secret: #secret,
11632        })
11633    }};
11634
11635    // sqlx-sqlite has no `Decimal` decode: it goes through `FlatScalar`'s cell.
11636    let decimal_column = kind == DetectedKind::Decimal && !detected_auto && fk_inner.is_none();
11637    let (from_row_init, from_aliased_row_init) = if decimal_column {
11638        (
11639            quote! { #ident: #root::sql::try_get_flat(row, #column_lit)? },
11640            quote! {
11641                #ident: #root::sql::try_get_flat(
11642                    row,
11643                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11644                )?
11645            },
11646        )
11647    } else {
11648        (
11649            quote! { #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)? },
11650            quote! {
11651                #ident: #root::sql::sqlx::Row::try_get(
11652                    row,
11653                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11654                )?
11655            },
11656        )
11657    };
11658    // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11659    // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11660    let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11661    let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11662        (
11663            quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11664            quote! {
11665                #ident: #root::sql::try_get_flat_my(
11666                    row,
11667                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11668                )?
11669            },
11670        )
11671    } else {
11672        (from_row_init.clone(), from_aliased_row_init.clone())
11673    };
11674
11675    Ok(FieldInfo {
11676        ident,
11677        column,
11678        primary_key,
11679        auto,
11680        value_ty: &field.ty,
11681        field_type_tokens,
11682        schema,
11683        from_row_init,
11684        from_aliased_row_init,
11685        from_row_init_my,
11686        from_aliased_row_init_my,
11687        uuid_column,
11688        fk_inner: fk_inner.cloned(),
11689        fk_pk_kind: kind,
11690        nullable,
11691        auto_now: attrs.auto_now,
11692        auto_now_add: attrs.auto_now_add,
11693        soft_delete: attrs.soft_delete,
11694        generated_as: attrs.generated_as.clone(),
11695        default_uuid_v7: attrs.default_uuid_v7,
11696        related_name: attrs.related_name.clone(),
11697    })
11698}
11699
11700fn check_bound_compatibility(
11701    field: &syn::Field,
11702    attrs: &FieldAttrs,
11703    kind: DetectedKind,
11704) -> syn::Result<()> {
11705    if attrs.max_length.is_some() && kind != DetectedKind::String {
11706        return Err(syn::Error::new_spanned(
11707            field,
11708            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11709        ));
11710    }
11711    // These were silently dropped on the wrong type (#1937).
11712    if attrs.case_insensitive && kind != DetectedKind::String {
11713        return Err(syn::Error::new_spanned(
11714            field,
11715            "`citext` is only valid on `String` fields (or `Option<String>`)",
11716        ));
11717    }
11718    if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11719        return Err(syn::Error::new_spanned(
11720            field,
11721            "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11722        ));
11723    }
11724    if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11725        return Err(syn::Error::new_spanned(
11726            field,
11727            "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11728        ));
11729    }
11730    if attrs.choices.is_some() && kind != DetectedKind::String {
11731        return Err(syn::Error::new_spanned(
11732            field,
11733            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11734             integer-valued enumerations should be modeled with a Rust enum and \
11735             custom (de)serializer for now",
11736        ));
11737    }
11738    // They run only on string values, so elsewhere they would never fire.
11739    if !attrs.validators.is_empty() && kind != DetectedKind::String {
11740        return Err(syn::Error::new_spanned(
11741            field,
11742            "`validators` is only valid on `String` fields (or `Option<String>`)",
11743        ));
11744    }
11745    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11746        return Err(syn::Error::new_spanned(
11747            field,
11748            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11749        ));
11750    }
11751    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11752        if min > max {
11753            return Err(syn::Error::new_spanned(
11754                field,
11755                format!("`min` ({min}) is greater than `max` ({max})"),
11756            ));
11757        }
11758    }
11759    if let Some(default) = &attrs.default {
11760        check_default_expr(default, kind).map_err(|msg| syn::Error::new_spanned(field, msg))?;
11761    }
11762    Ok(())
11763}
11764
11765/// SQL words a `default` may be on its own; any other bare word is a
11766/// column reference, which Postgres and MySQL refuse in a DEFAULT.
11767const DEFAULT_KEYWORDS: &[&str] = &[
11768    "NULL",
11769    "TRUE",
11770    "FALSE",
11771    "CURRENT_TIMESTAMP",
11772    "CURRENT_DATE",
11773    "CURRENT_TIME",
11774    "LOCALTIMESTAMP",
11775    "LOCALTIME",
11776];
11777
11778/// `default` is raw SQL. Refuse the shapes only SQLite accepts: a bare
11779/// word (read as a column on PG/MySQL) and a non-numeric literal on a
11780/// numeric field.
11781fn check_default_expr(default: &str, kind: DetectedKind) -> Result<(), String> {
11782    let expr = default.trim();
11783    let is_word = expr.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
11784        && expr
11785            .chars()
11786            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ' ');
11787    let upper = expr.to_ascii_uppercase();
11788    if is_word && ["CURRENT_USER", "SESSION_USER", "USER"].contains(&upper.as_str()) {
11789        return Err(format!(
11790            "`default = \"{expr}\"` works on Postgres only: MySQL refuses it as a default \
11791             and SQLite stores the text `{expr}`; set the value in code instead"
11792        ));
11793    }
11794    if is_word && !DEFAULT_KEYWORDS.contains(&upper.as_str()) {
11795        return Err(format!(
11796            "`default = \"{expr}\"` is raw SQL, so `{expr}` is read as a column name; \
11797             for the text '{expr}' write `default = \"'{expr}'\"`"
11798        ));
11799    }
11800    let numeric = kind.is_integer()
11801        || matches!(
11802            kind,
11803            DetectedKind::F32 | DetectedKind::F64 | DetectedKind::Decimal
11804        );
11805    if numeric {
11806        let unquoted = expr
11807            .strip_prefix('\'')
11808            .and_then(|e| e.strip_suffix('\''))
11809            .unwrap_or(expr);
11810        let is_number = if kind.is_integer() {
11811            unquoted.parse::<i64>().is_ok()
11812        } else {
11813            unquoted.parse::<f64>().is_ok_and(f64::is_finite)
11814        };
11815        // Keep expressions (`(1 + 2)`, `abs(-1)`) and NULL; refuse a
11816        // literal that is no number. `""` renders as `''`.
11817        let is_literal = is_word || expr.is_empty() || expr.starts_with('\'');
11818        if is_literal && !is_number && upper != "NULL" {
11819            return Err(format!(
11820                "`default = \"{expr}\"` is not a number, but the field is numeric"
11821            ));
11822        }
11823    }
11824    Ok(())
11825}
11826
11827/// A `&'static [ty]` of `const fn` results. The slice needs its own
11828/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11829fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11830    quote!({
11831        const ITEMS: &[#ty] = &[ #(#items),* ];
11832        ITEMS
11833    })
11834}
11835
11836fn optional_u32(value: Option<u32>) -> TokenStream2 {
11837    if let Some(v) = value {
11838        quote!(::core::option::Option::Some(#v))
11839    } else {
11840        quote!(::core::option::Option::None)
11841    }
11842}
11843
11844fn optional_i64(value: Option<i64>) -> TokenStream2 {
11845    if let Some(v) = value {
11846        quote!(::core::option::Option::Some(#v))
11847    } else {
11848        quote!(::core::option::Option::None)
11849    }
11850}
11851
11852fn optional_str(value: Option<&str>) -> TokenStream2 {
11853    if let Some(v) = value {
11854        quote!(::core::option::Option::Some(#v))
11855    } else {
11856        quote!(::core::option::Option::None)
11857    }
11858}
11859
11860fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11861    let Some(pairs) = pairs else {
11862        return quote!(::core::option::Option::None);
11863    };
11864    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11865    quote!(::core::option::Option::Some(&[#(#entries),*]))
11866}
11867
11868fn relation_tokens(
11869    field: &syn::Field,
11870    attrs: &FieldAttrs,
11871    fk_inner: Option<&syn::Type>,
11872    table: &str,
11873) -> syn::Result<TokenStream2> {
11874    let root = rustango_root();
11875    if let Some(inner) = fk_inner {
11876        if attrs.fk.is_some() || attrs.o2o.is_some() {
11877            return Err(syn::Error::new_spanned(
11878                field,
11879                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11880                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11881            ));
11882        }
11883        // No `on`: the target's PK column, which may be renamed.
11884        let on = match attrs.on.as_deref() {
11885            Some(on) => quote!(#on),
11886            None => quote! {
11887                match <#inner as #root::core::Model>::SCHEMA.pk_column() {
11888                    ::core::option::Option::Some(c) => c,
11889                    ::core::option::Option::None => "id",
11890                }
11891            },
11892        };
11893        return Ok(quote! {
11894            ::core::option::Option::Some(#root::core::Relation::fk(
11895                <#inner as #root::core::Model>::SCHEMA.table,
11896                #on,
11897            ))
11898        });
11899    }
11900    match (&attrs.fk, &attrs.o2o) {
11901        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11902            field,
11903            "`fk` and `o2o` are mutually exclusive",
11904        )),
11905        (Some(to), None) => {
11906            let on = attrs.on.as_deref().unwrap_or("id");
11907            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11908            // the model's own table. Threaded as a literal string at
11909            // macro-expansion time to sidestep the const-eval cycle
11910            // that `Self::SCHEMA.table` would create when referenced
11911            // inside Self::SCHEMA's own initializer.
11912            let resolved = if to == "self" { table } else { to };
11913            Ok(quote! {
11914                ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11915            })
11916        }
11917        (None, Some(to)) => {
11918            let on = attrs.on.as_deref().unwrap_or("id");
11919            let resolved = if to == "self" { table } else { to };
11920            Ok(quote! {
11921                ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11922            })
11923        }
11924        (None, None) => {
11925            if attrs.on.is_some() {
11926                return Err(syn::Error::new_spanned(
11927                    field,
11928                    "`on` requires `fk` or `o2o`",
11929                ));
11930            }
11931            Ok(quote!(::core::option::Option::None))
11932        }
11933    }
11934}
11935
11936/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11937/// about kinds without depending on `rustango-core` (which would require a
11938/// proc-macro/normal split it doesn't have today).
11939#[derive(Clone, Copy, PartialEq, Eq)]
11940enum DetectedKind {
11941    I16,
11942    I32,
11943    I64,
11944    F32,
11945    F64,
11946    Bool,
11947    String,
11948    DateTime,
11949    Date,
11950    Time,
11951    Uuid,
11952    Json,
11953    Decimal,
11954    Binary,
11955    /// `Array<String>` → PG `text[]` (#341).
11956    ArrayText,
11957    /// `Array<i32>` → PG `integer[]` (#341).
11958    ArrayInt,
11959    /// `Array<i64>` → PG `bigint[]` (#341).
11960    ArrayBigInt,
11961    /// `Range<i32>` → PG `int4range` (#343).
11962    RangeInt,
11963    /// `Range<i64>` → PG `int8range` (#343).
11964    RangeBigInt,
11965    /// `Range<Decimal>` → PG `numrange` (#343).
11966    RangeNumeric,
11967    /// `Range<NaiveDate>` → PG `daterange` (#343).
11968    RangeDate,
11969    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11970    RangeDateTime,
11971    /// `HStore` → PG `hstore` (#342).
11972    HStore,
11973    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11974    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11975    /// in at the `FieldType` emission site (not carried on this enum).
11976    Vector,
11977    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11978    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11979    /// threaded in at the `FieldType` emission site.
11980    Geometry,
11981}
11982
11983impl DetectedKind {
11984    fn variant_tokens(self) -> TokenStream2 {
11985        let root = rustango_root();
11986        match self {
11987            Self::I16 => quote!(#root::core::FieldType::I16),
11988            Self::I32 => quote!(#root::core::FieldType::I32),
11989            Self::I64 => quote!(#root::core::FieldType::I64),
11990            Self::F32 => quote!(#root::core::FieldType::F32),
11991            Self::F64 => quote!(#root::core::FieldType::F64),
11992            Self::Bool => quote!(#root::core::FieldType::Bool),
11993            Self::String => quote!(#root::core::FieldType::String),
11994            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11995            Self::Date => quote!(#root::core::FieldType::Date),
11996            Self::Time => quote!(#root::core::FieldType::Time),
11997            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11998            Self::Json => quote!(#root::core::FieldType::Json),
11999            Self::Decimal => quote!(#root::core::FieldType::Decimal),
12000            Self::Binary => quote!(#root::core::FieldType::Binary),
12001            Self::ArrayText => {
12002                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
12003            }
12004            Self::ArrayInt => {
12005                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
12006            }
12007            Self::ArrayBigInt => {
12008                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
12009            }
12010            Self::RangeInt => {
12011                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
12012            }
12013            Self::RangeBigInt => {
12014                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
12015            }
12016            Self::RangeNumeric => {
12017                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
12018            }
12019            Self::RangeDate => {
12020                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
12021            }
12022            Self::RangeDateTime => {
12023                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
12024            }
12025            Self::HStore => quote!(#root::core::FieldType::HStore),
12026            // Dimension comes from the `vector(dims = N)` attribute,
12027            // applied at the emission site; `0` here is just a fallback.
12028            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
12029            // SRID comes from the `geometry(srid = N)` attribute, applied
12030            // at the emission site; `0` here is just a fallback.
12031            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
12032        }
12033    }
12034
12035    fn is_integer(self) -> bool {
12036        matches!(self, Self::I16 | Self::I32 | Self::I64)
12037    }
12038}
12039
12040/// Result of walking a field's Rust type. `kind` is the underlying
12041/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
12042/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
12043/// `Some(<T>)` when the field was `ForeignKey<T>` (or
12044/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
12045#[derive(Clone, Copy)]
12046struct DetectedType<'a> {
12047    kind: DetectedKind,
12048    nullable: bool,
12049    auto: bool,
12050    fk_inner: Option<&'a syn::Type>,
12051}
12052
12053/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
12054/// non-`Auto` types — the caller should already have routed Auto-only
12055/// codegen through this helper, so a `None` indicates a macro-internal
12056/// invariant break.
12057fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
12058    let Type::Path(TypePath { path, qself: None }) = ty else {
12059        return None;
12060    };
12061    let last = path.segments.last()?;
12062    if last.ident != "Auto" {
12063        return None;
12064    }
12065    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12066        return None;
12067    };
12068    args.args.iter().find_map(|a| match a {
12069        syn::GenericArgument::Type(t) => Some(t),
12070        _ => None,
12071    })
12072}
12073
12074/// The `Cast<C>` inside `ty` (bare or in an `Option`), for its schema record.
12075fn cast_type(ty: &syn::Type) -> Option<&syn::Type> {
12076    let Type::Path(TypePath { path, qself: None }) = ty else {
12077        return None;
12078    };
12079    let last = path.segments.last()?;
12080    if last.ident == "Cast" {
12081        return Some(ty);
12082    }
12083    if last.ident != "Option" {
12084        return None;
12085    }
12086    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12087        return None;
12088    };
12089    match args.args.first()? {
12090        syn::GenericArgument::Type(inner) => cast_type(inner).filter(|_| args.args.len() == 1),
12091        _ => None,
12092    }
12093}
12094
12095fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
12096    let Type::Path(TypePath { path, qself: None }) = ty else {
12097        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
12098    };
12099    let last = path
12100        .segments
12101        .last()
12102        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
12103
12104    if last.ident == "Option" {
12105        let inner = generic_inner(ty, &last.arguments, "Option")?;
12106        let inner_det = detect_type(inner)?;
12107        if inner_det.nullable {
12108            return Err(syn::Error::new_spanned(
12109                ty,
12110                "nested Option is not supported",
12111            ));
12112        }
12113        if inner_det.auto {
12114            return Err(syn::Error::new_spanned(
12115                ty,
12116                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12117            ));
12118        }
12119        return Ok(DetectedType {
12120            nullable: true,
12121            ..inner_det
12122        });
12123    }
12124
12125    if last.ident == "Auto" {
12126        let inner = generic_inner(ty, &last.arguments, "Auto")?;
12127        let inner_det = detect_type(inner)?;
12128        if inner_det.auto {
12129            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
12130        }
12131        if inner_det.nullable {
12132            return Err(syn::Error::new_spanned(
12133                ty,
12134                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12135            ));
12136        }
12137        if inner_det.fk_inner.is_some() {
12138            return Err(syn::Error::new_spanned(
12139                ty,
12140                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
12141            ));
12142        }
12143        if !matches!(
12144            inner_det.kind,
12145            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
12146        ) {
12147            return Err(syn::Error::new_spanned(
12148                ty,
12149                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
12150                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
12151                 (DEFAULT now())",
12152            ));
12153        }
12154        return Ok(DetectedType {
12155            auto: true,
12156            ..inner_det
12157        });
12158    }
12159
12160    if last.ident == "ForeignKey" {
12161        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
12162        // Resolve the FK column's underlying SQL type from `K`. When the
12163        // user wrote `ForeignKey<T>` without a key parameter, the type
12164        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
12165        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
12166        // recurse into K so the column DDL emits the right SQL type
12167        // (VARCHAR for String, UUID for Uuid, …) and the load_related
12168        // emitter knows which `SqlValue` variant to match.
12169        let kind = match key_ty {
12170            Some(k) => detect_type(k)?.kind,
12171            None => DetectedKind::I64,
12172        };
12173        return Ok(DetectedType {
12174            kind,
12175            nullable: false,
12176            auto: false,
12177            fk_inner: Some(inner),
12178        });
12179    }
12180
12181    let kind = match last.ident.to_string().as_str() {
12182        "i16" => DetectedKind::I16,
12183        "i32" => DetectedKind::I32,
12184        "i64" => DetectedKind::I64,
12185        "f32" => DetectedKind::F32,
12186        "f64" => DetectedKind::F64,
12187        "bool" => DetectedKind::Bool,
12188        "String" => DetectedKind::String,
12189        "DateTime" => DetectedKind::DateTime,
12190        "NaiveDate" => DetectedKind::Date,
12191        "NaiveTime" => DetectedKind::Time,
12192        "Uuid" => DetectedKind::Uuid,
12193        "Value" => DetectedKind::Json,
12194        "Decimal" => DetectedKind::Decimal,
12195        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
12196        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
12197        // — that would emit Binary DDL and decode-fail at runtime.
12198        "Vec" => {
12199            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
12200            if let Type::Path(TypePath { path, qself: None }) = inner {
12201                if let Some(seg) = path.segments.last() {
12202                    if seg.ident == "u8" && seg.arguments.is_empty() {
12203                        return Ok(DetectedType {
12204                            kind: DetectedKind::Binary,
12205                            nullable: false,
12206                            auto: false,
12207                            fk_inner: None,
12208                        });
12209                    }
12210                }
12211            }
12212            return Err(syn::Error::new_spanned(
12213                ty,
12214                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
12215                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
12216            ));
12217        }
12218        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
12219        // `integer[]` / `bigint[]` (#341).
12220        "Array" => {
12221            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
12222            let elem = match inner {
12223                Type::Path(TypePath { path, qself: None }) => {
12224                    path.segments.last().map(|s| s.ident.to_string())
12225                }
12226                _ => None,
12227            };
12228            let kind = match elem.as_deref() {
12229                Some("String") => DetectedKind::ArrayText,
12230                Some("i32") => DetectedKind::ArrayInt,
12231                Some("i64") => DetectedKind::ArrayBigInt,
12232                _ => {
12233                    return Err(syn::Error::new_spanned(
12234                        ty,
12235                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12236                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12237                    ));
12238                }
12239            };
12240            return Ok(DetectedType {
12241                kind,
12242                nullable: false,
12243                auto: false,
12244                fk_inner: None,
12245            });
12246        }
12247        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12248        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12249        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12250        "Range" => {
12251            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12252            let elem = match inner {
12253                Type::Path(TypePath { path, qself: None }) => {
12254                    path.segments.last().map(|s| s.ident.to_string())
12255                }
12256                _ => None,
12257            };
12258            let kind = match elem.as_deref() {
12259                Some("i32") => DetectedKind::RangeInt,
12260                Some("i64") => DetectedKind::RangeBigInt,
12261                Some("Decimal") => DetectedKind::RangeNumeric,
12262                Some("NaiveDate") => DetectedKind::RangeDate,
12263                Some("DateTime") => DetectedKind::RangeDateTime,
12264                _ => {
12265                    return Err(syn::Error::new_spanned(
12266                        ty,
12267                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12268                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12269                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12270                         (→ tstzrange) are supported (#343)",
12271                    ));
12272                }
12273            };
12274            return Ok(DetectedType {
12275                kind,
12276                nullable: false,
12277                auto: false,
12278                fk_inner: None,
12279            });
12280        }
12281        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12282        // (the `CastValue` impl bridges logical↔stored); the field's own
12283        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12284        // schema just needs `FieldType::String`.
12285        "Cast" => {
12286            return Ok(DetectedType {
12287                kind: DetectedKind::String,
12288                nullable: false,
12289                auto: false,
12290                fk_inner: None,
12291            });
12292        }
12293        // `HStore` → PG `hstore` (#342). No generic
12294        // parameter — always a string→string map.
12295        "HStore" => {
12296            return Ok(DetectedType {
12297                kind: DetectedKind::HStore,
12298                nullable: false,
12299                auto: false,
12300                fk_inner: None,
12301            });
12302        }
12303        // `Vector` → pgvector `vector(N)` (#824). The dimension is
12304        // supplied by `#[rustango(vector(dims = N))]`, not the type.
12305        "Vector" => {
12306            return Ok(DetectedType {
12307                kind: DetectedKind::Vector,
12308                nullable: false,
12309                auto: false,
12310                fk_inner: None,
12311            });
12312        }
12313        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12314        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12315        "Point" => {
12316            return Ok(DetectedType {
12317                kind: DetectedKind::Geometry,
12318                nullable: false,
12319                auto: false,
12320                fk_inner: None,
12321            });
12322        }
12323        other => {
12324            return Err(syn::Error::new_spanned(
12325                ty,
12326                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)"),
12327            ));
12328        }
12329    };
12330    Ok(DetectedType {
12331        kind,
12332        nullable: false,
12333        auto: false,
12334        fk_inner: None,
12335    })
12336}
12337
12338fn generic_inner<'a>(
12339    ty: &'a Type,
12340    arguments: &'a PathArguments,
12341    wrapper: &str,
12342) -> syn::Result<&'a Type> {
12343    let PathArguments::AngleBracketed(args) = arguments else {
12344        return Err(syn::Error::new_spanned(
12345            ty,
12346            format!("{wrapper} requires a generic argument"),
12347        ));
12348    };
12349    args.args
12350        .iter()
12351        .find_map(|a| match a {
12352            GenericArgument::Type(t) => Some(t),
12353            _ => None,
12354        })
12355        .ok_or_else(|| {
12356            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12357        })
12358}
12359
12360/// Like [`generic_inner`] but pulls *two* type args — the first is
12361/// required, the second is optional. Used by the `ForeignKey<T, K>`
12362/// detection where K defaults to `i64` when omitted.
12363fn generic_pair<'a>(
12364    ty: &'a Type,
12365    arguments: &'a PathArguments,
12366    wrapper: &str,
12367) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12368    let PathArguments::AngleBracketed(args) = arguments else {
12369        return Err(syn::Error::new_spanned(
12370            ty,
12371            format!("{wrapper} requires a generic argument"),
12372        ));
12373    };
12374    let mut types = args.args.iter().filter_map(|a| match a {
12375        GenericArgument::Type(t) => Some(t),
12376        _ => None,
12377    });
12378    let first = types.next().ok_or_else(|| {
12379        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12380    })?;
12381    let second = types.next();
12382    Ok((first, second))
12383}
12384
12385fn to_snake_case(s: &str) -> String {
12386    let mut out = String::with_capacity(s.len() + 4);
12387    for (i, ch) in s.chars().enumerate() {
12388        if ch.is_ascii_uppercase() {
12389            if i > 0 {
12390                out.push('_');
12391            }
12392            out.push(ch.to_ascii_lowercase());
12393        } else {
12394            out.push(ch);
12395        }
12396    }
12397    out
12398}
12399
12400// ============================================================
12401//  #[derive(Form)]  —  slice 8.4B
12402// ============================================================
12403
12404/// Per-field `#[form(...)]` attributes recognised by the derive.
12405#[derive(Default)]
12406struct FormFieldAttrs {
12407    min: Option<i64>,
12408    max: Option<i64>,
12409    min_length: Option<u32>,
12410    max_length: Option<u32>,
12411    /// `#[form(clean = "fn_name")]` — per-field clean hook.
12412    /// The named static method on the form struct is called after the
12413    /// field's typed parse + length/range checks; it gets the parsed
12414    /// value by reference and returns `Result<<FieldType>, String>`.
12415    /// On Ok, the returned value replaces the parsed one; on Err, the
12416    /// message is attached to the field error list. Issue #372.
12417    clean: Option<syn::Ident>,
12418}
12419
12420/// Container-level `#[form(...)]` attributes. Currently only the
12421/// cross-field `validate` hook (issue #373).
12422#[derive(Default)]
12423struct FormContainerAttrs {
12424    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12425    /// After every per-field parse succeeds, the named method on the
12426    /// form struct is called with `&self` and may return
12427    /// `Result<(), FormErrors>`. Errors merge into the field error
12428    /// list. Issue #373.
12429    validate: Option<syn::Ident>,
12430}
12431
12432/// Detected shape of a form field's Rust type.
12433#[derive(Clone, Copy)]
12434enum FormFieldKind {
12435    String,
12436    I16,
12437    I32,
12438    I64,
12439    F32,
12440    F64,
12441    Bool,
12442}
12443
12444impl FormFieldKind {
12445    fn parse_method(self) -> &'static str {
12446        match self {
12447            Self::I16 => "i16",
12448            Self::I32 => "i32",
12449            Self::I64 => "i64",
12450            Self::F32 => "f32",
12451            Self::F64 => "f64",
12452            // String + Bool don't go through `str::parse`; the codegen
12453            // handles them inline.
12454            Self::String | Self::Bool => "",
12455        }
12456    }
12457}
12458
12459/// A generated `derive(Form)` local; mixed-site so a same-named field can't shadow it.
12460fn form_local(name: &str) -> syn::Ident {
12461    syn::Ident::new(name, proc_macro2::Span::mixed_site())
12462}
12463
12464fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12465    let root = rustango_root();
12466    let (errs, cleaned, data_param) = (
12467        form_local("__errors"),
12468        form_local("__cleaned"),
12469        form_local("data"),
12470    );
12471    let struct_name = &input.ident;
12472
12473    let Data::Struct(data) = &input.data else {
12474        return Err(syn::Error::new_spanned(
12475            struct_name,
12476            "Form can only be derived on structs",
12477        ));
12478    };
12479    let Fields::Named(named) = &data.fields else {
12480        return Err(syn::Error::new_spanned(
12481            struct_name,
12482            "Form requires a struct with named fields",
12483        ));
12484    };
12485
12486    // #373 — container-level `#[form(validate = "fn")]` hook.
12487    let container = parse_form_container_attrs(input)?;
12488    let post_field_clean: Vec<TokenStream2> = Vec::new();
12489    let _ = post_field_clean;
12490
12491    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12492    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12493
12494    for field in &named.named {
12495        let ident = field
12496            .ident
12497            .as_ref()
12498            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12499        let attrs = parse_form_field_attrs(field)?;
12500        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12501        let is_string = matches!(kind, FormFieldKind::String);
12502        let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12503        if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12504            return Err(syn::Error::new(
12505                field.span(),
12506                "`min_length` / `max_length` need a String field",
12507            ));
12508        }
12509        if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12510            return Err(syn::Error::new(
12511                field.span(),
12512                "`min` / `max` need a numeric field",
12513            ));
12514        }
12515
12516        let name_lit = ident_name(ident);
12517        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12518        // #372 — append the per-field `clean_<field>` call right after
12519        // the parse block when the attribute is set. The clean fn
12520        // takes &T and returns Result<T, String>; on Err we attach
12521        // the message to the field error list without aborting, so
12522        // the caller sees every field error at once.
12523        let clean_block = if let Some(clean_fn) = &attrs.clean {
12524            quote! {
12525                if #errs.fields().get(#name_lit).is_none() {
12526                    match Self::#clean_fn(&#ident) {
12527                        ::core::result::Result::Ok(#cleaned) => { #ident = #cleaned; }
12528                        ::core::result::Result::Err(__msg) => {
12529                            #errs.add(#name_lit, __msg);
12530                        }
12531                    }
12532                }
12533            }
12534        } else {
12535            quote! {}
12536        };
12537        field_blocks.push(quote! {
12538            #parse_block
12539            #clean_block
12540        });
12541        field_idents.push(ident);
12542    }
12543
12544    // #373 — after every per-field parse + clean succeeds, call the
12545    // cross-field validator if declared. Errors merge into the
12546    // outgoing FormErrors via the existing `FormErrors::merge` helper
12547    // (same primitive the serializer cross-field hook uses).
12548    let cross_field_call = if let Some(validate_fn) = &container.validate {
12549        quote! {
12550            if #errs.is_empty() {
12551                let __candidate = Self { #( #field_idents ),* };
12552                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12553                    #errs.merge(__other);
12554                }
12555                if !#errs.is_empty() {
12556                    return ::core::result::Result::Err(#errs);
12557                }
12558                return ::core::result::Result::Ok(__candidate);
12559            }
12560        }
12561    } else {
12562        quote! {}
12563    };
12564
12565    Ok(quote! {
12566        impl #root::forms::Form for #struct_name {
12567            fn parse(
12568                #data_param: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12569            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12570                let mut #errs = #root::forms::FormErrors::default();
12571                #( #field_blocks )*
12572                #cross_field_call
12573                if !#errs.is_empty() {
12574                    return ::core::result::Result::Err(#errs);
12575                }
12576                ::core::result::Result::Ok(Self {
12577                    #( #field_idents ),*
12578                })
12579            }
12580        }
12581    })
12582}
12583
12584fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12585    let mut out = FormContainerAttrs::default();
12586    for attr in &input.attrs {
12587        if !attr.path().is_ident("form") {
12588            continue;
12589        }
12590        attr.parse_nested_meta(|meta| {
12591            if meta.path.is_ident("validate") {
12592                let s: LitStr = meta.value()?.parse()?;
12593                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12594                return Ok(());
12595            }
12596            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12597        })?;
12598    }
12599    Ok(out)
12600}
12601
12602fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12603    let mut out = FormFieldAttrs::default();
12604    for attr in &field.attrs {
12605        if !attr.path().is_ident("form") {
12606            continue;
12607        }
12608        attr.parse_nested_meta(|meta| {
12609            if meta.path.is_ident("min") {
12610                let lit: syn::LitInt = meta.value()?.parse()?;
12611                out.min = Some(lit.base10_parse::<i64>()?);
12612                return Ok(());
12613            }
12614            if meta.path.is_ident("max") {
12615                let lit: syn::LitInt = meta.value()?.parse()?;
12616                out.max = Some(lit.base10_parse::<i64>()?);
12617                return Ok(());
12618            }
12619            if meta.path.is_ident("min_length") {
12620                let lit: syn::LitInt = meta.value()?.parse()?;
12621                out.min_length = Some(lit.base10_parse::<u32>()?);
12622                return Ok(());
12623            }
12624            if meta.path.is_ident("max_length") {
12625                let lit: syn::LitInt = meta.value()?.parse()?;
12626                out.max_length = Some(lit.base10_parse::<u32>()?);
12627                return Ok(());
12628            }
12629            if meta.path.is_ident("clean") {
12630                let s: LitStr = meta.value()?.parse()?;
12631                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12632                return Ok(());
12633            }
12634            Err(meta.error(
12635                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12636            ))
12637        })?;
12638    }
12639    Ok(out)
12640}
12641
12642fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12643    let Type::Path(TypePath { path, qself: None }) = ty else {
12644        return Err(syn::Error::new(
12645            span,
12646            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12647        ));
12648    };
12649    let last = path
12650        .segments
12651        .last()
12652        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12653
12654    if last.ident == "Option" {
12655        let inner = generic_inner(ty, &last.arguments, "Option")?;
12656        let (kind, nested) = detect_form_field(inner, span)?;
12657        if nested {
12658            return Err(syn::Error::new(
12659                span,
12660                "nested Option in Form fields is not supported",
12661            ));
12662        }
12663        return Ok((kind, true));
12664    }
12665
12666    let kind = match last.ident.to_string().as_str() {
12667        "String" => FormFieldKind::String,
12668        "i16" => FormFieldKind::I16,
12669        "i32" => FormFieldKind::I32,
12670        "i64" => FormFieldKind::I64,
12671        "f32" => FormFieldKind::F32,
12672        "f64" => FormFieldKind::F64,
12673        "bool" => FormFieldKind::Bool,
12674        other => {
12675            return Err(syn::Error::new(
12676                span,
12677                format!(
12678                    "Form field type `{other}` is not supported in v0.8 — use String / \
12679                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12680                ),
12681            ));
12682        }
12683    };
12684    Ok((kind, false))
12685}
12686
12687#[allow(clippy::too_many_lines)]
12688fn render_form_field_parse(
12689    ident: &syn::Ident,
12690    name_lit: &str,
12691    kind: FormFieldKind,
12692    nullable: bool,
12693    attrs: &FormFieldAttrs,
12694) -> TokenStream2 {
12695    let (errs, data_param) = (form_local("__errors"), form_local("data"));
12696    // Pull the raw &str from the payload `Form::parse(data: &HashMap<…>)`.
12697    let lookup = quote! {
12698        let __raw: ::core::option::Option<&::std::string::String> = #data_param.get(#name_lit);
12699    };
12700
12701    let root = rustango_root();
12702    // Strict, as ModelForm: an unknown spelling is an error, not `true`.
12703    let checkbox = quote! {
12704        match #root::forms::checkbox(__s) {
12705            ::core::option::Option::Some(__b) => __b,
12706            ::core::option::Option::None => {
12707                #errs.add(#name_lit, "Enter true or false.");
12708                false
12709            }
12710        }
12711    };
12712    let parsed_value = match kind {
12713        // `Option<bool>`: absent or blank is `None`, as for the other types.
12714        FormFieldKind::Bool if nullable => quote! {
12715            let __v: ::core::option::Option<bool> = match __raw {
12716                ::core::option::Option::None => ::core::option::Option::None,
12717                ::core::option::Option::Some(__s) if __s.is_empty() => {
12718                    ::core::option::Option::None
12719                }
12720                ::core::option::Option::Some(__s) => ::core::option::Option::Some(#checkbox),
12721            };
12722        },
12723        FormFieldKind::Bool => quote! {
12724            let __v: bool = match __raw {
12725                ::core::option::Option::None => false,
12726                ::core::option::Option::Some(__s) => #checkbox,
12727            };
12728        },
12729        FormFieldKind::String => {
12730            if nullable {
12731                quote! {
12732                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12733                        ::core::option::Option::None => ::core::option::Option::None,
12734                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12735                            ::core::option::Option::None
12736                        }
12737                        ::core::option::Option::Some(__s) => {
12738                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12739                        }
12740                    };
12741                }
12742            } else {
12743                quote! {
12744                    let __v: ::std::string::String = match __raw {
12745                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12746                            ::core::clone::Clone::clone(__s)
12747                        }
12748                        _ => {
12749                            #errs.add(#name_lit, "This field is required.");
12750                            ::std::string::String::new()
12751                        }
12752                    };
12753                }
12754            }
12755        }
12756        FormFieldKind::I16
12757        | FormFieldKind::I32
12758        | FormFieldKind::I64
12759        | FormFieldKind::F32
12760        | FormFieldKind::F64 => {
12761            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12762            let ty_lit = kind.parse_method();
12763            let default_val = match kind {
12764                FormFieldKind::I16 => quote! { 0i16 },
12765                FormFieldKind::I32 => quote! { 0i32 },
12766                FormFieldKind::I64 => quote! { 0i64 },
12767                FormFieldKind::F32 => quote! { 0f32 },
12768                FormFieldKind::F64 => quote! { 0f64 },
12769                _ => quote! { Default::default() },
12770            };
12771            // NaN and inf parse, then pass every min/max compare.
12772            let finite = if matches!(kind, FormFieldKind::F32 | FormFieldKind::F64) {
12773                quote! {
12774                    .and_then(|__n| if __n.is_finite() {
12775                        ::core::result::Result::Ok(__n)
12776                    } else {
12777                        ::core::result::Result::Err(::std::string::String::from("not a finite number"))
12778                    })
12779                }
12780            } else {
12781                quote! {}
12782            };
12783            if nullable {
12784                quote! {
12785                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12786                        ::core::option::Option::None => ::core::option::Option::None,
12787                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12788                            ::core::option::Option::None
12789                        }
12790                        ::core::option::Option::Some(__s) => {
12791                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12792                                ::core::result::Result::Ok(__n) => {
12793                                    ::core::option::Option::Some(__n)
12794                                }
12795                                ::core::result::Result::Err(__e) => {
12796                                    #errs.add(
12797                                        #name_lit,
12798                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12799                                    );
12800                                    ::core::option::Option::None
12801                                }
12802                            }
12803                        }
12804                    };
12805                }
12806            } else {
12807                quote! {
12808                    let __v: #parse_ty = match __raw {
12809                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12810                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12811                                ::core::result::Result::Ok(__n) => __n,
12812                                ::core::result::Result::Err(__e) => {
12813                                    #errs.add(
12814                                        #name_lit,
12815                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12816                                    );
12817                                    #default_val
12818                                }
12819                            }
12820                        }
12821                        _ => {
12822                            #errs.add(#name_lit, "This field is required.");
12823                            #default_val
12824                        }
12825                    };
12826                }
12827            }
12828        }
12829    };
12830
12831    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12832
12833    quote! {
12834        // `mut` so the per-field `clean` hook (#372) can rewrite the
12835        // parsed value in-place when it returns Ok with a normalized
12836        // form (e.g. trim / lowercase).
12837        let mut #ident = {
12838            #lookup
12839            #parsed_value
12840            #validators
12841            __v
12842        };
12843    }
12844}
12845
12846fn render_form_validators(
12847    name_lit: &str,
12848    kind: FormFieldKind,
12849    nullable: bool,
12850    attrs: &FormFieldAttrs,
12851) -> TokenStream2 {
12852    let errs = form_local("__errors");
12853    let mut checks: Vec<TokenStream2> = Vec::new();
12854
12855    let val_ref = if nullable {
12856        quote! { __v.as_ref() }
12857    } else {
12858        quote! { ::core::option::Option::Some(&__v) }
12859    };
12860
12861    let is_string = matches!(kind, FormFieldKind::String);
12862    let is_numeric = matches!(
12863        kind,
12864        FormFieldKind::I16
12865            | FormFieldKind::I32
12866            | FormFieldKind::I64
12867            | FormFieldKind::F32
12868            | FormFieldKind::F64
12869    );
12870
12871    if is_string {
12872        if let Some(min_len) = attrs.min_length {
12873            let min_len_usize = min_len as usize;
12874            checks.push(quote! {
12875                if let ::core::option::Option::Some(__s) = #val_ref {
12876                    if __s.chars().count() < #min_len_usize {
12877                        #errs.add(
12878                            #name_lit,
12879                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12880                        );
12881                    }
12882                }
12883            });
12884        }
12885        if let Some(max_len) = attrs.max_length {
12886            let max_len_usize = max_len as usize;
12887            checks.push(quote! {
12888                if let ::core::option::Option::Some(__s) = #val_ref {
12889                    if __s.chars().count() > #max_len_usize {
12890                        #errs.add(
12891                            #name_lit,
12892                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12893                        );
12894                    }
12895                }
12896            });
12897        }
12898    }
12899
12900    if is_numeric {
12901        if let Some(min) = attrs.min {
12902            checks.push(quote! {
12903                if let ::core::option::Option::Some(__n) = #val_ref {
12904                    if (*__n as f64) < (#min as f64) {
12905                        #errs.add(
12906                            #name_lit,
12907                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12908                        );
12909                    }
12910                }
12911            });
12912        }
12913        if let Some(max) = attrs.max {
12914            checks.push(quote! {
12915                if let ::core::option::Option::Some(__n) = #val_ref {
12916                    if (*__n as f64) > (#max as f64) {
12917                        #errs.add(
12918                            #name_lit,
12919                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12920                        );
12921                    }
12922                }
12923            });
12924        }
12925    }
12926
12927    quote! { #( #checks )* }
12928}
12929
12930// ============================================================
12931//  #[derive(ViewSet)]
12932// ============================================================
12933
12934struct ViewSetAttrs {
12935    model: syn::Path,
12936    fields: Vec<String>,
12937    filter_fields: Vec<String>,
12938    search_fields: Vec<String>,
12939    /// (field_name, desc)
12940    ordering: Vec<(String, bool)>,
12941    page_size: Option<usize>,
12942    read_only: bool,
12943    allow_anonymous: bool,
12944    perms: ViewSetPermsAttrs,
12945    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12946    /// retrieve / create responses through this `#[derive(Serializer)]`
12947    /// type instead of the default field-level projection (requires the
12948    /// `serializer` feature). Tri-dialect.
12949    serializer: Option<syn::Path>,
12950}
12951
12952#[derive(Default)]
12953struct ViewSetPermsAttrs {
12954    list: Vec<String>,
12955    retrieve: Vec<String>,
12956    create: Vec<String>,
12957    update: Vec<String>,
12958    destroy: Vec<String>,
12959}
12960
12961fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12962    let root = rustango_root();
12963    let struct_name = &input.ident;
12964
12965    // Must be a unit struct or an empty named struct.
12966    match &input.data {
12967        Data::Struct(s) => match &s.fields {
12968            Fields::Unit | Fields::Named(_) => {}
12969            Fields::Unnamed(_) => {
12970                return Err(syn::Error::new_spanned(
12971                    struct_name,
12972                    "ViewSet can only be derived on a unit struct or an empty named struct",
12973                ));
12974            }
12975        },
12976        _ => {
12977            return Err(syn::Error::new_spanned(
12978                struct_name,
12979                "ViewSet can only be derived on a struct",
12980            ));
12981        }
12982    }
12983
12984    let attrs = parse_viewset_attrs(input)?;
12985    let model_path = &attrs.model;
12986
12987    let field_lits = attrs.fields.iter().map(|f| f.as_str());
12988
12989    let filter_fields_call = if attrs.filter_fields.is_empty() {
12990        quote!()
12991    } else {
12992        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12993        quote!(.filter_fields(&[ #(#lits),* ]))
12994    };
12995
12996    let search_fields_call = if attrs.search_fields.is_empty() {
12997        quote!()
12998    } else {
12999        let lits = attrs.search_fields.iter().map(|f| f.as_str());
13000        quote!(.search_fields(&[ #(#lits),* ]))
13001    };
13002
13003    let ordering_call = if attrs.ordering.is_empty() {
13004        quote!()
13005    } else {
13006        let pairs = attrs.ordering.iter().map(|(f, desc)| {
13007            let f = f.as_str();
13008            quote!((#f, #desc))
13009        });
13010        quote!(.ordering(&[ #(#pairs),* ]))
13011    };
13012
13013    let page_size_call = if let Some(n) = attrs.page_size {
13014        quote!(.page_size(#n))
13015    } else {
13016        quote!()
13017    };
13018
13019    let read_only_call = if attrs.read_only {
13020        quote!(.read_only())
13021    } else {
13022        quote!()
13023    };
13024    let allow_anonymous_call = if attrs.allow_anonymous {
13025        quote!(.allow_anonymous())
13026    } else {
13027        quote!()
13028    };
13029
13030    // `.serializer::<S>()` — reshape responses through a derived
13031    // serializer. Requires the downstream crate to enable the
13032    // `serializer` feature (the method is gated on it).
13033    let serializer_call = if let Some(ref ser) = attrs.serializer {
13034        quote!(.serializer::<#ser>())
13035    } else {
13036        quote!()
13037    };
13038
13039    let perms = &attrs.perms;
13040    let perms_call = if perms.list.is_empty()
13041        && perms.retrieve.is_empty()
13042        && perms.create.is_empty()
13043        && perms.update.is_empty()
13044        && perms.destroy.is_empty()
13045    {
13046        quote!()
13047    } else {
13048        let list_lits = perms.list.iter().map(|s| s.as_str());
13049        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
13050        let create_lits = perms.create.iter().map(|s| s.as_str());
13051        let update_lits = perms.update.iter().map(|s| s.as_str());
13052        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
13053        quote! {
13054            .permissions(#root::viewset::ViewSetPerms {
13055                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
13056                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
13057                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
13058                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
13059                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
13060            })
13061        }
13062    };
13063
13064    Ok(quote! {
13065        impl #struct_name {
13066            /// Build an `axum::Router` with the six standard REST endpoints
13067            /// for this ViewSet, mounted at `prefix`.
13068            ///
13069            /// Accepts any backend's pool — a `sqlx::PgPool`,
13070            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
13071            /// enum — because they all `Into<Pool>` (#1273). This is why
13072            /// the derive no longer names `PgPool`, which would not
13073            /// exist on a project built without the `postgres` feature.
13074            pub fn router(
13075                prefix: &str,
13076                pool: impl ::core::convert::Into<#root::sql::Pool>,
13077            ) -> #root::__axum::Router {
13078                #root::viewset::ViewSet::for_model(
13079                    <#model_path as #root::core::Model>::SCHEMA,
13080                    &[ #(#field_lits),* ],
13081                )
13082                    #filter_fields_call
13083                    #search_fields_call
13084                    #ordering_call
13085                    #page_size_call
13086                    #perms_call
13087                    #read_only_call
13088                    #allow_anonymous_call
13089                    #serializer_call
13090                    .router(prefix, pool.into())
13091            }
13092        }
13093    })
13094}
13095
13096fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
13097    let mut model: Option<syn::Path> = None;
13098    let mut fields: Option<Vec<String>> = None;
13099    let mut filter_fields: Vec<String> = Vec::new();
13100    let mut search_fields: Vec<String> = Vec::new();
13101    let mut ordering: Vec<(String, bool)> = Vec::new();
13102    let mut page_size: Option<usize> = None;
13103    let mut read_only = false;
13104    let mut allow_anonymous = false;
13105    let mut perms = ViewSetPermsAttrs::default();
13106    let mut serializer: Option<syn::Path> = None;
13107
13108    for attr in &input.attrs {
13109        if !attr.path().is_ident("viewset") {
13110            continue;
13111        }
13112        attr.parse_nested_meta(|meta| {
13113            if meta.path.is_ident("model") {
13114                let path: syn::Path = meta.value()?.parse()?;
13115                model = Some(path);
13116                return Ok(());
13117            }
13118            if meta.path.is_ident("serializer") {
13119                let path: syn::Path = meta.value()?.parse()?;
13120                serializer = Some(path);
13121                return Ok(());
13122            }
13123            if meta.path.is_ident("fields") {
13124                let s: LitStr = meta.value()?.parse()?;
13125                fields = Some(split_field_list(&s.value()));
13126                return Ok(());
13127            }
13128            if meta.path.is_ident("filter_fields") {
13129                let s: LitStr = meta.value()?.parse()?;
13130                filter_fields = split_field_list(&s.value());
13131                return Ok(());
13132            }
13133            if meta.path.is_ident("search_fields") {
13134                let s: LitStr = meta.value()?.parse()?;
13135                search_fields = split_field_list(&s.value());
13136                return Ok(());
13137            }
13138            if meta.path.is_ident("ordering") {
13139                let s: LitStr = meta.value()?.parse()?;
13140                ordering = parse_ordering_list(&s.value());
13141                return Ok(());
13142            }
13143            if meta.path.is_ident("page_size") {
13144                let lit: syn::LitInt = meta.value()?.parse()?;
13145                page_size = Some(lit.base10_parse::<usize>()?);
13146                return Ok(());
13147            }
13148            if meta.path.is_ident("read_only") {
13149                read_only = true;
13150                return Ok(());
13151            }
13152            if meta.path.is_ident("allow_anonymous") {
13153                allow_anonymous = true;
13154                return Ok(());
13155            }
13156            if meta.path.is_ident("permissions") {
13157                meta.parse_nested_meta(|inner| {
13158                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
13159                        let s: LitStr = inner.value()?.parse()?;
13160                        Ok(split_field_list(&s.value()))
13161                    };
13162                    if inner.path.is_ident("list") {
13163                        perms.list = parse_codenames(&inner)?;
13164                    } else if inner.path.is_ident("retrieve") {
13165                        perms.retrieve = parse_codenames(&inner)?;
13166                    } else if inner.path.is_ident("create") {
13167                        perms.create = parse_codenames(&inner)?;
13168                    } else if inner.path.is_ident("update") {
13169                        perms.update = parse_codenames(&inner)?;
13170                    } else if inner.path.is_ident("destroy") {
13171                        perms.destroy = parse_codenames(&inner)?;
13172                    } else {
13173                        return Err(inner.error(
13174                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
13175                        ));
13176                    }
13177                    Ok(())
13178                })?;
13179                return Ok(());
13180            }
13181            Err(meta.error(
13182                "unknown viewset attribute (supported: model, fields, filter_fields, \
13183                 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
13184                 permissions(...))",
13185            ))
13186        })?;
13187    }
13188
13189    let model = model.ok_or_else(|| {
13190        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
13191    })?;
13192    // The field list is the write allow-list, so it is never implied.
13193    let fields = fields.ok_or_else(|| {
13194        syn::Error::new_spanned(
13195            &input.ident,
13196            "`#[viewset(fields = \"id, title\")]` is required: the fields the API renders and writes",
13197        )
13198    })?;
13199
13200    Ok(ViewSetAttrs {
13201        model,
13202        fields,
13203        filter_fields,
13204        search_fields,
13205        ordering,
13206        page_size,
13207        read_only,
13208        allow_anonymous,
13209        perms,
13210        serializer,
13211    })
13212}
13213
13214// ============================================================ #[derive(Serializer)]
13215
13216struct SerializerContainerAttrs {
13217    model: syn::Path,
13218    /// `#[serializer(validate = "fn_name")]` on the struct — the
13219    /// cross-field validation hook (#436). The named inherent method
13220    /// must take `&self` and return
13221    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
13222    /// `validate()` runs every per-field validator first; then calls
13223    /// the cross-field method if declared; aggregates all errors into
13224    /// one `FormErrors`.
13225    cross_validate: Option<syn::Ident>,
13226}
13227
13228#[derive(Default)]
13229struct SerializerFieldAttrs {
13230    read_only: bool,
13231    write_only: bool,
13232    source: Option<String>,
13233    skip: bool,
13234    /// `#[serializer(method = "fn_name")]` — computed field. The macro
13235    /// emits a `from_model` initializer that calls
13236    /// `Self::fn_name(&model)` and stores the return value.
13237    method: Option<String>,
13238    /// `#[serializer(validate = "fn_name")]` — per-field validator
13239    /// callable run by `Self::validate(&self)`. Must return
13240    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
13241    /// the field name.
13242    validate: Option<String>,
13243    /// `#[serializer(nested)]` on a field whose type is another
13244    /// `Serializer` — the macro emits `from_model` initializer that
13245    /// reads the parent via `model.<source>.value()` then calls the
13246    /// child serializer's `from_model(parent)`. When the FK is
13247    /// unloaded the field falls back to `Default::default()` (does
13248    /// NOT panic) so a missing prefetch in prod degrades gracefully.
13249    /// Source field on the model defaults to the field name; override
13250    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
13251    /// panic-on-unloaded behavior for tests.
13252    nested: bool,
13253    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
13254    /// strict behavior: panic when the FK isn't loaded. Useful in
13255    /// test code where forgetting select_related must trip a hard
13256    /// failure rather than render a blank nested object.
13257    nested_strict: bool,
13258    /// `#[serializer(many = TagSerializer)]` — declare the field as
13259    /// a list of nested serializers. Field type must be `Vec<S>`
13260    /// where `S` is the inner serializer. The macro initializes the
13261    /// field to `Vec::new()` in `from_model` and emits a typed
13262    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13263    /// maps each model row through `S::from_model`. Auto-load isn't
13264    /// possible (the M2M / one-to-many accessor is async); callers
13265    /// fetch the children + call the setter post-from_model.
13266    many: Option<syn::Type>,
13267    /// `#[serializer(slug = "name")]` — render a relation as one of
13268    /// its fields.
13269    /// Source field on the model must be a `ForeignKey<T>`; the
13270    /// macro emits `from_model` glue that walks
13271    /// `model.<source>.value()?.<slug>` and clones it. Field type on
13272    /// the serializer is typically `String` (whatever type the slug
13273    /// column has). When the FK is unloaded the field falls back to
13274    /// `Default::default()`, same graceful-degrade contract as
13275    /// `nested`. Source defaults to the field name; override with
13276    /// `source = "..."`. v0.44.
13277    slug: Option<String>,
13278    /// `#[serializer(max_length = N)]` — caps
13279    /// the character count of a string field on write. Overrides the
13280    /// model's `max_length`; when absent the model value is inherited.
13281    max_length: Option<u64>,
13282    /// `#[serializer(min_length = N)]` — minimum character count.
13283    /// Serializer-only (the model has no `min_length` column).
13284    min_length: Option<u64>,
13285    /// `#[serializer(min = N)]` — inclusive
13286    /// integer lower bound; overrides the model's `min` when given.
13287    min: Option<i64>,
13288    /// `#[serializer(max = N)]` — inclusive
13289    /// integer upper bound; overrides the model's `max` when given.
13290    max: Option<i64>,
13291}
13292
13293fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13294    let mut model: Option<syn::Path> = None;
13295    let mut cross_validate: Option<syn::Ident> = None;
13296    for attr in &input.attrs {
13297        if !attr.path().is_ident("serializer") {
13298            continue;
13299        }
13300        attr.parse_nested_meta(|meta| {
13301            if meta.path.is_ident("model") {
13302                let _eq: syn::Token![=] = meta.input.parse()?;
13303                model = Some(meta.input.parse()?);
13304                return Ok(());
13305            }
13306            if meta.path.is_ident("validate") {
13307                // #436 — container-level `validate = "fn_name"` for
13308                // cross-field validation. Field-level
13309                // `#[serializer(validate = "...")]` on a field is
13310                // parsed separately in `parse_serializer_field_attrs`.
13311                let s: LitStr = meta.value()?.parse()?;
13312                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13313                return Ok(());
13314            }
13315            Err(meta.error(
13316                "unknown serializer container attribute \
13317                 (supported: `model`, `validate`)",
13318            ))
13319        })?;
13320    }
13321    let model = model.ok_or_else(|| {
13322        syn::Error::new_spanned(
13323            &input.ident,
13324            "`#[serializer(model = SomeModel)]` is required",
13325        )
13326    })?;
13327    Ok(SerializerContainerAttrs {
13328        model,
13329        cross_validate,
13330    })
13331}
13332
13333/// The `#[serde(...)]` keys the Serializer derive honours. Its own
13334/// `Serialize` impl would otherwise drop them silently.
13335#[derive(Default)]
13336struct SerdeWireAttrs {
13337    rename_ser: Option<String>,
13338    rename_de: Option<String>,
13339    skip_ser: bool,
13340    skip_de: bool,
13341    skip_ser_if: Option<syn::ExprPath>,
13342}
13343
13344/// A serde `rename_all` rule, applied to a `snake_case` field name.
13345#[derive(Clone, Copy)]
13346enum SerdeRenameRule {
13347    Lower,
13348    Upper,
13349    Pascal,
13350    Camel,
13351    ScreamingSnake,
13352    Kebab,
13353    ScreamingKebab,
13354}
13355
13356impl SerdeRenameRule {
13357    fn parse(s: &LitStr) -> syn::Result<Self> {
13358        Ok(match s.value().as_str() {
13359            "lowercase" | "snake_case" => Self::Lower,
13360            "UPPERCASE" => Self::Upper,
13361            "PascalCase" => Self::Pascal,
13362            "camelCase" => Self::Camel,
13363            "SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
13364            "kebab-case" => Self::Kebab,
13365            "SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
13366            _ => {
13367                return Err(syn::Error::new_spanned(
13368                    s,
13369                    "unknown serde `rename_all` rule",
13370                ))
13371            }
13372        })
13373    }
13374
13375    /// Same output as serde's own field renaming.
13376    fn apply(self, field: &str) -> String {
13377        match self {
13378            Self::Lower => field.to_owned(),
13379            Self::Upper | Self::ScreamingSnake => field.to_ascii_uppercase(),
13380            Self::Pascal | Self::Camel => {
13381                let mut out = String::new();
13382                let mut cap = matches!(self, Self::Pascal);
13383                for ch in field.chars() {
13384                    if ch == '_' {
13385                        cap = !out.is_empty() || matches!(self, Self::Pascal);
13386                    } else if cap {
13387                        out.push(ch.to_ascii_uppercase());
13388                        cap = false;
13389                    } else {
13390                        out.push(ch);
13391                    }
13392                }
13393                out
13394            }
13395            Self::Kebab => field.replace('_', "-"),
13396            Self::ScreamingKebab => field.to_ascii_uppercase().replace('_', "-"),
13397        }
13398    }
13399}
13400
13401/// Consume the value of a `serde` key the derive does not act on.
13402fn skip_serde_meta_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
13403    if meta.input.peek(syn::Token![=]) {
13404        let _: syn::Expr = meta.value()?.parse()?;
13405    } else if meta.input.peek(syn::token::Paren) {
13406        let _: proc_macro2::Group = meta.input.parse()?;
13407    }
13408    Ok(())
13409}
13410
13411/// `rename = "x"` or `rename(serialize = "a", deserialize = "b")`.
13412fn parse_serde_rename(
13413    meta: &syn::meta::ParseNestedMeta,
13414    mut set: impl FnMut(bool, LitStr) -> syn::Result<()>,
13415) -> syn::Result<()> {
13416    if meta.input.peek(syn::Token![=]) {
13417        let s: LitStr = meta.value()?.parse()?;
13418        set(true, s.clone())?;
13419        return set(false, s);
13420    }
13421    meta.parse_nested_meta(|inner| {
13422        let is_ser = inner.path.is_ident("serialize");
13423        if !is_ser && !inner.path.is_ident("deserialize") {
13424            return Err(inner.error("expected `serialize` or `deserialize`"));
13425        }
13426        set(is_ser, inner.value()?.parse()?)
13427    })
13428}
13429
13430/// Container `rename_all` rules: (serialize, deserialize).
13431fn parse_serde_container_rules(
13432    input: &DeriveInput,
13433) -> syn::Result<(Option<SerdeRenameRule>, Option<SerdeRenameRule>)> {
13434    let (mut ser, mut de) = (None, None);
13435    for attr in input.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13436        attr.parse_nested_meta(|meta| {
13437            if meta.path.is_ident("rename_all") {
13438                return parse_serde_rename(&meta, |is_ser, s| {
13439                    let rule = Some(SerdeRenameRule::parse(&s)?);
13440                    if is_ser {
13441                        ser = rule;
13442                    } else {
13443                        de = rule;
13444                    }
13445                    Ok(())
13446                });
13447            }
13448            skip_serde_meta_value(&meta)
13449        })?;
13450    }
13451    Ok((ser, de))
13452}
13453
13454fn parse_serde_field_attrs(field: &syn::Field) -> syn::Result<SerdeWireAttrs> {
13455    let mut out = SerdeWireAttrs::default();
13456    for attr in field.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13457        attr.parse_nested_meta(|meta| {
13458            if meta.path.is_ident("rename") {
13459                return parse_serde_rename(&meta, |is_ser, s| {
13460                    if is_ser {
13461                        out.rename_ser = Some(s.value());
13462                    } else {
13463                        out.rename_de = Some(s.value());
13464                    }
13465                    Ok(())
13466                });
13467            }
13468            if meta.path.is_ident("skip") {
13469                out.skip_ser = true;
13470                out.skip_de = true;
13471                return Ok(());
13472            }
13473            if meta.path.is_ident("skip_serializing") {
13474                out.skip_ser = true;
13475                return Ok(());
13476            }
13477            if meta.path.is_ident("skip_deserializing") {
13478                out.skip_de = true;
13479                return Ok(());
13480            }
13481            if meta.path.is_ident("skip_serializing_if") {
13482                let s: LitStr = meta.value()?.parse()?;
13483                out.skip_ser_if = Some(s.parse()?);
13484                return Ok(());
13485            }
13486            skip_serde_meta_value(&meta)
13487        })?;
13488    }
13489    Ok(out)
13490}
13491
13492fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13493    let mut out = SerializerFieldAttrs::default();
13494    for attr in &field.attrs {
13495        if !attr.path().is_ident("serializer") {
13496            continue;
13497        }
13498        attr.parse_nested_meta(|meta| {
13499            if meta.path.is_ident("read_only") {
13500                out.read_only = true;
13501                return Ok(());
13502            }
13503            if meta.path.is_ident("write_only") {
13504                out.write_only = true;
13505                return Ok(());
13506            }
13507            if meta.path.is_ident("skip") {
13508                out.skip = true;
13509                return Ok(());
13510            }
13511            if meta.path.is_ident("source") {
13512                let s: LitStr = meta.value()?.parse()?;
13513                out.source = Some(s.value());
13514                return Ok(());
13515            }
13516            if meta.path.is_ident("method") {
13517                let s: LitStr = meta.value()?.parse()?;
13518                out.method = Some(s.value());
13519                return Ok(());
13520            }
13521            if meta.path.is_ident("validate") {
13522                let s: LitStr = meta.value()?.parse()?;
13523                out.validate = Some(s.value());
13524                return Ok(());
13525            }
13526            if meta.path.is_ident("many") {
13527                let _eq: syn::Token![=] = meta.input.parse()?;
13528                out.many = Some(meta.input.parse()?);
13529                return Ok(());
13530            }
13531            if meta.path.is_ident("nested") {
13532                out.nested = true;
13533                // Optional strict flag inside parentheses:
13534                //   #[serializer(nested(strict))]
13535                if meta.input.peek(syn::token::Paren) {
13536                    meta.parse_nested_meta(|inner| {
13537                        if inner.path.is_ident("strict") {
13538                            out.nested_strict = true;
13539                            return Ok(());
13540                        }
13541                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13542                    })?;
13543                }
13544                return Ok(());
13545            }
13546            if meta.path.is_ident("slug") {
13547                let s: LitStr = meta.value()?.parse()?;
13548                out.slug = Some(s.value());
13549                return Ok(());
13550            }
13551            if meta.path.is_ident("max_length") {
13552                let lit: syn::LitInt = meta.value()?.parse()?;
13553                out.max_length = Some(lit.base10_parse::<u64>()?);
13554                return Ok(());
13555            }
13556            if meta.path.is_ident("min_length") {
13557                let lit: syn::LitInt = meta.value()?.parse()?;
13558                out.min_length = Some(lit.base10_parse::<u64>()?);
13559                return Ok(());
13560            }
13561            if meta.path.is_ident("min") {
13562                let lit: syn::LitInt = meta.value()?.parse()?;
13563                out.min = Some(lit.base10_parse::<i64>()?);
13564                return Ok(());
13565            }
13566            if meta.path.is_ident("max") {
13567                let lit: syn::LitInt = meta.value()?.parse()?;
13568                out.max = Some(lit.base10_parse::<i64>()?);
13569                return Ok(());
13570            }
13571            Err(meta.error(
13572                "unknown serializer field attribute (supported: \
13573                 `read_only`, `write_only`, `source`, `skip`, `method`, \
13574                 `validate`, `nested`, `many`, `slug`, `max_length`, \
13575                 `min_length`, `min`, `max`)",
13576            ))
13577        })?;
13578    }
13579    // Validate: read_only + write_only is nonsensical
13580    if out.read_only && out.write_only {
13581        return Err(syn::Error::new_spanned(
13582            field,
13583            "a field cannot be both `read_only` and `write_only`",
13584        ));
13585    }
13586    if out.method.is_some() && out.source.is_some() {
13587        return Err(syn::Error::new_spanned(
13588            field,
13589            "`method` and `source` are mutually exclusive — `method` computes \
13590             the value from a method, `source` reads it from a different model field",
13591        ));
13592    }
13593    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13594        return Err(syn::Error::new_spanned(
13595            field,
13596            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13597             — pick one strategy for populating the field",
13598        ));
13599    }
13600    Ok(out)
13601}
13602
13603fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13604    let root = rustango_root();
13605    let struct_name = &input.ident;
13606    let struct_name_lit = struct_name.to_string();
13607
13608    let Data::Struct(data) = &input.data else {
13609        return Err(syn::Error::new_spanned(
13610            struct_name,
13611            "Serializer can only be derived on structs",
13612        ));
13613    };
13614    let Fields::Named(named) = &data.fields else {
13615        return Err(syn::Error::new_spanned(
13616            struct_name,
13617            "Serializer requires a struct with named fields",
13618        ));
13619    };
13620
13621    let container = parse_serializer_container_attrs(input)?;
13622    let model_path = &container.model;
13623
13624    // Classify each field. `ty` is only consumed by the
13625    // `#[cfg(feature = "openapi")]` block below, but we always
13626    // capture it to keep the field-info build a single pass.
13627    #[allow(dead_code)]
13628    struct FieldInfo {
13629        ident: syn::Ident,
13630        ty: syn::Type,
13631        attrs: SerializerFieldAttrs,
13632        serde: SerdeWireAttrs,
13633        /// JSON key in the output / in a request body.
13634        ser_name: String,
13635        de_name: String,
13636    }
13637    let (ser_rule, de_rule) = parse_serde_container_rules(input)?;
13638    let mut fields_info: Vec<FieldInfo> = Vec::new();
13639    for field in &named.named {
13640        let ident = field.ident.clone().expect("named field has ident");
13641        let attrs = parse_serializer_field_attrs(field)?;
13642        let serde = parse_serde_field_attrs(field)?;
13643        let name = ident_name(&ident);
13644        let wire = |rename: &Option<String>, rule: Option<SerdeRenameRule>| {
13645            rename
13646                .clone()
13647                .unwrap_or_else(|| rule.map_or_else(|| name.clone(), |r| r.apply(&name)))
13648        };
13649        fields_info.push(FieldInfo {
13650            ser_name: wire(&serde.rename_ser, ser_rule),
13651            de_name: wire(&serde.rename_de, de_rule),
13652            ident,
13653            ty: field.ty.clone(),
13654            attrs,
13655            serde,
13656        });
13657    }
13658
13659    // Generate from_model body: struct literal with each field assigned.
13660    let from_model_fields = fields_info.iter().map(|fi| {
13661        let ident = &fi.ident;
13662        if let Some(_inner) = &fi.attrs.many {
13663            // Many — collection field. Initialize empty; caller
13664            // populates via the macro-emitted set_<field> helper
13665            // after fetching the M2M children.
13666            quote! { #ident: ::std::vec::Vec::new() }
13667        } else if let Some(method) = &fi.attrs.method {
13668            // SerializerMethodField: call Self::<method>(&model) to
13669            // compute the value. Method signature must be
13670            // `fn <method>(model: &T) -> <field type>`.
13671            let method_ident = syn::Ident::new(method, ident.span());
13672            quote! { #ident: Self::#method_ident(model) }
13673        } else if let Some(slug_field) = &fi.attrs.slug {
13674            // v0.44 — SlugRelatedField. Source defaults to the field
13675            // name on this struct; override via `source = "..."`. The
13676            // source field on the model is expected to be a
13677            // `ForeignKey<T>`; the slug field on the parent is named
13678            // by the attribute value. When the FK is unloaded the
13679            // field falls back to `Default::default()` — same
13680            // graceful-degrade contract as `nested`; a NULL
13681            // `Option<ForeignKey>` renders `None`.
13682            let src_name = fi
13683                .attrs
13684                .source
13685                .as_deref()
13686                .unwrap_or(&ident_name(&fi.ident))
13687                .to_owned();
13688            let src_ident = ident_from_name(&src_name, ident.span());
13689            let slug_ident = syn::Ident::new(slug_field, ident.span());
13690            quote! {
13691                #ident: #root::serializer::LoadedRelation::render(
13692                    &model.#src_ident,
13693                    |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
13694                    ::core::default::Default::default,
13695                )
13696            }
13697        } else if fi.attrs.nested {
13698            // Nested serializer. Source defaults to the field name on
13699            // this struct; override via `source = "..."`. The source
13700            // field on the model is a `ForeignKey<T>` or
13701            // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
13702            //
13703            // Behavior matrix (tweakable per-field):
13704            //   * FK loaded   → nested object materializes via
13705            //                   ChildSerializer::from_model(parent).
13706            //   * FK unloaded → fall back to ChildSerializer::default()
13707            //                   (so prod doesn't crash on a missing
13708            //                   prefetch — just renders a blank nested
13709            //                   object). Add `#[serializer(nested(
13710            //                   strict))]` to keep the v0.18.1
13711            //                   panic-on-unloaded behavior for tests
13712            //                   that want hard guardrails.
13713            //   * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
13714            //                   field (JSON null), strict or not.
13715            let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13716            let src_ident = ident_from_name(&src_name, ident.span());
13717            let unloaded = if fi.attrs.nested_strict {
13718                let panic_msg = format!(
13719                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13720                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13721                );
13722                quote! { || ::core::panic!("{}", #panic_msg) }
13723            } else {
13724                quote! { ::core::default::Default::default }
13725            };
13726            quote! {
13727                #ident: #root::serializer::LoadedRelation::render(
13728                    &model.#src_ident,
13729                    |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
13730                    #unloaded,
13731                )
13732            }
13733        } else if fi.attrs.write_only || fi.attrs.skip {
13734            // Not read from model — use default
13735            quote! { #ident: ::core::default::Default::default() }
13736        } else if let Some(src) = &fi.attrs.source {
13737            let src_ident = ident_from_name(src, ident.span());
13738            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13739        } else {
13740            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13741        }
13742    });
13743
13744    // is_writable predicate (also used by writable_lits /
13745    // writable_source_lits below).
13746    let is_writable = |fi: &&FieldInfo| {
13747        !fi.attrs.read_only
13748            && !fi.attrs.skip
13749            && !fi.serde.skip_de
13750            && fi.attrs.method.is_none()
13751            && !fi.attrs.nested
13752            && fi.attrs.many.is_none()
13753            && fi.attrs.slug.is_none()
13754    };
13755
13756    // Declarative field constraints: one block
13757    // per writable field, run inside `validate()`. Each resolves its
13758    // bounds as the serializer attr when given (`#[serializer(max_length
13759    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13760    // `max` / `choices`), then dispatches on the field's JSON value via
13761    // `forms::validators::check_value` (string → length/choices, integer
13762    // → min/max).
13763    let opt_usize = |v: Option<u64>| match v {
13764        Some(n) => {
13765            let n = n as usize;
13766            quote!(::core::option::Option::Some(#n))
13767        }
13768        None => quote!(::core::option::Option::None),
13769    };
13770    #[cfg(feature = "openapi")]
13771    let opt_u64 = |v: Option<u64>| match v {
13772        Some(n) => quote!(::core::option::Option::Some(#n)),
13773        None => quote!(::core::option::Option::None),
13774    };
13775    let opt_i64 = |v: Option<i64>| match v {
13776        Some(n) => quote!(::core::option::Option::Some(#n)),
13777        None => quote!(::core::option::Option::None),
13778    };
13779    let constraint_blocks: Vec<_> = fields_info
13780        .iter()
13781        .filter(is_writable)
13782        .map(|fi| {
13783            let ident = &fi.ident;
13784            let fname = &fi.de_name;
13785            let mname = fi
13786                .attrs
13787                .source
13788                .clone()
13789                .unwrap_or_else(|| ident_name(&fi.ident));
13790            let attr_max_len = opt_usize(fi.attrs.max_length);
13791            let attr_min_len = opt_usize(fi.attrs.min_length);
13792            let attr_min = opt_i64(fi.attrs.min);
13793            let attr_max = opt_i64(fi.attrs.max);
13794            // A bound the field's type cannot take is a compile error, not a no-op.
13795            let ty = &fi.ty;
13796            let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
13797                .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
13798            let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
13799                .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
13800            quote! {
13801                #len_check
13802                #num_check
13803                if __sent(#fname) {
13804                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13805                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13806                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13807                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13808                    let __min: ::core::option::Option<i64> =
13809                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13810                    let __max: ::core::option::Option<i64> =
13811                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13812                    let __choices = __sf.and_then(|__f| __f.choices);
13813                    let __v = #root::__serde_json::to_value(&self.#ident)
13814                        .unwrap_or(#root::__serde_json::Value::Null);
13815                    #root::forms::validators::check_value(
13816                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13817                        &mut __errors,
13818                    );
13819                }
13820            }
13821        })
13822        .collect();
13823    let has_constraints = !constraint_blocks.is_empty();
13824
13825    // Per-field validators. Emit a
13826    // `validate(&self)` method that runs each user-defined validator
13827    // and aggregates errors into `FormErrors`.
13828    let validator_calls: Vec<_> = fields_info
13829        .iter()
13830        .filter_map(|fi| {
13831            let ident = &fi.ident;
13832            let name_lit = &fi.de_name;
13833            let method = fi.attrs.validate.as_ref()?;
13834            let method_ident = syn::Ident::new(method, ident.span());
13835            Some(quote! {
13836                if __sent(#name_lit) {
13837                    if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13838                        __errors.add(#name_lit.to_owned(), __e);
13839                    }
13840                }
13841            })
13842        })
13843        .collect();
13844    // #436 — cross-field `validate(&self)`. If the
13845    // container declared `#[serializer(validate = "fn_name")]`,
13846    // the macro-generated `validate(&self)` runs every per-field
13847    // validator first, then calls the user's cross-field method,
13848    // merging its `FormErrors` into the per-field errors. Either
13849    // alone is enough to emit the wrapper.
13850    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13851        quote! {
13852            // Merge cross-field errors into the per-field bucket so
13853            // a single .validate() call surfaces both layers.
13854            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13855                __errors.merge(__cross);
13856            }
13857        }
13858    });
13859    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13860    // Anything to run? Declarative constraints (for any writable field) +
13861    // per-field validators + cross-field hook.
13862    let has_run_validations = has_validators || has_constraints;
13863    // Shared body: declarative field constraints first (length / range /
13864    // choices, serializer-attr-or-model), then per-field validators, then
13865    // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13866    let checks = quote! {
13867        let mut __errors = #root::forms::FormErrors::default();
13868        #( #constraint_blocks )*
13869        #( #validator_calls )*
13870        #cross_validate_call
13871        if __errors.is_empty() {
13872            ::core::result::Result::Ok(())
13873        } else {
13874            ::core::result::Result::Err(__errors)
13875        }
13876    };
13877    let validate_body = quote! {
13878        let __sent = |_: &str| true;
13879        #checks
13880    };
13881    // PATCH: only the keys the body carries.
13882    let validate_patch_body = quote! {
13883        let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13884        #checks
13885    };
13886    // Inherent `validate(&self)` — kept for back-compat with direct
13887    // `serializer.validate()` calls that don't import `ModelSerializer`.
13888    // Emitted only when the serializer declares per-field/cross-field
13889    // validators (unchanged rule) so it never collides with a
13890    // hand-written inherent `validate`.
13891    let validate_method = if has_validators {
13892        quote! {
13893            impl #struct_name {
13894                /// Run the declarative field constraints, every
13895                /// `#[serializer(validate = "...")]` per-field validator,
13896                /// and (when declared) the container-level cross-field
13897                /// validator. Aggregates errors into `FormErrors` keyed by
13898                /// the field name. Returns `Ok(())` when all pass.
13899                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13900                    #validate_body
13901                }
13902            }
13903        }
13904    } else {
13905        quote! {}
13906    };
13907    // Trait-method override so the type-erased ViewSet write path can
13908    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13909    // there's anything to run (declarative constraints inherited from the
13910    // model count); otherwise the trait's default no-op is used.
13911    let trait_validate_override = if has_run_validations {
13912        quote! {
13913            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13914                #validate_body
13915            }
13916        }
13917    } else {
13918        quote! {}
13919    };
13920
13921    // For every `#[serializer(many = S)]` field, emit a
13922    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13923    // helper that maps the parents through `S::from_model`.
13924    let many_setters: Vec<_> = fields_info
13925        .iter()
13926        .filter_map(|fi| {
13927            let many_ty = fi.attrs.many.as_ref()?;
13928            let ident = &fi.ident;
13929            let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13930            Some(quote! {
13931                /// Populate this `many` field by mapping each parent model
13932                /// through the inner serializer's `from_model`. Call after
13933                /// fetching the M2M / one-to-many children since
13934                /// `from_model` itself can't await an SQL query.
13935                pub fn #setter(
13936                    &mut self,
13937                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13938                ) -> &mut Self {
13939                    self.#ident = models.iter()
13940                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13941                        .collect();
13942                    self
13943                }
13944            })
13945        })
13946        .collect();
13947    let many_setters_impl = if many_setters.is_empty() {
13948        quote! {}
13949    } else {
13950        quote! {
13951            impl #struct_name {
13952                #( #many_setters )*
13953            }
13954        }
13955    };
13956
13957    // Generate custom Serialize: skip write_only and serde-skipped fields
13958    let output_fields: Vec<_> = fields_info
13959        .iter()
13960        .filter(|fi| !fi.attrs.write_only && !fi.serde.skip_ser)
13961        .collect();
13962    // `skip_serializing_if` fields count only when rendered.
13963    let output_field_count = output_fields.iter().map(|fi| {
13964        let ident = &fi.ident;
13965        if let Some(pred) = &fi.serde.skip_ser_if {
13966            quote! { + if #pred(&self.#ident) { 0 } else { 1 } }
13967        } else {
13968            quote! { + 1 }
13969        }
13970    });
13971    let output_field_count = quote! { 0usize #( #output_field_count )* };
13972    let serialize_fields = output_fields.iter().map(|fi| {
13973        let ident = &fi.ident;
13974        let name_lit = &fi.ser_name;
13975        if let Some(pred) = &fi.serde.skip_ser_if {
13976            quote! {
13977                if #pred(&self.#ident) {
13978                    __state.skip_field(#name_lit)?;
13979                } else {
13980                    __state.serialize_field(#name_lit, &self.#ident)?;
13981                }
13982            }
13983        } else {
13984            quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13985        }
13986    });
13987
13988    // writable_fields: normal + write_only.
13989    // Exclude:
13990    //   - `read_only` — server-computed.
13991    //   - `skip` — caller sets manually post-from_model.
13992    //   - `method` — computed from a Self::fn(&model) call; accepting
13993    //     it on write is meaningless.
13994    //   - `nested` / `many` — populated from related-model data, not
13995    //     from a field on the wire body.
13996    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13997    // `many` in `writable_fields()`, which made the ViewSet write
13998    // path accept those fields from the JSON body and try to bind
13999    // them to the SQL UPDATE — a silent no-op at best, a type
14000    // mismatch at worst.
14001    // (`is_writable` is defined above, near the constraint codegen.)
14002    let writable_lits: Vec<_> = fields_info
14003        .iter()
14004        .filter(is_writable)
14005        .map(|fi| fi.de_name.clone())
14006        .collect();
14007
14008    // `writable_source_fields`: the MODEL field names of writable
14009    // serializer fields (source-resolved). The ViewSet write path skips
14010    // every model column NOT in this set, so `read_only` / `method` /
14011    // computed fields a client posts are ignored instead of written.
14012    let writable_source_lits: Vec<String> = fields_info
14013        .iter()
14014        .filter(is_writable)
14015        .map(|fi| {
14016            fi.attrs
14017                .source
14018                .clone()
14019                .unwrap_or_else(|| ident_name(&fi.ident))
14020        })
14021        .collect();
14022
14023    // `readable_source_fields`: model columns the JSON output copies
14024    // straight from the model, so `?ordering=` cannot sort on a column
14025    // the API never shows (#1845).
14026    let readable: Vec<&FieldInfo> = fields_info
14027        .iter()
14028        .filter(|fi| {
14029            !fi.attrs.write_only
14030                && !fi.serde.skip_ser
14031                && !fi.attrs.skip
14032                && fi.attrs.method.is_none()
14033                && !fi.attrs.nested
14034                && fi.attrs.many.is_none()
14035                && fi.attrs.slug.is_none()
14036        })
14037        .collect();
14038    let readable_source_lits: Vec<String> = readable
14039        .iter()
14040        .map(|fi| {
14041            fi.attrs
14042                .source
14043                .clone()
14044                .unwrap_or_else(|| ident_name(&fi.ident))
14045        })
14046        .collect();
14047    // Their JSON keys, so the ViewSet finds a renamed cursor column.
14048    let readable_lits: Vec<String> = readable.iter().map(|fi| fi.ser_name.clone()).collect();
14049
14050    // `from_writable_json`: build a partial instance for input
14051    // validation. Writable fields are parsed from the JSON body (keyed
14052    // by serializer field name); every other field defaults. Per-field
14053    // type errors collect into `FormErrors` keyed by the field name.
14054    // Construction is field-by-field (not `Self::default()`) so it works
14055    // for serializers regardless of a struct-level `Default`.
14056    let from_writable_json_inits: Vec<_> = fields_info
14057        .iter()
14058        .map(|fi| {
14059            let ident = &fi.ident;
14060            let fname = &fi.de_name;
14061            let ty = &fi.ty;
14062            if is_writable(&fi) {
14063                quote! {
14064                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
14065                        ::core::option::Option::Some(__v) => {
14066                            match #root::__serde_json::from_value::<#ty>(
14067                                ::core::clone::Clone::clone(__v),
14068                            ) {
14069                                ::core::result::Result::Ok(__x) => __x,
14070                                ::core::result::Result::Err(__e) => {
14071                                    __errors.add(#fname.to_owned(), __e.to_string());
14072                                    ::core::default::Default::default()
14073                                }
14074                            }
14075                        }
14076                        ::core::option::Option::None => ::core::default::Default::default(),
14077                    }
14078                }
14079            } else {
14080                quote! { #ident: ::core::default::Default::default() }
14081            }
14082        })
14083        .collect();
14084    // `validate_patch`: overlay each sent writable field on the stored row.
14085    let patch_overlays: Vec<_> = fields_info
14086        .iter()
14087        .filter(is_writable)
14088        .map(|fi| {
14089            let ident = &fi.ident;
14090            let fname = &fi.de_name;
14091            let ty = &fi.ty;
14092            quote! {
14093                if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
14094                    match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
14095                        ::core::result::Result::Ok(__x) => __merged.#ident = __x,
14096                        ::core::result::Result::Err(__e) => {
14097                            __errors.add(#fname.to_owned(), __e.to_string());
14098                        }
14099                    }
14100                }
14101            }
14102        })
14103        .collect();
14104    let patch_validate_call = if has_run_validations {
14105        quote! { __merged.__rustango_validate_patch(__obj) }
14106    } else {
14107        quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
14108    };
14109    let patch_validate_helper = if has_run_validations {
14110        quote! {
14111            impl #struct_name {
14112                #[doc(hidden)]
14113                fn __rustango_validate_patch(
14114                    &self,
14115                    __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
14116                ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14117                    #validate_patch_body
14118                }
14119            }
14120        }
14121    } else {
14122        quote! {}
14123    };
14124
14125    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
14126    // Only includes fields shown in JSON output (skips write_only). For each
14127    // `Option<T>` field, omit from `required` and add `.nullable()`.
14128    let openapi_impl = {
14129        #[cfg(feature = "openapi")]
14130        {
14131            let property_calls = output_fields.iter().map(|fi| {
14132                let name_lit = &fi.ser_name;
14133                let ty = &fi.ty;
14134                let nullable_call = if is_option(ty) {
14135                    quote! { .nullable() }
14136                } else {
14137                    quote! {}
14138                };
14139                // The bounds `validate()` enforces on a written field.
14140                let limits_call = is_writable(fi).then(|| {
14141                    let mname = fi
14142                        .attrs
14143                        .source
14144                        .clone()
14145                        .unwrap_or_else(|| ident_name(&fi.ident));
14146                    let (a, b, c, d) = (
14147                        opt_u64(fi.attrs.max_length),
14148                        opt_u64(fi.attrs.min_length),
14149                        opt_i64(fi.attrs.min),
14150                        opt_i64(fi.attrs.max),
14151                    );
14152                    quote! {
14153                        .field_limits(
14154                            <#model_path as #root::core::Model>::SCHEMA.field(#mname),
14155                            #a, #b, #c, #d,
14156                        )
14157                    }
14158                });
14159                quote! {
14160                    .property(
14161                        #name_lit,
14162                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
14163                            #limits_call
14164                            #nullable_call,
14165                    )
14166                }
14167            });
14168            let required_lits: Vec<_> = output_fields
14169                .iter()
14170                .filter(|fi| !is_option(&fi.ty) && fi.serde.skip_ser_if.is_none())
14171                .map(|fi| fi.ser_name.clone())
14172                .collect();
14173            quote! {
14174                impl #root::openapi::OpenApiSchema for #struct_name {
14175                    fn openapi_schema() -> #root::openapi::Schema {
14176                        #root::openapi::Schema::object()
14177                            #( #property_calls )*
14178                            .required([ #( #required_lits ),* ])
14179                    }
14180                }
14181            }
14182        }
14183        #[cfg(not(feature = "openapi"))]
14184        {
14185            quote! {}
14186        }
14187    };
14188
14189    // The other tables a `nested`, `many` or `slug` field renders rows of.
14190    let related_tables = fields_info.iter().filter_map(|fi| {
14191        let name = &fi.ser_name;
14192        if let Some(inner) = &fi.attrs.many {
14193            return Some(quote! {
14194                (#name, <<#inner as #root::serializer::ModelSerializer>::Model
14195                    as #root::core::Model>::SCHEMA.table)
14196            });
14197        }
14198        if !(fi.attrs.nested || fi.attrs.slug.is_some()) {
14199            return None;
14200        }
14201        let src_name = fi
14202            .attrs
14203            .source
14204            .as_deref()
14205            .unwrap_or(&ident_name(&fi.ident))
14206            .to_owned();
14207        let src_ident = ident_from_name(&src_name, fi.ident.span());
14208        Some(quote! {
14209            (#name, #root::serializer::relation_table(|__m: &#model_path| &__m.#src_ident))
14210        })
14211    });
14212
14213    Ok(quote! {
14214        impl #root::serializer::ModelSerializer for #struct_name {
14215            type Model = #model_path;
14216
14217            fn related_tables() -> ::std::vec::Vec<(&'static str, &'static str)> {
14218                ::std::vec![ #( #related_tables ),* ]
14219            }
14220
14221            fn from_model(model: &Self::Model) -> Self {
14222                Self {
14223                    #( #from_model_fields ),*
14224                }
14225            }
14226
14227            fn writable_fields() -> &'static [&'static str] {
14228                &[ #( #writable_lits ),* ]
14229            }
14230
14231            fn writable_source_fields() -> &'static [&'static str] {
14232                &[ #( #writable_source_lits ),* ]
14233            }
14234
14235            fn readable_source_fields() -> &'static [&'static str] {
14236                &[ #( #readable_source_lits ),* ]
14237            }
14238
14239            fn readable_fields() -> &'static [&'static str] {
14240                &[ #( #readable_lits ),* ]
14241            }
14242
14243            fn from_writable_json(
14244                __body: &#root::__serde_json::Value,
14245            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
14246                let mut __errors = #root::forms::FormErrors::default();
14247                let __obj = __body.as_object();
14248                let __out = Self {
14249                    #( #from_writable_json_inits ),*
14250                };
14251                if __errors.is_empty() {
14252                    ::core::result::Result::Ok(__out)
14253                } else {
14254                    ::core::result::Result::Err(__errors)
14255                }
14256            }
14257
14258            fn validate_patch(
14259                __stored: &Self::Model,
14260                __body: &#root::__serde_json::Value,
14261            ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14262                let mut __errors = #root::forms::FormErrors::default();
14263                let __obj = __body.as_object();
14264                #[allow(unused_mut)]
14265                let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
14266                #( #patch_overlays )*
14267                if !__errors.is_empty() {
14268                    return ::core::result::Result::Err(__errors);
14269                }
14270                #patch_validate_call
14271            }
14272
14273            #trait_validate_override
14274        }
14275
14276        #patch_validate_helper
14277
14278        impl #root::__serde::Serialize for #struct_name {
14279            fn serialize<S>(&self, serializer: S)
14280                -> ::core::result::Result<S::Ok, S::Error>
14281            where
14282                S: #root::__serde::Serializer,
14283            {
14284                use #root::__serde::ser::SerializeStruct;
14285                let mut __state = serializer.serialize_struct(
14286                    #struct_name_lit,
14287                    #output_field_count,
14288                )?;
14289                #( #serialize_fields )*
14290                __state.end()
14291            }
14292        }
14293
14294        #openapi_impl
14295
14296        #validate_method
14297
14298        #many_setters_impl
14299    })
14300}
14301
14302/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
14303/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
14304/// when the feature is off.
14305#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
14306fn is_option(ty: &syn::Type) -> bool {
14307    if let syn::Type::Path(p) = ty {
14308        if let Some(last) = p.path.segments.last() {
14309            return last.ident == "Option";
14310        }
14311    }
14312    false
14313}
14314
14315#[cfg(test)]
14316mod main_attr_tests {
14317    use super::*;
14318
14319    fn expand(args: &str) -> String {
14320        let body = quote! { async fn main() { run().await } };
14321        expand_main(args.parse().unwrap(), body)
14322            .expect("expansion failed")
14323            .to_string()
14324    }
14325
14326    /// Asserts on the expansion rather than on `parse_main_args`,
14327    /// because the defect was a subscriber reaching the output, not
14328    /// an argument being misread (#1465).
14329    #[test]
14330    fn logging_false_emits_no_subscriber() {
14331        let out = expand("logging = false");
14332        assert!(
14333            !out.contains("logging :: Setup"),
14334            "`logging = false` must not install a subscriber:\n{out}",
14335        );
14336        assert!(
14337            out.contains("block_on"),
14338            "the runtime must still be built:\n{out}",
14339        );
14340    }
14341
14342    #[test]
14343    fn default_still_installs_a_subscriber() {
14344        // The control. Without it, an `expand_main` that dropped the
14345        // prologue unconditionally would pass the test above while
14346        // leaving every default project with no logging at all.
14347        let out = expand("");
14348        assert!(
14349            out.contains("logging :: Setup"),
14350            "the default entrypoint must still install one:\n{out}",
14351        );
14352        // `./app serve` or `./app --port 8080` must keep logging to stdout:
14353        // only `Cli`, which knows its verbs, moves logs to stderr.
14354        assert!(
14355            !out.contains("args ()"),
14356            "the entrypoint must not route logs by argv:\n{out}",
14357        );
14358    }
14359
14360    #[test]
14361    fn logging_false_composes_with_flavor() {
14362        let out = expand("flavor = \"current_thread\", logging = false");
14363        assert!(!out.contains("logging :: Setup"), "{out}");
14364        assert!(out.contains("new_current_thread"), "{out}");
14365    }
14366
14367    /// The body runs on the big-stack thread; `stack_size` overrides it.
14368    #[test]
14369    fn body_runs_on_a_big_stack_thread() {
14370        let out = expand("");
14371        assert!(out.contains("run_on_main_thread"), "{out}");
14372        assert!(out.contains("MAIN_STACK_SIZE"), "{out}");
14373        let out = expand("stack_size = 16777216, logging = false");
14374        assert!(out.contains("16777216usize"), "{out}");
14375        assert!(!out.contains("MAIN_STACK_SIZE"), "{out}");
14376        assert!(
14377            out.contains("thread_stack_size"),
14378            "workers get it too:\n{out}"
14379        );
14380    }
14381
14382    /// A stack too small to run anything is a compile error.
14383    #[test]
14384    fn a_tiny_stack_size_is_refused() {
14385        let body = quote! { async fn main() {} };
14386        let err = expand_main("stack_size = 4096".parse().unwrap(), body)
14387            .expect_err("4 KiB")
14388            .to_string();
14389        assert!(err.contains("at least"), "{err}");
14390    }
14391
14392    /// Lint attributes reach the inner body fn; others do not.
14393    #[test]
14394    fn lint_attributes_reach_the_body() {
14395        let body = quote! {
14396            #[allow(unused_variables)]
14397            #[doc = "entry"]
14398            async fn main() { let x = 1; }
14399        };
14400        let out = expand_main(quote!(), body).unwrap().to_string();
14401        assert_eq!(out.matches("allow (unused_variables)").count(), 2, "{out}");
14402        assert_eq!(out.matches("doc = \"entry\"").count(), 1, "{out}");
14403    }
14404
14405    #[test]
14406    fn logging_true_is_the_default_shape() {
14407        assert!(
14408            parse_main_args(&"logging = true".parse().unwrap())
14409                .unwrap()
14410                .logging
14411        );
14412        assert!(
14413            !parse_main_args(&"logging=false".parse().unwrap())
14414                .unwrap()
14415                .logging
14416        );
14417    }
14418}
14419
14420#[cfg(test)]
14421mod main_arg_tests {
14422    use super::*;
14423
14424    fn expand(args: &str) -> syn::Result<TokenStream2> {
14425        let body = quote! { async fn main() { run().await } };
14426        expand_main(args.parse().unwrap(), body)
14427    }
14428
14429    /// A typo or unknown key is a compile error, not silently ignored.
14430    #[test]
14431    fn unknown_or_misspelt_args_are_errors() {
14432        for args in [
14433            "logging = flase",
14434            "multi_thread",
14435            "workers = 4",
14436            r#"flavor = "curent_thread""#,
14437            "worker_threads = 4",
14438        ] {
14439            let err = expand(args).expect_err(args).to_string();
14440            assert!(err.contains("#[rustango::main]"), "{args}: {err}");
14441        }
14442    }
14443
14444    #[test]
14445    fn supported_args_expand() {
14446        for args in [
14447            "",
14448            r#"flavor = "current_thread""#,
14449            r#"flavor = "multi_thread", logging = false"#,
14450            "logging = true",
14451        ] {
14452            assert!(expand(args).is_ok(), "{args}");
14453        }
14454    }
14455}
14456
14457#[cfg(test)]
14458mod embed_migrations_tests {
14459    use super::*;
14460
14461    fn expand(path: &str) -> String {
14462        expand_embed_migrations(quote! { #path })
14463            .expect("expansion failed")
14464            .to_string()
14465    }
14466
14467    #[test]
14468    fn missing_dir_warns() {
14469        let out = expand("./no_such_migrations_dir");
14470        assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
14471        assert!(out.contains("no_such_migrations_dir"), "{out}");
14472    }
14473
14474    #[test]
14475    fn existing_dir_does_not_warn() {
14476        let out = expand("./src");
14477        assert!(!out.contains("deprecated"), "{out}");
14478    }
14479}
14480
14481#[cfg(test)]
14482mod field_attr_tests {
14483    use super::*;
14484
14485    fn error_for(field: syn::Field) -> Option<String> {
14486        let attrs = parse_field_attrs(&field).unwrap();
14487        let kind = detect_type(&field.ty).unwrap().kind;
14488        check_bound_compatibility(&field, &attrs, kind)
14489            .err()
14490            .map(|e| e.to_string())
14491    }
14492
14493    /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
14494    #[test]
14495    fn type_specific_attrs_reject_other_types() {
14496        let f: syn::FieldsNamed = syn::parse_quote!({
14497            #[rustango(citext)] a: i64,
14498            #[rustango(vector(dims = 3))] b: String,
14499            #[rustango(geometry(srid = 4326))] c: String,
14500            #[rustango(citext)] d: String,
14501        });
14502        let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
14503        assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
14504        assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
14505        assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
14506        assert_eq!(errs[3], None);
14507    }
14508
14509    /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
14510    #[test]
14511    fn manager_ext_is_refused() {
14512        let input: DeriveInput = syn::parse_quote! {
14513            #[rustango(table = "p", manager(ext = "PostManagerExt"))]
14514            struct Post { id: i64 }
14515        };
14516        let err = parse_container_attrs(&input).err().expect("refused");
14517        assert!(err.to_string().contains("#2132"), "{err}");
14518    }
14519
14520    /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
14521    #[test]
14522    fn set_null_needs_an_optional_foreign_key() {
14523        let input: DeriveInput = syn::parse_quote! {
14524            #[rustango(table = "c")]
14525            struct Child {
14526                #[rustango(primary_key)] id: i64,
14527                #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
14528            }
14529        };
14530        let err = expand(&input).err().expect("refused");
14531        assert!(err.to_string().contains("set_null"), "{err}");
14532        let input: DeriveInput = syn::parse_quote! {
14533            #[rustango(table = "c")]
14534            struct Child {
14535                #[rustango(primary_key)] id: i64,
14536                #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
14537            }
14538        };
14539        assert!(expand(&input).is_ok());
14540    }
14541
14542    /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
14543    #[test]
14544    fn set_null_needs_a_nullable_fk_column() {
14545        let input: DeriveInput = syn::parse_quote! {
14546            #[rustango(table = "c")]
14547            struct Child {
14548                #[rustango(primary_key)] id: i64,
14549                #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
14550                #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
14551            }
14552        };
14553        let err = expand(&input).err().expect("refused");
14554        assert!(err.to_string().contains("set_null"), "{err}");
14555        let input: DeriveInput = syn::parse_quote! {
14556            #[rustango(table = "c")]
14557            struct Child {
14558                #[rustango(primary_key)] id: i64,
14559                #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
14560                #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
14561                #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
14562            }
14563        };
14564        assert!(expand(&input).is_ok());
14565    }
14566
14567    /// An index or `get_latest_by` naming no column is a compile error,
14568    /// one per name.
14569    #[test]
14570    fn unknown_index_columns_are_errors() {
14571        let input: DeriveInput = syn::parse_quote! {
14572            #[rustango(
14573                table = "w",
14574                unique_together = "a, nope",
14575                index_together = "a, b_typo",
14576                index("missing"),
14577                get_latest_by = "-when_typo"
14578            )]
14579            struct W { #[rustango(primary_key)] id: i64, a: i64, b: i64 }
14580        };
14581        let err = expand(&input).expect_err("must not compile");
14582        let all: Vec<String> = err.into_iter().map(|e| e.to_string()).collect();
14583        for name in ["nope", "b_typo", "missing", "when_typo"] {
14584            assert!(
14585                all.iter().any(|m| m.contains(&format!("`{name}`"))),
14586                "{name}: {all:?}"
14587            );
14588        }
14589    }
14590
14591    /// Real columns, field names for `get_latest_by` and expression
14592    /// indexes compile.
14593    #[test]
14594    fn known_index_columns_compile() {
14595        let input: DeriveInput = syn::parse_quote! {
14596            #[rustango(
14597                table = "w",
14598                unique_together = "a, b_col",
14599                index("lower(a)"),
14600                get_latest_by = "-b"
14601            )]
14602            struct W {
14603                #[rustango(primary_key)] id: i64,
14604                a: i64,
14605                #[rustango(column = "b_col")] b: i64,
14606            }
14607        };
14608        expand(&input).expect("compiles");
14609    }
14610
14611    /// Two FKs to one parent would emit the same reverse accessor twice.
14612    #[test]
14613    fn reverse_accessor_clash_names_related_name() {
14614        let input: DeriveInput = syn::parse_quote! {
14615            #[rustango(table = "m")]
14616            struct Message {
14617                #[rustango(primary_key)] id: i64,
14618                sender: ForeignKey<User>,
14619                recipient: ForeignKey<User>,
14620            }
14621        };
14622        let err = expand(&input).err().expect("refused");
14623        assert!(err.to_string().contains("related_name"), "{err}");
14624    }
14625
14626    /// `related_name = "+"` emits no reverse accessor, so a parent from
14627    /// another crate, which cannot take an inherent impl, still derives.
14628    #[test]
14629    fn related_name_plus_skips_the_reverse_accessor() {
14630        let input: DeriveInput = syn::parse_quote! {
14631            #[rustango(table = "m")]
14632            struct Message {
14633                #[rustango(primary_key)] id: i64,
14634                sender: ForeignKey<User>,
14635                #[rustango(related_name = "+")] recipient: ForeignKey<User>,
14636                #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
14637            }
14638        };
14639        let out = expand(&input).expect("derives").to_string();
14640        assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
14641        assert!(!out.contains("impl other :: Account {"), "{out}");
14642    }
14643
14644    fn field_error(src: &str) -> Option<String> {
14645        let f: syn::FieldsNamed = syn::parse_str(&format!("{{ {src} }}")).unwrap();
14646        let field = f.named.into_iter().next().unwrap();
14647        match parse_field_attrs(&field) {
14648            Err(e) => Some(e.to_string()),
14649            Ok(_) => error_for(field),
14650        }
14651    }
14652
14653    /// Flag attrs take `= true` like `blank`; `= false` is refused by name.
14654    #[test]
14655    fn flag_attrs_accept_eq_true() {
14656        for key in [
14657            "primary_key",
14658            "unique",
14659            "soft_delete",
14660            "index",
14661            "auto_now",
14662            "auto_now_add",
14663        ] {
14664            let f: syn::FieldsNamed =
14665                syn::parse_str(&format!("{{ #[rustango({key} = true)] a: i64 }}")).unwrap();
14666            let field = f.named.into_iter().next().unwrap();
14667            assert!(parse_field_attrs(&field).is_ok(), "{key} = true");
14668            let err = field_error(&format!("#[rustango({key} = false)] a: i64")).unwrap();
14669            assert!(err.contains(&format!("`{key} = false`")), "{err}");
14670        }
14671        let f: syn::Field =
14672            syn::parse_quote!(#[rustango(primary_key = true, unique = true)] a: i64);
14673        let attrs = parse_field_attrs(&f).unwrap();
14674        assert!(attrs.primary_key && attrs.unique);
14675    }
14676
14677    /// An unknown key is named, the nearest key suggested and the list shown.
14678    #[test]
14679    fn unknown_key_is_named_with_a_suggestion() {
14680        let err = field_error("#[rustango(max_lenght = 5)] a: String").unwrap();
14681        assert!(err.contains("`max_lenght`"), "{err}");
14682        assert!(err.contains("did you mean `max_length`"), "{err}");
14683        assert!(err.contains("Supported: column"), "{err}");
14684        let input: DeriveInput = syn::parse_quote! {
14685            #[rustango(tabel = "p")]
14686            struct P { id: i64 }
14687        };
14688        let err = parse_container_attrs(&input).err().unwrap().to_string();
14689        assert!(
14690            err.contains("`tabel`") && err.contains("did you mean `table`"),
14691            "{err}"
14692        );
14693        let err = field_error("#[rustango(zzzzzz)] a: i64").unwrap();
14694        assert!(!err.contains("did you mean"), "{err}");
14695    }
14696
14697    /// The listed keys are the parsed keys; a stale list would mislead.
14698    #[test]
14699    fn listed_keys_are_all_known() {
14700        for key in FIELD_ATTR_KEYS {
14701            let err = field_error(&format!("#[rustango({key})] a: i64")).unwrap_or_default();
14702            assert!(!err.contains("unknown rustango field"), "{key}: {err}");
14703        }
14704        for key in CONTAINER_ATTR_KEYS {
14705            let input: DeriveInput =
14706                syn::parse_str(&format!("#[rustango({key})] struct P {{ id: i64 }}")).unwrap();
14707            let err = parse_container_attrs(&input)
14708                .err()
14709                .map(|e| e.to_string())
14710                .unwrap_or_default();
14711            assert!(!err.contains("unknown rustango container"), "{key}: {err}");
14712        }
14713    }
14714
14715    /// `default` is raw SQL: a bare word or a non-number on a numeric field is refused.
14716    #[test]
14717    fn default_shapes_only_sqlite_takes_are_refused() {
14718        for bad in [
14719            r#"#[rustango(default = "draft")] s: String"#,
14720            r#"#[rustango(default = "draft")] s: Option<String>"#,
14721            r#"#[rustango(default = "abc")] n: i32"#,
14722            r#"#[rustango(default = "'abc'")] n: i64"#,
14723            r#"#[rustango(default = "true")] n: i32"#,
14724            r#"#[rustango(default = "")] n: i32"#,
14725            r#"#[rustango(default = "'x'")] n: f64"#,
14726        ] {
14727            assert!(field_error(bad).is_some(), "{bad} must be refused");
14728        }
14729        let err = field_error(r#"#[rustango(default = "draft")] s: String"#).unwrap();
14730        assert!(err.contains(r#"default = "'draft'""#), "{err}");
14731        // Valid on Postgres alone, so the message says that, not "a column".
14732        let err = field_error(r#"#[rustango(default = "current_user")] s: String"#).unwrap();
14733        assert!(
14734            err.contains("Postgres only") && !err.contains("column"),
14735            "{err}"
14736        );
14737        for good in [
14738            r#"#[rustango(default = "'draft'")] s: String"#,
14739            r#"#[rustango(default = "")] s: String"#,
14740            r#"#[rustango(default = "now()")] s: String"#,
14741            r#"#[rustango(default = "CURRENT_TIMESTAMP")] s: String"#,
14742            r#"#[rustango(default = "NULL")] s: Option<String>"#,
14743            r#"#[rustango(default = "0")] n: i32"#,
14744            r#"#[rustango(default = "-1")] n: i64"#,
14745            r#"#[rustango(default = "'5'")] n: i64"#,
14746            r#"#[rustango(default = "(1 + 2)")] n: i64"#,
14747            r#"#[rustango(default = "1.5")] n: f64"#,
14748            r#"#[rustango(default = "true")] b: bool"#,
14749        ] {
14750            assert_eq!(field_error(good), None, "{good}");
14751        }
14752    }
14753
14754    /// A raw ident derives names without `r#`; this used to panic (#1937).
14755    #[test]
14756    fn raw_idents_derive_plain_names() {
14757        let ident: syn::Ident = syn::parse_quote!(r#type);
14758        assert_eq!(column_type_ident(&ident).to_string(), "type_col");
14759        assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
14760    }
14761}
14762
14763#[cfg(test)]
14764mod model_attr_name_tests {
14765    use super::*;
14766
14767    fn container_error(item: proc_macro2::TokenStream) -> Option<String> {
14768        let input: DeriveInput = syn::parse2(item).unwrap();
14769        parse_container_attrs(&input).err().map(|e| e.to_string())
14770    }
14771
14772    fn field_error(field: proc_macro2::TokenStream) -> Option<String> {
14773        let f: syn::FieldsNamed = syn::parse2(quote!({ #field })).unwrap();
14774        let field = f.named.into_iter().next().unwrap();
14775        parse_field_attrs(&field).err().map(|e| e.to_string())
14776    }
14777
14778    /// `#[rgo(...)]` reads exactly as `#[rustango(...)]`.
14779    #[test]
14780    fn rgo_is_read_like_rustango() {
14781        let long: DeriveInput = syn::parse_quote! {
14782            #[rustango(table = "t", display = "name")] struct S;
14783        };
14784        let short: DeriveInput = syn::parse_quote! {
14785            #[rgo(table = "t", display = "name")] struct S;
14786        };
14787        let (long, short) = (
14788            parse_container_attrs(&long).unwrap(),
14789            parse_container_attrs(&short).unwrap(),
14790        );
14791        assert_eq!(long.table, short.table);
14792        assert_eq!(short.table.as_deref(), Some("t"));
14793        assert_eq!(long.display.map(|d| d.0), short.display.map(|d| d.0));
14794    }
14795
14796    #[test]
14797    fn mixing_names_on_one_item_or_field_is_refused() {
14798        let err = container_error(quote! {
14799            #[rustango(table = "t")] #[rgo(display = "name")] struct S;
14800        })
14801        .expect("a mix on an item must fail");
14802        assert!(
14803            err.contains("`#[rustango(...)]` and `#[rgo(...)]`"),
14804            "{err}"
14805        );
14806        let err = field_error(quote! { #[rgo(primary_key)] #[rustango(column = "c")] a: i64 })
14807            .expect("a mix on a field must fail");
14808        assert!(
14809            err.contains("`#[rgo(...)]` and `#[rustango(...)]`"),
14810            "{err}"
14811        );
14812        // Either name repeated alone is fine.
14813        assert_eq!(
14814            container_error(quote! { #[rgo(table = "t")] #[rgo(display = "name")] struct S; }),
14815            None
14816        );
14817        assert_eq!(
14818            field_error(quote! { #[rustango(primary_key)] #[rustango(column = "c")] a: i64 }),
14819            None
14820        );
14821    }
14822
14823    /// A hint quotes the attribute under the name the user wrote.
14824    #[test]
14825    fn hints_use_the_written_name() {
14826        let err = container_error(quote! { #[rgo(table = "t", unique_together = "a")] struct S; })
14827            .expect("one column must fail");
14828        assert!(err.contains("#[rgo(unique)]"), "{err}");
14829        let err =
14830            container_error(quote! { #[rustango(table = "t", index_together = "a")] struct S; })
14831                .expect("one column must fail");
14832        assert!(err.contains("#[rustango(index)]"), "{err}");
14833    }
14834}
14835
14836/// A migration file's text: a UTF-8 BOM dropped, UTF-16 LE / BE decoded by
14837/// its BOM. A copy of `rustango::text_file`'s decoder, which this crate
14838/// cannot depend on.
14839fn decode_text(bytes: Vec<u8>) -> Result<String, String> {
14840    fn utf16(bytes: &[u8], unit: fn([u8; 2]) -> u16) -> Result<String, String> {
14841        if bytes.len() % 2 != 0 {
14842            return Err("odd-length UTF-16".into());
14843        }
14844        let units = bytes.chunks_exact(2).map(|c| unit([c[0], c[1]]));
14845        char::decode_utf16(units)
14846            .collect::<Result<String, _>>()
14847            .map_err(|e| e.to_string())
14848    }
14849    match bytes.as_slice() {
14850        [0xFF, 0xFE, rest @ ..] => utf16(rest, u16::from_le_bytes),
14851        [0xFE, 0xFF, rest @ ..] => utf16(rest, u16::from_be_bytes),
14852        _ => {
14853            let mut bytes = bytes;
14854            if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
14855                bytes.drain(..3);
14856            }
14857            String::from_utf8(bytes).map_err(|e| e.to_string())
14858        }
14859    }
14860}
14861
14862#[cfg(test)]
14863mod decode_text_tests {
14864    use super::decode_text;
14865
14866    #[test]
14867    fn a_bom_or_utf16_migration_decodes() {
14868        let text = r#"{"name": "0001_é"}"#;
14869        let mut bom = vec![0xEF, 0xBB, 0xBF];
14870        bom.extend(text.bytes());
14871        let mut le = vec![0xFF, 0xFE];
14872        le.extend(text.encode_utf16().flat_map(u16::to_le_bytes));
14873        let mut be = vec![0xFE, 0xFF];
14874        be.extend(text.encode_utf16().flat_map(u16::to_be_bytes));
14875        for bytes in [text.as_bytes().to_vec(), bom, le, be] {
14876            assert_eq!(decode_text(bytes).unwrap(), text);
14877        }
14878        assert!(decode_text(vec![0xFF, 0xFE, 0x41]).is_err());
14879    }
14880}