Skip to main content

rustango_macros/
lib.rs

1//! Proc-macros for rustango.
2//!
3//! v0.1 ships `#[derive(Model)]`, which emits:
4//! * a `Model` impl carrying a static `ModelSchema`,
5//! * an `inventory::submit!` so the model is discoverable from the registry,
6//! * an inherent `objects()` returning a `QuerySet<Self>`,
7//! * a `sqlx::FromRow` impl so query results decode into the struct.
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13    parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14    PathArguments, Type, TypePath,
15};
16
17/// Resolve the consumer's local name for the `rustango` crate so the
18/// macro-emitted code keeps compiling when a downstream `Cargo.toml`
19/// renames the dep (`[dependencies] orm = { package = "rustango", … }`)
20/// or when the standalone `rustango-orm` crate ships in a future
21/// slice of the [orm-extract epic](https://github.com/ujeenet/rustango/issues/149).
22///
23/// Returns one of:
24/// - `quote!(::rustango)` — the consumer IS the `rustango` crate
25///   itself. We emit the absolute path `::rustango` (NOT `crate`)
26///   because examples and integration tests inside the rustango
27///   package compile as separate binaries — their own `crate::`
28///   namespace is the example/test file, not rustango's lib root.
29///   The absolute `::rustango` resolves correctly in both contexts
30///   (rustango lib code AND rustango/examples/*.rs).
31/// - `quote!(::ident)` — the consumer renamed the dep; emit the
32///   user's chosen ident.
33/// - `quote!(::rustango)` — fallback when `proc-macro-crate` can't
34///   read the manifest (rare; preserves today's behavior).
35///
36/// Issue [#142](https://github.com/ujeenet/rustango/issues/142).
37fn rustango_root() -> TokenStream2 {
38    use proc_macro_crate::{crate_name, FoundCrate};
39    match crate_name("rustango") {
40        Ok(FoundCrate::Itself) => quote!(::rustango),
41        Ok(FoundCrate::Name(name)) => {
42            let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
43            quote!(::#ident)
44        }
45        Err(_) => quote!(::rustango),
46    }
47}
48
49/// Derive a `Model` impl. See crate docs for the supported attributes.
50#[proc_macro_derive(Model, attributes(rustango, rgo))]
51pub fn derive_model(input: TokenStream) -> TokenStream {
52    let input = parse_macro_input!(input as DeriveInput);
53    expand(&input)
54        .unwrap_or_else(syn::Error::into_compile_error)
55        .into()
56}
57
58/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
59/// marker struct, wiring the full CRUD ViewSet in one annotation.
60///
61/// ```ignore
62/// #[derive(ViewSet)]
63/// #[viewset(
64///     model        = Post,
65///     fields       = "id, title, body, author_id",
66///     filter_fields = "author_id",
67///     search_fields = "title, body",
68///     ordering     = "-published_at",
69///     page_size    = 20,
70///     allow_anonymous, // or `read_only` / `permissions(...)`
71/// )]
72/// pub struct PostViewSet;
73///
74/// // Mount into your app:
75/// let app = Router::new()
76///     .merge(PostViewSet::router("/api/posts", pool.clone()));
77/// ```
78///
79/// Attributes:
80/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
81///   `SCHEMA` constant drives the endpoints.
82/// * `fields = "a, b, c"` — *required*. The only scalar fields list/retrieve
83///   JSON renders and create/update accept; other body keys are ignored.
84/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
85/// * `search_fields = "a, b"` — fields searched by `?search=...`.
86/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
87/// * `page_size = N` — default page size (default: 20; requests are capped
88///   at `ViewSet::max_page_size`, default 100).
89/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
90/// * `allow_anonymous` — flag; write actions without codenames are
91///   intended. Without it (or `read_only`) they refuse to mount.
92/// * `serializer = SomeSerializer` — render list / retrieve / create
93///   responses through a `#[derive(Serializer)]` type instead of the
94///   default field-level projection (`read_only` / `source` / `method`
95///   / `write_only` overrides apply). Tri-dialect. Requires the
96///   `serializer` feature.
97/// * `permissions(list = "...", retrieve = "...", create = "...",
98///   update = "...", destroy = "...")` — codenames required per action.
99/// * `filter_backend = expr` — a `ViewSetFilter` (a unit struct, a call
100///   or a closure), as `ViewSet::filter_backend`; repeat it to stack.
101#[proc_macro_derive(ViewSet, attributes(viewset))]
102pub fn derive_viewset(input: TokenStream) -> TokenStream {
103    let input = parse_macro_input!(input as DeriveInput);
104    expand_viewset(&input)
105        .unwrap_or_else(syn::Error::into_compile_error)
106        .into()
107}
108
109/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
110/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
111/// that walks every named field and:
112///
113/// * Parses the string value into the field's Rust type (`String`,
114///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
115///   nullable case).
116/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
117///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
118///   validators in declaration order, returning `FormError::Parse`
119///   on the first failure.
120///
121/// Example:
122///
123/// ```ignore
124/// #[derive(Form)]
125/// pub struct CreateItemForm {
126///     #[form(min_length = 1, max_length = 64)]
127///     pub name: String,
128///     #[form(min = 0, max = 150)]
129///     pub age: i32,
130///     pub active: bool,
131///     pub email: Option<String>,
132/// }
133///
134/// let parsed = CreateItemForm::parse(&form_map)?;
135/// ```
136#[proc_macro_derive(Form, attributes(form))]
137pub fn derive_form(input: TokenStream) -> TokenStream {
138    let input = parse_macro_input!(input as DeriveInput);
139    expand_form(&input)
140        .unwrap_or_else(syn::Error::into_compile_error)
141        .into()
142}
143
144/// Derive `rustango::serializer::ModelSerializer` for a struct.
145/// (intra-doc link disabled — the macro crate doesn't depend on
146/// `rustango` itself, so rustdoc can't resolve the path.)
147///
148/// # Container attribute (required)
149/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
150///
151/// # Field attributes
152/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
153/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
154/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
155/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
156/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
157/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
158/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
159/// - `#[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)
160/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
161/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
162///   write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
163///   no attr is given, override it when given (`min_length` is serializer-only)
164///
165/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
166/// It honours `#[serde(rename, rename_all, skip, skip_serializing, skip_serializing_if,
167/// skip_deserializing)]`; other `serde` keys are ignored.
168#[proc_macro_derive(Serializer, attributes(serializer, serde))]
169pub fn derive_serializer(input: TokenStream) -> TokenStream {
170    let input = parse_macro_input!(input as DeriveInput);
171    expand_serializer(&input)
172        .unwrap_or_else(syn::Error::into_compile_error)
173        .into()
174}
175
176/// Bake every `*.json` migration file in a directory into the binary
177/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
178/// of `(name, json_content)` pairs, lex-sorted by file stem.
179///
180/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
181/// behaviour as `migrate(pool, dir)` but with no filesystem access.
182/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
183/// (i.e. the crate that invokes the macro). Default is
184/// `"./migrations"` if no argument is supplied.
185///
186/// ```ignore
187/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
188/// // or:
189/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
190///
191/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
192/// ```
193///
194/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
195/// file's `name` field must equal its file stem, every `prev`
196/// reference must point to another migration in the same directory,
197/// and the JSON must parse. A broken chain — orphan `prev`, missing
198/// predecessor, malformed file — fails at macro-expansion time with
199/// a clear `compile_error!`. *No other Rust web framework
200/// validates migration chains at compile time*: Cot's migrations are
201/// imperative Rust code (no static chain), Loco's are SeaORM
202/// up/down (same), Rwf's are raw SQL (no chain at all).
203///
204/// Each migration is included via `include_str!` so cargo's rebuild
205/// detection picks up file *content* changes. Cargo does not watch
206/// directory listings, so a new migration file is missed until the
207/// crate rebuilds. Add a `build.rs` that tracks the directory:
208///
209/// ```ignore
210/// fn main() { println!("cargo:rerun-if-changed=migrations"); }
211/// ```
212///
213/// A missing directory embeds nothing and emits a deprecation warning.
214#[proc_macro]
215pub fn embed_migrations(input: TokenStream) -> TokenStream {
216    expand_embed_migrations(input.into())
217        .unwrap_or_else(syn::Error::into_compile_error)
218        .into()
219}
220
221/// Compile a static directory into the binary as a
222/// `rustango::static_files::EmbeddedDir`. The path is relative to the
223/// crate's `Cargo.toml`; names starting with `.` are left out.
224///
225/// ```text
226/// static ASSETS: rustango::static_files::EmbeddedDir = rustango::embed_static!("static");
227/// ```
228///
229/// Edits to embedded files rebuild the crate; a new file needs a
230/// `build.rs` with `println!("cargo:rerun-if-changed=static");`.
231#[proc_macro]
232pub fn embed_static(input: TokenStream) -> TokenStream {
233    expand_embed_static(input.into())
234        .unwrap_or_else(syn::Error::into_compile_error)
235        .into()
236}
237
238fn expand_embed_static(input: TokenStream2) -> syn::Result<TokenStream2> {
239    let lit: LitStr = syn::parse2(input)?;
240    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
241        syn::Error::new(
242            lit.span(),
243            "embed_static! needs a Cargo build (CARGO_MANIFEST_DIR not set)",
244        )
245    })?;
246    let root = std::path::Path::new(&manifest).join(lit.value());
247    if !root.is_dir() {
248        return Err(syn::Error::new(
249            lit.span(),
250            format!("embed_static!: {} is not a directory", root.display()),
251        ));
252    }
253    let mut files = Vec::new();
254    walk_static(&root, "", &mut files)
255        .map_err(|e| syn::Error::new(lit.span(), format!("embed_static!: {e}")))?;
256    files.sort();
257    let entries = files.iter().map(|(rel, abs)| {
258        quote! { (#rel, ::core::include_bytes!(#abs) as &'static [u8]) }
259    });
260    let root_path = rustango_root();
261    Ok(quote! {
262        #root_path::static_files::EmbeddedDir::__new(&[ #( #entries ),* ])
263    })
264}
265
266/// `(relative path, absolute path)` of every non-hidden file under `dir`.
267fn walk_static(
268    dir: &std::path::Path,
269    rel: &str,
270    out: &mut Vec<(String, String)>,
271) -> std::io::Result<()> {
272    for entry in std::fs::read_dir(dir)? {
273        let entry = entry?;
274        let name = entry.file_name();
275        let Some(name) = name.to_str() else { continue };
276        if name.starts_with('.') {
277            continue;
278        }
279        let path = if rel.is_empty() {
280            name.to_owned()
281        } else {
282            format!("{rel}/{name}")
283        };
284        let kind = entry.file_type()?;
285        if kind.is_dir() {
286            walk_static(&entry.path(), &path, out)?;
287        } else if kind.is_file() {
288            let abs = entry.path().to_str().map(str::to_owned).ok_or_else(|| {
289                std::io::Error::other(format!("{} is not UTF-8", entry.path().display()))
290            })?;
291            out.push((path, abs));
292        }
293    }
294    Ok(())
295}
296
297/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
298/// compile time against typed columns. Issue #269 / T1.7.
299///
300/// Each invocation lowers to the equivalent typed-column method call:
301///
302/// ```ignore
303/// // These expand identically:
304/// Q!(User.email__icontains = "alice")
305/// User::email.ilike("%alice%")
306/// ```
307///
308/// Field-name typos fail the build (the macro emits `User::no_such_field`
309/// which doesn't exist) — the headline ergonomic win of this slice over
310/// filters keyed by a plain string.
311///
312/// # Supported lookup suffixes
313///
314/// * bare `=` / `__exact` → `.eq(value)`
315/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
316/// * `__ne` → `.ne(value)`
317/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
318/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
319/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
320/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
321/// * `__in` → `.is_in(iterable)`
322/// * `__not_in` → `.not_in(iterable)`
323/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
324/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
325/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
326///
327/// Unknown suffixes fail the build with a `compile_error!` pointing at
328/// the lookup token.
329///
330/// # Combine
331///
332/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
333/// `.and()` / `.or()` / `.not()` methods:
334///
335/// ```ignore
336/// User::objects()
337///     .where_(
338///         Q!(User.active = true)
339///             .and(Q!(User.email__icontains = "alice"))
340///     )
341///     .fetch(&pool).await?;
342/// ```
343///
344/// All emitted code routes through existing per-dialect writers — no new
345/// SQL emission machinery. Tri-dialect support is inherent.
346#[allow(non_snake_case)]
347#[proc_macro]
348pub fn Q(input: TokenStream) -> TokenStream {
349    expand_q(input.into())
350        .unwrap_or_else(syn::Error::into_compile_error)
351        .into()
352}
353
354/// `#[rustango::main]` — the runserver entrypoint. Wraps
355/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
356/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
357/// functions are zero-boilerplate. `RUST_LOG` and
358/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
359/// environment, else from `./.env` (only those keys are read):
360///
361/// ```ignore
362/// #[rustango::main]
363/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
364///     rustango::server::Builder::from_env().await?
365///         .migrate("migrations").await?
366///         .api(my_app::urls::api())
367///         .seed_with(my_app::seed::run).await?
368///         .serve("0.0.0.0:8080").await
369/// }
370/// ```
371///
372/// Optional `flavor = "current_thread"` passes through to
373/// `#[tokio::main]`; default is the multi-threaded runtime.
374///
375/// The body runs on a thread named `main` with an 8 MiB stack, since
376/// Windows gives the real main thread 1 MiB; `stack_size = <bytes>`
377/// overrides it. A panic there resumes on the real main thread.
378///
379/// `logging = false` suppresses the default subscriber, for apps
380/// that install their own. Needed whenever the body reaches
381/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
382/// macro installs gets there first, and `tracing` keeps the first
383/// one, so the `[logging]` section would otherwise be read and
384/// discarded (#1465).
385///
386/// ```ignore
387/// #[rustango::main(logging = false)]
388/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
389///     rustango::manage::Cli::new()
390///         .with_settings_from_env()
391///         .with_logging()          // now the first installer
392///         .run().await
393/// }
394/// ```
395///
396/// Pulls `tracing-subscriber` into the rustango crate behind the
397/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
398/// out get plain `#[tokio::main]` ergonomics without the dependency.
399#[proc_macro_attribute]
400pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
401    expand_main(args.into(), item.into())
402        .unwrap_or_else(syn::Error::into_compile_error)
403        .into()
404}
405
406fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
407    let mut input: syn::ItemFn = syn::parse2(item)?;
408    if input.sig.asyncness.is_none() {
409        return Err(syn::Error::new(
410            input.sig.ident.span(),
411            "`#[rustango::main]` must wrap an `async fn`",
412        ));
413    }
414
415    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
416    // to `#[tokio::main]`. Tokio's proc-macro internally emits
417    // `::tokio::*` paths that resolve against the user crate's deps,
418    // so calling it through the rustango re-export still requires the
419    // user to add tokio to their own Cargo.toml. Building the
420    // runtime ourselves keeps the dep transitive through the
421    // `runtime` feature on rustango.
422    //
423    // `flavor` and `logging`; anything else is a compile error.
424    let root = rustango_root();
425    let MainArgs {
426        flavor,
427        logging,
428        stack_size,
429    } = parse_main_args(&args)?;
430    let stack_size = match stack_size {
431        Some(n) => quote!(#n),
432        None => quote!(#root::__private_runtime::MAIN_STACK_SIZE),
433    };
434    let builder_call = match flavor {
435        Flavor::CurrentThread => quote! {
436            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
437        },
438        // Worker threads run handlers and `tokio::spawn` tasks: same stack.
439        Flavor::MultiThread => quote! {
440            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
441                .thread_stack_size(#stack_size)
442        },
443    };
444
445    // The user's fn becomes an inner `async fn` with its own return type;
446    // the outer sync fn runs it on a big-stack thread and reports its exit code.
447    let mut inner = input.clone();
448    inner.sig.ident = syn::Ident::new("__rustango_main_body", input.sig.ident.span());
449    // Lint attributes still apply to the body; the rest stay on the outer fn.
450    inner.attrs.retain(|a| {
451        ["allow", "expect", "deny", "warn", "forbid"]
452            .iter()
453            .any(|l| a.path().is_ident(l))
454    });
455    inner.vis = syn::Visibility::Inherited;
456    input.sig.asyncness = None;
457    input.sig.output = syn::parse2(quote!(-> ::std::process::ExitCode))?;
458
459    // `logging = false` drops the block entirely, so a later
460    // `Cli::with_logging()` is the first installer and its
461    // `[logging]` settings actually take effect (#1465).
462    let logging_prologue = if logging {
463        quote! {
464            // `Setup`'s defaults, not a copy of them: filter, format and
465            // the `Color::Auto` rule live in one place (#1493).
466            // `logging` and this macro share the `runtime` gate.
467            // The real `RUST_LOG` still wins over `./.env`'s (#2204).
468            let _ = #root::logging::Setup::new()
469                .with_default_env_filter(#root::__private_runtime::main_default_filter())
470                .with_format(#root::__private_runtime::main_format())
471                .install();
472        }
473    } else {
474        quote! {}
475    };
476
477    input.block = syn::parse2(quote! {{
478        { #logging_prologue }
479        #inner
480        #root::__private_runtime::run_on_main_thread(#stack_size, || {
481            let __rt = #builder_call
482                .enable_all()
483                .build()
484                .expect("failed to build tokio runtime");
485            ::std::process::Termination::report(__rt.block_on(__rustango_main_body()))
486        })
487    }})?;
488
489    Ok(quote! {
490        #input
491    })
492}
493
494enum Flavor {
495    MultiThread,
496    CurrentThread,
497}
498
499/// What `#[rustango::main]` was asked for.
500struct MainArgs {
501    flavor: Flavor,
502    logging: bool,
503    /// Bytes of stack for the main thread; `None` keeps the default.
504    stack_size: Option<usize>,
505}
506
507/// The smallest `stack_size` `#[rustango::main]` accepts: 64 KiB.
508const MIN_STACK_SIZE: usize = 64 * 1024;
509
510const MAIN_ARGS_HELP: &str =
511    "supported: `flavor = \"current_thread\" | \"multi_thread\"`, `logging = true | false`, `stack_size = <bytes>`";
512
513/// Parse `#[rustango::main(...)]`; an unknown or misspelt argument is an error.
514fn parse_main_args(args: &TokenStream2) -> syn::Result<MainArgs> {
515    use syn::parse::Parser as _;
516    let metas = syn::punctuated::Punctuated::<syn::MetaNameValue, syn::Token![,]>::parse_terminated
517        .parse2(args.clone())
518        .map_err(|e| {
519            syn::Error::new(
520                e.span(),
521                format!("#[rustango::main]: {e}; {MAIN_ARGS_HELP}"),
522            )
523        })?;
524    let mut out = MainArgs {
525        flavor: Flavor::MultiThread,
526        logging: true,
527        stack_size: None,
528    };
529    for meta in &metas {
530        let lit = match &meta.value {
531            syn::Expr::Lit(l) => Some(&l.lit),
532            _ => None,
533        };
534        let name = meta.path.get_ident().map(ToString::to_string);
535        match (name.as_deref(), lit) {
536            (Some("flavor"), Some(syn::Lit::Str(s))) if s.value() == "current_thread" => {
537                out.flavor = Flavor::CurrentThread;
538            }
539            (Some("flavor"), Some(syn::Lit::Str(s))) if s.value() == "multi_thread" => {
540                out.flavor = Flavor::MultiThread;
541            }
542            (Some("logging"), Some(syn::Lit::Bool(b))) => out.logging = b.value,
543            (Some("stack_size"), Some(syn::Lit::Int(n))) => {
544                let bytes = n.base10_parse::<usize>()?;
545                if bytes < MIN_STACK_SIZE {
546                    return Err(syn::Error::new(
547                        n.span(),
548                        format!("#[rustango::main]: `stack_size` must be at least {MIN_STACK_SIZE} bytes"),
549                    ));
550                }
551                out.stack_size = Some(bytes);
552            }
553            _ => {
554                return Err(syn::Error::new(
555                    meta.span(),
556                    format!("#[rustango::main]: unsupported argument; {MAIN_ARGS_HELP}"),
557                ))
558            }
559        }
560    }
561    Ok(out)
562}
563
564/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
565struct QInput {
566    base_path: syn::Path,
567    field: syn::Ident,
568    value: syn::Expr,
569}
570
571impl syn::parse::Parse for QInput {
572    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
573        let base_path: syn::Path = input.parse()?;
574        input.parse::<syn::Token![.]>()?;
575        let field: syn::Ident = input.parse()?;
576        input.parse::<syn::Token![=]>()?;
577        let value: syn::Expr = input.parse()?;
578        Ok(QInput {
579            base_path,
580            field,
581            value,
582        })
583    }
584}
585
586fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
587    let q: QInput = syn::parse2(input)?;
588    let root = rustango_root();
589    let field_str = ident_name(&q.field);
590    let field_span = q.field.span();
591    let (base, suffix) = match field_str.find("__") {
592        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
593        None => (field_str.as_str(), ""),
594    };
595    if base.is_empty() {
596        return Err(syn::Error::new(
597            field_span,
598            "Q!(): field name is empty before `__` suffix",
599        ));
600    }
601    let base_ident = ident_from_name(base, field_span);
602    let value = &q.value;
603    let path = &q.base_path;
604
605    // Most suffixes map directly to a Column method with the value
606    // forwarded unchanged. Some need value-shape massaging (wildcards
607    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
608    // ISNULL). Unknown suffixes fail the build.
609    let expanded = match suffix {
610        "" | "exact" => quote! {
611            #root::core::Column::eq(#path::#base_ident, #value)
612        },
613        "ne" => quote! {
614            #root::core::Column::ne(#path::#base_ident, #value)
615        },
616        "gt" => quote! {
617            #root::core::Column::gt(#path::#base_ident, #value)
618        },
619        "gte" => quote! {
620            #root::core::Column::gte(#path::#base_ident, #value)
621        },
622        "lt" => quote! {
623            #root::core::Column::lt(#path::#base_ident, #value)
624        },
625        "lte" => quote! {
626            #root::core::Column::lte(#path::#base_ident, #value)
627        },
628        // The escaped-LIKE lookups delegate to the typed Column
629        // methods, which escape the value and pair it with the
630        // *Escaped ops so `%` / `_` match literally on every dialect
631        // (#1257) — the macro no longer owns any pattern wrapping.
632        "iexact" => quote! {
633            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
634        },
635        "contains" => quote! {
636            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
637        },
638        "icontains" => quote! {
639            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
640        },
641        "startswith" => quote! {
642            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
643        },
644        "istartswith" => quote! {
645            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
646        },
647        "endswith" => quote! {
648            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
649        },
650        "iendswith" => quote! {
651            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
652        },
653        "in" => quote! {
654            #root::core::Column::is_in(#path::#base_ident, #value)
655        },
656        "not_in" => quote! {
657            #root::core::Column::not_in(#path::#base_ident, #value)
658        },
659        "isnull" => {
660            // Must be a bool literal at macro time so we can route to
661            // is_null vs is_not_null without a runtime branch.
662            let b = match value {
663                syn::Expr::Lit(syn::ExprLit {
664                    lit: syn::Lit::Bool(b),
665                    ..
666                }) => b.value(),
667                _ => {
668                    return Err(syn::Error::new_spanned(
669                        value,
670                        "Q!(): `__isnull` requires a `true` or `false` literal",
671                    ));
672                }
673            };
674            if b {
675                quote! { #root::core::Column::is_null(#path::#base_ident) }
676            } else {
677                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
678            }
679        }
680        "between" => {
681            // Accept a tuple literal `(lo, hi)`.
682            let tuple = match value {
683                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
684                _ => {
685                    return Err(syn::Error::new_spanned(
686                        value,
687                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
688                    ));
689                }
690            };
691            let lo = &tuple.elems[0];
692            let hi = &tuple.elems[1];
693            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
694        }
695        "regex" => quote! {
696            #root::core::Column::regex(#path::#base_ident, #value)
697        },
698        "iregex" => quote! {
699            #root::core::Column::iregex(#path::#base_ident, #value)
700        },
701        _ => {
702            return Err(syn::Error::new(
703                field_span,
704                format!(
705                    "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",
706                    suffix
707                ),
708            ));
709        }
710    };
711    Ok(expanded)
712}
713
714fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
715    // Default to "./migrations" if invoked without args.
716    let path_str = if input.is_empty() {
717        "./migrations".to_string()
718    } else {
719        let lit: LitStr = syn::parse2(input)?;
720        lit.value()
721    };
722
723    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
724        syn::Error::new(
725            proc_macro2::Span::call_site(),
726            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
727        )
728    })?;
729    let abs = std::path::Path::new(&manifest).join(&path_str);
730
731    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
732    if abs.is_dir() {
733        let read = std::fs::read_dir(&abs).map_err(|e| {
734            syn::Error::new(
735                proc_macro2::Span::call_site(),
736                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
737            )
738        })?;
739        for entry in read.flatten() {
740            let path = entry.path();
741            if !path.is_file() {
742                continue;
743            }
744            if path.extension().and_then(|s| s.to_str()) != Some("json") {
745                continue;
746            }
747            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
748                continue;
749            };
750            entries.push((stem.to_owned(), path));
751        }
752    }
753    entries.sort_by(|a, b| a.0.cmp(&b.0));
754
755    // Compile-time chain validation: read each migration's JSON,
756    // pull `name` and `prev` (file-stem-keyed for the chain check),
757    // and verify every `prev` points to another migration in the
758    // slice. Mismatches between the file stem and the embedded
759    // `name` field — and broken `prev` chains — fail at MACRO
760    // EXPANSION time so a misshapen migration set never compiles.
761    //
762    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
763    // migrations + a Rust proc-macro that reads them is a combo no
764    // other Rust web framework matches (Cot's are imperative Rust
765    // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
766    // a static chain to validate).
767    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
768    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
769    let mut texts: Vec<String> = Vec::with_capacity(entries.len());
770    for (stem, path) in &entries {
771        let raw = std::fs::read(path)
772            .map_err(|e| e.to_string())
773            .and_then(decode_text)
774            .map_err(|e| {
775                syn::Error::new(
776                    proc_macro2::Span::call_site(),
777                    format!(
778                        "embed_migrations!: cannot read {} for chain validation: {e}",
779                        path.display()
780                    ),
781                )
782            })?;
783        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
784            syn::Error::new(
785                proc_macro2::Span::call_site(),
786                format!(
787                    "embed_migrations!: {} is not valid JSON: {e}",
788                    path.display()
789                ),
790            )
791        })?;
792        let name = json
793            .get("name")
794            .and_then(|v| v.as_str())
795            .ok_or_else(|| {
796                syn::Error::new(
797                    proc_macro2::Span::call_site(),
798                    format!(
799                        "embed_migrations!: {} is missing the `name` field",
800                        path.display()
801                    ),
802                )
803            })?
804            .to_owned();
805        if name != *stem {
806            return Err(syn::Error::new(
807                proc_macro2::Span::call_site(),
808                format!(
809                    "embed_migrations!: file stem `{stem}` does not match the migration's \
810                     `name` field `{name}` — rename the file or fix the JSON",
811                ),
812            ));
813        }
814        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
815        chain_names.push(name.clone());
816        prev_refs.push((name, prev));
817        texts.push(raw);
818    }
819
820    let name_set: std::collections::HashSet<&str> =
821        chain_names.iter().map(String::as_str).collect();
822    for (name, prev) in &prev_refs {
823        if let Some(p) = prev {
824            if !name_set.contains(p.as_str()) {
825                return Err(syn::Error::new(
826                    proc_macro2::Span::call_site(),
827                    format!(
828                        "embed_migrations!: broken migration chain — `{name}` declares \
829                         prev=`{p}` but no migration with that name exists in {}",
830                        abs.display()
831                    ),
832                ));
833            }
834        }
835    }
836
837    // The decoded text, so a BOM or UTF-16 file embeds as UTF-8;
838    // `include_bytes!` keeps the file a rebuild input.
839    let pairs: Vec<TokenStream2> = entries
840        .iter()
841        .zip(&texts)
842        .map(|((name, path), text)| {
843            let path_lit = path.display().to_string();
844            quote! { (#name, {
845                const _: &[u8] = ::core::include_bytes!(#path_lit);
846                #text
847            }) }
848        })
849        .collect();
850
851    // A wrong path used to embed nothing silently; a deprecation use is
852    // the only stable way for a proc macro to warn without breaking builds.
853    let missing_warning = if abs.is_dir() {
854        quote! {}
855    } else {
856        let note = format!(
857            "embed_migrations!: directory {} does not exist; no migrations are embedded",
858            abs.display()
859        );
860        quote! {
861            #[deprecated(note = #note)]
862            const __RUSTANGO_MIGRATIONS_DIR_MISSING: () = ();
863            #[allow(clippy::let_unit_value)]
864            let _ = __RUSTANGO_MIGRATIONS_DIR_MISSING;
865        }
866    };
867
868    Ok(quote! {
869        {
870            #missing_warning
871            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
872            __RUSTANGO_EMBEDDED
873        }
874    })
875}
876
877fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
878    let root = rustango_root();
879    let struct_name = &input.ident;
880
881    let Data::Struct(data) = &input.data else {
882        return Err(syn::Error::new_spanned(
883            struct_name,
884            "Model can only be derived on structs",
885        ));
886    };
887    let Fields::Named(named) = &data.fields else {
888        return Err(syn::Error::new_spanned(
889            struct_name,
890            "Model requires a struct with named fields",
891        ));
892    };
893
894    let mut container = parse_container_attrs(input)?;
895    let table = container
896        .table
897        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
898    let model_name = struct_name.to_string();
899
900    let collected = collect_fields(named, &table)?;
901    // An explicit `self_pk_column` must name a column of this model.
902    let explicit_pks = container
903        .reverse_has_relations
904        .iter()
905        .map(|r| (&r.self_pk_column, r.self_pk_span))
906        .chain(
907            container
908                .generic_has_relations
909                .iter()
910                .map(|r| (&r.self_pk_column, r.self_pk_span)),
911        );
912    for (column, span) in explicit_pks {
913        if let (Some(column), Some(span)) = (column, span) {
914            if !collected.column_entries.iter().any(|c| &c.column == column) {
915                return Err(syn::Error::new(
916                    span,
917                    format!("`self_pk_column = \"{column}\"` names no column of `{model_name}`"),
918                ));
919            }
920        }
921    }
922    // `reverse_has` / `generic_has` join on the PK column unless they name another.
923    if let Some((_, pk_column)) = &collected.primary_key {
924        for rel in &mut container.reverse_has_relations {
925            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
926        }
927        for rel in &mut container.generic_has_relations {
928            rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
929        }
930    }
931
932    // Validate that #[rustango(display = "…")] names a real field.
933    if let Some((ref display, span)) = container.display {
934        if !collected.field_names.iter().any(|n| n == display) {
935            return Err(syn::Error::new(
936                span,
937                format!("`display = \"{display}\"` does not match any field on this struct"),
938            ));
939        }
940    }
941    let display = container.display.map(|(name, _)| name);
942    let app_label = container.app.clone();
943
944    // Validate admin field-name lists against declared field names.
945    // Note: `list_display` is intentionally NOT validated here. As of
946    // v0.32 it may also reference inventory-registered computed
947    // fields (via `register_admin_computed!`) whose existence the
948    // macro can't see at compile time — they're submitted from any
949    // crate that depends on rustango. The runtime list-view resolves
950    // unknown names against the inventory + silently drops the
951    // truly-bogus ones, which is the cheaper trade-off versus
952    // forcing a per-Model attr to opt out.
953    if let Some(admin) = &container.admin {
954        for (label, list) in [
955            ("search_fields", &admin.search_fields),
956            ("readonly_fields", &admin.readonly_fields),
957            ("list_filter", &admin.list_filter),
958        ] {
959            if let Some((names, span)) = list {
960                for name in names {
961                    if !collected.field_names.iter().any(|n| n == name) {
962                        return Err(syn::Error::new(
963                            *span,
964                            format!(
965                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
966                            ),
967                        ));
968                    }
969                }
970            }
971        }
972        if let Some((pairs, span)) = &admin.ordering {
973            for (name, _) in pairs {
974                if !collected.field_names.iter().any(|n| n == name) {
975                    return Err(syn::Error::new(
976                        *span,
977                        format!(
978                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
979                        ),
980                    ));
981                }
982            }
983        }
984        if let Some((groups, span)) = &admin.fieldsets {
985            for (_, fields) in groups {
986                for name in fields {
987                    if !collected.field_names.iter().any(|n| n == name) {
988                        return Err(syn::Error::new(
989                            *span,
990                            format!(
991                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
992                            ),
993                        ));
994                    }
995                }
996            }
997        }
998    }
999    if let Some(audit) = &container.audit {
1000        if let Some((names, span)) = &audit.track {
1001            for name in names {
1002                if !collected.field_names.iter().any(|n| n == name) {
1003                    return Err(syn::Error::new(
1004                        *span,
1005                        format!(
1006                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
1007                        ),
1008                    ));
1009                }
1010            }
1011        }
1012    }
1013
1014    // Issue #291 / T2.5 — validate each `default_order` column name
1015    // against the model's collected fields. Typos fail at macro-expand
1016    // time, not at the database.
1017    for (col, _desc, span) in &container.default_order {
1018        if !collected.field_names.iter().any(|n| n == col) {
1019            return Err(syn::Error::new(
1020                *span,
1021                format!(
1022                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
1023                ),
1024            ));
1025        }
1026    }
1027
1028    check_known_columns(&container.indexes, &container.get_latest_by, &collected)?;
1029
1030    // Build the audit_track list for ModelSchema: None when no audit attr,
1031    // Some(empty) when audit present without track, Some(names) when explicit.
1032    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
1033        audit
1034            .track
1035            .as_ref()
1036            .map(|(names, _)| names.clone())
1037            .unwrap_or_default()
1038    });
1039
1040    // The upsert conflict target: a container-level composite unique,
1041    // never a field `index(unique)` or a partial index PG can't target.
1042    let upsert_unique: Option<Vec<String>> = container
1043        .indexes
1044        .iter()
1045        .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
1046        .map(|i| i.columns.clone());
1047
1048    // Merge field-level indexes into the container's index list.
1049    let mut all_indexes: Vec<IndexAttr> = container.indexes;
1050    for field in &named.named {
1051        let ident = field.ident.as_ref().expect("named");
1052        // The same default column as `process_field` (#1937).
1053        let col = ident_name(ident);
1054        // Re-parse field attrs to check for index flag
1055        if let Ok(fa) = parse_field_attrs(field) {
1056            if fa.index {
1057                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
1058                let auto_name = capped_index_name(if fa.index_unique {
1059                    format!("{table}_{col_name}_uq_idx")
1060                } else {
1061                    format!("{table}_{col_name}_idx")
1062                });
1063                all_indexes.push(IndexAttr {
1064                    span: field.span(),
1065                    name: fa.index_name.or(Some(auto_name)),
1066                    columns: vec![col_name],
1067                    unique: fa.index_unique,
1068                    method: fa.index_method,
1069                    where_clause: None,
1070                    include: Vec::new(),
1071                });
1072            }
1073        }
1074    }
1075    // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
1076    for field in &named.named {
1077        let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
1078            continue;
1079        };
1080        if det.kind != DetectedKind::String || fa.max_length.is_some() {
1081            continue;
1082        }
1083        let col = fa
1084            .column
1085            .clone()
1086            .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
1087        // MySQL indexes every foreign-key column too.
1088        let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
1089        if fa.primary_key
1090            || fa.unique
1091            || relation
1092            || all_indexes.iter().any(|i| i.columns.contains(&col))
1093        {
1094            return Err(syn::Error::new_spanned(
1095                field,
1096                "a primary-key, unique, indexed or foreign-key `String` needs \
1097                 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
1098            ));
1099        }
1100    }
1101
1102    let model_impl = model_impl_tokens(
1103        struct_name,
1104        &model_name,
1105        &table,
1106        display.as_deref(),
1107        app_label.as_deref(),
1108        container.admin.as_ref(),
1109        &container.default_order,
1110        &collected.field_schemas,
1111        collected.soft_delete_column.as_deref(),
1112        container.permissions,
1113        audit_track_names.as_deref(),
1114        &container.m2m,
1115        &all_indexes,
1116        &container.checks,
1117        &container.excludes,
1118        &container.composite_fks,
1119        &container.generic_fks,
1120        container.scope.as_deref(),
1121        container.access.as_deref(),
1122        container.is_view,
1123        container.verbose_name.as_deref(),
1124        container.verbose_name_plural.as_deref(),
1125        container.managed,
1126        container.base_manager_name.as_deref(),
1127        container.order_with_respect_to.as_deref(),
1128        container.proxy,
1129        &container.required_db_features,
1130        container.required_db_vendor.as_deref(),
1131        container.default_related_name.as_deref(),
1132        container.db_table_comment.as_deref(),
1133        container
1134            .get_latest_by
1135            .as_ref()
1136            .map(|(c, d, _)| (c.as_str(), *d)),
1137        &container.extra_permissions,
1138        &container.default_permissions,
1139        &container.global_scopes,
1140        &container.reverse_has_relations,
1141        &container.generic_has_relations,
1142    );
1143    let module_ident = column_module_ident(struct_name);
1144    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
1145    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
1146        let track_set: Option<std::collections::HashSet<&str>> = audit
1147            .track
1148            .as_ref()
1149            .map(|(names, _)| names.iter().map(String::as_str).collect());
1150        collected
1151            .column_entries
1152            .iter()
1153            .filter(|c| {
1154                track_set
1155                    .as_ref()
1156                    .map_or(true, |s| s.contains(c.name.as_str()))
1157            })
1158            .collect()
1159    });
1160    let inherent_impl = inherent_impl_tokens(
1161        struct_name,
1162        &collected,
1163        collected.primary_key.as_ref(),
1164        &column_consts,
1165        audited_fields.as_deref(),
1166        upsert_unique.as_deref(),
1167        &container.manager_fns,
1168    );
1169    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
1170    let from_row_impl = from_row_impl_tokens(
1171        struct_name,
1172        &collected.from_row_inits,
1173        &collected.from_row_inits_my,
1174    );
1175    let reverse_helpers = reverse_helper_tokens(
1176        struct_name,
1177        &collected.fk_relations,
1178        container.default_related_name.as_deref(),
1179    )?;
1180    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
1181    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
1182    // Issue #817 — `#[rustango(through(...))]` accessors.
1183    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
1184    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
1185    let reverse_has_accessors = reverse_has_accessor_tokens(
1186        struct_name,
1187        &container.reverse_has_relations,
1188        &collected.column_entries,
1189    );
1190    let generic_fk_accessors = generic_fk_accessor_tokens(
1191        struct_name,
1192        &container.generic_fks,
1193        &collected.column_entries,
1194    );
1195
1196    Ok(quote! {
1197        #model_impl
1198        #inherent_impl
1199        #from_row_impl
1200        #column_module
1201        #reverse_helpers
1202        #m2m_accessors
1203        #generic_m2m_accessors
1204        #through_accessors
1205        #reverse_has_accessors
1206        #generic_fk_accessors
1207
1208        #root::core::inventory::submit! {
1209            // `module_path!()` evaluates at the registration site,
1210            // so a Model declared in `crate::blog::models` records
1211            // `"<crate>::blog::models"` and `resolved_app_label()`
1212            // can infer "blog" without an explicit attribute.
1213            #root::core::ModelEntry::new(
1214                <#struct_name as #root::core::Model>::SCHEMA,
1215                ::core::module_path!(),
1216            )
1217            .with_audited(
1218                <#struct_name as #root::core::Model>::__rustango_audited_update,
1219                <#struct_name as #root::core::Model>::__rustango_audited_delete,
1220            )
1221            .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1222            .with_audited_update_record(
1223                <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1224            )
1225            .with_audited_cascade(<#struct_name as #root::core::Model>::__rustango_audited_cascade)
1226        }
1227    })
1228}
1229
1230/// An index or `get_latest_by` name that is no column of the model is a
1231/// compile error; an expression index such as `lower(email)` is not checked.
1232fn check_known_columns(
1233    indexes: &[IndexAttr],
1234    get_latest_by: &Option<(String, bool, proc_macro2::Span)>,
1235    collected: &CollectedFields,
1236) -> syn::Result<()> {
1237    let is_column = |c: &str| collected.column_entries.iter().any(|e| e.column == c);
1238    // Only plain names: an expression index such as `lower(email)` is legal.
1239    let plain = |c: &str| c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
1240    let mut found: Vec<(proc_macro2::Span, String)> = Vec::new();
1241    for idx in indexes {
1242        for col in idx.columns.iter().filter(|c| plain(c) && !is_column(c)) {
1243            found.push((
1244                idx.span,
1245                format!("index column `{col}` is not a column of this model; its DDL will fail"),
1246            ));
1247        }
1248    }
1249    if let Some((col, _, span)) = get_latest_by {
1250        if !is_column(col) && !collected.field_names.iter().any(|n| n == col) {
1251            found.push((
1252                *span,
1253                format!("`get_latest_by`: `{col}` is not a field of this model"),
1254            ));
1255        }
1256    }
1257    let mut errors = found
1258        .into_iter()
1259        .map(|(span, note)| syn::Error::new(span, note));
1260    let Some(mut first) = errors.next() else {
1261        return Ok(());
1262    };
1263    first.extend(errors);
1264    Err(first)
1265}
1266
1267/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1268/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1269fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1270    let root = rustango_root();
1271    let parent_ty = &rel.parent_type;
1272    let field_ident = &rel.field_ident;
1273    let null_fk = if rel.nullable {
1274        quote! {
1275            if #receiver.#field_ident.is_none() {
1276                return ::core::result::Result::Ok(false);
1277            }
1278        }
1279    } else {
1280        quote! {}
1281    };
1282    quote! {
1283        #null_fk
1284        #root::sql::__rustango_require_join(
1285            row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1286        )?;
1287    }
1288}
1289
1290/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1291/// reused, and the deeper chain stitched into it; re-decoding it would
1292/// drop what that chain loaded.
1293fn loaded_reuse_tokens(
1294    rel: &FkRelation,
1295    receiver: &TokenStream2,
1296    load: &TokenStream2,
1297) -> TokenStream2 {
1298    let root = rustango_root();
1299    let field_ident = &rel.field_ident;
1300    let pattern = if rel.nullable {
1301        quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1302    } else {
1303        quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1304    };
1305    quote! {
1306        if let #pattern = &mut #receiver.#field_ident {
1307            if let ::core::option::Option::Some(__r) = __rest {
1308                let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1309            }
1310            return ::core::result::Result::Ok(true);
1311        }
1312    }
1313}
1314
1315/// Store the decoded `_parent` on the FK field, keeping the key the row
1316/// holds: with `on = <unique column>` it is not the parent's PK.
1317fn loaded_assign_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1318    let root = rustango_root();
1319    let field_ident = &rel.field_ident;
1320    if rel.nullable {
1321        quote! {
1322            if let ::core::option::Option::Some(__fk) = &mut #receiver.#field_ident {
1323                let _pk = ::core::clone::Clone::clone(__fk.pk_ref());
1324                *__fk = #root::sql::ForeignKey::loaded(_pk, _parent);
1325            }
1326        }
1327    } else {
1328        quote! {
1329            let _pk = ::core::clone::Clone::clone(#receiver.#field_ident.pk_ref());
1330            #receiver.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1331        }
1332    }
1333}
1334
1335/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1336/// matches `field_name` against the model's FK fields and, for a
1337/// match, decodes the FK target via the parent's macro-generated
1338/// `__rustango_from_aliased_row`, and stores `ForeignKey::Loaded` on
1339/// `self` under the key the row already holds.
1340///
1341/// Always emitted (with empty arms for FK-less models, which
1342/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1343/// trait bound on `fetch_on` is universally satisfied — users
1344/// never have to think about implementing it.
1345fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> 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::LoadRelated::__rustango_load_related),
1357        );
1358        let assign = loaded_assign_tokens(rel, &quote!(self));
1359        quote! {
1360            #fk_col => {
1361                #guard
1362                #reuse
1363                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1364                // Audit #451 — multi-hop `select_related("a__b__c")`:
1365                // stitch the deeper relation onto this parent first,
1366                // decoding it at the accumulated `__next_alias`. The
1367                // parent type also impls `LoadRelated`, so this recurses
1368                // the FK chain to arbitrary depth.
1369                if let ::core::option::Option::Some(__r) = __rest {
1370                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1371                        &mut _parent, row, __r, &__next_alias,
1372                    )?;
1373                }
1374                #assign
1375                ::core::result::Result::Ok(true)
1376            }
1377        }
1378    });
1379    quote! {
1380        #[cfg(feature = "postgres")]
1381        impl #root::sql::LoadRelated for #struct_name {
1382            #[allow(unused_variables)]
1383            fn __rustango_load_related(
1384                &mut self,
1385                row: &#root::sql::sqlx::postgres::PgRow,
1386                field_name: &str,
1387                alias: &str,
1388            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1389                // Audit #451 — split the multi-hop path: `base` is the FK
1390                // on THIS model, `__rest` (if any) is the remaining chain
1391                // to stitch onto the loaded parent. `__next_alias` is the
1392                // accumulated join alias the parent's columns live under
1393                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1394                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1395                    match field_name.split_once("__") {
1396                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1397                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1398                    };
1399                let __next_alias: ::std::string::String = match __rest {
1400                    ::core::option::Option::Some(__r) => {
1401                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1402                        ::std::format!("{}__{}", alias, __rb)
1403                    }
1404                    ::core::option::Option::None => ::std::string::String::new(),
1405                };
1406                match __base {
1407                    #( #arms )*
1408                    _ => ::core::result::Result::Ok(false),
1409                }
1410            }
1411        }
1412    }
1413}
1414
1415/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1416/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1417/// which expands to a `LoadRelatedMy` impl when rustango is built with
1418/// the `mysql` feature, and to nothing otherwise. The decoded parent
1419/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1420/// decoder, also batch8) so the dual emission is symmetric across
1421/// backends.
1422fn load_related_impl_my_tokens(
1423    struct_name: &syn::Ident,
1424    fk_relations: &[FkRelation],
1425) -> TokenStream2 {
1426    let root = rustango_root();
1427    let arms = fk_relations.iter().map(|rel| {
1428        let parent_ty = &rel.parent_type;
1429        let field_ident = &rel.field_ident;
1430        let fk_name = ident_name(field_ident);
1431        let fk_col = fk_name.as_str();
1432        let guard = join_guard_tokens(rel, &quote!(__self));
1433        let reuse = loaded_reuse_tokens(
1434            rel,
1435            &quote!(__self),
1436            &quote!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1437        );
1438        let assign = loaded_assign_tokens(rel, &quote!(__self));
1439        // `self` IS hygiene-tracked through macro_rules — emitted from
1440        // a different context than the `&mut self` parameter inside
1441        // the macro_rules-expanded fn. Pass it through as `__self`
1442        // and let the macro_rules rebind it to the receiver.
1443        quote! {
1444            #fk_col => {
1445                #guard
1446                #reuse
1447                let mut _parent: #parent_ty =
1448                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1449                // Audit #451 — multi-hop: stitch the deeper relation onto
1450                // the parent at the accumulated alias (see PG twin).
1451                if let ::core::option::Option::Some(__r) = __rest {
1452                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1453                        &mut _parent, row, __r, &__next_alias,
1454                    )?;
1455                }
1456                #assign
1457                ::core::result::Result::Ok(true)
1458            }
1459        }
1460    });
1461    quote! {
1462        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1463            #( #arms )*
1464        });
1465    }
1466}
1467
1468/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1469/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1470/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1471fn load_related_impl_sqlite_tokens(
1472    struct_name: &syn::Ident,
1473    fk_relations: &[FkRelation],
1474) -> TokenStream2 {
1475    let root = rustango_root();
1476    let arms = fk_relations.iter().map(|rel| {
1477        let parent_ty = &rel.parent_type;
1478        let field_ident = &rel.field_ident;
1479        let fk_name = ident_name(field_ident);
1480        let fk_col = fk_name.as_str();
1481        let guard = join_guard_tokens(rel, &quote!(__self));
1482        let reuse = loaded_reuse_tokens(
1483            rel,
1484            &quote!(__self),
1485            &quote!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1486        );
1487        let assign = loaded_assign_tokens(rel, &quote!(__self));
1488        quote! {
1489            #fk_col => {
1490                #guard
1491                #reuse
1492                let mut _parent: #parent_ty =
1493                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1494                // Audit #451 — multi-hop: stitch the deeper relation onto
1495                // the parent at the accumulated alias (see PG twin).
1496                if let ::core::option::Option::Some(__r) = __rest {
1497                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1498                        &mut _parent, row, __r, &__next_alias,
1499                    )?;
1500                }
1501                #assign
1502                ::core::result::Result::Ok(true)
1503            }
1504        }
1505    });
1506    quote! {
1507        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1508            #( #arms )*
1509        });
1510    }
1511}
1512
1513/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1514/// matches `field_name` against the model's FK fields and returns
1515/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1516/// group children by parent PK.
1517///
1518/// Always emitted (with `_ => None` for FK-less models) so the
1519/// trait bound on `fetch_with_prefetch` is universally satisfied.
1520fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1521    let root = rustango_root();
1522    let arms = fk_relations.iter().map(|rel| {
1523        let field_ident = &rel.field_ident;
1524        let fk_name = ident_name(field_ident);
1525        let fk_col = fk_name.as_str();
1526        if rel.pk_kind == DetectedKind::I64 {
1527            // i64 FK — return the stored PK so prefetch_related can
1528            // group children by it. Nullable variant unwraps via
1529            // `as_ref().map(...)`: an unset (NULL) FK column yields
1530            // `None` and that child sits out of the grouping (correct
1531            // semantics — it has no parent to attach to).
1532            if rel.nullable {
1533                quote! {
1534                    #fk_col => self.#field_ident
1535                        .as_ref()
1536                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1537                }
1538            } else {
1539                quote! {
1540                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1541                }
1542            }
1543        } else {
1544            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1545            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1546            // i64-keyed grouping path — the trait signature is
1547            // `Option<i64>` and a non-i64 PK can't lower into it.
1548            // The FK still works for everything else (CRUD, lazy
1549            // load via `.get()`, select_related JOINs); only the
1550            // bulk prefetch grouper needs the integer key.
1551            quote! {
1552                #fk_col => ::core::option::Option::None,
1553            }
1554        }
1555    });
1556    // PK-type-agnostic version: every FK arm emits an
1557    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1558    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1559    // opt OUT of the legacy i64 method (it returns None) but opt IN
1560    // here.
1561    let value_arms = fk_relations.iter().map(|rel| {
1562        let field_ident = &rel.field_ident;
1563        let fk_name = ident_name(field_ident);
1564        let fk_col = fk_name.as_str();
1565        if rel.nullable {
1566            quote! {
1567                #fk_col => self.#field_ident
1568                    .as_ref()
1569                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1570                        #root::sql::ForeignKey::pk(fk)
1571                    )),
1572            }
1573        } else {
1574            quote! {
1575                #fk_col => ::core::option::Option::Some(
1576                    ::core::convert::Into::<#root::core::SqlValue>::into(
1577                        self.#field_ident.pk()
1578                    )
1579                ),
1580            }
1581        }
1582    });
1583    quote! {
1584        impl #root::sql::FkPkAccess for #struct_name {
1585            #[allow(unused_variables)]
1586            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1587                match field_name {
1588                    #( #arms )*
1589                    _ => ::core::option::Option::None,
1590                }
1591            }
1592            #[allow(unused_variables)]
1593            fn __rustango_fk_pk_value(
1594                &self,
1595                field_name: &str,
1596            ) -> ::core::option::Option<#root::core::SqlValue> {
1597                match field_name {
1598                    #( #value_arms )*
1599                    _ => ::core::option::Option::None,
1600                }
1601            }
1602        }
1603    }
1604}
1605
1606/// `related_name = "+"`: the FK gets no reverse accessor on its parent.
1607const NO_REVERSE_ACCESSOR: &str = "+";
1608
1609/// For every `ForeignKey<Parent>` field on `Child`, emit
1610/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1611/// Reads the column the FK points at (`on`, else the PK) and runs one
1612/// `SELECT … WHERE <fk_column> = $1`: one round trip, no N+1.
1613///
1614/// The accessor takes any `SqlExecutor`; `_set_pool` is its deprecated
1615/// old name.
1616fn reverse_helper_tokens(
1617    child_ident: &syn::Ident,
1618    fk_relations: &[FkRelation],
1619    default_related_name: Option<&str>,
1620) -> syn::Result<TokenStream2> {
1621    let root = rustango_root();
1622    // `related_name = "+"`: no accessor, so a parent from another crate works.
1623    let fk_relations: Vec<&FkRelation> = fk_relations
1624        .iter()
1625        .filter(|rel| rel.related_name.as_deref() != Some(NO_REVERSE_ACCESSOR))
1626        .collect();
1627    if fk_relations.is_empty() {
1628        return Ok(TokenStream2::new());
1629    }
1630    // Method-name resolution per FK (issue #816 + follow-up):
1631    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1632    //      itself — wins over everything else.
1633    //   2. Container-level `default_related_name = "..."` on the
1634    //      child. Applies to every FK on this model that didn't
1635    //      override.
1636    //   3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1637    //      `BlogComment` → `blog_comment_set`. Avoids English-plural
1638    //      edge cases.
1639    //
1640    // The PG-on-executor variant keeps the resolved name; the
1641    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1642    // framework's convention for the `&Pool` flavor of every helper).
1643    let default_pg_suffix = default_related_name
1644        .map(str::to_owned)
1645        .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1646    let name_of = |rel: &FkRelation| {
1647        rel.related_name
1648            .clone()
1649            .unwrap_or_else(|| default_pg_suffix.clone())
1650    };
1651    // Two FKs to one parent would define the same method twice (E0592).
1652    for (i, rel) in fk_relations.iter().enumerate() {
1653        let parent_ty = &rel.parent_type;
1654        let parent = quote!(#parent_ty).to_string();
1655        let name = name_of(rel);
1656        if let Some(first) = fk_relations[..i].iter().find(|r| {
1657            let other = &r.parent_type;
1658            quote!(#other).to_string() == parent && name_of(r) == name
1659        }) {
1660            return Err(syn::Error::new_spanned(
1661                &rel.field_ident,
1662                format!(
1663                    "`{}` and `{}` both point at `{parent}` and would both add `{parent}::{name}`; \
1664                     give one `#[rustango(related_name = \"...\")]`, or `related_name = \"+\"` for none",
1665                    first.field_ident, rel.field_ident
1666                ),
1667            ));
1668        }
1669    }
1670    let impls = fk_relations.iter().map(|rel| {
1671        let pg_suffix = name_of(rel);
1672        let pool_suffix = format!("{}_pool", pg_suffix);
1673        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1674        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1675        let parent_ty = &rel.parent_type;
1676        let fk_name = ident_name(&rel.field_ident);
1677        let fk_col = fk_name.as_str();
1678        let doc = format!(
1679            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1680             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1681             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1682             further `{child_ident}::objects()` filters via direct queryset use."
1683        );
1684        quote! {
1685            impl #parent_ty {
1686                #[doc = #doc]
1687                ///
1688                /// Runs on any [`#root::sql::SqlExecutor`]: a `&Pool`, a held
1689                /// `PoolConn`, a `PoolTx` or a Postgres sqlx executor.
1690                ///
1691                /// # Errors
1692                /// Returns [`#root::sql::ExecError`] for SQL-writing
1693                /// or driver failures.
1694                pub async fn #pg_method_ident<'_c, _E: #root::sql::SqlExecutor<'_c>>(
1695                    &self,
1696                    _executor: _E,
1697                ) -> ::core::result::Result<
1698                    ::std::vec::Vec<#child_ident>,
1699                    #root::sql::ExecError,
1700                > {
1701                    let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1702                    #root::query::QuerySet::<#child_ident>::new()
1703                        .filter_op(#fk_col, #root::core::Op::Eq, _key)
1704                        .fetch_on(_executor)
1705                        .await
1706                }
1707
1708                /// Old name of the reverse accessor without `_pool`.
1709                ///
1710                /// # Errors
1711                /// As the accessor.
1712                #[deprecated(since = "0.62.0", note = "drop the `_pool` suffix")]
1713                pub async fn #pool_method_ident(
1714                    &self,
1715                    pool: &#root::sql::Pool,
1716                ) -> ::core::result::Result<
1717                    ::std::vec::Vec<#child_ident>,
1718                    #root::sql::ExecError,
1719                > {
1720                    self.#pg_method_ident(pool).await
1721                }
1722            }
1723        }
1724    });
1725    Ok(quote! { #( #impls )* })
1726}
1727
1728/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1729/// relation declared on the model.
1730/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1731/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1732///
1733/// Closes #239 + #240 — `comment.content_object` /
1734/// `comment.content_object = post` ergonomics on top of the existing
1735/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1736///
1737/// `column_entries` is passed so we can resolve each `ct_column` /
1738/// `pk_column` SQL name back to its Rust field ident — the macro
1739/// only sees the column-side strings in the attribute, but the
1740/// emitted accessor needs to read the actual struct field.
1741fn generic_fk_accessor_tokens(
1742    struct_name: &syn::Ident,
1743    generic_fks: &[GenericFkAttr],
1744    column_entries: &[ColumnEntry],
1745) -> TokenStream2 {
1746    let root = rustango_root();
1747    if generic_fks.is_empty() {
1748        return TokenStream2::new();
1749    }
1750    let methods = generic_fks.iter().filter_map(|gfk| {
1751        // Resolve `ct_column` + `pk_column` to the struct's Rust
1752        // field idents. A typo (column name doesn't match any field)
1753        // emits no method for that registration — the user will see
1754        // the compiler reject the SCHEMA literal anyway, so there's
1755        // a clear error path without us double-reporting.
1756        let ct_ident = column_entries
1757            .iter()
1758            .find(|c| c.column == gfk.ct_column)
1759            .map(|c| c.ident.clone())?;
1760        let pk_ident = column_entries
1761            .iter()
1762            .find(|c| c.column == gfk.pk_column)
1763            .map(|c| c.ident.clone())?;
1764
1765        let accessor_ident =
1766            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1767        let setter_ident =
1768            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1769        let name_literal = gfk.name.as_str();
1770
1771        Some(quote! {
1772            #[doc = concat!(
1773                "Resolve the polymorphic `",
1774                #name_literal,
1775                "` relation. Reads `self.",
1776                stringify!(#ct_ident),
1777                "` + `self.",
1778                stringify!(#pk_ident),
1779                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1780                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1781                #name_literal,
1782                "\", ...))]`."
1783            )]
1784            pub async fn #accessor_ident(
1785                &self,
1786                pool: &#root::sql::Pool,
1787            ) -> ::core::result::Result<
1788                ::core::option::Option<#root::__serde_json::Value>,
1789                #root::sql::ExecError,
1790            > {
1791                let gfk = #root::contenttypes::GenericForeignKey::new(
1792                    self.#ct_ident as i64,
1793                    self.#pk_ident as i64,
1794                );
1795                gfk.get_object(pool).await
1796            }
1797
1798            #[doc = concat!(
1799                "Set the polymorphic `",
1800                #name_literal,
1801                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1802                stringify!(#ct_ident),
1803                "` and `self.",
1804                stringify!(#pk_ident),
1805                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1806                #name_literal,
1807                "\", ...))]`."
1808            )]
1809            pub async fn #setter_ident<T: #root::core::Model>(
1810                &mut self,
1811                pool: &#root::sql::Pool,
1812                target_pk: i64,
1813            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1814                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1815                    pool,
1816                    target_pk,
1817                ).await?;
1818                self.#ct_ident = gfk.content_type_id as _;
1819                self.#pk_ident = gfk.object_pk as _;
1820                ::core::result::Result::Ok(())
1821            }
1822        })
1823    });
1824    quote! {
1825        impl #struct_name {
1826            #( #methods )*
1827        }
1828    }
1829}
1830
1831fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1832    let root = rustango_root();
1833    if m2m_relations.is_empty() {
1834        return TokenStream2::new();
1835    }
1836    let methods = m2m_relations.iter().map(|rel| {
1837        let method_name = format!("{}_m2m", rel.name);
1838        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1839        let through = rel.through.as_str();
1840        let src_col = rel.src.as_str();
1841        let dst_col = rel.dst.as_str();
1842        quote! {
1843            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1844                #root::sql::M2MManager {
1845                    src_pk: self.__rustango_pk_value(),
1846                    through: #through,
1847                    src_col: #src_col,
1848                    dst_col: #dst_col,
1849                }
1850            }
1851        }
1852    });
1853    quote! {
1854        impl #struct_name {
1855            #( #methods )*
1856        }
1857    }
1858}
1859
1860/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1861/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1862fn generic_m2m_accessor_tokens(
1863    struct_name: &syn::Ident,
1864    relations: &[GenericM2MAttr],
1865) -> TokenStream2 {
1866    let root = rustango_root();
1867    if relations.is_empty() {
1868        return TokenStream2::new();
1869    }
1870    let methods = relations.iter().map(|rel| {
1871        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1872        let through = rel.through.as_str();
1873        let pk_col = rel.pk_column.as_str();
1874        let ct_col = rel.ct_column.as_str();
1875        let related_col = rel.related_column.as_str();
1876        quote! {
1877            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1878                #root::sql::GenericM2MManager {
1879                    src_pk: self.__rustango_pk_value(),
1880                    src_schema: <Self as #root::core::Model>::SCHEMA,
1881                    through: #through,
1882                    pk_col: #pk_col,
1883                    ct_col: #ct_col,
1884                    dst_col: #related_col,
1885                }
1886            }
1887        }
1888    });
1889    // Lets an owner delete find and clear its junction rows.
1890    let entries = relations.iter().map(|rel| {
1891        let through = rel.through.as_str();
1892        let pk_col = rel.pk_column.as_str();
1893        let ct_col = rel.ct_column.as_str();
1894        quote! {
1895            #root::core::inventory::submit! {
1896                #root::core::GenericM2MEntry::new(
1897                    <#struct_name as #root::core::Model>::SCHEMA,
1898                    #through,
1899                    #pk_col,
1900                    #ct_col,
1901                )
1902            }
1903        }
1904    });
1905    quote! {
1906        impl #struct_name {
1907            #( #methods )*
1908        }
1909        #( #entries )*
1910    }
1911}
1912
1913/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1914/// associated functions for each `#[rustango(reverse_has(...))]`
1915/// attribute. Issue #830.
1916///
1917/// The two emitted functions return ready-to-use `WhereExpr` nodes
1918/// that downstream callers drop into
1919/// `QuerySet::<Self>::where_raw(...)`:
1920///
1921/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1922///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1923///   `whereHas` parity (without the closure-style sub-predicate
1924///   refinement — that's a follow-up; users can layer additional
1925///   predicates by constructing the SelectQuery themselves and
1926///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1927/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1928///   `whereDoesntHave` parity.
1929///
1930/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1931/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1932/// machinery threads the outer-table reference through automatically
1933/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1934fn reverse_has_accessor_tokens(
1935    struct_name: &syn::Ident,
1936    reverse_has_relations: &[ReverseHasAttr],
1937    columns: &[ColumnEntry],
1938) -> TokenStream2 {
1939    let root = rustango_root();
1940    if reverse_has_relations.is_empty() {
1941        return TokenStream2::new();
1942    }
1943    let methods = reverse_has_relations.iter().map(|rel| {
1944        let exists_name = format!("{}_exists_expr", rel.name);
1945        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1946        let count_name = format!("{}_count", rel.name);
1947        let fetch_name = format!("{}_fetch", rel.name);
1948        let first_name = format!("{}_first", rel.name);
1949        let pluck_name = format!("{}_pluck", rel.name);
1950        let accessor_name = rel.name.as_str();
1951        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1952        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1953        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1954        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1955        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1956        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1957        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1958        let child = &rel.child;
1959        let child_fk_column = rel.child_fk_column.as_str();
1960        let self_pk_column = rel.self_pk_column();
1961        // The instance helpers filter on the value the EXISTS form joins on.
1962        let self_value = columns
1963            .iter()
1964            .find(|c| c.column == self_pk_column)
1965            .map_or_else(
1966                || quote! { self.__rustango_pk_value() },
1967                |c| {
1968                    let ident = &c.ident;
1969                    quote! {
1970                        ::core::convert::Into::<#root::core::SqlValue>::into(
1971                            ::core::clone::Clone::clone(&self.#ident)
1972                        )
1973                    }
1974                },
1975            );
1976        let exists_doc = format!(
1977            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1978             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1979             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1980             filter to {struct_name}s with at least one matching child.",
1981        );
1982        let not_exists_doc = format!(
1983            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1984             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1985             <outer>.{self_pk_column})`. Drop into \
1986             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1987             {struct_name}s with **no** matching child.",
1988        );
1989        let count_doc = format!(
1990            "Eloquent `$model->{name}->count()` analog — returns \
1991             the number of `{child}` rows whose `{child_fk_column}` \
1992             matches this `{struct_name}` instance's `{self_pk_column}`. \
1993             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1994             {child_fk_column} = <self.{self_pk_column}>`.",
1995            name = rel.name,
1996        );
1997        let accessor_doc = format!(
1998            "Eloquent `$model->{name}` accessor — returns a \
1999             `QuerySet<{child}>` filtered to rows whose \
2000             `{child_fk_column}` matches this `{struct_name}` \
2001             instance's `{self_pk_column}`. **Chainable**: compose `.filter()` \
2002             / `.order_by()` / `.limit()` etc. on top, then call \
2003             `.fetch(&pool)` (the QuerySet trait method) when \
2004             done. For the simple \"fetch all\" hot path with no \
2005             further composition, prefer the bare-name \
2006             `{name}_fetch(&pool)` companion.",
2007            name = rel.name,
2008        );
2009        let fetch_doc = format!(
2010            "Eloquent `$model->{name}->get()` — bare-name hot-path \
2011             over `{name}(&self).fetch(&pool)`. Use this when \
2012             you don't need further `.filter()` / `.order_by()` \
2013             composition; falls back to the chainable accessor when \
2014             you do. Avoids the `_pool` suffix on the most common \
2015             call-site shape.",
2016            name = rel.name,
2017        );
2018        quote! {
2019            #[doc = #accessor_doc]
2020            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
2021                #root::query::QuerySet::<#child>::new()
2022                    .filter(#child_fk_column, #self_value)
2023            }
2024
2025            #[doc = #fetch_doc]
2026            pub async fn #fetch_ident(
2027                &self,
2028                pool: &#root::sql::Pool,
2029            ) -> ::core::result::Result<
2030                ::std::vec::Vec<#child>,
2031                #root::sql::ExecError,
2032            > {
2033                use #root::sql::FetcherPool as _;
2034                self.#accessor_ident().fetch(pool).await
2035            }
2036
2037            /// Eloquent `$model->relation->first()` / `hasOne`
2038            /// semantics — bare-name shortcut over
2039            /// `self.<name>().first(&pool)`. Returns `None` when no
2040            /// child rows match. Useful when the relation is
2041            /// nominally many-to-one in shape but at most one row is
2042            /// expected (latest comment, primary tag, etc.).
2043            pub async fn #first_ident(
2044                &self,
2045                pool: &#root::sql::Pool,
2046            ) -> ::core::result::Result<
2047                ::core::option::Option<#child>,
2048                #root::sql::ExecError,
2049            > {
2050                self.#accessor_ident().first(pool).await
2051            }
2052
2053            /// Eloquent `$model->relation->pluck($col)` — project a
2054            /// single column from the child rows into a `Vec<U>`.
2055            /// Skips the typed `Child` decode, which is cheaper when
2056            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
2057            pub async fn #pluck_ident<U>(
2058                &self,
2059                col: &'static str,
2060                pool: &#root::sql::Pool,
2061            ) -> ::core::result::Result<
2062                ::std::vec::Vec<U>,
2063                #root::sql::ExecError,
2064            >
2065            where
2066                U: #root::sql::FlatScalar,
2067            {
2068                self.#accessor_ident()
2069                    .values_list_flat(col)
2070                    .fetch::<U>(pool)
2071                    .await
2072            }
2073
2074            #[doc = #exists_doc]
2075            pub fn #exists_ident() -> #root::core::WhereExpr {
2076                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
2077                let child_schema =
2078                    <#child as #root::core::Model>::SCHEMA;
2079                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
2080                    lhs: Expr::Column(#child_fk_column),
2081                    op: Op::Eq,
2082                    rhs: Expr::OuterRef(#self_pk_column),
2083                });
2084                WhereExpr::Exists(::std::boxed::Box::new(inner))
2085            }
2086
2087            #[doc = #not_exists_doc]
2088            pub fn #not_exists_ident() -> #root::core::WhereExpr {
2089                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
2090                let child_schema =
2091                    <#child as #root::core::Model>::SCHEMA;
2092                let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
2093                    lhs: Expr::Column(#child_fk_column),
2094                    op: Op::Eq,
2095                    rhs: Expr::OuterRef(#self_pk_column),
2096                });
2097                WhereExpr::NotExists(::std::boxed::Box::new(inner))
2098            }
2099
2100            #[doc = #count_doc]
2101            pub async fn #count_ident(
2102                &self,
2103                pool: &#root::sql::Pool,
2104            ) -> ::core::result::Result<
2105                i64,
2106                #root::sql::ExecError,
2107            > {
2108                use #root::sql::CounterPool as _;
2109                self.#accessor_ident().count(pool).await
2110            }
2111        }
2112    });
2113    quote! {
2114        impl #struct_name {
2115            #( #methods )*
2116        }
2117    }
2118}
2119
2120/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
2121/// `#[rustango(through(...))]` attribute. Issue #817.
2122///
2123/// Each accessor builds a correlated subquery via
2124/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
2125/// intermediate table, filters on the FK column pointing at this
2126/// model, and projects the intermediate PK. The outer queryset
2127/// filters the far model by its FK-to-intermediate column being in
2128/// that set.
2129///
2130/// The returned `QuerySet<Far>` is **chainable** — the subquery
2131/// lives inside a `where_raw` clause so the user's later
2132/// `.filter()` / `.order_by()` / `.limit()` compositions don't
2133/// disturb it.
2134///
2135/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
2136/// SQLite — no LATERAL or backend-specific syntax involved.
2137fn through_accessor_tokens(
2138    struct_name: &syn::Ident,
2139    through_relations: &[ThroughAttr],
2140) -> TokenStream2 {
2141    let root = rustango_root();
2142    if through_relations.is_empty() {
2143        return TokenStream2::new();
2144    }
2145    let methods = through_relations.iter().map(|rel| {
2146        let method_name = format!("{}_through", rel.name);
2147        let count_name = format!("{}_through_count", rel.name);
2148        let fetch_name = format!("{}_through_fetch", rel.name);
2149        let first_name = format!("{}_through_first", rel.name);
2150        let pluck_name = format!("{}_through_pluck", rel.name);
2151        let method_ident = syn::Ident::new(&method_name, struct_name.span());
2152        let count_ident = syn::Ident::new(&count_name, struct_name.span());
2153        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
2154        let first_ident = syn::Ident::new(&first_name, struct_name.span());
2155        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
2156        let far = &rel.far;
2157        let intermediate = &rel.intermediate;
2158        let far_fk_column = rel.far_fk_column.as_str();
2159        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
2160        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
2161        let doc = format!(
2162            "Eloquent `hasManyThrough` accessor — returns a \
2163             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
2164             instance through the intermediate `{intermediate}` table. \
2165             Generated SQL shape: \
2166             `… WHERE {far_fk_column} IN (SELECT \
2167             {intermediate_pk_column} FROM <{intermediate}> WHERE \
2168             {intermediate_fk_column} = self.pk)`. Chainable like any \
2169             other QuerySet — compose `.filter()` / `.order_by()` / \
2170             `.limit()` etc. on top.",
2171        );
2172        let count_doc = format!(
2173            "Eloquent `$model->{name}->count()` analog for the \
2174             through-relation — returns the number of `{far}` rows \
2175             reachable through `{intermediate}`. Equivalent to \
2176             `self.{name}_through().count(pool)` but spelled \
2177             as a bare instance method for parity with the \
2178             `reverse_has` `<name>_count` shape.",
2179            name = rel.name,
2180        );
2181        quote! {
2182            #[doc = #doc]
2183            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
2184                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
2185                let intermediate_schema =
2186                    <#intermediate as #root::core::Model>::SCHEMA;
2187                let sub = SelectQuery::new(intermediate_schema)
2188                    .where_clause(WhereExpr::Predicate(Filter::new(
2189                        #intermediate_fk_column,
2190                        Op::Eq,
2191                        self.__rustango_pk_value(),
2192                    )))
2193                    .projection(::std::vec![#intermediate_pk_column]);
2194                #root::query::QuerySet::<#far>::new().where_raw(
2195                    WhereExpr::InSubquery {
2196                        column: #far_fk_column,
2197                        negated: false,
2198                        subquery: ::std::boxed::Box::new(sub),
2199                    },
2200                )
2201            }
2202
2203            #[doc = #count_doc]
2204            pub async fn #count_ident(
2205                &self,
2206                pool: &#root::sql::Pool,
2207            ) -> ::core::result::Result<
2208                i64,
2209                #root::sql::ExecError,
2210            > {
2211                use #root::sql::CounterPool as _;
2212                self.#method_ident().count(pool).await
2213            }
2214
2215            /// Eloquent `$model->relation->get()` for the
2216            /// through-relation — bare-name hot-path over
2217            /// `self.<name>_through().fetch(&pool)`. Use this
2218            /// when you don't need further `.filter()` /
2219            /// `.order_by()` composition; falls back to the
2220            /// chainable accessor when you do. Avoids the `_pool`
2221            /// suffix on the most common call-site shape.
2222            pub async fn #fetch_ident(
2223                &self,
2224                pool: &#root::sql::Pool,
2225            ) -> ::core::result::Result<
2226                ::std::vec::Vec<#far>,
2227                #root::sql::ExecError,
2228            > {
2229                use #root::sql::FetcherPool as _;
2230                self.#method_ident().fetch(pool).await
2231            }
2232
2233            /// Eloquent `hasOneThrough` analog — bare-name shortcut
2234            /// over `self.<name>_through().first(&pool)`. Returns
2235            /// `None` when no far rows are reachable through the
2236            /// intermediate. Useful when at most one row is
2237            /// expected (latest comment by country, primary tag,
2238            /// etc.).
2239            pub async fn #first_ident(
2240                &self,
2241                pool: &#root::sql::Pool,
2242            ) -> ::core::result::Result<
2243                ::core::option::Option<#far>,
2244                #root::sql::ExecError,
2245            > {
2246                self.#method_ident().first(pool).await
2247            }
2248
2249            /// Pluck a single column from the far rows into `Vec<U>`
2250            /// — cheaper than the typed `<Far>` decode when only one
2251            /// scalar column is needed.
2252            pub async fn #pluck_ident<U>(
2253                &self,
2254                col: &'static str,
2255                pool: &#root::sql::Pool,
2256            ) -> ::core::result::Result<
2257                ::std::vec::Vec<U>,
2258                #root::sql::ExecError,
2259            >
2260            where
2261                U: #root::sql::FlatScalar,
2262            {
2263                self.#method_ident()
2264                    .values_list_flat(col)
2265                    .fetch::<U>(pool)
2266                    .await
2267            }
2268        }
2269    });
2270    quote! {
2271        impl #struct_name {
2272            #( #methods )*
2273        }
2274    }
2275}
2276
2277struct ColumnEntry {
2278    /// The struct field ident, used both for the inherent const name on
2279    /// the model and for the inner column type's name.
2280    ident: syn::Ident,
2281    /// The struct's field type, used as `Column::Value`.
2282    value_ty: Type,
2283    /// Rust-side field name (e.g. `"id"`).
2284    name: String,
2285    /// SQL-side column name (e.g. `"user_id"`).
2286    column: String,
2287    /// `#root::core::FieldType::I64` etc.
2288    field_type_tokens: TokenStream2,
2289    /// A bare or optional `Uuid` field; MySQL decodes it from text.
2290    uuid_column: bool,
2291}
2292
2293struct CollectedFields {
2294    field_schemas: Vec<TokenStream2>,
2295    from_row_inits: Vec<TokenStream2>,
2296    /// Aliased counterparts of `from_row_inits` — read columns via
2297    /// `format!("{prefix}__{col}")` aliases so a Model can be
2298    /// decoded from a JOINed row's projected target columns.
2299    from_aliased_row_inits: Vec<TokenStream2>,
2300    /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2301    from_row_inits_my: Vec<TokenStream2>,
2302    from_aliased_row_inits_my: Vec<TokenStream2>,
2303    /// Static column-name list — used by the simple insert path
2304    /// (no `Auto<T>` fields). Aligned with `insert_values`.
2305    insert_columns: Vec<TokenStream2>,
2306    /// Static `Into<SqlValue>` expressions, one per field. Aligned
2307    /// with `insert_columns`. Used by the simple insert path only.
2308    insert_values: Vec<TokenStream2>,
2309    /// Per-field push expressions for the dynamic (Auto-aware)
2310    /// insert path. Each statement either unconditionally pushes
2311    /// `(column, value)` or, for an `Auto<T>` field, conditionally
2312    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2313    insert_pushes: Vec<TokenStream2>,
2314    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2315    /// when `has_auto == false`.
2316    returning_cols: Vec<TokenStream2>,
2317    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2318    /// field. Run after `insert_returning` to populate the model.
2319    auto_assigns: Vec<TokenStream2>,
2320    /// `(ident, column_literal, is_uuid)` for every Auto field. Used by
2321    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2322    /// instead of `self`.
2323    auto_field_idents: Vec<(syn::Ident, String, bool)>,
2324    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2325    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2326    /// back into the struct after insert (MySQL has no RETURNING → the
2327    /// field stays at its placeholder until the next read).
2328    generated_field_idents: Vec<(syn::Ident, String)>,
2329    /// First `Auto<T>` field the database fills, with its inner `T`: the
2330    /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2331    first_db_auto: Option<(syn::Ident, Type)>,
2332    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2333    /// by the all-Auto-Unset bulk path (Auto cols dropped from
2334    /// `columns`).
2335    bulk_pushes_no_auto: Vec<TokenStream2>,
2336    /// Bulk-insert per-row pushes for **all fields including Auto**.
2337    /// Used by the all-Auto-Set bulk path (Auto col included with the
2338    /// caller-supplied value).
2339    bulk_pushes_all: Vec<TokenStream2>,
2340    /// Column-name literals for non-Auto fields only (paired with
2341    /// `bulk_pushes_no_auto`).
2342    bulk_columns_no_auto: Vec<TokenStream2>,
2343    /// Column-name literals for every field including Auto (paired
2344    /// with `bulk_pushes_all`).
2345    bulk_columns_all: Vec<TokenStream2>,
2346    /// Fills each `default_uuid_v7` field of `_row` in place, so a
2347    /// mutable bulk insert hands the PKs back (#1934).
2348    bulk_fill_in_place: Vec<TokenStream2>,
2349    /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2350    rust_filled_idents: Vec<syn::Ident>,
2351    /// `auto_now` fields: `upsert` stamps them before binding.
2352    auto_now_idents: Vec<syn::Ident>,
2353    /// `default_uuid_v7` fields: an `Unset` one gets a fresh id, so the
2354    /// upsert marks it `generated` for the MySQL key guard.
2355    uuid_v7_fields: Vec<(syn::Ident, String)>,
2356    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2357    /// + the loop that asserts every row matches. One pair per Auto
2358    /// field. Empty when `has_auto == false`.
2359    bulk_auto_uniformity: Vec<TokenStream2>,
2360    /// Identifier of the first Auto field, used as the witness for
2361    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2362    first_auto_ident: Option<syn::Ident>,
2363    /// `true` if any field on the struct is `Auto<T>`.
2364    has_auto: bool,
2365    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2366    /// Gates `save()` codegen — only Auto PKs let us infer
2367    /// insert-vs-update from the in-memory value.
2368    pk_is_auto: bool,
2369    /// `Assignment` constructors for every non-PK column. Drives the
2370    /// UPDATE branch of `save()`.
2371    update_assignments: Vec<TokenStream2>,
2372    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2373    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2374    upsert_update_columns: Vec<TokenStream2>,
2375    primary_key: Option<(syn::Ident, String)>,
2376    column_entries: Vec<ColumnEntry>,
2377    /// Rust-side field names, in declaration order. Used to validate
2378    /// container attributes like `display = "…"`.
2379    field_names: Vec<String>,
2380    /// FK fields on this child model. Drives the reverse-relation
2381    /// helper emit — for each FK, the macro adds an inherent
2382    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2383    /// method on the parent type.
2384    fk_relations: Vec<FkRelation>,
2385    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2386    /// the model has one. Drives emission of the `soft_delete_on` /
2387    /// `restore_on` inherent methods. At most one such column per
2388    /// model is allowed; collect_fields rejects duplicates.
2389    soft_delete_column: Option<String>,
2390    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2391    /// companion to `soft_delete_column` for emitting predicates
2392    /// that need to read the field off `&self` (e.g. `trashed()`).
2393    soft_delete_field_ident: Option<syn::Ident>,
2394}
2395
2396#[derive(Clone)]
2397struct FkRelation {
2398    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2399    /// helper is emitted as `impl <ParentType> { … }`.
2400    parent_type: Type,
2401    /// Rust field ident of the FK. Its name is the key for select_related,
2402    /// prefetch and filters; the SQL column may differ (#1936).
2403    field_ident: syn::Ident,
2404    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2405    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2406    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2407    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2408    pk_kind: DetectedKind,
2409    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2410    /// FK column). Drives the `Some(...)` wrapping in load_related
2411    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2412    /// the codegen matches the field's declared shape.
2413    nullable: bool,
2414    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2415    /// override. When set, the reverse helper picks this name instead
2416    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2417    /// #816 (issue's "Related" note re: per-FK override).
2418    related_name: Option<String>,
2419}
2420
2421fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2422    let root = rustango_root();
2423    let cap = named.named.len();
2424    let mut out = CollectedFields {
2425        field_schemas: Vec::with_capacity(cap),
2426        from_row_inits: Vec::with_capacity(cap),
2427        from_aliased_row_inits: Vec::with_capacity(cap),
2428        from_row_inits_my: Vec::with_capacity(cap),
2429        from_aliased_row_inits_my: Vec::with_capacity(cap),
2430        insert_columns: Vec::with_capacity(cap),
2431        insert_values: Vec::with_capacity(cap),
2432        insert_pushes: Vec::with_capacity(cap),
2433        returning_cols: Vec::new(),
2434        auto_assigns: Vec::new(),
2435        auto_field_idents: Vec::new(),
2436        generated_field_idents: Vec::new(),
2437        first_db_auto: None,
2438        bulk_pushes_no_auto: Vec::with_capacity(cap),
2439        bulk_pushes_all: Vec::with_capacity(cap),
2440        bulk_columns_no_auto: Vec::with_capacity(cap),
2441        bulk_columns_all: Vec::with_capacity(cap),
2442        bulk_fill_in_place: Vec::new(),
2443        rust_filled_idents: Vec::new(),
2444        auto_now_idents: Vec::new(),
2445        uuid_v7_fields: Vec::new(),
2446        bulk_auto_uniformity: Vec::new(),
2447        first_auto_ident: None,
2448        has_auto: false,
2449        pk_is_auto: false,
2450        update_assignments: Vec::with_capacity(cap),
2451        upsert_update_columns: Vec::with_capacity(cap),
2452        primary_key: None,
2453        column_entries: Vec::with_capacity(cap),
2454        field_names: Vec::with_capacity(cap),
2455        fk_relations: Vec::new(),
2456        soft_delete_column: None,
2457        soft_delete_field_ident: None,
2458    };
2459
2460    for field in &named.named {
2461        let info = process_field(field, table)?;
2462        out.field_names.push(ident_name(info.ident));
2463        out.field_schemas.push(info.schema);
2464        out.from_row_inits.push(info.from_row_init);
2465        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2466        out.from_row_inits_my.push(info.from_row_init_my);
2467        out.from_aliased_row_inits_my
2468            .push(info.from_aliased_row_init_my);
2469        if let Some(parent_ty) = info.fk_inner.clone() {
2470            out.fk_relations.push(FkRelation {
2471                parent_type: parent_ty,
2472                field_ident: info.ident.clone(),
2473                pk_kind: info.fk_pk_kind,
2474                nullable: info.nullable,
2475                related_name: info.related_name.clone(),
2476            });
2477        }
2478        if info.soft_delete {
2479            if out.soft_delete_column.is_some() {
2480                return Err(syn::Error::new_spanned(
2481                    field,
2482                    "only one field may be marked `#[rustango(soft_delete)]`",
2483                ));
2484            }
2485            out.soft_delete_column = Some(info.column.clone());
2486            out.soft_delete_field_ident = Some(info.ident.clone());
2487        }
2488        let column = info.column.as_str();
2489        let ident = info.ident;
2490        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2491        // skip every write path — Postgres recomputes the value
2492        // from EXPR. Push only the column-entry record (so typed
2493        // column constants still exist for filtering / projection)
2494        // and the schema literal (already pushed above) and move
2495        // on. No insert_columns/values, no insert_pushes, no
2496        // bulk_*, no update_assignments, no upsert_update_columns,
2497        // no returning_cols.
2498        if info.generated_as.is_some() {
2499            out.column_entries.push(ColumnEntry {
2500                ident: ident.clone(),
2501                value_ty: info.value_ty.clone(),
2502                name: ident_name(ident),
2503                column: info.column.clone(),
2504                field_type_tokens: info.field_type_tokens,
2505                uuid_column: info.uuid_column,
2506            });
2507            // #1028 — refresh the DB-computed value after insert on
2508            // RETURNING-capable backends. The column joins `returning_cols`
2509            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2510            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2511            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2512            // it keeps the placeholder until the next read.
2513            out.returning_cols.push(quote!(#column));
2514            out.generated_field_idents
2515                .push((ident.clone(), info.column.clone()));
2516            continue;
2517        }
2518        out.insert_columns.push(quote!(#column));
2519        out.insert_values.push(quote! {
2520            ::core::convert::Into::<#root::core::SqlValue>::into(
2521                ::core::clone::Clone::clone(&self.#ident)
2522            )
2523        });
2524        if info.auto {
2525            out.has_auto = true;
2526            if out.first_auto_ident.is_none() {
2527                out.first_auto_ident = Some(ident.clone());
2528            }
2529            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2530            // before binding, so the value is already in
2531            // `self.#ident` after the insert_push — RETURNING is
2532            // redundant. Skip adding this column to returning_cols /
2533            // auto_assigns / auto_field_idents to avoid (a) an
2534            // unnecessary RETURNING column on every dialect, and (b)
2535            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2536            // integer PK.
2537            // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2538            // for the same reason: they are filled Rust-side below and
2539            // bound, so the value is already in `self` and RETURNING
2540            // would be a redundant column on every dialect.
2541            if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2542                if out.first_db_auto.is_none() {
2543                    out.first_db_auto = auto_inner_type(info.value_ty)
2544                        .cloned()
2545                        .map(|ty| (ident.clone(), ty));
2546                }
2547                out.returning_cols.push(quote!(#column));
2548                out.auto_field_idents.push((
2549                    ident.clone(),
2550                    info.column.clone(),
2551                    info.fk_pk_kind == DetectedKind::Uuid,
2552                ));
2553                out.auto_assigns.push(quote! {
2554                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2555                });
2556            }
2557            if info.default_uuid_v7 {
2558                // Rust-side UUIDv7 generation (issue #823, Eloquent
2559                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2560                // and bind; Auto::Set → bind the user's value. The
2561                // column is ALWAYS present in the INSERT statement —
2562                // no RETURNING / no DB DEFAULT needed.
2563                out.insert_pushes.push(quote! {
2564                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2565                        self.#ident = #root::sql::Auto::Set(
2566                            #root::__uuid::Uuid::now_v7(),
2567                        );
2568                    }
2569                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2570                        _columns.push(#column);
2571                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2572                            ::core::clone::Clone::clone(_v)
2573                        ));
2574                    }
2575                });
2576            } else if info.auto_now_add || info.auto_now {
2577                // Rust-side write timestamp (#1464), mirroring
2578                // `default_uuid_v7` above. `auto_now` has always bound
2579                // `Utc::now()` on UPDATE but not on INSERT, so the
2580                // first write of an `updated_at` took the DB default
2581                // while every later one took the clock — the same
2582                // column, filled two different ways.
2583                //
2584                // The default is the problem. `CREATE TABLE IF NOT
2585                // EXISTS` leaves an upgraded table's
2586                // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2587                // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2588                // statement that changes it — so every insert relying
2589                // on that default wrote the legacy shape forever, while
2590                // the migrate sweep converted the rows around it. The
2591                // column went permanently mixed and `ORDER BY` inverted
2592                // the admin audit log (#1616 rework review,
2593                // correctness-001 / dialects-005 / security-002).
2594                //
2595                // Binding it here makes the stale default unreachable
2596                // rather than trying to migrate it: the timestamp comes
2597                // from the application, not the database.
2598                out.insert_pushes.push(quote! {
2599                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2600                        self.#ident = #root::sql::Auto::Set(
2601                            #root::__chrono::Utc::now(),
2602                        );
2603                    }
2604                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2605                        _columns.push(#column);
2606                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2607                            ::core::clone::Clone::clone(_v)
2608                        ));
2609                    }
2610                });
2611            } else {
2612                out.insert_pushes.push(quote! {
2613                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2614                        _columns.push(#column);
2615                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2616                            ::core::clone::Clone::clone(_v)
2617                        ));
2618                    }
2619                });
2620            }
2621            // …except the timestamp columns, which must appear in BOTH
2622            // paths (#1464). The path is chosen by the *first* Auto
2623            // field — in practice the PK — so a bulk insert of rows
2624            // with an unset PK took the no-auto branch and dropped the
2625            // timestamp column, falling back to the DB default that
2626            // this change exists to stop depending on.
2627            //
2628            // `rows` is borrowed immutably here, so this fills per row
2629            // at push time rather than writing back into the struct. An
2630            // explicitly-Set value is still honoured; only `Unset`
2631            // takes the clock.
2632            // `default_uuid_v7` likewise: its column has no DB default (#1934).
2633            if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2634                out.rust_filled_idents.push(ident.clone());
2635            }
2636            if info.auto_now {
2637                out.auto_now_idents.push(ident.clone());
2638            }
2639            if info.default_uuid_v7 {
2640                out.uuid_v7_fields
2641                    .push((ident.clone(), info.column.clone()));
2642            }
2643            let rust_fill = if info.default_uuid_v7 {
2644                Some(quote!(#root::__uuid::Uuid::now_v7()))
2645            } else if info.auto_now_add || info.auto_now {
2646                Some(quote!(#root::__chrono::Utc::now()))
2647            } else {
2648                None
2649            };
2650            // Bulk: Auto fields appear in the all-Set path. An unset filled
2651            // field takes its fill there too, not NULL (#1950).
2652            out.bulk_columns_all.push(quote!(#column));
2653            if let Some(fill) = &rust_fill {
2654                let push = quote! {
2655                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2656                        match &_row.#ident {
2657                            #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2658                            #root::sql::Auto::Unset => #fill,
2659                        }
2660                    ));
2661                };
2662                out.bulk_columns_no_auto.push(quote!(#column));
2663                out.bulk_pushes_no_auto.push(push.clone());
2664                out.bulk_pushes_all.push(push);
2665            } else {
2666                out.bulk_pushes_all.push(quote! {
2667                    _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2668                        ::core::clone::Clone::clone(&_row.#ident)
2669                    ));
2670                });
2671            }
2672            if info.default_uuid_v7 {
2673                out.bulk_fill_in_place.push(quote! {
2674                    if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2675                        _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2676                    }
2677                });
2678            }
2679            // Uniformity check: every row's Auto state must match the
2680            // first row's. Mixed Set/Unset within one bulk_insert is
2681            // rejected here so the column list stays consistent.
2682            // A filled field may be unset on any row, so it is exempt.
2683            let ident_clone = ident.clone();
2684            if rust_fill.is_none() {
2685                out.bulk_auto_uniformity.push(quote! {
2686                    for _r in rows.iter().skip(1) {
2687                        if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2688                            return ::core::result::Result::Err(
2689                                #root::sql::ExecError::Sql(
2690                                    #root::sql::SqlError::BulkAutoMixed
2691                                )
2692                            );
2693                        }
2694                    }
2695                });
2696            }
2697        } else {
2698            out.insert_pushes.push(quote! {
2699                _columns.push(#column);
2700                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2701                    ::core::clone::Clone::clone(&self.#ident)
2702                ));
2703            });
2704            // Bulk: non-Auto fields appear in BOTH paths.
2705            out.bulk_columns_no_auto.push(quote!(#column));
2706            out.bulk_columns_all.push(quote!(#column));
2707            let push_expr = quote! {
2708                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2709                    ::core::clone::Clone::clone(&_row.#ident)
2710                ));
2711            };
2712            out.bulk_pushes_no_auto.push(push_expr.clone());
2713            out.bulk_pushes_all.push(push_expr);
2714        }
2715        if info.primary_key {
2716            if out.primary_key.is_some() {
2717                return Err(syn::Error::new_spanned(
2718                    field,
2719                    "only one field may be marked `#[rustango(primary_key)]`",
2720                ));
2721            }
2722            out.primary_key = Some((ident.clone(), info.column.clone()));
2723            if info.auto {
2724                out.pk_is_auto = true;
2725            }
2726        } else if info.auto_now_add {
2727            // Immutable post-insert: skip from UPDATE entirely.
2728        } else if info.auto_now {
2729            // `auto_now` columns: bind `chrono::Utc::now()` on every
2730            // UPDATE, and write it back so `self` matches the row.
2731            out.update_assignments.push(quote! {
2732                #root::core::Assignment::new(
2733                    #column,
2734                    {
2735                        let _now = #root::__chrono::Utc::now();
2736                        self.#ident = #root::sql::Auto::Set(_now);
2737                        ::core::convert::Into::<#root::core::SqlValue>::into(_now)
2738                    },
2739                )
2740            });
2741            out.upsert_update_columns.push(quote!(#column));
2742        } else {
2743            out.update_assignments.push(quote! {
2744                #root::core::Assignment::new(
2745                    #column,
2746                    ::core::convert::Into::<#root::core::SqlValue>::into(
2747                        ::core::clone::Clone::clone(&self.#ident)
2748                    ),
2749                )
2750            });
2751            out.upsert_update_columns.push(quote!(#column));
2752        }
2753        out.column_entries.push(ColumnEntry {
2754            ident: ident.clone(),
2755            value_ty: info.value_ty.clone(),
2756            name: ident_name(ident),
2757            column: info.column.clone(),
2758            field_type_tokens: info.field_type_tokens,
2759            uuid_column: info.uuid_column,
2760        });
2761    }
2762    Ok(out)
2763}
2764
2765/// A derived index name within PG's 63 bytes (MySQL takes 64); a longer one
2766/// keeps a prefix plus a hash of the whole, so two long names stay distinct.
2767fn capped_index_name(name: String) -> String {
2768    const MAX: usize = 63;
2769    if name.len() <= MAX {
2770        return name;
2771    }
2772    // FNV-1a: stable across builds, unlike `DefaultHasher`.
2773    let hash = name.bytes().fold(0x811c_9dc5_u32, |h, b| {
2774        (h ^ u32::from(b)).wrapping_mul(0x0100_0193)
2775    });
2776    let mut cut = MAX - 9;
2777    while !name.is_char_boundary(cut) {
2778        cut -= 1;
2779    }
2780    format!("{}_{hash:08x}", &name[..cut])
2781}
2782
2783fn model_impl_tokens(
2784    struct_name: &syn::Ident,
2785    model_name: &str,
2786    table: &str,
2787    display: Option<&str>,
2788    app_label: Option<&str>,
2789    admin: Option<&AdminAttrs>,
2790    default_order: &[(String, bool, proc_macro2::Span)],
2791    field_schemas: &[TokenStream2],
2792    soft_delete_column: Option<&str>,
2793    permissions: bool,
2794    audit_track: Option<&[String]>,
2795    m2m_relations: &[M2MAttr],
2796    indexes: &[IndexAttr],
2797    checks: &[CheckAttr],
2798    excludes: &[ExcludeAttr],
2799    composite_fks: &[CompositeFkAttr],
2800    generic_fks: &[GenericFkAttr],
2801    scope: Option<&str>,
2802    access: Option<&str>,
2803    is_view: bool,
2804    verbose_name: Option<&str>,
2805    verbose_name_plural: Option<&str>,
2806    managed: bool,
2807    base_manager_name: Option<&str>,
2808    order_with_respect_to: Option<&str>,
2809    proxy: bool,
2810    required_db_features: &[String],
2811    required_db_vendor: Option<&str>,
2812    default_related_name: Option<&str>,
2813    db_table_comment: Option<&str>,
2814    get_latest_by: Option<(&str, bool)>,
2815    extra_permissions: &[(String, String)],
2816    default_permissions: &[String],
2817    global_scopes: &[GlobalScopeAttr],
2818    reverse_has_relations: &[ReverseHasAttr],
2819    generic_has_relations: &[GenericHasAttr],
2820) -> TokenStream2 {
2821    let root = rustango_root();
2822    let display_tokens = if let Some(name) = display {
2823        quote!(::core::option::Option::Some(#name))
2824    } else {
2825        quote!(::core::option::Option::None)
2826    };
2827    let app_label_tokens = if let Some(name) = app_label {
2828        quote!(::core::option::Option::Some(#name))
2829    } else {
2830        quote!(::core::option::Option::None)
2831    };
2832    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2833        quote!(::core::option::Option::Some(#col))
2834    } else {
2835        quote!(::core::option::Option::None)
2836    };
2837    let audit_track_tokens = match audit_track {
2838        None => quote!(::core::option::Option::None),
2839        Some(names) => {
2840            let lits = names.iter().map(|n| n.as_str());
2841            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2842        }
2843    };
2844    let admin_tokens = admin_config_tokens(admin);
2845    // Default `tenant` so single-tenant projects (no `scope` attr
2846    // anywhere) keep the v0.24.x behavior. Container-attr parser
2847    // already validated the value is "registry" or "tenant".
2848    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2849        Some("registry") => quote!(#root::core::ModelScope::Registry),
2850        _ => quote!(#root::core::ModelScope::Tenant),
2851    };
2852    // The container-attr parser already validated the value.
2853    let access_tokens = match access {
2854        Some("admin_only") => {
2855            quote!(::core::option::Option::Some(#root::core::TableAccess::AdminOnly))
2856        }
2857        Some("never") => quote!(::core::option::Option::Some(#root::core::TableAccess::Never)),
2858        Some(_) => quote!(::core::option::Option::Some(#root::core::TableAccess::Open)),
2859        None => quote!(::core::option::Option::None),
2860    };
2861    let verbose_name_tokens = optional_str(verbose_name);
2862    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2863    let base_manager_name_tokens = optional_str(base_manager_name);
2864    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2865    let required_db_features_lits: Vec<&str> =
2866        required_db_features.iter().map(String::as_str).collect();
2867    let required_db_vendor_tokens = optional_str(required_db_vendor);
2868    let default_related_name_tokens = optional_str(default_related_name);
2869    let db_table_comment_tokens = optional_str(db_table_comment);
2870    let get_latest_by_tokens = match get_latest_by {
2871        Some((col, desc)) => {
2872            quote!(::core::option::Option::Some((#col, #desc)))
2873        }
2874        None => quote!(::core::option::Option::None),
2875    };
2876    let extra_permission_tokens: Vec<_> = extra_permissions
2877        .iter()
2878        .map(|(c, l)| quote!((#c, #l)))
2879        .collect();
2880    let default_permission_tokens: Vec<_> = default_permissions
2881        .iter()
2882        .map(|action| quote!(#action))
2883        .collect();
2884    let indexes_tokens = indexes.iter().map(|idx| {
2885        // When no explicit `name = "..."` was given, derive a stable,
2886        // collision-free name from the table + columns
2887        // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2888        // would clash the moment a model declares two unnamed indexes.
2889        let derived_name = idx
2890            .name
2891            .clone()
2892            .unwrap_or_else(|| capped_index_name(format!("{table}_{}_idx", idx.columns.join("_"))));
2893        let name = derived_name.as_str();
2894        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2895        let unique = idx.unique;
2896        // Map the parsed method string onto the IndexMethod enum
2897        // variant — kept at the codegen layer so the IR doesn't
2898        // carry the string form.
2899        let method_variant = match idx.method.as_str() {
2900            "gin" => quote!(#root::core::IndexMethod::Gin),
2901            "gist" => quote!(#root::core::IndexMethod::Gist),
2902            "brin" => quote!(#root::core::IndexMethod::Brin),
2903            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2904            "hash" => quote!(#root::core::IndexMethod::Hash),
2905            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2906            _ => quote!(#root::core::IndexMethod::BTree),
2907        };
2908        let where_clause = match &idx.where_clause {
2909            Some(s) => quote!(::core::option::Option::Some(#s)),
2910            None => quote!(::core::option::Option::None),
2911        };
2912        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2913        quote! {{
2914            let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2915            i.unique = #unique;
2916            i.method = #method_variant;
2917            i.where_clause = #where_clause;
2918            i.include = &[ #(#include_lits),* ];
2919            i
2920        }}
2921    });
2922    let checks_tokens = checks.iter().map(|c| {
2923        let name = c.name.as_str();
2924        let expr = c.expr.as_str();
2925        quote!(#root::core::CheckConstraint::new(#name, #expr))
2926    });
2927    let excludes_tokens = excludes.iter().map(|e| {
2928        let name = e.name.as_str();
2929        let using = e.using.as_str();
2930        let element_tokens = e.elements.iter().map(|(col, op)| {
2931            let col_s = col.as_str();
2932            let op_s = op.as_str();
2933            quote!((#col_s, #op_s))
2934        });
2935        let where_tokens = match e.where_clause.as_deref() {
2936            Some(w) => quote!(::core::option::Option::Some(#w)),
2937            None => quote!(::core::option::Option::None),
2938        };
2939        quote! {{
2940            let mut e = #root::core::ExclusionConstraint::new(
2941                #name,
2942                #using,
2943                &[ #(#element_tokens),* ],
2944            );
2945            e.where_clause = #where_tokens;
2946            e
2947        }}
2948    });
2949    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2950        let name = rel.name.as_str();
2951        let to = rel.to.as_str();
2952        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2953        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2954        quote! {
2955            #root::core::CompositeFkRelation::new(
2956                #name,
2957                #to,
2958                &[ #(#from_cols),* ],
2959                &[ #(#on_cols),* ],
2960            )
2961        }
2962    });
2963    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2964        let name = rel.name.as_str();
2965        let ct_col = rel.ct_column.as_str();
2966        let pk_col = rel.pk_column.as_str();
2967        quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2968    });
2969    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2970    // no `#[rustango(default_order = "...")]` attribute was supplied.
2971    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2972        let col_lit = col.as_str();
2973        quote! { (#col_lit, #desc) }
2974    });
2975
2976    // Issue #820 — `global_scopes` slice literal. Empty when no
2977    // `#[rustango(global_scope(...))]` attribute was supplied. The
2978    // `apply` path is re-emitted verbatim so it resolves in the
2979    // consumer's scope; the name is stored as a string literal.
2980    let global_scope_tokens = global_scopes.iter().map(|s| {
2981        let name = s.name.as_str();
2982        // The slice sits in a nested `const`, where `Self` does not resolve.
2983        let mut apply = s.apply.clone();
2984        if let Some(first) = apply.segments.first_mut() {
2985            if first.ident == "Self" {
2986                first.ident = struct_name.clone();
2987            }
2988        }
2989        quote!(#root::core::GlobalScope::new(#name, #apply))
2990    });
2991
2992    let m2m_tokens = m2m_relations.iter().map(|rel| {
2993        let name = rel.name.as_str();
2994        let to = rel.to.as_str();
2995        let through = rel.through.as_str();
2996        let src = rel.src.as_str();
2997        let dst = rel.dst.as_str();
2998        let auto_create = rel.auto_create;
2999        quote! {{
3000            let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
3001            m.auto_create = #auto_create;
3002            m
3003        }}
3004    });
3005    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
3006    // when the model declares `#[rustango(reverse_has(...))]`. Each
3007    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
3008    // a const expression. Models without reverse_has fall through to
3009    // the trait's empty default — no override emitted.
3010    let reverse_relations_override = if reverse_has_relations.is_empty() {
3011        quote!()
3012    } else {
3013        let entries = reverse_has_relations.iter().map(|rel| {
3014            let name = rel.name.as_str();
3015            let child = &rel.child;
3016            let child_fk_column = rel.child_fk_column.as_str();
3017            let self_pk_column = rel.self_pk_column();
3018            quote! {
3019                #root::core::ReverseRelation::new(
3020                    #name,
3021                    <#child as #root::core::Model>::SCHEMA,
3022                    #child_fk_column,
3023                    #self_pk_column,
3024                )
3025            }
3026        });
3027        quote! {
3028            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
3029                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
3030                RELS
3031            }
3032        }
3033    };
3034    // Issue #830 — `Model::generic_reverse_relations()` override for
3035    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
3036    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
3037        quote!()
3038    } else {
3039        let entries = generic_has_relations.iter().map(|rel| {
3040            let name = rel.name.as_str();
3041            let child = &rel.child;
3042            let ct_column = rel.ct_column.as_str();
3043            let pk_column = rel.pk_column.as_str();
3044            let self_pk_column = rel.self_pk_column();
3045            quote! {
3046                #root::core::GenericReverseRelation::new(
3047                    #name,
3048                    <#child as #root::core::Model>::SCHEMA,
3049                    #ct_column,
3050                    #pk_column,
3051                    #self_pk_column,
3052                )
3053            }
3054        });
3055        quote! {
3056            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
3057                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
3058                RELS
3059            }
3060        }
3061    };
3062    let fields_slice = const_slice(
3063        &quote!(#root::core::FieldSchema),
3064        field_schemas.iter().cloned(),
3065    );
3066    let m2m_slice = const_slice(&quote!(#root::core::M2MRelation), m2m_tokens);
3067    let indexes_slice = const_slice(&quote!(#root::core::IndexSchema), indexes_tokens);
3068    let checks_slice = const_slice(&quote!(#root::core::CheckConstraint), checks_tokens);
3069    let excludes_slice = const_slice(&quote!(#root::core::ExclusionConstraint), excludes_tokens);
3070    let composite_fk_slice = const_slice(
3071        &quote!(#root::core::CompositeFkRelation),
3072        composite_fk_tokens,
3073    );
3074    let generic_fk_slice = const_slice(&quote!(#root::core::GenericRelation), generic_fk_tokens);
3075    let audited_update_override = if audit_track.is_some() {
3076        quote! {
3077            fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
3078                ::core::option::Option::Some(Self::__rustango_update_audited)
3079            }
3080            fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
3081                ::core::option::Option::Some(Self::__rustango_delete_audited)
3082            }
3083            fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
3084                ::core::option::Option::Some(Self::__rustango_create_audited)
3085            }
3086            fn __rustango_audited_update_record(
3087            ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
3088                ::core::option::Option::Some(Self::__rustango_update_record_audited)
3089            }
3090            fn __rustango_audited_cascade() -> ::core::option::Option<#root::audit::AuditedCascade> {
3091                ::core::option::Option::Some(Self::__rustango_cascade_audited)
3092            }
3093        }
3094    } else {
3095        quote!()
3096    };
3097    let global_scope_slice = const_slice(&quote!(#root::core::GlobalScope), global_scope_tokens);
3098    quote! {
3099        impl #root::core::Model for #struct_name {
3100            const SCHEMA: &'static #root::core::ModelSchema = &{
3101                #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
3102                    name: #model_name,
3103                    table: #table,
3104                    fields: #fields_slice,
3105                    display: #display_tokens,
3106                    app_label: #app_label_tokens,
3107                    admin: #admin_tokens,
3108                    soft_delete_column: #soft_delete_tokens,
3109                    permissions: #permissions,
3110                    audit_track: #audit_track_tokens,
3111                    m2m: #m2m_slice,
3112                    indexes: #indexes_slice,
3113                    check_constraints: #checks_slice,
3114                    exclusion_constraints: #excludes_slice,
3115                    composite_relations: #composite_fk_slice,
3116                    generic_relations: #generic_fk_slice,
3117                    scope: #scope_tokens,
3118                    default_order: &[ #(#default_order_tokens),* ],
3119                    is_view: #is_view,
3120                    verbose_name: #verbose_name_tokens,
3121                    verbose_name_plural: #verbose_name_plural_tokens,
3122                    managed: #managed,
3123                    base_manager_name: #base_manager_name_tokens,
3124                    order_with_respect_to: #order_with_respect_to_tokens,
3125                    proxy: #proxy,
3126                    required_db_features: &[ #(#required_db_features_lits),* ],
3127                    required_db_vendor: #required_db_vendor_tokens,
3128                    default_related_name: #default_related_name_tokens,
3129                    db_table_comment: #db_table_comment_tokens,
3130                    get_latest_by: #get_latest_by_tokens,
3131                    extra_permissions: &[ #(#extra_permission_tokens),* ],
3132                    default_permissions: &[ #(#default_permission_tokens),* ],
3133                    global_scopes: #global_scope_slice,
3134                    access: #access_tokens,
3135                })
3136            };
3137
3138            #reverse_relations_override
3139            #generic_reverse_relations_override
3140            #audited_update_override
3141        }
3142    }
3143}
3144
3145/// Emit the `admin: Option<&'static AdminConfig>` field for the model
3146/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
3147/// otherwise a static reference to a populated `AdminConfig`.
3148fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
3149    let root = rustango_root();
3150    let Some(admin) = admin else {
3151        return quote!(::core::option::Option::None);
3152    };
3153
3154    let list_display = admin
3155        .list_display
3156        .as_ref()
3157        .map(|(v, _)| v.as_slice())
3158        .unwrap_or(&[]);
3159    let list_display_lits = list_display.iter().map(|s| s.as_str());
3160
3161    let search_fields = admin
3162        .search_fields
3163        .as_ref()
3164        .map(|(v, _)| v.as_slice())
3165        .unwrap_or(&[]);
3166    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
3167
3168    let readonly_fields = admin
3169        .readonly_fields
3170        .as_ref()
3171        .map(|(v, _)| v.as_slice())
3172        .unwrap_or(&[]);
3173    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
3174
3175    let list_filter = admin
3176        .list_filter
3177        .as_ref()
3178        .map(|(v, _)| v.as_slice())
3179        .unwrap_or(&[]);
3180    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
3181
3182    let actions = admin
3183        .actions
3184        .as_ref()
3185        .map(|(v, _)| v.as_slice())
3186        .unwrap_or(&[]);
3187    let actions_lits = actions.iter().map(|s| s.as_str());
3188
3189    let fieldsets = admin
3190        .fieldsets
3191        .as_ref()
3192        .map(|(v, _)| v.as_slice())
3193        .unwrap_or(&[]);
3194    let fieldset_tokens = const_slice(
3195        &quote!(#root::core::Fieldset),
3196        fieldsets.iter().map(|(title, fields)| {
3197            let title = title.as_str();
3198            let field_lits = fields.iter().map(String::as_str);
3199            quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
3200        }),
3201    );
3202
3203    let list_display_links = admin
3204        .list_display_links
3205        .as_ref()
3206        .map(|(v, _)| v.as_slice())
3207        .unwrap_or(&[]);
3208    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
3209
3210    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
3211    let actions_on_top = admin.actions_on_top.unwrap_or(true);
3212    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
3213    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
3214
3215    let prepopulated = admin
3216        .prepopulated_fields
3217        .as_ref()
3218        .map(|(v, _)| v.as_slice())
3219        .unwrap_or(&[]);
3220    let prepopulated_tokens = const_slice(
3221        &quote!(#root::core::PrepopulatedField),
3222        prepopulated.iter().map(|(target, sources)| {
3223            let target = target.as_str();
3224            let source_lits = sources.iter().map(String::as_str);
3225            quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
3226        }),
3227    );
3228
3229    let raw_id_fields = admin
3230        .raw_id_fields
3231        .as_ref()
3232        .map(|(v, _)| v.as_slice())
3233        .unwrap_or(&[]);
3234    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
3235
3236    let autocomplete_fields = admin
3237        .autocomplete_fields
3238        .as_ref()
3239        .map(|(v, _)| v.as_slice())
3240        .unwrap_or(&[]);
3241    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
3242
3243    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
3244    let list_select_related_tokens = match admin.list_select_related.as_deref() {
3245        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
3246        Some("none") => quote!(#root::core::ListSelectRelated::None),
3247        Some(raw) => {
3248            let names: Vec<&str> = raw
3249                .split(',')
3250                .map(str::trim)
3251                .filter(|s| !s.is_empty())
3252                .collect();
3253            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
3254        }
3255    };
3256
3257    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
3258    // a Vec<(String, String)>. Empty / unset → no overrides.
3259    let formfield_pairs: Vec<(&str, &str)> = admin
3260        .formfield_overrides
3261        .as_ref()
3262        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
3263        .unwrap_or_default();
3264    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
3265        let field = *field;
3266        let widget = *widget;
3267        quote!((#field, #widget))
3268    });
3269
3270    let list_per_page = admin.list_per_page.unwrap_or(0);
3271
3272    let ordering_pairs = admin
3273        .ordering
3274        .as_ref()
3275        .map(|(v, _)| v.as_slice())
3276        .unwrap_or(&[]);
3277    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3278        let name = name.as_str();
3279        let desc = *desc;
3280        quote!((#name, #desc))
3281    });
3282
3283    quote! {
3284        ::core::option::Option::Some({
3285            const ADMIN: &#root::core::AdminConfig = &{
3286                #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3287                    list_display: &[ #( #list_display_lits ),* ],
3288                    search_fields: &[ #( #search_fields_lits ),* ],
3289                    list_per_page: #list_per_page,
3290                    ordering: &[ #( #ordering_tokens ),* ],
3291                    readonly_fields: &[ #( #readonly_fields_lits ),* ],
3292                    list_filter: &[ #( #list_filter_lits ),* ],
3293                    actions: &[ #( #actions_lits ),* ],
3294                    fieldsets: #fieldset_tokens,
3295                    list_display_links: &[ #( #list_display_links_lits ),* ],
3296                    search_help_text: #search_help_text,
3297                    actions_on_top: #actions_on_top,
3298                    actions_on_bottom: #actions_on_bottom,
3299                    date_hierarchy: #date_hierarchy,
3300                    prepopulated_fields: #prepopulated_tokens,
3301                    raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3302                    autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3303                    list_select_related: #list_select_related_tokens,
3304                    formfield_overrides: &[ #( #formfield_tokens ),* ],
3305                })
3306            };
3307            ADMIN
3308        })
3309    }
3310}
3311
3312fn inherent_impl_tokens(
3313    struct_name: &syn::Ident,
3314    fields: &CollectedFields,
3315    primary_key: Option<&(syn::Ident, String)>,
3316    column_consts: &TokenStream2,
3317    audited_fields: Option<&[&ColumnEntry]>,
3318    upsert_unique: Option<&[String]>,
3319    manager_fns: &[syn::Ident],
3320) -> TokenStream2 {
3321    let root = rustango_root();
3322
3323    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3324    // (audit-on-connection over &Pool needs a bi-dialect transaction
3325    // helper, deferred). Two body shapes:
3326    // - has_auto: build InsertQuery skipping Auto::Unset columns,
3327    //   request Auto cols in `returning`, dispatch via
3328    //   `insert_returning_pool`, then on the returned `PgRow` /
3329    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
3330    //   row OR drop the single i64 into the first Auto field on MySQL
3331    //   (multi-Auto models on MySQL error at runtime since
3332    //   `LAST_INSERT_ID()` only reports one)
3333    // - non-Auto: build InsertQuery with explicit columns/values and
3334    //   call `insert_pool` (no returning needed)
3335    // pool_insert_method body for the audited Auto-PK case is moved
3336    // to after audit_pair_tokens / audit_pk_to_string (they live
3337    // ~150 lines below). This block keeps the non-audited and
3338    // non-Auto branches in place — the audited Auto-PK arm is
3339    // computed below and merged via the dispatch helper variable.
3340    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3341        // Audited models with explicit (non-Auto) PKs go through
3342        // the non-Auto insert path below — the audit emit is one
3343        // round-trip after the INSERT inside the same tx via
3344        // audit::save_one_with_audit? No, INSERT semantics
3345        // differ. For non-Auto PK + audited, route through a
3346        // dedicated insert + audit emit on the same tx, but defer
3347        // the macro emission to the audit-bundle-aware block below
3348        // — this `quote!()` placeholder gets overwritten there.
3349        quote!()
3350    } else if audited_fields.is_some() && fields.has_auto {
3351        // Audited Auto-PK insert_pool — assembled after the audit
3352        // bundles. Placeholder; real emission below.
3353        quote!()
3354    } else if fields.has_auto {
3355        let pushes = &fields.insert_pushes;
3356        // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3357        let filled = &fields.rust_filled_idents;
3358        let was_unset: Vec<syn::Ident> = filled
3359            .iter()
3360            .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3361            .collect();
3362        let insert_or_ignore_call = quote! {
3363            #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3364            let mut _columns: ::std::vec::Vec<&'static str> =
3365                ::std::vec::Vec::new();
3366            let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3367                ::std::vec::Vec::new();
3368            #( #pushes )*
3369            let _query = #root::core::InsertQuery::new(
3370                <Self as #root::core::Model>::SCHEMA,
3371                _columns,
3372                _values,
3373            );
3374            let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3375            if !matches!(_result, ::core::result::Result::Ok(true)) {
3376                #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3377            }
3378            _result
3379        };
3380        // When every `Auto<T>` field is filled Rust-side
3381        // (`default_uuid_v7`, issue #823), there is no column to read
3382        // back from the database — `returning_cols` is empty. Route
3383        // through plain `insert_pool` instead of
3384        // `insert_returning_pool` to skip the redundant RETURNING /
3385        // LAST_INSERT_ID round-trip.
3386        if fields.returning_cols.is_empty() {
3387            quote! {
3388
3389
3390                /// Eloquent `Model::insertOrIgnore()` — same shape
3391                /// as the auto-PK branch above. Returns `Ok(true)`
3392                /// when inserted, `Ok(false)` when a conflict caused
3393                /// the INSERT to silently skip.
3394                ///
3395                /// # Errors
3396                /// As [`Self::insert`].
3397                pub async fn insert_or_ignore(
3398                    &mut self,
3399                    pool: &#root::sql::Pool,
3400                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3401                    #insert_or_ignore_call
3402                }
3403            }
3404        } else {
3405            quote! {
3406
3407
3408                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3409                /// row or silently skip on unique-constraint
3410                /// violation. Returns `Ok(true)` when a row was
3411                /// inserted, `Ok(false)` when a conflict caused the
3412                /// INSERT to silently skip.
3413                ///
3414                /// **Caveat on auto-PK models**: when the row is
3415                /// skipped (conflict), this instance's `Auto<T>`
3416                /// fields stay `Unset` — no PK is back-populated
3417                /// because the server didn't auto-assign one. For
3418                /// "insert then read back the PK or the existing
3419                /// row's PK", use the `upsert` family or
3420                /// `get_or_create`.
3421                ///
3422                /// # Errors
3423                /// As [`Self::insert`].
3424                pub async fn insert_or_ignore(
3425                    &mut self,
3426                    pool: &#root::sql::Pool,
3427                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3428                    #insert_or_ignore_call
3429                }
3430            }
3431        }
3432    } else {
3433        let insert_columns = &fields.insert_columns;
3434        let insert_values = &fields.insert_values;
3435        quote! {
3436
3437
3438            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3439            /// or silently skip on unique-constraint violation. Maps
3440            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3441            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3442            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3443            /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3444            ///
3445            /// Returns `Ok(true)` when a row was inserted,
3446            /// `Ok(false)` when a conflict caused the INSERT to
3447            /// silently skip.
3448            ///
3449            /// Use for "create-if-absent" patterns where you don't
3450            /// need the row back. For "find-or-create with the row
3451            /// returned", use the queryset-level
3452            /// `crate::sql::get_or_create` free function.
3453            ///
3454            /// # Errors
3455            /// As [`Self::insert`], plus any dialect-specific
3456            /// translation error from the ConflictClause writer.
3457            pub async fn insert_or_ignore(
3458                &self,
3459                pool: &#root::sql::Pool,
3460            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3461                let _query = #root::core::InsertQuery::new(
3462                    <Self as #root::core::Model>::SCHEMA,
3463                    ::std::vec![ #( #insert_columns ),* ],
3464                    ::std::vec![ #( #insert_values ),* ],
3465                );
3466                #root::sql::insert_or_ignore(pool, &_query).await
3467            }
3468        }
3469    };
3470
3471    // The tracked (column, JSON value) pairs, spliced only into
3472    // `__rustango_audit_pairs`. Empty when the model isn't audited.
3473    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3474        .map(|tracked| {
3475            tracked
3476                .iter()
3477                .map(|c| {
3478                    let column_lit = c.column.as_str();
3479                    let ident = &c.ident;
3480                    quote! {
3481                        (
3482                            #column_lit,
3483                            #root::core::__json_value(&self.#ident),
3484                        )
3485                    }
3486                })
3487                .collect()
3488        })
3489        .unwrap_or_default();
3490    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3491        if fields.pk_is_auto {
3492            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3493        } else {
3494            quote!(::std::format!("{}", &self.#pk_ident))
3495        }
3496    } else {
3497        quote!(::std::string::String::new())
3498    };
3499    // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3500    // is built, so every audited write path records the same shape.
3501    let delete_audited_body = if primary_key.is_some() {
3502        quote! {
3503            ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3504                pool,
3505                query,
3506                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3507            ))
3508        }
3509    } else {
3510        quote! {
3511            let _ = (pool, query);
3512            ::std::boxed::Box::pin(async {
3513                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3514                    table: <Self as #root::core::Model>::SCHEMA.table,
3515                })
3516            })
3517        }
3518    };
3519    let cascade_audited_body = if primary_key.is_some() {
3520        quote! {
3521            ::std::boxed::Box::pin(#root::audit::cascade_rows::<Self>(
3522                tx,
3523                <Self as #root::core::Model>::SCHEMA,
3524                rows,
3525                |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3526            ))
3527        }
3528    } else {
3529        quote! {
3530            let _ = (tx, rows);
3531            ::std::boxed::Box::pin(async {
3532                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3533                    table: <Self as #root::core::Model>::SCHEMA.table,
3534                })
3535            })
3536        }
3537    };
3538    let update_audited_body = if primary_key.is_some() {
3539        quote! {
3540            ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3541                pool,
3542                query,
3543                move |_r: &Self| _r.__rustango_audit_entry(op),
3544            ))
3545        }
3546    } else {
3547        quote! {
3548            let _ = (pool, query, op);
3549            ::std::boxed::Box::pin(async {
3550                ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3551                    table: <Self as #root::core::Model>::SCHEMA.table,
3552                })
3553            })
3554        }
3555    };
3556    let audit_entry_method = if audited_fields.is_some() {
3557        let pairs = audit_pair_tokens.iter();
3558        let pk_str = audit_pk_to_string.clone();
3559        quote! {
3560            /// This row's tracked `(column, value)` pairs.
3561            #[doc(hidden)]
3562            pub fn __rustango_audit_pairs(
3563                &self,
3564            ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3565                ::std::vec![ #( #pairs ),* ]
3566            }
3567
3568            /// This row's audit PK.
3569            #[doc(hidden)]
3570            pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3571                #pk_str
3572            }
3573
3574            /// Snapshot audit entry for `operation` on this row.
3575            #[doc(hidden)]
3576            pub fn __rustango_audit_entry(
3577                &self,
3578                operation: #root::audit::AuditOp,
3579            ) -> #root::audit::PendingEntry {
3580                #root::audit::PendingEntry {
3581                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3582                    entity_pk: self.__rustango_audit_pk(),
3583                    operation,
3584                    source: #root::audit::current_source(),
3585                    changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3586                }
3587            }
3588
3589            /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3590            #[doc(hidden)]
3591            pub fn __rustango_update_audited<'a>(
3592                pool: &'a #root::sql::Pool,
3593                query: &'a #root::core::UpdateQuery,
3594                op: #root::audit::AuditOp,
3595            ) -> ::std::pin::Pin<::std::boxed::Box<
3596                dyn ::core::future::Future<
3597                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3598                > + ::core::marker::Send + 'a,
3599            >> {
3600                #update_audited_body
3601            }
3602
3603            /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3604            #[doc(hidden)]
3605            pub fn __rustango_create_audited<'a, 't>(
3606                tx: &'a mut #root::sql::PoolTx<'t>,
3607                pk: #root::core::SqlValue,
3608            ) -> ::std::pin::Pin<::std::boxed::Box<
3609                dyn ::core::future::Future<
3610                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3611                > + ::core::marker::Send + 'a,
3612            >> {
3613                ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3614                    tx,
3615                    <Self as #root::core::Model>::SCHEMA,
3616                    pk,
3617                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3618                ))
3619            }
3620
3621            /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3622            #[doc(hidden)]
3623            pub fn __rustango_update_record_audited<'a, 't>(
3624                tx: &'a mut #root::sql::PoolTx<'t>,
3625                pk: #root::core::SqlValue,
3626            ) -> ::std::pin::Pin<::std::boxed::Box<
3627                dyn ::core::future::Future<
3628                    Output = ::core::result::Result<(), #root::sql::ExecError>,
3629                > + ::core::marker::Send + 'a,
3630            >> {
3631                ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3632                    tx,
3633                    <Self as #root::core::Model>::SCHEMA,
3634                    pk,
3635                    |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3636                ))
3637            }
3638
3639            /// Rows an FK cascade will delete, behind `Model::__rustango_audited_cascade`.
3640            #[doc(hidden)]
3641            pub fn __rustango_cascade_audited<'a, 't>(
3642                tx: &'a mut #root::sql::PoolTx<'t>,
3643                rows: #root::core::WhereExpr,
3644            ) -> ::std::pin::Pin<::std::boxed::Box<
3645                dyn ::core::future::Future<
3646                    Output = ::core::result::Result<
3647                        ::std::vec::Vec<(#root::core::SqlValue, #root::audit::PendingEntry)>,
3648                        #root::sql::ExecError,
3649                    >,
3650                > + ::core::marker::Send + 'a,
3651            >> {
3652                #cascade_audited_body
3653            }
3654
3655            /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3656            #[doc(hidden)]
3657            pub fn __rustango_delete_audited<'a>(
3658                pool: &'a #root::sql::Pool,
3659                query: &'a #root::core::DeleteQuery,
3660            ) -> ::std::pin::Pin<::std::boxed::Box<
3661                dyn ::core::future::Future<
3662                    Output = ::core::result::Result<u64, #root::sql::ExecError>,
3663                > + ::core::marker::Send + 'a,
3664            >> {
3665                #delete_audited_body
3666            }
3667        }
3668    } else {
3669        quote!()
3670    };
3671    // Builds `_audit_entry`; `written` is the soft-delete column's new
3672    // value, recorded in place of the stale `self` field.
3673    let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3674        if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3675            quote! {
3676                let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3677                _audit_entry.set_tracked(#col, #v);
3678            }
3679        } else {
3680            quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3681        }
3682    };
3683    let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3684        .unwrap_or(#root::__serde_json::Value::Null));
3685    let restored_json = quote!(#root::__serde_json::Value::Null);
3686    // `&Pool` update runner for soft_delete / restore: one tx with the
3687    // audit row when audited, a plain UPDATE otherwise.
3688    let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3689        if audited_fields.is_some() {
3690            let entry = make_entry(op_path, Some(written));
3691            quote! {
3692                #entry
3693                #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3694            }
3695        } else {
3696            quote!(#root::sql::update_pool(pool, &_query).await)
3697        }
3698    };
3699    let pool_softdelete_run =
3700        make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3701    let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3702
3703    // `save_pool(&Pool)` — emitted for every model with a PK. The
3704    // audited `save_pool` is built below on the diff path; both share
3705    // `save_partial_methods`, which differ only in how they run `_query`.
3706    let save_ret = quote!(::core::result::Result<u64, #root::sql::ExecError>);
3707    let save_keep = quote!(matches!(_saved, ::core::result::Result::Ok(n) if n > 0));
3708    let save_partial_methods = |pk_ident: &syn::Ident,
3709                                pk_column_lit: &str,
3710                                run: TokenStream2|
3711     -> TokenStream2 {
3712        let assignments = &fields.update_assignments;
3713        // Pushed in step with `update_assignments`: one column each.
3714        let assignment_cols = &fields.upsert_update_columns;
3715        let write = restore_auto_now_unless(
3716            &fields.auto_now_idents,
3717            &save_ret,
3718            &save_keep,
3719            &quote! {
3720                // Build only the wanted assignments, so an unnamed
3721                // `auto_now` field is not stamped.
3722                let mut _filtered: ::std::vec::Vec<#root::core::Assignment> =
3723                    ::std::vec::Vec::with_capacity(_wanted_cols.len());
3724                #(
3725                    if _wanted_cols.contains(#assignment_cols) {
3726                        _filtered.push(#assignments);
3727                    }
3728                )*
3729                if _filtered.is_empty() {
3730                    // All field names valid, but they all map to
3731                    // non-assignable slots (PK column, computed/
3732                    // virtual fields, relations without an
3733                    // assignment). Same no-op as an empty list.
3734                    #root::__tracing::warn!(
3735                        target: "rustango::save_partial",
3736                        model = _schema.name,
3737                        "save_partial: every named field maps to a non-assignable column — no-op"
3738                    );
3739                    return ::core::result::Result::Ok(0);
3740                }
3741                let _query = #root::core::UpdateQuery::new(
3742                    _schema,
3743                    _filtered,
3744                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3745                        #pk_column_lit,
3746                        #root::core::Op::Eq,
3747                        ::core::convert::Into::<#root::core::SqlValue>::into(
3748                            ::core::clone::Clone::clone(&self.#pk_ident)
3749                        ),
3750                    )),
3751                );
3752                #run
3753            },
3754        );
3755        quote! {
3756            /// Save (UPDATE) only the listed Rust-side fields,
3757            /// leaving every other column untouched. Issue #66.
3758            ///
3759            /// `fields` are Rust-side struct field names; the macro
3760            /// resolves each to its SQL column. Unknown field
3761            /// names return [`#root::core::QueryError::UnknownField`]
3762            /// wrapped in `ExecError::Query`. An empty list is a
3763            /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3764            /// On an audited model the audit diff covers only these fields.
3765            ///
3766            /// Use this when:
3767            /// * you only mutated a couple of fields on a wide row
3768            ///   (avoid re-writing every column on every save), or
3769            /// * two writers diverged after their initial read and
3770            ///   you want to preserve the other writer's changes to
3771            ///   columns you didn't touch.
3772            ///
3773            /// Auto-PK models with an unset PK return
3774            /// [`#root::core::QueryError::UnknownField`] with
3775            /// field name `<pk>` — `save_partial` is an
3776            /// UPDATE-only path. Call [`Self::insert`]
3777            /// (or [`Self::save`] which dispatches based on
3778            /// PK state) for the INSERT case.
3779            ///
3780            /// # Errors
3781            /// As [`Self::save`], plus `UnknownField` for
3782            /// unknown / empty / Auto-Unset cases.
3783            pub async fn save_partial(
3784                &mut self,
3785                fields: &[&str],
3786                pool: &#root::sql::Pool,
3787            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3788                if fields.is_empty() {
3789                    #root::__tracing::warn!(
3790                        target: "rustango::save_partial",
3791                        model = <Self as #root::core::Model>::SCHEMA.name,
3792                        "save_partial called with empty field list — no-op"
3793                    );
3794                    return ::core::result::Result::Ok(0);
3795                }
3796                let _schema = <Self as #root::core::Model>::SCHEMA;
3797                // Validate field names against the schema.
3798                let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3799                    ::std::collections::HashSet::with_capacity(fields.len());
3800                for f in fields {
3801                    match _schema.field(f) {
3802                        ::core::option::Option::Some(fs) => {
3803                            _wanted_cols.insert(fs.column);
3804                        }
3805                        ::core::option::Option::None => {
3806                            return ::core::result::Result::Err(
3807                                #root::sql::ExecError::Query(
3808                                    #root::core::QueryError::UnknownField {
3809                                        model: _schema.name,
3810                                        field: (*f).to_owned(),
3811                                    }
3812                                )
3813                            );
3814                        }
3815                    }
3816                }
3817                #write
3818            }
3819
3820            /// Typed-column counterpart of [`Self::save_partial`] —
3821            /// issue #67. `fields` is a tuple of [`Column`]
3822            /// constants whose `Model` matches `Self`; typos and
3823            /// model mismatches surface at *compile time*
3824            /// (`Author::name` inside a `Post::save_partial_typed`
3825            /// call is a type error, no runtime check).
3826            ///
3827            /// ```ignore
3828            /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3829            /// ```
3830            ///
3831            /// Lowers to [`Self::save_partial`] under the hood — the
3832            /// tuple is reduced to a `&[&str]` slice of Rust-side
3833            /// field names and forwarded.
3834            ///
3835            /// [`Column`]: #root::core::Column
3836            ///
3837            /// # Errors
3838            /// As [`Self::save_partial`].
3839            pub async fn save_partial_typed<
3840                L: #root::core::TypedFieldList<Self>,
3841            >(
3842                &mut self,
3843                fields: L,
3844                pool: &#root::sql::Pool,
3845            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3846                let _names = fields.rust_field_names();
3847                let _refs: ::std::vec::Vec<&str> =
3848                    _names.iter().copied().collect();
3849                self.save_partial(&_refs, pool).await
3850            }
3851        }
3852    };
3853    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3854        let pk_column_lit = pk_col.as_str();
3855        if audited_fields.is_some() {
3856            quote!()
3857        } else {
3858            let partial = save_partial_methods(
3859                pk_ident,
3860                pk_column_lit,
3861                quote!(#root::sql::update_pool(pool, &_query).await),
3862            );
3863            quote! {
3864                /// Save this row to its table against either backend.
3865                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3866                /// `UPDATE` keyed on the PK; global scopes do not apply.
3867                /// A PK with no row is `RowNotFound`: use `insert` for a new row.
3868                ///
3869                /// # Errors
3870                /// As [`Self::save`].
3871                pub async fn save(
3872                    &mut self,
3873                    pool: &#root::sql::Pool,
3874                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3875                    self.__rustango_save_exec(pool).await
3876                }
3877
3878                #partial
3879            }
3880        }
3881    } else {
3882        quote!()
3883    };
3884
3885    // Audited `insert_pool` (overrides the placeholder set higher up
3886    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3887    // audited models get insert_pool routing through
3888    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3889    // + auto-PK readback + audit emit). Snapshot-style audit (the
3890    // PendingEntry's `changes` carries post-write field values).
3891    let pool_insert_method = if audited_fields.is_some() {
3892        if let Some(_) = primary_key {
3893            // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
3894            let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
3895            {
3896                fields.returning_cols.clone()
3897            } else {
3898                // Non-Auto-PK: still need RETURNING something for the
3899                // audit helper's contract (it errors on empty
3900                // returning). Return the PK column so the audit row
3901                // can carry the assigned PK back. Some non-Auto PKs
3902                // are server-side-default (e.g. UUIDv4 default), so
3903                // RETURNING is genuinely useful.
3904                primary_key
3905                    .map(|(_, col)| {
3906                        let lit = col.as_str();
3907                        vec![quote!(#lit)]
3908                    })
3909                    .unwrap_or_default()
3910            };
3911            // A failed COMMIT rolls the row back: put the read-back values back too.
3912            let assigned: Vec<syn::Ident> = fields
3913                .auto_field_idents
3914                .iter()
3915                .map(|(ident, _, _)| ident.clone())
3916                .collect();
3917            let body = restore_auto_now_unless(
3918                &assigned,
3919                &quote!(::core::result::Result<(), #root::sql::ExecError>),
3920                &quote!(_saved.is_ok()),
3921                &quote! {
3922                    let mut _query = self.__rustango_insert_query();
3923                    // The audit row needs the PK even when Rust filled it.
3924                    if _query.returning.is_empty() {
3925                        _query.returning = ::std::vec![ #( #returning_cols ),* ];
3926                    }
3927                    #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self, _op| {
3928                        _m.__rustango_audit_entry(_op)
3929                    })
3930                    .await
3931                },
3932            );
3933            quote! {
3934                /// Insert this row on any backend, with its audit row in the
3935                /// same transaction.
3936                ///
3937                /// Snapshot-style audit (post-write field values).
3938                ///
3939                /// # Errors
3940                /// As [`Self::insert`].
3941                pub async fn insert(
3942                    &mut self,
3943                    pool: &#root::sql::Pool,
3944                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3945                    #body
3946                }
3947            }
3948        } else {
3949            quote!()
3950        }
3951    } else {
3952        // Keep the non-audited pool_insert_method we built earlier.
3953        pool_insert_method
3954    };
3955
3956    // Update audited save_pool: now that insert_pool is wired for
3957    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3958    // insert_pool. Non-audited save_pool already does this.
3959    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3960    // Replaces the snapshot-only emission with a per-backend transaction
3961    // body that:
3962    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3963    //     column), capturing BEFORE values
3964    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3965    //  3. builds AFTER pairs from &self
3966    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3967    //     AuditOp::Update + diff_changes(...) on the same tx connection
3968    //  5. commits
3969    //
3970    // Per-backend arms inline the SQL string + placeholder shape, then
3971    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3972    // is polymorphic over Row type — the same tokens work against
3973    // PgRow and MySqlRow as long as the field's Rust type implements
3974    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3975    // primitives + chrono/uuid/serde_json::Value all do).
3976    let pool_save_method = if let Some(tracked) = audited_fields {
3977        if let Some((pk_ident, pk_col)) = primary_key {
3978            let pk_column_lit = pk_col.as_str();
3979            let partial = save_partial_methods(
3980                pk_ident,
3981                pk_column_lit,
3982                quote! {
3983                    self.__rustango_save_with_diff(
3984                        pool,
3985                        &_query,
3986                        ::core::option::Option::Some(&_wanted_cols),
3987                    )
3988                    .await
3989                },
3990            );
3991            // Per-tracked-column BEFORE-pair token list. Each entry
3992            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3993            // The Row alias resolves to PgRow / MySqlRow per call site,
3994            // so the same template generates both the PG and MySQL bodies.
3995            let mk_before_pairs =
3996                |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
3997                    tracked
3998                        .iter()
3999                        .map(|c| {
4000                            let getter = getter(c);
4001                            let column_lit = c.column.as_str();
4002                            let value_ty = &c.value_ty;
4003                            quote! {
4004                                (
4005                                    #column_lit,
4006                                    match #getter::<#value_ty>(
4007                                        _audit_before_row, #column_lit,
4008                                    ) {
4009                                        ::core::result::Result::Ok(v) => {
4010                                            #root::core::__json_value(&v)
4011                                        }
4012                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
4013                                    },
4014                                )
4015                            }
4016                        })
4017                        .collect()
4018                };
4019            let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
4020            let before_pairs_my = mk_before_pairs(&|c| {
4021                if c.uuid_column {
4022                    quote!(#root::sql::try_get_flat_my)
4023                } else {
4024                    quote!(#root::sql::try_get_returning_my)
4025                }
4026            });
4027            let before_pairs_sqlite =
4028                mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
4029            let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
4030            let pk_value_for_bind = if fields.pk_is_auto {
4031                quote!(self.#pk_ident.get().copied().unwrap_or_default())
4032            } else {
4033                quote!(::core::clone::Clone::clone(&self.#pk_ident))
4034            };
4035            let assignments = &fields.update_assignments;
4036            let unset_dispatch = if fields.has_auto {
4037                quote! {
4038                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
4039                        return self.insert(pool).await.map(|()| 1u64);
4040                    }
4041                }
4042            } else {
4043                quote!()
4044            };
4045            let write = restore_auto_now_unless(
4046                &fields.auto_now_idents,
4047                &save_ret,
4048                &save_keep,
4049                &quote! {
4050                    let _query = #root::core::UpdateQuery::new(
4051                        <Self as #root::core::Model>::SCHEMA,
4052                        ::std::vec![ #( #assignments ),* ],
4053                        #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4054                            #pk_column_lit,
4055                            #root::core::Op::Eq,
4056                            ::core::convert::Into::<#root::core::SqlValue>::into(
4057                                ::core::clone::Clone::clone(&self.#pk_ident)
4058                            ),
4059                        )),
4060                    );
4061                    self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
4062                        .await
4063                        .and_then(#root::sql::__macro_internals::row_saved)
4064                },
4065            );
4066            quote! {
4067                /// Save this row on any backend, with a diff audit row
4068                /// (before and after) in the same transaction. An `Unset`
4069                /// `Auto` PK inserts instead; a PK with no row is `RowNotFound`.
4070                ///
4071                /// The audit row's `changes` JSON contains one
4072                /// `{ "field": { "before": …, "after": … } }` entry
4073                /// per tracked column whose value actually changed
4074                /// Keyed on the PK; global scopes do not apply.
4075                ///
4076                /// # Errors
4077                /// As [`Self::save`].
4078                pub async fn save(
4079                    &mut self,
4080                    pool: &#root::sql::Pool,
4081                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4082                    #unset_dispatch
4083                    #write
4084                }
4085
4086                /// Run `query` and write its diff entry, limited to `only` columns.
4087                #[doc(hidden)]
4088                async fn __rustango_save_with_diff(
4089                    &self,
4090                    pool: &#root::sql::Pool,
4091                    query: &#root::core::UpdateQuery,
4092                    only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
4093                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4094                    let mut _after_pairs = self.__rustango_audit_pairs();
4095                    if let ::core::option::Option::Some(only) = only {
4096                        _after_pairs.retain(|(col, _)| only.contains(col));
4097                    }
4098                    let _before_query = #root::audit::before_image_query(
4099                        <Self as #root::core::Model>::SCHEMA,
4100                        #pk_column_lit,
4101                        ::core::convert::Into::<#root::core::SqlValue>::into(
4102                            #pk_value_for_bind,
4103                        ),
4104                        &[ #( #tracked_cols ),* ],
4105                    );
4106                    #root::audit::save_one_with_diff(
4107                        pool,
4108                        query,
4109                        &_before_query,
4110                        <Self as #root::core::Model>::SCHEMA.table,
4111                        self.__rustango_audit_pk(),
4112                        _after_pairs,
4113                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4114                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4115                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4116                    ).await
4117                }
4118
4119                #partial
4120            }
4121        } else {
4122            quote!()
4123        }
4124    } else {
4125        pool_save_method
4126    };
4127
4128    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4129    // body shapes:
4130    // - non-audited: simple dispatch through `sql::delete_pool`
4131    // - audited: routes through `audit::delete_one_with_audit`,
4132    //   which opens a per-backend transaction wrapping DELETE +
4133    //   audit emit so the data write and audit row commit atomically.
4134    let pool_delete_method = {
4135        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4136        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4137        if let Some(pk_ident) = pk_ident_for_pool {
4138            if audited_fields.is_some() {
4139                quote! {
4140                    /// Delete this row on any backend, with its audit row in
4141                    /// the same transaction.
4142                    /// Keyed on the PK; global scopes do not apply.
4143                    ///
4144                    /// # Errors
4145                    /// As [`Self::delete`].
4146                    pub async fn delete(
4147                        &self,
4148                        pool: &#root::sql::Pool,
4149                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4150                        let _query = #root::core::DeleteQuery::by_pk(
4151                            <Self as #root::core::Model>::SCHEMA,
4152                            #pk_column_lit,
4153                            ::core::convert::Into::<#root::core::SqlValue>::into(
4154                                ::core::clone::Clone::clone(&self.#pk_ident)
4155                            ),
4156                        );
4157                        let _audit_entry =
4158                            self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4159                        #root::audit::delete_one_with_audit(
4160                            pool, &_query, &_audit_entry,
4161                        ).await
4162                    }
4163                }
4164            } else {
4165                quote! {
4166                    /// Delete the row identified by this instance's primary key
4167                    /// against either backend. Equivalent to [`Self::delete`] but
4168                    /// takes [`#root::sql::Pool`] and dispatches per backend.
4169                    /// Global scopes do not apply.
4170                    ///
4171                    /// # Errors
4172                    /// As [`Self::delete`].
4173                    pub async fn delete(
4174                        &self,
4175                        pool: &#root::sql::Pool,
4176                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4177                        self.__rustango_delete_exec(pool).await
4178                    }
4179                }
4180            }
4181        } else {
4182            quote!()
4183        }
4184    };
4185
4186    // Bulk writers on audited models write one audit row per affected
4187    // row, in the write's transaction (#1747).
4188    let (bulk_delete_run, destroy_run, bulk_update_run, truncate_run, truncate_cascade_run) =
4189        if audited_fields.is_some() {
4190            (
4191                quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4192                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4193            })
4194            .await),
4195                quote!(#root::audit::delete_each_with_audit::<Self>(pool, &_queries, |_r: &Self| {
4196                _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4197            })
4198            .await),
4199                quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4200                _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4201            })
4202            .await),
4203                quote!(#root::audit::truncate_with_audit(
4204                pool,
4205                <Self as #root::core::Model>::SCHEMA.table,
4206                #root::sql::TruncateMode::Plain,
4207            )
4208            .await),
4209                quote!(#root::audit::truncate_with_audit(
4210                pool,
4211                <Self as #root::core::Model>::SCHEMA.table,
4212                #root::sql::TruncateMode::Cascade,
4213            )
4214            .await
4215            .map(drop)),
4216            )
4217        } else {
4218            (
4219                quote!(#root::sql::delete_pool(pool, &_query).await),
4220                quote!(#root::sql::__rustango_delete_all_or_none(pool, _queries).await),
4221                quote!(#root::sql::update_pool(pool, &_query).await),
4222                quote!(#root::sql::truncate(pool, <Self as #root::core::Model>::SCHEMA).await),
4223                quote!(#root::sql::truncate_cascade(pool, <Self as #root::core::Model>::SCHEMA).await),
4224            )
4225        };
4226
4227    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4228    // matching this instance's PK and overwrite the in-memory state
4229    // with the freshly-fetched columns.
4230    // Issue #825. Only emitted when the model declares a PK; non-PK
4231    // models can't address a specific row.
4232    //
4233    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4234    // every field from `self`; resets the PK to `Auto::Unset` when
4235    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4236    // a fresh autoincrement value. Non-Auto PKs preserve the source
4237    // PK — the caller must overwrite before insert. Pure-Rust, no
4238    // I/O, no dialect surface.
4239    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4240        let other_field_clones: Vec<TokenStream2> = fields
4241            .column_entries
4242            .iter()
4243            .filter(|c| &c.ident != pk_ident)
4244            .map(|c| {
4245                let ident = &c.ident;
4246                quote! {
4247                    #ident: ::core::clone::Clone::clone(&self.#ident)
4248                }
4249            })
4250            .collect();
4251        let pk_clone_token = if fields.pk_is_auto {
4252            quote! { #pk_ident: #root::sql::Auto::Unset }
4253        } else {
4254            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4255        };
4256        let replicate_doc = if fields.pk_is_auto {
4257            quote! {
4258                /// Eloquent-style `replicate()` — return a clone of this
4259                /// row with the primary key reset to [`Auto::Unset`] so
4260                /// the copy is ready for `insert_pool` / `save_pool` to
4261                /// allocate a fresh autoincrement value. Every other
4262                /// field is `Clone`d verbatim. Issue #825.
4263                ///
4264                /// `auto_now_add` / `auto_now` timestamp fields are
4265                /// **not** reset (Eloquent's `replicate` doesn't reset
4266                /// them either) — pass them through the normal insert
4267                /// path if you want fresh values, or assign them
4268                /// explicitly after the call.
4269            }
4270        } else {
4271            quote! {
4272                /// Eloquent-style `replicate()` — clone this row
4273                /// verbatim. Because the primary key is **not** an
4274                /// `Auto<T>`, the clone keeps the source PK; the
4275                /// caller must overwrite `copy.<pk>` before inserting
4276                /// to avoid a unique-key violation. Issue #825.
4277            }
4278        };
4279        // 2026-06-07 — field-name / shortcut collision guard.
4280        //
4281        // The macro emits both `pub const <field>: <field>_col = ...`
4282        // (per-field typed-column const, used by the typed-builder
4283        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4284        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4285        // model has a field named e.g. `count`, both items would land
4286        // in the same inherent impl with the same name, and the
4287        // compiler rejects the derive with "duplicate definitions".
4288        //
4289        // Drop the conflicting shortcut for that model. Callers can
4290        // still reach the same behavior via
4291        // `QuerySet::<Model>::default().count(&pool)`.
4292        let column_names: ::std::collections::HashSet<String> = fields
4293            .column_entries
4294            .iter()
4295            .map(|c| ident_name(&c.ident))
4296            .collect();
4297        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4298            if column_names.contains(name) {
4299                quote! {}
4300            } else {
4301                tokens
4302            }
4303        };
4304        let count_method = emit_if_no_field_collision(
4305            "count",
4306            quote! {
4307                /// Count rows of this model — `SELECT COUNT(*) FROM
4308                /// <table>`. Eloquent `Model::count()` parity.
4309                ///
4310                /// Skipped on models that already declare a field named
4311                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4312                /// in that case.
4313                ///
4314                /// # Errors
4315                /// As [`CounterPool::count`].
4316                ///
4317                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4318                pub async fn count(
4319                    pool: &#root::sql::Pool,
4320                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4321                    use #root::sql::CounterPool as _;
4322                    #root::query::QuerySet::<Self>::default()
4323                        .count(pool)
4324                        .await
4325                }
4326            },
4327        );
4328        let value_method = emit_if_no_field_collision(
4329            "value",
4330            quote! {
4331                /// Pluck a single scalar from the first row.
4332                /// Eloquent `Model::query()->value($col)` parity.
4333                ///
4334                /// Skipped on models that already declare a field named
4335                /// `value`. Drop into
4336                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4337                ///
4338                /// # Errors
4339                /// As `ValuesFlatQuerySet::first`.
4340                pub async fn value<U>(
4341                    col: &str,
4342                    pool: &#root::sql::Pool,
4343                ) -> ::core::result::Result<
4344                    ::core::option::Option<U>,
4345                    #root::sql::ExecError,
4346                >
4347                where
4348                    U: #root::sql::FlatScalar,
4349                {
4350                    let _col_static: &'static str = Self::__resolve_col(col)?;
4351                    #root::query::QuerySet::<Self>::default()
4352                        .values_list_flat(_col_static)
4353                        .first::<U>(pool)
4354                        .await
4355                }
4356            },
4357        );
4358        let sum_method = emit_if_no_field_collision(
4359            "sum",
4360            quote! {
4361                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4362                /// Skipped on models that already declare a field named
4363                /// `sum`.
4364                ///
4365                /// # Errors
4366                /// As [`#root::sql::fetch_aggregate_pool`].
4367                pub async fn sum<U>(
4368                    col: &str,
4369                    pool: &#root::sql::Pool,
4370                ) -> ::core::result::Result<
4371                    ::core::option::Option<U>,
4372                    #root::sql::ExecError,
4373                >
4374                where
4375                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4376                        + #root::sql::MaybeMyFromRow
4377                        + #root::sql::MaybeSqliteFromRow
4378                        + ::core::marker::Send
4379                        + ::core::marker::Unpin,
4380                {
4381                    #root::query::QuerySet::<Self>::default()
4382                        .sum::<U>(col, pool)
4383                        .await
4384                }
4385            },
4386        );
4387        let avg_method = emit_if_no_field_collision(
4388            "avg",
4389            quote! {
4390                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4391                /// models that already declare a field named `avg`.
4392                ///
4393                /// # Errors
4394                /// As [`#root::sql::fetch_aggregate_pool`].
4395                pub async fn avg<U>(
4396                    col: &str,
4397                    pool: &#root::sql::Pool,
4398                ) -> ::core::result::Result<
4399                    ::core::option::Option<U>,
4400                    #root::sql::ExecError,
4401                >
4402                where
4403                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4404                        + #root::sql::MaybeMyFromRow
4405                        + #root::sql::MaybeSqliteFromRow
4406                        + ::core::marker::Send
4407                        + ::core::marker::Unpin,
4408                {
4409                    #root::query::QuerySet::<Self>::default()
4410                        .avg::<U>(col, pool)
4411                        .await
4412                }
4413            },
4414        );
4415        let min_method = emit_if_no_field_collision(
4416            "min",
4417            quote! {
4418                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4419                /// models that already declare a field named `min`.
4420                ///
4421                /// # Errors
4422                /// As [`#root::sql::fetch_aggregate_pool`].
4423                pub async fn min<U>(
4424                    col: &str,
4425                    pool: &#root::sql::Pool,
4426                ) -> ::core::result::Result<
4427                    ::core::option::Option<U>,
4428                    #root::sql::ExecError,
4429                >
4430                where
4431                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4432                        + #root::sql::MaybeMyFromRow
4433                        + #root::sql::MaybeSqliteFromRow
4434                        + ::core::marker::Send
4435                        + ::core::marker::Unpin,
4436                {
4437                    #root::query::QuerySet::<Self>::default()
4438                        .min::<U>(col, pool)
4439                        .await
4440                }
4441            },
4442        );
4443        let max_method = emit_if_no_field_collision(
4444            "max",
4445            quote! {
4446                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4447                /// models that already declare a field named `max`.
4448                ///
4449                /// # Errors
4450                /// As [`#root::sql::fetch_aggregate_pool`].
4451                pub async fn max<U>(
4452                    col: &str,
4453                    pool: &#root::sql::Pool,
4454                ) -> ::core::result::Result<
4455                    ::core::option::Option<U>,
4456                    #root::sql::ExecError,
4457                >
4458                where
4459                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4460                        + #root::sql::MaybeMyFromRow
4461                        + #root::sql::MaybeSqliteFromRow
4462                        + ::core::marker::Send
4463                        + ::core::marker::Unpin,
4464                {
4465                    #root::query::QuerySet::<Self>::default()
4466                        .max::<U>(col, pool)
4467                        .await
4468                }
4469            },
4470        );
4471        let first_method = emit_if_no_field_collision(
4472            "first",
4473            quote! {
4474                /// First row of this model. Eloquent `Model::first()`.
4475                /// Skipped on models that already declare a field named
4476                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4477                ///
4478                /// # Errors
4479                /// As `QuerySet::first`.
4480                pub async fn first(
4481                    pool: &#root::sql::Pool,
4482                ) -> ::core::result::Result<
4483                    ::core::option::Option<Self>,
4484                    #root::sql::ExecError,
4485                > {
4486                    #root::query::QuerySet::<Self>::default()
4487                        .first(pool)
4488                        .await
4489                }
4490            },
4491        );
4492        let last_method = emit_if_no_field_collision(
4493            "last",
4494            quote! {
4495                /// Last row of this model by primary-key DESC.
4496                /// Eloquent `Model::query()->latest('id')->first()`
4497                /// parity — fetches the highest-PK row without
4498                /// requiring the caller to spell the PK column.
4499                /// Returns `None` on an empty table.
4500                ///
4501                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4502                /// Skipped on models that already declare a field
4503                /// named `last`.
4504                ///
4505                /// # Errors
4506                /// As `QuerySet::last`.
4507                pub async fn last(
4508                    pool: &#root::sql::Pool,
4509                ) -> ::core::result::Result<
4510                    ::core::option::Option<Self>,
4511                    #root::sql::ExecError,
4512                > {
4513                    #root::query::QuerySet::<Self>::default()
4514                        .last(pool)
4515                        .await
4516                }
4517            },
4518        );
4519        quote! {
4520            /// Re-SELECT this row by its primary key and overwrite
4521            /// every in-memory field with the freshly-fetched value.
4522            /// Issue #825.
4523            ///
4524            /// Use this when the row may have been modified by another
4525            /// process / connection / job since you read it — e.g. after
4526            /// a queued task callback, or to re-sync stale UI state
4527            /// before re-saving.
4528            ///
4529            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4530            /// when the primary key no longer matches any row (e.g.
4531            /// the row was deleted concurrently).
4532            ///
4533            /// # Errors
4534            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4535            /// the PK no longer exists.
4536            ///
4537            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4538            pub async fn refresh_from_db(
4539                &mut self,
4540                pool: &#root::sql::Pool,
4541            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4542                match self.fresh(pool).await? {
4543                    ::core::option::Option::Some(_fresh) => {
4544                        *self = _fresh;
4545                        ::core::result::Result::Ok(())
4546                    }
4547                    ::core::option::Option::None => ::core::result::Result::Err(
4548                        #root::sql::ExecError::Driver(
4549                            #root::sql::sqlx::Error::RowNotFound,
4550                        ),
4551                    ),
4552                }
4553            }
4554
4555            /// Atomically increment the integer column `col` by
4556            /// `by` for this row. Equivalent to
4557            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4558            ///
4559            /// **Doesn't mutate `self`** — the in-memory copy is now
4560            /// stale; call [`Self::refresh_from_db`] /
4561            /// [`Self::fresh`] to re-sync. Returns the rows-
4562            /// affected count (0 when the PK doesn't match any row,
4563            /// 1 on success).
4564            ///
4565            /// `col` is the Rust field name as a string; unknown
4566            /// fields surface as `UnknownField` at runtime. Negative
4567            /// `by` values atomically decrement (see also
4568            /// [`Self::decrement`]).
4569            ///
4570            /// # Errors
4571            /// As [`UpdaterPool::execute_pool`].
4572            ///
4573            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4574            pub async fn increment(
4575                &self,
4576                col: &str,
4577                by: i64,
4578                pool: &#root::sql::Pool,
4579            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4580                Self::__increment_one(self, col, by, pool).await
4581            }
4582
4583            /// Sibling of [`Self::increment`] — atomically
4584            /// decrement this row's `col` by `by`. Eloquent
4585            /// `$model->decrement($col, $by)` parity. Equivalent to
4586            /// `self.increment(col, -by, &pool)`; the separate name
4587            /// keeps call sites readable.
4588            ///
4589            /// # Errors
4590            /// As [`Self::increment`].
4591            pub async fn decrement(
4592                &self,
4593                col: &str,
4594                by: i64,
4595                pool: &#root::sql::Pool,
4596            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4597                Self::__increment_one(self, col, -by, pool).await
4598            }
4599
4600            /// Bulk-increment: add `by` to `col` on every row of the
4601            /// table. Eloquent `Model::query()->increment($col, $by)`
4602            /// parity. Use for counters, score adjustments, view
4603            /// rollups.
4604            ///
4605            /// # Errors
4606            /// As [`UpdaterPool::execute_pool`].
4607            ///
4608            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4609            pub async fn increment_each(
4610                col: &str,
4611                by: i64,
4612                pool: &#root::sql::Pool,
4613            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4614                Self::__increment_all(col, by, pool).await
4615            }
4616
4617            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4618            ///
4619            /// # Errors
4620            /// As [`Self::increment_each`].
4621            pub async fn decrement_each(
4622                col: &str,
4623                by: i64,
4624                pool: &#root::sql::Pool,
4625            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4626                Self::__increment_all(col, -by, pool).await
4627            }
4628
4629            /// Internal: forward to
4630            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4631            /// One-line wrapper kept as a per-Model method so the
4632            /// macro's emitted `increment` / `decrement` instance
4633            /// calls don't have to thread `Self` through manually.
4634            #[doc(hidden)]
4635            pub async fn __increment_one(
4636                this: &Self,
4637                col: &str,
4638                by: i64,
4639                pool: &#root::sql::Pool,
4640            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4641                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4642                    ::core::clone::Clone::clone(&this.#pk_ident),
4643                );
4644                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4645                    ::core::stringify!(#pk_ident),
4646                    _pk_val,
4647                    col,
4648                    by,
4649                    pool,
4650                )
4651                .await
4652            }
4653
4654            /// Internal: run
4655            /// [`#root::sql::model_shortcuts::increment_all_query`].
4656            #[doc(hidden)]
4657            pub async fn __increment_all(
4658                col: &str,
4659                by: i64,
4660                pool: &#root::sql::Pool,
4661            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4662                let _query =
4663                    #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4664                #bulk_update_run
4665            }
4666
4667            /// Internal: forward to
4668            /// [`#root::sql::model_shortcuts::resolve_col`].
4669            #[doc(hidden)]
4670            pub fn __resolve_col(
4671                col: &str,
4672            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4673                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4674            }
4675
4676            /// Internal: forward to
4677            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4678            #[doc(hidden)]
4679            #[must_use]
4680            pub fn __add_signed_expr(
4681                col_static: &'static str,
4682                signed_by: i64,
4683            ) -> #root::core::Expr {
4684                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4685            }
4686
4687            /// Re-SELECT this row by its primary key and return a
4688            /// **new** instance with the freshly-fetched fields.
4689            /// Eloquent `Model::fresh()` parity — non-mutating
4690            /// counterpart of [`Self::refresh_from_db`].
4691            ///
4692            /// Returns `Ok(None)` when the row was deleted
4693            /// concurrently — vs [`Self::refresh_from_db`]
4694            /// which surfaces that as `RowNotFound` because
4695            /// in-place mutation has nothing to write to.
4696            ///
4697            /// Useful when you want to compare the in-memory
4698            /// instance against the persisted state (audit-style
4699            /// diffs, conflict detection) without mutating the
4700            /// reference you already hold.
4701            ///
4702            /// # Errors
4703            /// As [`FetcherPool::fetch`].
4704            ///
4705            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4706            pub async fn fresh(
4707                &self,
4708                pool: &#root::sql::Pool,
4709            ) -> ::core::result::Result<
4710                ::core::option::Option<Self>,
4711                #root::sql::ExecError,
4712            > {
4713                use #root::sql::FetcherPool as _;
4714                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4715                    ::core::clone::Clone::clone(&self.#pk_ident),
4716                );
4717                // By key alone, as `save()` and `delete()` write it.
4718                let _rows: ::std::vec::Vec<Self> =
4719                    #root::query::QuerySet::<Self>::default()
4720                        .without_global_scopes()
4721                        .filter(::core::stringify!(#pk_ident), _pk_val)
4722                        .limit(1)
4723                        .fetch(pool)
4724                        .await?;
4725                ::core::result::Result::Ok(_rows.into_iter().next())
4726            }
4727
4728            #replicate_doc
4729            #[must_use]
4730            pub fn replicate(&self) -> Self {
4731                Self {
4732                    #pk_clone_token,
4733                    #( #other_field_clones, )*
4734                }
4735            }
4736
4737            #first_method
4738
4739            #last_method
4740
4741            /// Throwing counterpart of [`Self::first`] —
4742            /// errors with `RowNotFound` when the table is empty.
4743            /// Eloquent `Model::firstOrFail()` parity.
4744            ///
4745            /// # Errors
4746            /// As [`Self::first`]; additionally
4747            /// [`sqlx::Error::RowNotFound`] on empty tables.
4748            ///
4749            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4750            pub async fn first_or_fail(
4751                pool: &#root::sql::Pool,
4752            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4753                // Route through the queryset rather than `Self::first`,
4754                // which is suppressed on models with a field named
4755                // `first` (field/shortcut collision guard).
4756                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4757                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4758                    ::core::option::Option::None => ::core::result::Result::Err(
4759                        #root::sql::ExecError::Driver(
4760                            #root::sql::sqlx::Error::RowNotFound,
4761                        ),
4762                    ),
4763                }
4764            }
4765
4766            /// Single-column projection — `SELECT <col> FROM
4767            /// <table>`. Returns `Vec<U>` where each element is the
4768            /// decoded value of the column. Eloquent
4769            /// `Model::pluck($column)` parity.
4770            ///
4771            /// Thin wrapper over `QuerySet::<Self>::default()
4772            /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4773            /// `FlatScalar`; use `Option<_>` for a nullable column.
4774            ///
4775            /// # Errors
4776            /// As `ValuesFlatQuerySet::fetch`.
4777            pub async fn pluck<U>(
4778                col: &'static str,
4779                pool: &#root::sql::Pool,
4780            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4781            where
4782                U: #root::sql::FlatScalar,
4783            {
4784                #root::query::QuerySet::<Self>::default()
4785                    .values_list_flat(col)
4786                    .fetch::<U>(pool)
4787                    .await
4788            }
4789
4790            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4791            /// stream every row of this model in batches of `n`,
4792            /// invoking the callback once per batch. Stable PK-ASC
4793            /// ordering so the LIMIT/OFFSET pagination is
4794            /// deterministic across drivers.
4795            ///
4796            /// The callback is async — it can do further DB work
4797            /// (writes, related-row lookups, queue dispatch) per
4798            /// batch. Return `Err(...)` from the callback to abort
4799            /// the iteration early; the error bubbles up.
4800            ///
4801            /// **When to use**: bulk processing flows that can't
4802            /// fit the whole table in memory — sending newsletters,
4803            /// running data migrations, computing summary
4804            /// statistics. For small / known-bounded tables, plain
4805            /// `Self::all(&pool)` is simpler.
4806            ///
4807            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4808            /// large tables. For multi-million-row scans prefer
4809            /// keyset-by-PK (the standard "WHERE id > last_seen"
4810            /// shape) over `chunk(...)`.
4811            ///
4812            /// Skipped on models without a primary key — chunking
4813            /// needs a stable order to avoid skipping / repeating
4814            /// rows across batches.
4815            pub async fn chunk<F, Fut>(
4816                n: i64,
4817                pool: &#root::sql::Pool,
4818                mut cb: F,
4819            ) -> ::core::result::Result<(), #root::sql::ExecError>
4820            where
4821                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4822                Fut: ::core::future::Future<
4823                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4824                >,
4825            {
4826                use #root::sql::FetcherPool as _;
4827                // The field name: `filter` / `order_by` take fields, not columns.
4828                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4829                    ::core::option::Option::Some(f) => f.name,
4830                    ::core::option::Option::None => {
4831                        return ::core::result::Result::Ok(());
4832                    }
4833                };
4834                let mut offset: i64 = 0;
4835                loop {
4836                    let rows: ::std::vec::Vec<Self> =
4837                        #root::query::QuerySet::<Self>::default()
4838                            .order_by(&[(pk_col, false)])
4839                            .limit(n)
4840                            .offset(offset)
4841                            .fetch(pool)
4842                            .await?;
4843                    if rows.is_empty() {
4844                        return ::core::result::Result::Ok(());
4845                    }
4846                    let len = rows.len() as i64;
4847                    cb(rows).await?;
4848                    if len < n {
4849                        return ::core::result::Result::Ok(());
4850                    }
4851                    offset += n;
4852                }
4853            }
4854
4855            /// Eloquent `Model::chunkById($n, fn (...))` — same
4856            /// per-batch callback shape as [`Self::chunk`], but uses
4857            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4858            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4859            /// — the right choice for multi-million-row sweeps.
4860            ///
4861            /// The keyset compares the primary key in its own type,
4862            /// so any orderable PK works (`i32`, `i64`, UUID, string).
4863            ///
4864            /// Callback errors abort iteration; the error bubbles up
4865            /// unchanged. Empty table → callback invoked zero times.
4866            pub async fn chunk_by_id<F, Fut>(
4867                n: i64,
4868                pool: &#root::sql::Pool,
4869                mut cb: F,
4870            ) -> ::core::result::Result<(), #root::sql::ExecError>
4871            where
4872                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4873                Fut: ::core::future::Future<
4874                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4875                >,
4876            {
4877                use #root::sql::FetcherPool as _;
4878                // The field name: `filter` / `order_by` take fields, not columns.
4879                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4880                    ::core::option::Option::Some(f) => f.name,
4881                    ::core::option::Option::None => {
4882                        return ::core::result::Result::Ok(());
4883                    }
4884                };
4885                // The largest PK seen, in the PK's own type; none before
4886                // the first batch.
4887                let key = ::std::format!("{}__gt", pk_col);
4888                let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
4889                    ::core::option::Option::None;
4890                loop {
4891                    let mut qs = #root::query::QuerySet::<Self>::default();
4892                    if let ::core::option::Option::Some(v) = last_seen.take() {
4893                        qs = qs.filter(key.as_str(), v);
4894                    }
4895                    let rows: ::std::vec::Vec<Self> = qs
4896                        .order_by(&[(pk_col, false)])
4897                        .limit(n)
4898                        .fetch(pool)
4899                        .await?;
4900                    let len = rows.len() as i64;
4901                    // Capture the last row's PK BEFORE moving rows
4902                    // into the callback.
4903                    let ::core::option::Option::Some(max_pk) =
4904                        rows.last().map(|r| r.__rustango_pk_value())
4905                    else {
4906                        return ::core::result::Result::Ok(());
4907                    };
4908                    cb(rows).await?;
4909                    if len < n {
4910                        return ::core::result::Result::Ok(());
4911                    }
4912                    last_seen = ::core::option::Option::Some(max_pk);
4913                }
4914            }
4915
4916            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4917            /// per-row callback companion to [`Self::chunk`].
4918            /// Streams every row in keyset-paginated batches of
4919            /// `batch` size, calling `cb` once per row.
4920            ///
4921            /// Inherits the keyset-paginated scan of
4922            /// [`Self::chunk_by_id`] (O(N) total).
4923            ///
4924            /// ```ignore
4925            /// Post::each(500, &pool, |p| async move {
4926            ///     reindex(p).await?;
4927            ///     Ok(())
4928            /// }).await?;
4929            /// ```
4930            ///
4931            /// Return `Err(...)` from the callback to abort
4932            /// iteration; the error bubbles up unchanged.
4933            pub async fn each<F, Fut>(
4934                batch: i64,
4935                pool: &#root::sql::Pool,
4936                mut cb: F,
4937            ) -> ::core::result::Result<(), #root::sql::ExecError>
4938            where
4939                F: ::core::ops::FnMut(Self) -> Fut,
4940                Fut: ::core::future::Future<
4941                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4942                >,
4943            {
4944                use #root::sql::FetcherPool as _;
4945                // The field name: `filter` / `order_by` take fields, not columns.
4946                let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4947                    ::core::option::Option::Some(f) => f.name,
4948                    ::core::option::Option::None => {
4949                        return ::core::result::Result::Ok(());
4950                    }
4951                };
4952                let key = ::std::format!("{}__gt", pk_col);
4953                let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
4954                    ::core::option::Option::None;
4955                loop {
4956                    let mut qs = #root::query::QuerySet::<Self>::default();
4957                    if let ::core::option::Option::Some(v) = last_seen.take() {
4958                        qs = qs.filter(key.as_str(), v);
4959                    }
4960                    let rows: ::std::vec::Vec<Self> = qs
4961                        .order_by(&[(pk_col, false)])
4962                        .limit(batch)
4963                        .fetch(pool)
4964                        .await?;
4965                    let len = rows.len() as i64;
4966                    let ::core::option::Option::Some(max_pk) =
4967                        rows.last().map(|r| r.__rustango_pk_value())
4968                    else {
4969                        return ::core::result::Result::Ok(());
4970                    };
4971                    for row in rows {
4972                        cb(row).await?;
4973                    }
4974                    if len < batch {
4975                        return ::core::result::Result::Ok(());
4976                    }
4977                    last_seen = ::core::option::Option::Some(max_pk);
4978                }
4979            }
4980
4981            /// Delete every row of this model with one `DELETE FROM
4982            /// <table>` on every backend; returns the rows deleted. Each
4983            /// FK's `ON DELETE` applies, so a `RESTRICT` / `NO ACTION`
4984            /// row still pointing here refuses it; see
4985            /// [`truncate_cascade`](Self::truncate_cascade).
4986            ///
4987            /// **Use only in tests / fixture-reset flows.** No per-row
4988            /// `pre_delete` / `post_delete` signals fire. An audited model
4989            /// gets one bulk audit entry, not one per row. Global scopes
4990            /// do not apply: every row goes, generic M2M links too.
4991            ///
4992            /// # Errors
4993            /// As [`truncate`].
4994            ///
4995            /// [`truncate`]: rustango::sql::truncate
4996            pub async fn truncate(
4997                pool: &#root::sql::Pool,
4998            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4999                #truncate_run
5000            }
5001
5002            /// [`truncate`](Self::truncate), also emptying every table
5003            /// that references this one, transitively and whatever its
5004            /// `ON DELETE`. The same on every backend.
5005            ///
5006            /// # Errors
5007            /// As [`truncate_cascade`].
5008            ///
5009            /// [`truncate_cascade`]: rustango::sql::truncate_cascade
5010            pub async fn truncate_cascade(
5011                pool: &#root::sql::Pool,
5012            ) -> ::core::result::Result<(), #root::sql::ExecError> {
5013                #truncate_cascade_run
5014            }
5015
5016            /// Bulk-delete every row whose primary key is in
5017            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
5018            /// Returns the affected row count.
5019            ///
5020            /// Eloquent `Model::destroy([1, 2, 3])` parity.
5021            /// Empty `pks` is a no-op (returns 0). All or none: past
5022            /// the bind cap the batches share one transaction.
5023            ///
5024            /// Accepts any iterable whose elements are
5025            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
5026            /// `[i64; N]`, etc.
5027            ///
5028            /// # Errors
5029            /// As `delete_pool`.
5030            pub async fn destroy<V>(
5031                pks: impl ::core::iter::IntoIterator<Item = V>,
5032                pool: &#root::sql::Pool,
5033            ) -> ::core::result::Result<u64, #root::sql::ExecError>
5034            where
5035                V: ::core::convert::Into<#root::core::SqlValue>,
5036            {
5037                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5038                    pks.into_iter().map(::core::convert::Into::into).collect();
5039                if _values.is_empty() {
5040                    return ::core::result::Result::Ok(0);
5041                }
5042                let _pk = <Self as #root::core::Model>::SCHEMA
5043                    .primary_key()
5044                    .ok_or_else(|| {
5045                        #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
5046                    })?;
5047                // One statement per `IN` list the dialect can bind, all or none.
5048                let mut _queries = ::std::vec::Vec::new();
5049                for _keys in #root::sql::__rustango_in_batches::<Self>(pool, _values)? {
5050                    _queries.push(
5051                        #root::query::QuerySet::<Self>::default()
5052                            .filter_op(
5053                                _pk.name,
5054                                #root::core::Op::In,
5055                                #root::core::SqlValue::List(_keys),
5056                            )
5057                            .compile_delete()?,
5058                    );
5059                }
5060                #destroy_run
5061            }
5062
5063            /// Fetch every row where `<col> = <val>`. Eloquent
5064            /// `Model::where($col, $val)->get()` parity.
5065            ///
5066            /// Thin wrapper over `QuerySet::<Self>::default()
5067            /// .filter(col, val).fetch(pool)`. For one row,
5068            /// use [`Self::first_where`]; for a chain that
5069            /// needs further `.filter()` / `.order_by()` /
5070            /// `.limit()`, drop down to `Self::query().filter(...)`
5071            /// directly.
5072            ///
5073            /// `val` accepts any value `Into<SqlValue>` so plain
5074            /// strings, ints, UUIDs, etc. all work.
5075            ///
5076            /// # Errors
5077            /// As [`FetcherPool::fetch`].
5078            ///
5079            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5080            pub async fn where_(
5081                col: &str,
5082                val: impl ::core::convert::Into<#root::core::SqlValue>,
5083                pool: &#root::sql::Pool,
5084            ) -> ::core::result::Result<
5085                ::std::vec::Vec<Self>,
5086                #root::sql::ExecError,
5087            > {
5088                use #root::sql::FetcherPool as _;
5089                #root::query::QuerySet::<Self>::default()
5090                    .filter(col, val)
5091                    .fetch(pool)
5092                    .await
5093            }
5094
5095            /// Fetch every row where `<col> IN (vals)`. Eloquent
5096            /// `Model::whereIn($col, $vals)->get()` parity. Empty
5097            /// `vals` returns no rows (matches SQL's empty-IN
5098            /// semantics).
5099            ///
5100            /// `vals` accepts any iterable whose items are
5101            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5102            /// etc.
5103            ///
5104            /// # Errors
5105            /// As [`FetcherPool::fetch`].
5106            ///
5107            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5108            pub async fn where_in<V>(
5109                col: &str,
5110                vals: impl ::core::iter::IntoIterator<Item = V>,
5111                pool: &#root::sql::Pool,
5112            ) -> ::core::result::Result<
5113                ::std::vec::Vec<Self>,
5114                #root::sql::ExecError,
5115            >
5116            where
5117                V: ::core::convert::Into<#root::core::SqlValue>,
5118            {
5119                use #root::sql::FetcherPool as _;
5120                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5121                    vals.into_iter().map(::core::convert::Into::into).collect();
5122                if _values.is_empty() {
5123                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5124                }
5125                let _key = ::std::format!("{}__in", col);
5126                let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
5127                let _split = _batches.len() > 1;
5128                let mut _rows = ::std::vec::Vec::new();
5129                for _keys in _batches {
5130                    _rows.extend(
5131                        #root::query::QuerySet::<Self>::default()
5132                            .filter(&_key, #root::core::SqlValue::List(_keys))
5133                            .fetch(pool)
5134                            .await?,
5135                    );
5136                }
5137                if _split {
5138                    _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
5139                }
5140                ::core::result::Result::Ok(_rows)
5141            }
5142
5143            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5144            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5145            /// `vals` returns every row (matches SQL's empty-NOT-IN
5146            /// semantics — vacuously true for every row).
5147            ///
5148            /// # Errors
5149            /// As [`FetcherPool::fetch`]; `ExecError::InListUnsplittable`
5150            /// when `vals` pass the backend's bind cap.
5151            ///
5152            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5153            pub async fn where_not_in<V>(
5154                col: &str,
5155                vals: impl ::core::iter::IntoIterator<Item = V>,
5156                pool: &#root::sql::Pool,
5157            ) -> ::core::result::Result<
5158                ::std::vec::Vec<Self>,
5159                #root::sql::ExecError,
5160            >
5161            where
5162                V: ::core::convert::Into<#root::core::SqlValue>,
5163            {
5164                use #root::sql::FetcherPool as _;
5165                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5166                    vals.into_iter().map(::core::convert::Into::into).collect();
5167                if _values.is_empty() {
5168                    return #root::query::QuerySet::<Self>::default()
5169                        .fetch(pool)
5170                        .await;
5171                }
5172                let _key = ::std::format!("{}__not_in", col);
5173                // A `NOT IN` cannot be split: past the bind cap it is refused.
5174                let _values = #root::sql::__rustango_one_in_list::<Self>(pool, _values)?;
5175                #root::query::QuerySet::<Self>::default()
5176                    .filter(&_key, #root::core::SqlValue::List(_values))
5177                    .fetch(pool)
5178                    .await
5179            }
5180
5181            /// Fetch every row where `<col> IS NULL`. Eloquent
5182            /// `Model::whereNull($col)->get()` parity.
5183            ///
5184            /// # Errors
5185            /// As [`FetcherPool::fetch`].
5186            ///
5187            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5188            pub async fn where_null(
5189                col: &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                let _key = ::std::format!("{}__isnull", col);
5197                #root::query::QuerySet::<Self>::default()
5198                    .filter(&_key, #root::core::SqlValue::Bool(true))
5199                    .fetch(pool)
5200                    .await
5201            }
5202
5203            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5204            /// `Model::whereNotNull($col)->get()` parity.
5205            ///
5206            /// # Errors
5207            /// As [`FetcherPool::fetch`].
5208            ///
5209            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5210            pub async fn where_not_null(
5211                col: &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                let _key = ::std::format!("{}__isnull", col);
5219                #root::query::QuerySet::<Self>::default()
5220                    .filter(&_key, #root::core::SqlValue::Bool(false))
5221                    .fetch(pool)
5222                    .await
5223            }
5224
5225            /// Fetch up to `n` rows in random order. Eloquent
5226            /// `Model::inRandomOrder()->limit($n)->get()` /
5227            /// `Model::query()->inRandomOrder()->get()->take($n)`
5228            /// parity. **Performance caveat**: random ordering
5229            /// forces a full table scan + per-row random key sort;
5230            /// the optimizer cannot use an index. Prefer a
5231            /// `pk >= rand_offset LIMIT N` walk for huge tables.
5232            ///
5233            /// # Errors
5234            /// As [`FetcherPool::fetch`].
5235            ///
5236            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5237            pub async fn random_n(
5238                n: i64,
5239                pool: &#root::sql::Pool,
5240            ) -> ::core::result::Result<
5241                ::std::vec::Vec<Self>,
5242                #root::sql::ExecError,
5243            > {
5244                use #root::sql::FetcherPool as _;
5245                #root::query::QuerySet::<Self>::default()
5246                    .order_random()
5247                    .limit(n)
5248                    .fetch(pool)
5249                    .await
5250            }
5251
5252            /// Fetch one row in random order. Eloquent
5253            /// `Model::inRandomOrder()->first()` parity. Same
5254            /// performance caveat as [`Self::random_n`].
5255            ///
5256            /// # Errors
5257            /// As [`FetcherPool::fetch`].
5258            ///
5259            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5260            pub async fn random(
5261                pool: &#root::sql::Pool,
5262            ) -> ::core::result::Result<
5263                ::core::option::Option<Self>,
5264                #root::sql::ExecError,
5265            > {
5266                ::core::result::Result::Ok(
5267                    Self::random_n(1, pool).await?.into_iter().next(),
5268                )
5269            }
5270
5271            /// Fetch every row ordered ASC by `field`. Eloquent
5272            /// `Model::oldest($field)->get()` parity — the multi-row
5273            /// counterpart of [`Self::earliest`].
5274            ///
5275            /// # Errors
5276            /// As [`FetcherPool::fetch`].
5277            ///
5278            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5279            pub async fn oldest(
5280                field: &str,
5281                pool: &#root::sql::Pool,
5282            ) -> ::core::result::Result<
5283                ::std::vec::Vec<Self>,
5284                #root::sql::ExecError,
5285            > {
5286                use #root::sql::FetcherPool as _;
5287                #root::query::QuerySet::<Self>::default()
5288                    .order_by(&[(field, false)])
5289                    .fetch(pool)
5290                    .await
5291            }
5292
5293            /// Fetch every row ordered DESC by `field`. Eloquent
5294            /// `Model::latest($field)->get()` parity — the multi-row
5295            /// counterpart of [`Self::latest`].
5296            ///
5297            /// # Errors
5298            /// As [`FetcherPool::fetch`].
5299            ///
5300            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5301            pub async fn newest(
5302                field: &str,
5303                pool: &#root::sql::Pool,
5304            ) -> ::core::result::Result<
5305                ::std::vec::Vec<Self>,
5306                #root::sql::ExecError,
5307            > {
5308                use #root::sql::FetcherPool as _;
5309                #root::query::QuerySet::<Self>::default()
5310                    .order_by(&[(field, true)])
5311                    .fetch(pool)
5312                    .await
5313            }
5314
5315            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5316            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5317            /// Routes through the existing `__year` lookup suffix
5318            /// (issue #829).
5319            ///
5320            /// # Errors
5321            /// As [`FetcherPool::fetch`].
5322            ///
5323            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5324            pub async fn where_year(
5325                col: &str,
5326                year: i64,
5327                pool: &#root::sql::Pool,
5328            ) -> ::core::result::Result<
5329                ::std::vec::Vec<Self>,
5330                #root::sql::ExecError,
5331            > {
5332                use #root::sql::FetcherPool as _;
5333                let _key = ::std::format!("{}__year", col);
5334                #root::query::QuerySet::<Self>::default()
5335                    .filter(&_key, #root::core::SqlValue::I64(year))
5336                    .fetch(pool)
5337                    .await
5338            }
5339
5340            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5341            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5342            /// `month` is 1–12.
5343            ///
5344            /// # Errors
5345            /// As [`FetcherPool::fetch`].
5346            ///
5347            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5348            pub async fn where_month(
5349                col: &str,
5350                month: i64,
5351                pool: &#root::sql::Pool,
5352            ) -> ::core::result::Result<
5353                ::std::vec::Vec<Self>,
5354                #root::sql::ExecError,
5355            > {
5356                use #root::sql::FetcherPool as _;
5357                let _key = ::std::format!("{}__month", col);
5358                #root::query::QuerySet::<Self>::default()
5359                    .filter(&_key, #root::core::SqlValue::I64(month))
5360                    .fetch(pool)
5361                    .await
5362            }
5363
5364            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5365            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5366            /// `day` is 1–31.
5367            ///
5368            /// # Errors
5369            /// As [`FetcherPool::fetch`].
5370            ///
5371            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5372            pub async fn where_day(
5373                col: &str,
5374                day: i64,
5375                pool: &#root::sql::Pool,
5376            ) -> ::core::result::Result<
5377                ::std::vec::Vec<Self>,
5378                #root::sql::ExecError,
5379            > {
5380                use #root::sql::FetcherPool as _;
5381                let _key = ::std::format!("{}__day", col);
5382                #root::query::QuerySet::<Self>::default()
5383                    .filter(&_key, #root::core::SqlValue::I64(day))
5384                    .fetch(pool)
5385                    .await
5386            }
5387
5388            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5389            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5390            /// `hour` is 0–23.
5391            ///
5392            /// # Errors
5393            /// As [`FetcherPool::fetch`].
5394            ///
5395            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5396            pub async fn where_hour(
5397                col: &str,
5398                hour: i64,
5399                pool: &#root::sql::Pool,
5400            ) -> ::core::result::Result<
5401                ::std::vec::Vec<Self>,
5402                #root::sql::ExecError,
5403            > {
5404                use #root::sql::FetcherPool as _;
5405                let _key = ::std::format!("{}__hour", col);
5406                #root::query::QuerySet::<Self>::default()
5407                    .filter(&_key, #root::core::SqlValue::I64(hour))
5408                    .fetch(pool)
5409                    .await
5410            }
5411
5412            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5413            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5414            /// `minute` is 0–59.
5415            ///
5416            /// # Errors
5417            /// As [`FetcherPool::fetch`].
5418            ///
5419            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5420            pub async fn where_minute(
5421                col: &str,
5422                minute: i64,
5423                pool: &#root::sql::Pool,
5424            ) -> ::core::result::Result<
5425                ::std::vec::Vec<Self>,
5426                #root::sql::ExecError,
5427            > {
5428                use #root::sql::FetcherPool as _;
5429                let _key = ::std::format!("{}__minute", col);
5430                #root::query::QuerySet::<Self>::default()
5431                    .filter(&_key, #root::core::SqlValue::I64(minute))
5432                    .fetch(pool)
5433                    .await
5434            }
5435
5436            /// Fetch every row where `<col> LIKE <pattern>` —
5437            /// caller-supplied pattern (must include `%` / `_`
5438            /// wildcards manually). Eloquent `Model::whereLike`
5439            /// parity.
5440            ///
5441            /// # Errors
5442            /// As [`FetcherPool::fetch`].
5443            ///
5444            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5445            pub async fn where_like(
5446                col: &str,
5447                pattern: impl ::core::convert::Into<::std::string::String>,
5448                pool: &#root::sql::Pool,
5449            ) -> ::core::result::Result<
5450                ::std::vec::Vec<Self>,
5451                #root::sql::ExecError,
5452            > {
5453                use #root::sql::FetcherPool as _;
5454                let _key = ::std::format!("{}__like", col);
5455                #root::query::QuerySet::<Self>::default()
5456                    .filter(
5457                        &_key,
5458                        #root::core::SqlValue::String(pattern.into()),
5459                    )
5460                    .fetch(pool)
5461                    .await
5462            }
5463
5464            /// Fetch every row where `<col> ILIKE <pattern>` —
5465            /// case-insensitive LIKE (PG native, MySQL/SQLite
5466            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5467            ///
5468            /// # Errors
5469            /// As [`FetcherPool::fetch`].
5470            ///
5471            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5472            pub async fn where_ilike(
5473                col: &str,
5474                pattern: impl ::core::convert::Into<::std::string::String>,
5475                pool: &#root::sql::Pool,
5476            ) -> ::core::result::Result<
5477                ::std::vec::Vec<Self>,
5478                #root::sql::ExecError,
5479            > {
5480                use #root::sql::FetcherPool as _;
5481                let _key = ::std::format!("{}__ilike", col);
5482                #root::query::QuerySet::<Self>::default()
5483                    .filter(
5484                        &_key,
5485                        #root::core::SqlValue::String(pattern.into()),
5486                    )
5487                    .fetch(pool)
5488                    .await
5489            }
5490
5491            /// Fetch every row where `<col>` starts with `prefix`
5492            /// (auto-appends `%`). Eloquent
5493            /// `whereLike("col", "$prefix%")` parity.
5494            ///
5495            /// # Errors
5496            /// As [`FetcherPool::fetch`].
5497            ///
5498            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5499            pub async fn where_starts_with(
5500                col: &str,
5501                prefix: impl ::core::convert::Into<::std::string::String>,
5502                pool: &#root::sql::Pool,
5503            ) -> ::core::result::Result<
5504                ::std::vec::Vec<Self>,
5505                #root::sql::ExecError,
5506            > {
5507                use #root::sql::FetcherPool as _;
5508                let _key = ::std::format!("{}__startswith", col);
5509                #root::query::QuerySet::<Self>::default()
5510                    .filter(
5511                        &_key,
5512                        #root::core::SqlValue::String(prefix.into()),
5513                    )
5514                    .fetch(pool)
5515                    .await
5516            }
5517
5518            /// Fetch every row where `<col>` ends with `suffix`
5519            /// (auto-prepends `%`). Eloquent
5520            /// `whereLike("col", "%$suffix")` parity.
5521            ///
5522            /// # Errors
5523            /// As [`FetcherPool::fetch`].
5524            ///
5525            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5526            pub async fn where_ends_with(
5527                col: &str,
5528                suffix: impl ::core::convert::Into<::std::string::String>,
5529                pool: &#root::sql::Pool,
5530            ) -> ::core::result::Result<
5531                ::std::vec::Vec<Self>,
5532                #root::sql::ExecError,
5533            > {
5534                use #root::sql::FetcherPool as _;
5535                let _key = ::std::format!("{}__endswith", col);
5536                #root::query::QuerySet::<Self>::default()
5537                    .filter(
5538                        &_key,
5539                        #root::core::SqlValue::String(suffix.into()),
5540                    )
5541                    .fetch(pool)
5542                    .await
5543            }
5544
5545            /// Fetch every row where `<col>` contains `substr`
5546            /// (auto-wraps with `%`).
5547            /// Eloquent `whereLike("col", "%$substr%")` parity.
5548            ///
5549            /// # Errors
5550            /// As [`FetcherPool::fetch`].
5551            ///
5552            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5553            pub async fn where_contains(
5554                col: &str,
5555                substr: impl ::core::convert::Into<::std::string::String>,
5556                pool: &#root::sql::Pool,
5557            ) -> ::core::result::Result<
5558                ::std::vec::Vec<Self>,
5559                #root::sql::ExecError,
5560            > {
5561                use #root::sql::FetcherPool as _;
5562                let _key = ::std::format!("{}__contains", col);
5563                #root::query::QuerySet::<Self>::default()
5564                    .filter(
5565                        &_key,
5566                        #root::core::SqlValue::String(substr.into()),
5567                    )
5568                    .fetch(pool)
5569                    .await
5570            }
5571
5572            /// Fetch every row where `<col> > val`. Eloquent
5573            /// `Model::where($col, ">", $val)->get()` parity.
5574            ///
5575            /// # Errors
5576            /// As [`FetcherPool::fetch`].
5577            ///
5578            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5579            pub async fn where_gt(
5580                col: &str,
5581                val: impl ::core::convert::Into<#root::core::SqlValue>,
5582                pool: &#root::sql::Pool,
5583            ) -> ::core::result::Result<
5584                ::std::vec::Vec<Self>,
5585                #root::sql::ExecError,
5586            > {
5587                use #root::sql::FetcherPool as _;
5588                let _key = ::std::format!("{}__gt", col);
5589                #root::query::QuerySet::<Self>::default()
5590                    .filter(&_key, val)
5591                    .fetch(pool)
5592                    .await
5593            }
5594
5595            /// Fetch every row where `<col> >= val`. Eloquent
5596            /// `Model::where($col, ">=", $val)->get()` parity.
5597            ///
5598            /// # Errors
5599            /// As [`FetcherPool::fetch`].
5600            ///
5601            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5602            pub async fn where_gte(
5603                col: &str,
5604                val: impl ::core::convert::Into<#root::core::SqlValue>,
5605                pool: &#root::sql::Pool,
5606            ) -> ::core::result::Result<
5607                ::std::vec::Vec<Self>,
5608                #root::sql::ExecError,
5609            > {
5610                use #root::sql::FetcherPool as _;
5611                let _key = ::std::format!("{}__gte", col);
5612                #root::query::QuerySet::<Self>::default()
5613                    .filter(&_key, val)
5614                    .fetch(pool)
5615                    .await
5616            }
5617
5618            /// Fetch every row where `<col> < val`. Eloquent
5619            /// `Model::where($col, "<", $val)->get()` parity.
5620            ///
5621            /// # Errors
5622            /// As [`FetcherPool::fetch`].
5623            ///
5624            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5625            pub async fn where_lt(
5626                col: &str,
5627                val: impl ::core::convert::Into<#root::core::SqlValue>,
5628                pool: &#root::sql::Pool,
5629            ) -> ::core::result::Result<
5630                ::std::vec::Vec<Self>,
5631                #root::sql::ExecError,
5632            > {
5633                use #root::sql::FetcherPool as _;
5634                let _key = ::std::format!("{}__lt", col);
5635                #root::query::QuerySet::<Self>::default()
5636                    .filter(&_key, val)
5637                    .fetch(pool)
5638                    .await
5639            }
5640
5641            /// Fetch every row where `<col> <= val`. Eloquent
5642            /// `Model::where($col, "<=", $val)->get()` parity.
5643            ///
5644            /// # Errors
5645            /// As [`FetcherPool::fetch`].
5646            ///
5647            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5648            pub async fn where_lte(
5649                col: &str,
5650                val: impl ::core::convert::Into<#root::core::SqlValue>,
5651                pool: &#root::sql::Pool,
5652            ) -> ::core::result::Result<
5653                ::std::vec::Vec<Self>,
5654                #root::sql::ExecError,
5655            > {
5656                use #root::sql::FetcherPool as _;
5657                let _key = ::std::format!("{}__lte", col);
5658                #root::query::QuerySet::<Self>::default()
5659                    .filter(&_key, val)
5660                    .fetch(pool)
5661                    .await
5662            }
5663
5664            /// Fetch every row where `<col> <> val`. Eloquent
5665            /// `Model::where($col, "!=", $val)->get()` parity.
5666            ///
5667            /// # Errors
5668            /// As [`FetcherPool::fetch`].
5669            ///
5670            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5671            pub async fn where_ne(
5672                col: &str,
5673                val: impl ::core::convert::Into<#root::core::SqlValue>,
5674                pool: &#root::sql::Pool,
5675            ) -> ::core::result::Result<
5676                ::std::vec::Vec<Self>,
5677                #root::sql::ExecError,
5678            > {
5679                use #root::sql::FetcherPool as _;
5680                let _key = ::std::format!("{}__ne", col);
5681                #root::query::QuerySet::<Self>::default()
5682                    .filter(&_key, val)
5683                    .fetch(pool)
5684                    .await
5685            }
5686
5687            /// Fetch every row matching `<col> = val` for ANY of the
5688            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5689            /// parity. Empty `cols` returns no rows.
5690            ///
5691            /// Resolves each `&str` column to its SCHEMA-registered
5692            /// `&'static str` once and builds a single OR-composed
5693            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5694            ///
5695            /// # Errors
5696            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5697            /// when any column is not declared on the model.
5698            ///
5699            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5700            pub async fn where_any(
5701                cols: &[&str],
5702                val: impl ::core::convert::Into<#root::core::SqlValue>,
5703                pool: &#root::sql::Pool,
5704            ) -> ::core::result::Result<
5705                ::std::vec::Vec<Self>,
5706                #root::sql::ExecError,
5707            > {
5708                Self::__where_multi(cols, val, false, pool).await
5709            }
5710
5711            /// Fetch every row matching `<col> = val` for ALL listed
5712            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5713            /// Empty `cols` returns every row (vacuous AND).
5714            ///
5715            /// # Errors
5716            /// As [`Self::where_any`].
5717            pub async fn where_all(
5718                cols: &[&str],
5719                val: impl ::core::convert::Into<#root::core::SqlValue>,
5720                pool: &#root::sql::Pool,
5721            ) -> ::core::result::Result<
5722                ::std::vec::Vec<Self>,
5723                #root::sql::ExecError,
5724            > {
5725                Self::__where_multi(cols, val, true, pool).await
5726            }
5727
5728            /// Internal: build a Q expression composing `cols` via
5729            /// AND (`all`) or OR (`!all`), then fetch. Backs
5730            /// `where_any` / `where_all`.
5731            #[doc(hidden)]
5732            pub async fn __where_multi(
5733                cols: &[&str],
5734                val: impl ::core::convert::Into<#root::core::SqlValue>,
5735                all: bool,
5736                pool: &#root::sql::Pool,
5737            ) -> ::core::result::Result<
5738                ::std::vec::Vec<Self>,
5739                #root::sql::ExecError,
5740            > {
5741                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5742                    .await
5743            }
5744
5745            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5746            /// parity. PK-ordered
5747            /// is NOT guaranteed without an explicit `order_by` —
5748            /// drop into `Self::query()` for that.
5749            ///
5750            /// # Errors
5751            /// As [`FetcherPool::fetch`].
5752            ///
5753            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5754            pub async fn take(
5755                n: i64,
5756                pool: &#root::sql::Pool,
5757            ) -> ::core::result::Result<
5758                ::std::vec::Vec<Self>,
5759                #root::sql::ExecError,
5760            > {
5761                use #root::sql::FetcherPool as _;
5762                #root::query::QuerySet::<Self>::default()
5763                    .limit(n)
5764                    .fetch(pool)
5765                    .await
5766            }
5767
5768            /// Fetch the page-th window of `per_page` rows
5769            /// (1-indexed). Eloquent
5770            /// `Model::query()->forPage($page, $perPage)->get()`
5771            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5772            /// LIMIT per_page`; for large offsets this is O(N) —
5773            /// prefer keyset pagination via PK on hot paths.
5774            ///
5775            /// # Errors
5776            /// As [`FetcherPool::fetch`].
5777            ///
5778            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5779            pub async fn for_page(
5780                page: i64,
5781                per_page: i64,
5782                pool: &#root::sql::Pool,
5783            ) -> ::core::result::Result<
5784                ::std::vec::Vec<Self>,
5785                #root::sql::ExecError,
5786            > {
5787                use #root::sql::FetcherPool as _;
5788                let _offset = #root::list_params::page_offset(page, per_page);
5789                #root::query::QuerySet::<Self>::default()
5790                    .limit(per_page)
5791                    .offset(_offset)
5792                    .fetch(pool)
5793                    .await
5794            }
5795
5796            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5797            /// one page of rows AND the total row count in one
5798            /// call. Returns `(rows, total)`. Two queries: a
5799            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5800            /// for the total.
5801            ///
5802            /// Useful for paginated UIs that need both the visible
5803            /// rows AND a "Page X of Y" / total-count widget. For
5804            /// large tables prefer keyset pagination + cached count;
5805            /// every call to `paginate` re-counts the full table.
5806            ///
5807            /// Same 1-indexed `page` convention as [`Self::for_page`].
5808            ///
5809            /// # Errors
5810            /// As [`Self::for_page`] and [`Self::count`].
5811            pub async fn paginate(
5812                page: i64,
5813                per_page: i64,
5814                pool: &#root::sql::Pool,
5815            ) -> ::core::result::Result<
5816                (::std::vec::Vec<Self>, i64),
5817                #root::sql::ExecError,
5818            > {
5819                // Route through the queryset rather than `Self::count`:
5820                // the `count` inherent method is suppressed on models
5821                // that declare a field named `count` (field/shortcut
5822                // collision guard), and `paginate` must still compile
5823                // for those models.
5824                let total = {
5825                    use #root::sql::CounterPool as _;
5826                    #root::query::QuerySet::<Self>::default()
5827                        .count(pool)
5828                        .await?
5829                };
5830                let rows = Self::for_page(page, per_page, pool).await?;
5831                ::core::result::Result::Ok((rows, total))
5832            }
5833
5834            /// Bulk-update — set `set_col = set_val` on every row
5835            /// matching `where_col = where_val`. Returns affected row
5836            /// count. Eloquent
5837            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5838            /// parity.
5839            ///
5840            /// For multi-column updates drop into the queryset
5841            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5842            ///
5843            /// # Errors
5844            /// As [`UpdaterPool::execute_pool`].
5845            ///
5846            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5847            pub async fn update_where(
5848                where_col: &str,
5849                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5850                set_col: &str,
5851                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5852                pool: &#root::sql::Pool,
5853            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5854                let _query = #root::query::QuerySet::<Self>::default()
5855                    .filter(where_col, where_val)
5856                    .update()
5857                    .set(set_col, set_val)
5858                    .compile()?;
5859                #bulk_update_run
5860            }
5861
5862            /// Bulk-delete — remove every row matching
5863            /// `where_col = where_val`. Returns affected row count.
5864            /// Eloquent
5865            /// `Model::where($where_col, $where_val)->delete()` parity.
5866            ///
5867            /// # Errors
5868            /// As [`#root::sql::delete_pool`].
5869            pub async fn delete_where(
5870                where_col: &str,
5871                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5872                pool: &#root::sql::Pool,
5873            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5874                let _query = #root::query::QuerySet::<Self>::default()
5875                    .filter_op(where_col, #root::core::Op::Eq, where_val)
5876                    .compile_delete()?;
5877                #bulk_delete_run
5878            }
5879
5880            /// Bulk-update — set `set_col = set_val` on EVERY row of
5881            /// this model's table. **No WHERE clause** — use with
5882            /// care. Eloquent `Model::query()->update([$col => $val])`
5883            /// parity.
5884            ///
5885            /// Use for backfills, one-shot reset flows, etc. The
5886            /// returned count is rows affected.
5887            ///
5888            /// # Errors
5889            /// As [`UpdaterPool::execute_pool`].
5890            ///
5891            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5892            pub async fn update_all(
5893                set_col: &str,
5894                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5895                pool: &#root::sql::Pool,
5896            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5897                let _query = #root::query::QuerySet::<Self>::default()
5898                    .update()
5899                    .set(set_col, set_val)
5900                    .compile()?;
5901                #bulk_update_run
5902            }
5903
5904            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5905            /// Eloquent `Model::whereNotLike` parity. Pattern is
5906            /// passed verbatim — caller controls `%` / `_`.
5907            ///
5908            /// # Errors
5909            /// As [`FetcherPool::fetch`].
5910            ///
5911            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5912            pub async fn where_not_like(
5913                col: &str,
5914                pattern: impl ::core::convert::Into<::std::string::String>,
5915                pool: &#root::sql::Pool,
5916            ) -> ::core::result::Result<
5917                ::std::vec::Vec<Self>,
5918                #root::sql::ExecError,
5919            > {
5920                use #root::sql::FetcherPool as _;
5921                let _key = ::std::format!("{}__not_like", col);
5922                #root::query::QuerySet::<Self>::default()
5923                    .filter(
5924                        &_key,
5925                        #root::core::SqlValue::String(pattern.into()),
5926                    )
5927                    .fetch(pool)
5928                    .await
5929            }
5930
5931            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5932            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5933            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5934            ///
5935            /// # Errors
5936            /// As [`FetcherPool::fetch`].
5937            ///
5938            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5939            pub async fn where_not_ilike(
5940                col: &str,
5941                pattern: impl ::core::convert::Into<::std::string::String>,
5942                pool: &#root::sql::Pool,
5943            ) -> ::core::result::Result<
5944                ::std::vec::Vec<Self>,
5945                #root::sql::ExecError,
5946            > {
5947                use #root::sql::FetcherPool as _;
5948                let _key = ::std::format!("{}__not_ilike", col);
5949                #root::query::QuerySet::<Self>::default()
5950                    .filter(
5951                        &_key,
5952                        #root::core::SqlValue::String(pattern.into()),
5953                    )
5954                    .fetch(pool)
5955                    .await
5956            }
5957
5958            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5959            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5960            /// parity.
5961            ///
5962            /// # Errors
5963            /// As [`FetcherPool::fetch`].
5964            ///
5965            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5966            pub async fn where_not_between(
5967                col: &str,
5968                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5969                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5970                pool: &#root::sql::Pool,
5971            ) -> ::core::result::Result<
5972                ::std::vec::Vec<Self>,
5973                #root::sql::ExecError,
5974            > {
5975                use #root::sql::FetcherPool as _;
5976                let _key = ::std::format!("{}__not_between", col);
5977                let _vals = #root::core::SqlValue::List(::std::vec![
5978                    ::core::convert::Into::into(lo),
5979                    ::core::convert::Into::into(hi),
5980                ]);
5981                #root::query::QuerySet::<Self>::default()
5982                    .filter(&_key, _vals)
5983                    .fetch(pool)
5984                    .await
5985            }
5986
5987            /// Returns the SQL table name for this model. Eloquent
5988            /// `$model->getTable()` parity.
5989            #[must_use]
5990            pub fn table_name() -> &'static str {
5991                <Self as #root::core::Model>::SCHEMA.table
5992            }
5993
5994            /// Returns the SQL column name of this model's primary
5995            /// key, or `None` when the model has no
5996            /// `#[rustango(primary_key)]`. Eloquent
5997            /// `$model->getKeyName()` parity.
5998            #[must_use]
5999            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
6000                <Self as #root::core::Model>::SCHEMA
6001                    .primary_key()
6002                    .map(|f| f.column)
6003            }
6004
6005            /// Fetch every row where `<col> BETWEEN lo AND hi`
6006            /// (inclusive on both ends — same as SQL). Eloquent
6007            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
6008            ///
6009            /// # Errors
6010            /// As [`FetcherPool::fetch`].
6011            ///
6012            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6013            pub async fn where_between(
6014                col: &str,
6015                lo: impl ::core::convert::Into<#root::core::SqlValue>,
6016                hi: impl ::core::convert::Into<#root::core::SqlValue>,
6017                pool: &#root::sql::Pool,
6018            ) -> ::core::result::Result<
6019                ::std::vec::Vec<Self>,
6020                #root::sql::ExecError,
6021            > {
6022                use #root::sql::FetcherPool as _;
6023                let _key = ::std::format!("{}__between", col);
6024                let _vals = #root::core::SqlValue::List(::std::vec![
6025                    ::core::convert::Into::into(lo),
6026                    ::core::convert::Into::into(hi),
6027                ]);
6028                #root::query::QuerySet::<Self>::default()
6029                    .filter(&_key, _vals)
6030                    .fetch(pool)
6031                    .await
6032            }
6033
6034            /// Fetch the first row where `<col> = <val>`. Returns
6035            /// `Ok(None)` when no row matches. Eloquent
6036            /// `Model::firstWhere($col, $val)` parity.
6037            ///
6038            /// Thin wrapper over `QuerySet::<Self>::default()
6039            /// .filter(col, val).first(pool)`. Use this when you
6040            /// want one row identified by a non-PK column (e.g.
6041            /// `User::first_where_pool("email", "x@y.com", &pool)`).
6042            ///
6043            /// `val` accepts any value `Into<SqlValue>` so plain
6044            /// strings, ints, UUIDs, etc. all work.
6045            ///
6046            /// # Errors
6047            /// As `QuerySet::first`.
6048            pub async fn first_where(
6049                col: &str,
6050                val: impl ::core::convert::Into<#root::core::SqlValue>,
6051                pool: &#root::sql::Pool,
6052            ) -> ::core::result::Result<
6053                ::core::option::Option<Self>,
6054                #root::sql::ExecError,
6055            > {
6056                #root::query::QuerySet::<Self>::default()
6057                    .filter(col, val)
6058                    .first(pool)
6059                    .await
6060            }
6061
6062            #value_method
6063
6064            /// Fetch the row with the largest non-NULL `field` value.
6065            /// Returns `Ok(None)` when the table is empty or every
6066            /// `field` is NULL. Thin wrapper over
6067            /// `QuerySet::<Self>::default().latest(field, pool)`.
6068            ///
6069            /// **Field name** is the Rust field ident as a string
6070            /// (not the SQL column). It is a runtime `&str`, so an
6071            /// unknown one compiles and surfaces as
6072            /// `ExecError::Query(QueryError::UnknownField)` when the
6073            /// query runs.
6074            ///
6075            /// # Errors
6076            /// As `QuerySet::latest`.
6077            pub async fn latest(
6078                field: &str,
6079                pool: &#root::sql::Pool,
6080            ) -> ::core::result::Result<
6081                ::core::option::Option<Self>,
6082                #root::sql::ExecError,
6083            > {
6084                #root::query::QuerySet::<Self>::default()
6085                    .latest(field, pool)
6086                    .await
6087            }
6088
6089            /// Sibling of [`Self::latest`]: fetches the row with the
6090            /// smallest non-NULL `field` value.
6091            ///
6092            /// # Errors
6093            /// As [`Self::latest`].
6094            pub async fn earliest(
6095                field: &str,
6096                pool: &#root::sql::Pool,
6097            ) -> ::core::result::Result<
6098                ::core::option::Option<Self>,
6099                #root::sql::ExecError,
6100            > {
6101                #root::query::QuerySet::<Self>::default()
6102                    .earliest(field, pool)
6103                    .await
6104            }
6105
6106            #count_method
6107
6108            /// `true` when the table contains at least one row.
6109            /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6110            /// over `QuerySet::<Self>::default().exists(pool)`.
6111            ///
6112            /// # Errors
6113            /// As [`ExistsPool::exists`].
6114            ///
6115            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6116            pub async fn exists(
6117                pool: &#root::sql::Pool,
6118            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6119                use #root::sql::ExistsPool as _;
6120                #root::query::QuerySet::<Self>::default()
6121                    .exists(pool)
6122                    .await
6123            }
6124
6125            /// Inverse of [`Self::exists`] — returns `true` when
6126            /// the table has zero rows. Eloquent
6127            /// `Model::doesntExist()` parity.
6128            ///
6129            /// # Errors
6130            /// As [`Self::exists`].
6131            pub async fn doesnt_exist(
6132                pool: &#root::sql::Pool,
6133            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6134                Self::exists(pool).await.map(|e| !e)
6135            }
6136
6137            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6138            /// `true` when a row with primary key `pk` exists in
6139            /// the table. Sugar over
6140            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6141            ///
6142            /// Differs from [`Self::exists`] (which checks "any row
6143            /// in the table") by checking a specific PK existence.
6144            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6145            /// because the row is never materialized — the SQL is
6146            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6147            ///
6148            /// # Errors
6149            /// As [`#root::sql::ExistsPool::contains_pk`].
6150            pub async fn contains_pk(
6151                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6152                pool: &#root::sql::Pool,
6153            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6154                use #root::sql::ExistsPool as _;
6155                #root::query::QuerySet::<Self>::default()
6156                    .contains_pk(pool, pk)
6157                    .await
6158            }
6159
6160            #sum_method
6161            #avg_method
6162            #min_method
6163            #max_method
6164
6165            /// Fetch every row of this model from `pool`. Eloquent
6166            /// `Model::all()` parity — a thin wrapper over
6167            /// `QuerySet::<Self>::default().fetch(pool)`.
6168            ///
6169            /// **Use with care on large tables** — there's no
6170            /// pagination or limit; the entire table is materialized
6171            /// into memory. For anything beyond fixture / lookup
6172            /// tables, page through `QuerySet::<Self>::default()
6173            /// .order_by(...).limit(N).offset(M).fetch(pool)`
6174            /// or stream via `.iterator(chunk_size)`.
6175            ///
6176            /// # Errors
6177            /// As [`FetcherPool::fetch`].
6178            ///
6179            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6180            pub async fn all(
6181                pool: &#root::sql::Pool,
6182            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6183            {
6184                use #root::sql::FetcherPool as _;
6185                #root::query::QuerySet::<Self>::default()
6186                    .fetch(pool)
6187                    .await
6188            }
6189
6190            /// Look up every row whose primary key is in `pks`.
6191            /// Returns the matching rows in **inventory** order — NOT
6192            /// the order of `pks`. Empty `pks` returns an empty
6193            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6194            /// with a list) parity.
6195            ///
6196            /// Thin wrapper over `QuerySet::<Self>::default()
6197            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6198            /// Caller-supplied PKs that don't match a row are
6199            /// silently skipped (the returned vec is shorter than
6200            /// the input list). For an order-preserving / "fail
6201            /// when any missing" variant, build the queryset
6202            /// explicitly with `in_bulk` instead.
6203            ///
6204            /// Accepts any iterable whose elements are
6205            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6206            /// `Vec<Uuid>`, etc.
6207            ///
6208            /// # Errors
6209            /// As [`FetcherPool::fetch`].
6210            ///
6211            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6212            pub async fn find_many<V>(
6213                pks: impl ::core::iter::IntoIterator<Item = V>,
6214                pool: &#root::sql::Pool,
6215            ) -> ::core::result::Result<
6216                ::std::vec::Vec<Self>,
6217                #root::sql::ExecError,
6218            >
6219            where
6220                V: ::core::convert::Into<#root::core::SqlValue>,
6221            {
6222                use #root::sql::FetcherPool as _;
6223                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6224                    pks.into_iter().map(::core::convert::Into::into).collect();
6225                if _values.is_empty() {
6226                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6227                }
6228                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6229                let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
6230                let _split = _batches.len() > 1;
6231                let mut _rows = ::std::vec::Vec::new();
6232                for _keys in _batches {
6233                    _rows.extend(
6234                        #root::query::QuerySet::<Self>::default()
6235                            .filter(&_key, #root::core::SqlValue::List(_keys))
6236                            .fetch(pool)
6237                            .await?,
6238                    );
6239                }
6240                if _split {
6241                    _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
6242                }
6243                ::core::result::Result::Ok(_rows)
6244            }
6245
6246            /// Look up the row whose primary key equals `pk`. Returns
6247            /// `Ok(None)` when no row matches, rather than erroring.
6248            /// Eloquent `Model::find`
6249            /// shape — accepts any value `Into<SqlValue>`.
6250            ///
6251            /// One-liner shortcut for the common
6252            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6253            /// .limit(1).fetch(pool).await?.into_iter().next()`
6254            /// dance.
6255            ///
6256            /// # Errors
6257            /// As [`FetcherPool::fetch`].
6258            ///
6259            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6260            pub async fn find(
6261                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6262                pool: &#root::sql::Pool,
6263            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6264            {
6265                use #root::sql::FetcherPool as _;
6266                let _pk_val: #root::core::SqlValue = pk.into();
6267                let mut _rows: ::std::vec::Vec<Self> =
6268                    #root::query::QuerySet::<Self>::default()
6269                        .filter(::core::stringify!(#pk_ident), _pk_val)
6270                        .limit(1)
6271                        .fetch(pool)
6272                        .await?;
6273                ::core::result::Result::Ok(_rows.into_iter().next())
6274            }
6275
6276            /// Look up the row whose primary key equals `pk`. Errors
6277            /// when no row matches — the throwing counterpart of
6278            /// [`Self::find`]. Eloquent `Model::findOrFail`
6279            /// parity.
6280            ///
6281            /// Translates the miss into
6282            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6283            /// so callers can `?`-bubble straight through the typical
6284            /// `ExecError` error chain.
6285            ///
6286            /// # Errors
6287            /// As [`Self::find`]; additionally
6288            /// [`sqlx::Error::RowNotFound`] when no row matches.
6289            ///
6290            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6291            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6292            pub async fn find_or_fail(
6293                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6294                pool: &#root::sql::Pool,
6295            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6296                match Self::find(pk, pool).await? {
6297                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6298                    ::core::option::Option::None => ::core::result::Result::Err(
6299                        #root::sql::ExecError::Driver(
6300                            #root::sql::sqlx::Error::RowNotFound,
6301                        ),
6302                    ),
6303                }
6304            }
6305
6306            /// Look up multiple rows by primary key, **failing** if
6307            /// any requested PK is missing. Eloquent
6308            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6309            /// inventory order (NOT request order — same as
6310            /// [`Self::find_many`]).
6311            ///
6312            /// Empty `pks` returns an empty `Vec` (no rows requested
6313            /// → nothing to fail on).
6314            ///
6315            /// Differs from [`Self::find_many`] in that this method
6316            /// requires every PK to resolve. If even one is missing,
6317            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6318            /// in `ExecError::Driver`). A key counts as found when the
6319            /// database matches it to a row, so repeats and keys its
6320            /// collation folds together (`abc` / `ABC` on MySQL) pass.
6321            ///
6322            /// # Errors
6323            /// As [`Self::find_many`], plus `RowNotFound` when a
6324            /// requested PK matches no row.
6325            pub async fn find_many_or_fail<V>(
6326                pks: impl ::core::iter::IntoIterator<Item = V>,
6327                pool: &#root::sql::Pool,
6328            ) -> ::core::result::Result<
6329                ::std::vec::Vec<Self>,
6330                #root::sql::ExecError,
6331            >
6332            where
6333                V: ::core::convert::Into<#root::core::SqlValue>,
6334            {
6335                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6336                    pks.into_iter().map(::core::convert::Into::into).collect();
6337                if _values.is_empty() {
6338                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6339                }
6340                use #root::sql::ExistsPool as _;
6341                let mut _seen: ::std::collections::HashSet<
6342                    ::std::string::String,
6343                > = ::std::collections::HashSet::new();
6344                let mut _keys: ::std::vec::Vec<#root::core::SqlValue> =
6345                    ::std::vec::Vec::new();
6346                for v in &_values {
6347                    if _seen.insert(v.to_display_string()) {
6348                        _keys.push(::core::clone::Clone::clone(v));
6349                    }
6350                }
6351                let _rows = Self::find_many(_values, pool).await?;
6352                let _found: ::std::collections::HashSet<::std::string::String> = _rows
6353                    .iter()
6354                    .map(|r| r.__rustango_pk_value().to_display_string())
6355                    .collect();
6356                // A key with no exact twin among the rows may still match one
6357                // under the column's collation: the database decides.
6358                for _key in _keys {
6359                    if _found.contains(&_key.to_display_string()) {
6360                        continue;
6361                    }
6362                    let _hit = #root::query::QuerySet::<Self>::default()
6363                        .filter(::core::stringify!(#pk_ident), _key)
6364                        .exists(pool)
6365                        .await?;
6366                    if !_hit {
6367                        return ::core::result::Result::Err(
6368                            #root::sql::ExecError::Driver(
6369                                #root::sql::sqlx::Error::RowNotFound,
6370                            ),
6371                        );
6372                    }
6373                }
6374                ::core::result::Result::Ok(_rows)
6375            }
6376
6377            /// Find by primary key or run `fallback` to produce a
6378            /// default row to return. Eloquent
6379            /// `Model::findOr($pk, fn() => …)` parity.
6380            ///
6381            /// Unlike [`Self::find_or_fail`] (which raises on
6382            /// miss), this is the "give me something sensible"
6383            /// branch: typical use is "fetch the user's row, else
6384            /// fall back to an anonymous/guest stub".
6385            ///
6386            /// `fallback` runs only when no row matches — the DB
6387            /// round-trip happens unconditionally.
6388            ///
6389            /// # Errors
6390            /// As [`Self::find`].
6391            pub async fn find_or<F>(
6392                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6393                pool: &#root::sql::Pool,
6394                fallback: F,
6395            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6396            where
6397                F: ::core::ops::FnOnce() -> Self,
6398            {
6399                ::core::result::Result::Ok(
6400                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6401                )
6402            }
6403
6404            /// Same as [`Self::find_or`] but also returns a `bool`
6405            /// indicating whether the row was found in the DB
6406            /// (`true`) or freshly built from `fallback` (`false`).
6407            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6408            /// in PHP the returned model also exposes `->exists`;
6409            /// here that's surfaced as the second tuple element.
6410            ///
6411            /// Useful for edit-or-create form handlers where the
6412            /// caller needs to know whether to PATCH or POST when
6413            /// the user submits the form.
6414            ///
6415            /// # Errors
6416            /// As [`Self::find`].
6417            pub async fn find_or_new<F>(
6418                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6419                pool: &#root::sql::Pool,
6420                fallback: F,
6421            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6422            where
6423                F: ::core::ops::FnOnce() -> Self,
6424            {
6425                match Self::find(pk, pool).await? {
6426                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6427                    ::core::option::Option::None => {
6428                        ::core::result::Result::Ok((fallback(), false))
6429                    }
6430                }
6431            }
6432
6433            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6434            /// [`Self::find_or_new`] but **persists** the new row
6435            /// when the PK isn't found. Returns `(row, exists: bool)`
6436            /// — `exists=true` when the PK was found,
6437            /// `false` when a fresh row was inserted.
6438            ///
6439            /// ```ignore
6440            /// let (post, found) = Post::find_or_insert(
6441            ///     pk,
6442            ///     &pool,
6443            ///     || Post { id: Auto::default(), title: "new".into() },
6444            /// ).await?;
6445            /// // `found` true → returned existing row;
6446            /// // `found` false → fallback was inserted, `post.id` populated.
6447            /// ```
6448            ///
6449            /// **Caveat**: two concurrent calls that both miss the
6450            /// find can both INSERT, violating uniqueness. Wrap in a
6451            /// transaction or rely on a UNIQUE constraint + handle
6452            /// the conflict error if you need race-free semantics.
6453            ///
6454            /// # Errors
6455            /// As [`Self::find`] and [`Self::save`].
6456            pub async fn find_or_insert<F>(
6457                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6458                pool: &#root::sql::Pool,
6459                fallback: F,
6460            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6461            where
6462                F: ::core::ops::FnOnce() -> Self,
6463            {
6464                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6465                    return ::core::result::Result::Ok((_row, true));
6466                }
6467                let mut _new = fallback();
6468                _new.save(pool).await?;
6469                ::core::result::Result::Ok((_new, false))
6470            }
6471
6472            /// Fetch the first row of the table, or run `fallback`
6473            /// when the table is empty. Eloquent
6474            /// `Model::firstOr(fn() => …)` parity.
6475            ///
6476            /// # Errors
6477            /// As [`Self::first`].
6478            pub async fn first_or<F>(
6479                pool: &#root::sql::Pool,
6480                fallback: F,
6481            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6482            where
6483                F: ::core::ops::FnOnce() -> Self,
6484            {
6485                // Route through the queryset rather than `Self::first`,
6486                // which is suppressed on models with a field named
6487                // `first` (field/shortcut collision guard).
6488                ::core::result::Result::Ok(
6489                    #root::query::QuerySet::<Self>::default()
6490                        .first(pool)
6491                        .await?
6492                        .unwrap_or_else(fallback),
6493                )
6494            }
6495
6496            /// Fetch exactly one row matching `<col> = <val>`. Errors
6497            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6498            /// or more than one matches
6499            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6500            /// Eloquent `Model::sole($col, $val)` parity.
6501            ///
6502            /// # Errors
6503            /// As [`Self::where_`] plus the explicit
6504            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6505            pub async fn sole(
6506                col: &str,
6507                val: impl ::core::convert::Into<#root::core::SqlValue>,
6508                pool: &#root::sql::Pool,
6509            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6510                let mut _rows = Self::where_(col, val, pool).await?;
6511                match _rows.len() {
6512                    0 => ::core::result::Result::Err(
6513                        #root::sql::ExecError::Driver(
6514                            #root::sql::sqlx::Error::RowNotFound,
6515                        ),
6516                    ),
6517                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6518                    n => ::core::result::Result::Err(
6519                        #root::sql::ExecError::MultipleRowsReturned {
6520                            op: "sole",
6521                            table: <Self as #root::core::Model>::SCHEMA.name,
6522                            count: n,
6523                        },
6524                    ),
6525                }
6526            }
6527        }
6528    } else {
6529        quote!()
6530    };
6531
6532    // `_tx` family: the shared `__rustango_*_exec` bodies on an open
6533    // `PoolTx`; an audited model writes its snapshot entry in the same tx.
6534    let tx_emit = |op: TokenStream2| -> TokenStream2 {
6535        if audited_fields.is_some() && primary_key.is_some() {
6536            quote! {
6537                #root::audit::__emit_many_tx(
6538                    tx,
6539                    &[self.__rustango_audit_entry(#root::audit::AuditOp::#op)],
6540                )
6541                .await?;
6542            }
6543        } else {
6544            quote!()
6545        }
6546    };
6547    let tx_insert_method = {
6548        let self_ref = if fields.has_auto {
6549            quote!(&mut self)
6550        } else {
6551            quote!(&self)
6552        };
6553        let emit = tx_emit(quote!(Create));
6554        let refuse_pkless = if audited_fields.is_some() && primary_key.is_none() {
6555            quote! {
6556                return ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
6557                    table: <Self as #root::core::Model>::SCHEMA.table,
6558                });
6559            }
6560        } else {
6561            quote!()
6562        };
6563        quote! {
6564            /// Insert this row inside an open transaction; an audited model
6565            /// writes its audit row there too.
6566            ///
6567            /// # Errors
6568            /// As [`Self::insert`].
6569            #[allow(unreachable_code)]
6570            pub async fn insert_tx(
6571                #self_ref,
6572                tx: &mut #root::sql::PoolTx<'_>,
6573            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6574                #refuse_pkless
6575                self.__rustango_insert_exec(&mut *tx).await?;
6576                #emit
6577                ::core::result::Result::Ok(())
6578            }
6579        }
6580    };
6581    let tx_save_method = if let Some((pk_ident, _)) = primary_key {
6582        let emit_create = tx_emit(quote!(Create));
6583        let emit_update = tx_emit(quote!(Update));
6584        let was_unset = if fields.pk_is_auto {
6585            quote!(matches!(self.#pk_ident, #root::sql::Auto::Unset))
6586        } else {
6587            quote!(false)
6588        };
6589        quote! {
6590            /// Save this row inside an open transaction; an audited model
6591            /// writes its audit row there too.
6592            ///
6593            /// # Errors
6594            /// As [`Self::save`].
6595            pub async fn save_tx(
6596                &mut self,
6597                tx: &mut #root::sql::PoolTx<'_>,
6598            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6599                let _was_unset = #was_unset;
6600                let _n = self.__rustango_save_exec(&mut *tx).await?;
6601                if _n > 0 {
6602                    if _was_unset {
6603                        #emit_create
6604                    } else {
6605                        #emit_update
6606                    }
6607                }
6608                ::core::result::Result::Ok(_n)
6609            }
6610        }
6611    } else {
6612        quote!()
6613    };
6614    let tx_delete_method = if primary_key.is_some() {
6615        let emit = tx_emit(quote!(Delete));
6616        quote! {
6617            /// Delete this row inside an open transaction; an audited model
6618            /// writes its audit row there too.
6619            ///
6620            /// # Errors
6621            /// As [`Self::delete`].
6622            pub async fn delete_tx(
6623                &self,
6624                tx: &mut #root::sql::PoolTx<'_>,
6625            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6626                let _n = self.__rustango_delete_exec(&mut *tx).await?;
6627                if _n > 0 {
6628                    #emit
6629                }
6630                ::core::result::Result::Ok(_n)
6631            }
6632        }
6633    } else {
6634        quote!()
6635    };
6636
6637    // The one statement-level write path: `_on` (refusing audited models)
6638    // and `_tx` share these `__rustango_*_exec` bodies; non-audited `&Pool`
6639    // methods forward to them too.
6640    let refuse = |method: &str| -> TokenStream2 {
6641        let reason = format!("`{method}` cannot audit; use the `&Pool` method");
6642        quote!(#root::sql::__macro_internals::refuse_audited::<Self>(#reason)?;)
6643    };
6644    let refuse_save = refuse("save_on");
6645    let save_method = if let Some((pk_ident, pk_column)) = primary_key {
6646        let pk_column_lit = pk_column.as_str();
6647        let assignments = &fields.update_assignments;
6648        let dispatch_unset = if fields.pk_is_auto {
6649            quote! {
6650                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6651                    return self.__rustango_insert_exec(_executor).await.map(|()| 1u64);
6652                }
6653            }
6654        } else {
6655            quote!()
6656        };
6657        let write = restore_auto_now_unless(
6658            &fields.auto_now_idents,
6659            &save_ret,
6660            &save_keep,
6661            &quote! {
6662                let _query = #root::core::UpdateQuery::new(
6663                    <Self as #root::core::Model>::SCHEMA,
6664                    ::std::vec![ #( #assignments ),* ],
6665                    #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6666                        #pk_column_lit,
6667                        #root::core::Op::Eq,
6668                        ::core::convert::Into::<#root::core::SqlValue>::into(
6669                            ::core::clone::Clone::clone(&self.#pk_ident)
6670                        ),
6671                    )),
6672                );
6673                #root::sql::__macro_internals::update_on(_executor, &_query)
6674                    .await
6675                    .and_then(#root::sql::__macro_internals::row_saved)
6676            },
6677        );
6678        let upsert = if fields.pk_is_auto {
6679            let upsert_cols = &fields.upsert_update_columns;
6680            let upsert_pushes = &fields.insert_pushes;
6681            let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6682                fields.uuid_v7_fields.iter().cloned().unzip();
6683            let upsert_returning = &fields.returning_cols;
6684            let auto_now_idents = &fields.auto_now_idents;
6685            // Prefer the first `unique_together`: a server-assigned PK never
6686            // collides, so a PK-only target would always insert.
6687            let upsert_target_columns: Vec<String> =
6688                upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6689            let upsert_target_lits = upsert_target_columns
6690                .iter()
6691                .map(String::as_str)
6692                .collect::<Vec<_>>();
6693            let conflict_clause = if fields.upsert_update_columns.is_empty() {
6694                quote!(#root::core::ConflictClause::DoNothing)
6695            } else {
6696                quote!(#root::core::ConflictClause::DoUpdate {
6697                    target: ::std::vec![ #( #upsert_target_lits ),* ],
6698                    update_columns: ::std::vec![ #( #upsert_cols ),* ],
6699                })
6700            };
6701            let unit_ret = quote!(::core::result::Result<(), #root::sql::ExecError>);
6702            let unit_keep = quote!(_saved.is_ok());
6703            let exec_body = restore_auto_now_unless(
6704                auto_now_idents,
6705                &unit_ret,
6706                &unit_keep,
6707                &quote! {
6708                    let _query = self.__rustango_upsert_query();
6709                    let _row = #root::sql::__macro_internals::insert_returning_on(
6710                        _executor, _query,
6711                    )
6712                    .await?;
6713                    #root::sql::apply_auto_pk(_row, self)
6714                },
6715            );
6716            let pool_body = if audited_fields.is_some() {
6717                restore_auto_now_unless(
6718                    auto_now_idents,
6719                    &unit_ret,
6720                    &unit_keep,
6721                    &quote! {
6722                        let _query = self.__rustango_upsert_query();
6723                        #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self, _op| {
6724                            _m.__rustango_audit_entry(_op)
6725                        })
6726                        .await
6727                    },
6728                )
6729            } else {
6730                quote!(self.__rustango_upsert_exec(pool).await)
6731            };
6732            let refuse_upsert = refuse("upsert_on");
6733            quote! {
6734                /// Insert this row, or update the row it conflicts with: one
6735                /// `INSERT … ON CONFLICT` (MySQL: `ON DUPLICATE KEY UPDATE`).
6736                /// The target is the first `unique_together`, else the PK.
6737                /// Fills the `Auto<T>` fields from the written row.
6738                ///
6739                /// # Errors
6740                /// As [`Self::insert`].
6741                pub async fn upsert(
6742                    &mut self,
6743                    pool: &#root::sql::Pool,
6744                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6745                    #pool_body
6746                }
6747
6748                /// [`Self::upsert`] on any [`#root::sql::SqlExecutor`]. An
6749                /// audited model refuses it: use [`Self::upsert`].
6750                ///
6751                /// # Errors
6752                /// As [`Self::upsert`].
6753                pub async fn upsert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6754                    &mut self,
6755                    _executor: _E,
6756                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6757                    #refuse_upsert
6758                    self.__rustango_upsert_exec(_executor).await
6759                }
6760
6761                #[doc(hidden)]
6762                pub async fn __rustango_upsert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6763                    &mut self,
6764                    _executor: _E,
6765                ) -> ::core::result::Result<(), #root::sql::ExecError> {
6766                    #exec_body
6767                }
6768
6769                /// The upsert statement; stamps `auto_now` like `save`.
6770                #[doc(hidden)]
6771                pub fn __rustango_upsert_query(&mut self) -> #root::core::InsertQuery {
6772                    let mut _columns: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
6773                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6774                        ::std::vec::Vec::new();
6775                    #[allow(unused_mut)]
6776                    let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
6777                    #(
6778                        if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
6779                            _generated.push(#gen_cols);
6780                        }
6781                    )*
6782                    #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
6783                    #( #upsert_pushes )*
6784                    #root::core::InsertQuery::new(
6785                        <Self as #root::core::Model>::SCHEMA,
6786                        _columns,
6787                        _values,
6788                    )
6789                    .returning(::std::vec![ #( #upsert_returning ),* ])
6790                    .on_conflict(#conflict_clause)
6791                    .generated(_generated)
6792                }
6793            }
6794        } else {
6795            quote!()
6796        };
6797        Some(quote! {
6798            /// [`Self::save`] on any [`#root::sql::SqlExecutor`]: a held
6799            /// `PoolConn`, a `PoolTx`, a tenant connection or a Postgres sqlx
6800            /// executor. An audited model refuses it: use [`Self::save`].
6801            ///
6802            /// # Errors
6803            /// As [`Self::save`].
6804            pub async fn save_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6805                &mut self,
6806                _executor: _E,
6807            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6808                #refuse_save
6809                self.__rustango_save_exec(_executor).await
6810            }
6811
6812            #[doc(hidden)]
6813            pub async fn __rustango_save_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6814                &mut self,
6815                _executor: _E,
6816            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6817                #dispatch_unset
6818                #write
6819            }
6820
6821            /// Old name of [`Self::save`].
6822            ///
6823            /// # Errors
6824            /// As [`Self::save`].
6825            #[deprecated(since = "0.62.0", note = "use `save`")]
6826            pub async fn save_pool(
6827                &mut self,
6828                pool: &#root::sql::Pool,
6829            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6830                self.save(pool).await
6831            }
6832
6833            #upsert
6834        })
6835    } else {
6836        None
6837    };
6838
6839    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6840        let pk_column_lit = pk_column.as_str();
6841        let refuse_delete = refuse("delete_on");
6842        // Optional `soft_delete_on` / `restore_on` companions when the
6843        // model has a `#[rustango(soft_delete)]` column. They land
6844        // alongside the regular `delete_on` so callers have both
6845        // options — a hard delete (audit-tracked as a real DELETE) and
6846        // a logical delete (audit-tracked as an UPDATE setting the
6847        // deleted_at column to NOW()).
6848        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6849            let col_lit = col;
6850            let sd_field_ident = fields
6851                .soft_delete_field_ident
6852                .clone()
6853                .expect("soft_delete_column without ident");
6854            let refuse_soft_delete = refuse("soft_delete_on");
6855            let refuse_restore = refuse("restore_on");
6856            // `self` takes the written value, so a later `save` keeps it.
6857            let mark_deleted = quote! {
6858                if _n > 0 {
6859                    self.#sd_field_ident = ::core::option::Option::Some(_now);
6860                }
6861            };
6862            let mark_restored = quote! {
6863                if _n > 0 {
6864                    self.#sd_field_ident = ::core::option::Option::None;
6865                }
6866            };
6867            quote! {
6868                /// Soft-delete this row on any [`#root::sql::SqlExecutor`]: set
6869                /// its `#[rustango(soft_delete)]` column to now. An audited
6870                /// model refuses it: use [`Self::soft_delete`].
6871                ///
6872                /// # Errors
6873                /// As [`Self::delete`].
6874                pub async fn soft_delete_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6875                    &mut self,
6876                    _executor: _E,
6877                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6878                    #refuse_soft_delete
6879                    let _now = #root::__chrono::Utc::now();
6880                    let _query = #root::soft_delete::__mark_query(
6881                        <Self as #root::core::Model>::SCHEMA,
6882                        #col_lit,
6883                        #pk_column_lit,
6884                        ::core::convert::Into::<#root::core::SqlValue>::into(
6885                            ::core::clone::Clone::clone(&self.#pk_ident)
6886                        ),
6887                        ::core::option::Option::Some(_now),
6888                    );
6889                    let _n = #root::sql::__macro_internals::update_on(_executor, &_query).await?;
6890                    #mark_deleted
6891                    ::core::result::Result::Ok(_n)
6892                }
6893
6894                /// Inverse of [`Self::soft_delete_on`]: clear the soft-delete
6895                /// column. An audited model refuses it: use [`Self::restore`].
6896                ///
6897                /// # Errors
6898                /// As [`Self::delete`].
6899                pub async fn restore_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
6900                    &mut self,
6901                    _executor: _E,
6902                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6903                    #refuse_restore
6904                    let _query = #root::soft_delete::__mark_query(
6905                        <Self as #root::core::Model>::SCHEMA,
6906                        #col_lit,
6907                        #pk_column_lit,
6908                        ::core::convert::Into::<#root::core::SqlValue>::into(
6909                            ::core::clone::Clone::clone(&self.#pk_ident)
6910                        ),
6911                        ::core::option::Option::None,
6912                    );
6913                    let _n = #root::sql::__macro_internals::update_on(_executor, &_query).await?;
6914                    #mark_restored
6915                    ::core::result::Result::Ok(_n)
6916                }
6917
6918                /// Soft-delete this row on any backend. Eloquent `Model::delete()` semantics on
6919                /// soft-delete-enabled models (closes #821 partial).
6920                ///
6921                /// Sets the `#[rustango(soft_delete)]` column to
6922                /// `NOW()` on every backend. Query helpers
6923                /// (`QuerySet::active()` / `only_trashed()`,
6924                /// `soft_delete::active_filter` /
6925                /// `compose_with_active`) filter trashed rows out by
6926                /// reading `IS NULL` on the same column. Keyed on the PK;
6927                /// global scopes do not apply. When a row changed, `self`
6928                /// takes the stamp too, so a later `save` keeps it.
6929                ///
6930                /// # Errors
6931                /// As [`#root::sql::update_pool`].
6932                pub async fn soft_delete(
6933                    &mut self,
6934                    pool: &#root::sql::Pool,
6935                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6936                    let _now = #root::__chrono::Utc::now();
6937                    let _query = #root::soft_delete::__mark_query(
6938                        <Self as #root::core::Model>::SCHEMA,
6939                        #col_lit,
6940                        #pk_column_lit,
6941                        ::core::convert::Into::<#root::core::SqlValue>::into(
6942                            ::core::clone::Clone::clone(&self.#pk_ident)
6943                        ),
6944                        ::core::option::Option::Some(_now),
6945                    );
6946                    let _n = { #pool_softdelete_run }?;
6947                    #mark_deleted
6948                    ::core::result::Result::Ok(_n)
6949                }
6950
6951                /// Tri-dialect counterpart of [`Self::restore_on`].
6952                /// Clears the `#[rustango(soft_delete)]` column back
6953                /// to `NULL`, marking the row live again. Eloquent
6954                /// `Model::restore()` parity. Keyed on the PK; global
6955                /// scopes do not apply. When a row changed, `self` is
6956                /// cleared too.
6957                ///
6958                /// # Errors
6959                /// As [`#root::sql::update_pool`].
6960                pub async fn restore(
6961                    &mut self,
6962                    pool: &#root::sql::Pool,
6963                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6964                    let _query = #root::soft_delete::__mark_query(
6965                        <Self as #root::core::Model>::SCHEMA,
6966                        #col_lit,
6967                        #pk_column_lit,
6968                        ::core::convert::Into::<#root::core::SqlValue>::into(
6969                            ::core::clone::Clone::clone(&self.#pk_ident)
6970                        ),
6971                        ::core::option::Option::None,
6972                    );
6973                    let _n = { #pool_restore_run }?;
6974                    #mark_restored
6975                    ::core::result::Result::Ok(_n)
6976                }
6977
6978                /// Hard-delete this row, ignoring the soft-delete
6979                /// column. Eloquent `Model::forceDelete()` parity —
6980                /// the escape hatch when you need to actually purge
6981                /// data (GDPR, fixture cleanup, etc.).
6982                ///
6983                /// Equivalent to [`Self::delete_pool`] (the framework's
6984                /// non-soft delete) — exposed under the Eloquent name
6985                /// for muscle-memory + so soft-delete-enabled models
6986                /// have all three operations (soft / restore / force)
6987                /// in one place.
6988                ///
6989                /// # Errors
6990                /// As [`Self::delete_pool`].
6991                pub async fn force_delete(
6992                    &self,
6993                    pool: &#root::sql::Pool,
6994                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6995                    Self::delete(self, pool).await
6996                }
6997
6998                /// Returns `true` when this row is soft-deleted (its
6999                /// `#[rustango(soft_delete)]` column is currently
7000                /// set — Eloquent `$model->trashed()` parity).
7001                ///
7002                /// Pure in-memory predicate over `&self`; does not
7003                /// hit the database. Useful in admin/template code
7004                /// like `{% if post.trashed() %}…{% endif %}` and in
7005                /// guard clauses on restore/force-delete flows.
7006                pub fn trashed(&self) -> bool {
7007                    ::core::option::Option::is_some(&self.#sd_field_ident)
7008                }
7009
7010                /// Fetch every row whose `#[rustango(soft_delete)]`
7011                /// column is `NULL` (a.k.a. the live, non-trashed
7012                /// rows). Eloquent's default `Model::all()` behavior on
7013                /// a soft-delete model (Eloquent auto-scopes trashed
7014                /// rows out; rustango doesn't have auto-scoping yet —
7015                /// see issue #820 — so this is the explicit shortcut).
7016                ///
7017                /// One-liner over `QuerySet::<Self>::default()
7018                /// .active().fetch(pool)`. Closes #821 partial.
7019                ///
7020                /// # Errors
7021                /// As [`#root::sql::FetcherPool::fetch`].
7022                pub async fn active(
7023                    pool: &#root::sql::Pool,
7024                ) -> ::core::result::Result<
7025                    ::std::vec::Vec<Self>,
7026                    #root::sql::ExecError,
7027                > {
7028                    use #root::sql::FetcherPool as _;
7029                    #root::query::QuerySet::<Self>::default()
7030                        .active()
7031                        .fetch(pool)
7032                        .await
7033                }
7034
7035                /// Fetch ONLY soft-deleted rows. Eloquent
7036                /// `Model::onlyTrashed()->get()` parity — drives the
7037                /// admin "Trash" page, restore flows, GDPR purge
7038                /// scans, etc.
7039                ///
7040                /// One-liner over `QuerySet::<Self>::default()
7041                /// .only_trashed().fetch(pool)`. Closes #821
7042                /// partial.
7043                ///
7044                /// # Errors
7045                /// As [`#root::sql::FetcherPool::fetch`].
7046                pub async fn only_trashed(
7047                    pool: &#root::sql::Pool,
7048                ) -> ::core::result::Result<
7049                    ::std::vec::Vec<Self>,
7050                    #root::sql::ExecError,
7051                > {
7052                    use #root::sql::FetcherPool as _;
7053                    #root::query::QuerySet::<Self>::default()
7054                        .only_trashed()
7055                        .fetch(pool)
7056                        .await
7057                }
7058
7059                /// Fetch every row, both live and soft-deleted.
7060                /// Eloquent `Model::withTrashed()->get()` parity.
7061                ///
7062                /// Today every queryset already includes trashed rows
7063                /// (rustango has no global-scope tracking yet — issue
7064                /// #820), so this is functionally equivalent to
7065                /// [`Self::all`]. Exposed as a named shortcut so
7066                /// soft-delete-aware code reads `Model::with_trashed_pool`
7067                /// rather than `Model::all_pool` — keeps intent visible
7068                /// in callers and stays correct when auto-scoping lands.
7069                ///
7070                /// Closes #821 partial.
7071                ///
7072                /// # Errors
7073                /// As [`#root::sql::FetcherPool::fetch`].
7074                pub async fn with_trashed(
7075                    pool: &#root::sql::Pool,
7076                ) -> ::core::result::Result<
7077                    ::std::vec::Vec<Self>,
7078                    #root::sql::ExecError,
7079                > {
7080                    use #root::sql::FetcherPool as _;
7081                    #root::query::QuerySet::<Self>::default()
7082                        .with_trashed()
7083                        .fetch(pool)
7084                        .await
7085                }
7086
7087            }
7088        } else {
7089            quote!()
7090        };
7091        quote! {
7092
7093
7094            /// [`Self::delete`] on any [`#root::sql::SqlExecutor`]. An audited
7095            /// model refuses it: use [`Self::delete`].
7096            ///
7097            /// # Errors
7098            /// As [`Self::delete`].
7099            pub async fn delete_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7100                &self,
7101                _executor: _E,
7102            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7103                #refuse_delete
7104                self.__rustango_delete_exec(_executor).await
7105            }
7106
7107            #[doc(hidden)]
7108            pub async fn __rustango_delete_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7109                &self,
7110                _executor: _E,
7111            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7112                let _query = #root::core::DeleteQuery::by_pk(
7113                    <Self as #root::core::Model>::SCHEMA,
7114                    #pk_column_lit,
7115                    ::core::convert::Into::<#root::core::SqlValue>::into(
7116                        ::core::clone::Clone::clone(&self.#pk_ident)
7117                    ),
7118                );
7119                #root::sql::__macro_internals::delete_on(_executor, &_query).await
7120            }
7121
7122            /// Old name of [`Self::delete`].
7123            ///
7124            /// # Errors
7125            /// As [`Self::delete`].
7126            #[deprecated(since = "0.62.0", note = "use `delete`")]
7127            pub async fn delete_pool(
7128                &self,
7129                pool: &#root::sql::Pool,
7130            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7131                self.delete(pool).await
7132            }
7133
7134
7135            #pool_delete_method
7136            #pool_insert_method
7137            #audit_entry_method
7138            #pool_save_method
7139            #refresh_replicate_methods
7140            #tx_delete_method
7141            #tx_insert_method
7142            #tx_save_method
7143            #soft_delete_methods
7144
7145            /// Returns `true` when `other` represents the same DB
7146            /// row as `self` — i.e. their primary keys compare
7147            /// equal. Eloquent `$model->is($other)` parity.
7148            ///
7149            /// Because both arguments are typed `&Self`, the
7150            /// model/table check is automatic — `Post::is` cannot
7151            /// be invoked against a `Comment` at compile time. Only
7152            /// the PK has to be compared at runtime.
7153            pub fn is(&self, other: &Self) -> bool {
7154                self.#pk_ident == other.#pk_ident
7155            }
7156
7157            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7158            /// parity.
7159            pub fn is_not(&self, other: &Self) -> bool {
7160                self.#pk_ident != other.#pk_ident
7161            }
7162
7163            /// Returns this row's primary-key value as an
7164            /// [`#root::core::SqlValue`]. Eloquent
7165            /// `$model->getKey()` parity.
7166            ///
7167            /// Useful when you need to thread the PK through a
7168            /// generic `Into<SqlValue>`-bound API without knowing the
7169            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7170            #[must_use]
7171            pub fn get_key(&self) -> #root::core::SqlValue {
7172                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7173            }
7174
7175        }
7176    });
7177
7178    let refuse_insert = refuse("insert_on");
7179    // The audited `insert` is built with the PK methods; an audited model
7180    // without a PK has no row to name, so its insert refuses.
7181    let audited_insert = audited_fields.is_some() && primary_key.is_some();
7182    let insert_pool_body = if audited_fields.is_some() {
7183        quote! {
7184            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7185                table: <Self as #root::core::Model>::SCHEMA.table,
7186            })
7187        }
7188    } else {
7189        quote!(self.__rustango_insert_exec(pool).await)
7190    };
7191    let plain_insert_auto = if audited_insert {
7192        quote!()
7193    } else {
7194        quote! {
7195            /// Insert this row on any backend, reading each `Auto` field
7196            /// back from the written row.
7197            ///
7198            /// # Errors
7199            /// Returns [`#root::sql::ExecError`] for SQL-writing or driver failures.
7200            pub async fn insert(
7201                &mut self,
7202                pool: &#root::sql::Pool,
7203            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7204                #insert_pool_body
7205            }
7206        }
7207    };
7208    let plain_insert_plain = if audited_insert {
7209        quote!()
7210    } else {
7211        quote! {
7212            /// Insert this row on any backend.
7213            ///
7214            /// # Errors
7215            /// Returns [`#root::sql::ExecError`] for SQL-writing or driver failures.
7216            pub async fn insert(
7217                &self,
7218                pool: &#root::sql::Pool,
7219            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7220                #insert_pool_body
7221            }
7222        }
7223    };
7224    let insert_method = if fields.has_auto {
7225        let pushes = &fields.insert_pushes;
7226        let returning_cols = &fields.returning_cols;
7227        quote! {
7228            #plain_insert_auto
7229            /// [`Self::insert`] on any [`#root::sql::SqlExecutor`]. An audited
7230            /// model refuses it: use [`Self::insert`].
7231            ///
7232            /// # Errors
7233            /// As [`Self::insert`].
7234            pub async fn insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7235                &mut self,
7236                _executor: _E,
7237            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7238                #refuse_insert
7239                self.__rustango_insert_exec(_executor).await
7240            }
7241
7242            #[doc(hidden)]
7243            pub async fn __rustango_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7244                &mut self,
7245                _executor: _E,
7246            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7247                let _query = self.__rustango_insert_query();
7248                // Every `Auto` filled Rust-side: nothing to read back.
7249                if _query.returning.is_empty() {
7250                    return #root::sql::__macro_internals::insert_owned_on(_executor, _query).await;
7251                }
7252                let _row = #root::sql::__macro_internals::insert_returning_on(
7253                    _executor, _query,
7254                )
7255                .await?;
7256                #root::sql::apply_auto_pk(_row, self)
7257            }
7258
7259            /// The INSERT for this row; fills Rust-side `Auto` values first.
7260            #[doc(hidden)]
7261            pub fn __rustango_insert_query(&mut self) -> #root::core::InsertQuery {
7262                let mut _columns: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7263                let mut _values: ::std::vec::Vec<#root::core::SqlValue> = ::std::vec::Vec::new();
7264                #( #pushes )*
7265                #root::core::InsertQuery::new(
7266                    <Self as #root::core::Model>::SCHEMA,
7267                    _columns,
7268                    _values,
7269                )
7270                .returning(::std::vec![ #( #returning_cols ),* ])
7271            }
7272
7273            /// Old name of [`Self::insert`].
7274            ///
7275            /// # Errors
7276            /// As [`Self::insert`].
7277            #[deprecated(since = "0.62.0", note = "use `insert`")]
7278            pub async fn insert_pool(
7279                &mut self,
7280                pool: &#root::sql::Pool,
7281            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7282                self.insert(pool).await
7283            }
7284        }
7285    } else {
7286        let insert_columns = &fields.insert_columns;
7287        let insert_values = &fields.insert_values;
7288        // The audited `insert` takes `&mut self` for its audit helper.
7289        let pool_self = if audited_fields.is_some() {
7290            quote!(&mut self)
7291        } else {
7292            quote!(&self)
7293        };
7294        quote! {
7295            #plain_insert_plain
7296            /// [`Self::insert`] on any [`#root::sql::SqlExecutor`]. An audited
7297            /// model refuses it: use [`Self::insert`].
7298            ///
7299            /// # Errors
7300            /// As [`Self::insert`].
7301            pub async fn insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7302                &self,
7303                _executor: _E,
7304            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7305                #refuse_insert
7306                self.__rustango_insert_exec(_executor).await
7307            }
7308
7309            #[doc(hidden)]
7310            pub async fn __rustango_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7311                &self,
7312                _executor: _E,
7313            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7314                #root::sql::__macro_internals::insert_owned_on(
7315                    _executor,
7316                    self.__rustango_insert_query(),
7317                )
7318                .await
7319            }
7320
7321            /// The INSERT for this row.
7322            #[doc(hidden)]
7323            pub fn __rustango_insert_query(&self) -> #root::core::InsertQuery {
7324                #root::core::InsertQuery::new(
7325                    <Self as #root::core::Model>::SCHEMA,
7326                    ::std::vec![ #( #insert_columns ),* ],
7327                    ::std::vec![ #( #insert_values ),* ],
7328                )
7329            }
7330
7331            /// Old name of [`Self::insert`].
7332            ///
7333            /// # Errors
7334            /// As [`Self::insert`].
7335            #[deprecated(since = "0.62.0", note = "use `insert`")]
7336            pub async fn insert_pool(
7337                #pool_self,
7338                pool: &#root::sql::Pool,
7339            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7340                self.insert(pool).await
7341            }
7342        }
7343    };
7344
7345    // `bulk_insert(&Pool)`: audited models write their rows and one
7346    // `Create` entry each in one transaction.
7347    let bulk_pool_body = if audited_fields.is_some() {
7348        quote! {
7349            let mut _tx = #root::sql::__macro_internals::write_tx(pool).await?;
7350            Self::__rustango_bulk_insert_exec(rows, &mut *_tx).await?;
7351            // MySQL has no RETURNING: a database-filled PK stays unset.
7352            if rows.iter().any(|_row| _row.__rustango_audit_pk().is_empty()) {
7353                return ::core::result::Result::Err(#root::sql::ExecError::AuditUnsupported {
7354                    table: <Self as #root::core::Model>::SCHEMA.table,
7355                    reason: "MySQL has no RETURNING to read the written rows",
7356                });
7357            }
7358            let _entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
7359                .iter()
7360                .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
7361                .collect();
7362            #root::audit::__emit_many_tx(&mut _tx, &_entries).await?;
7363            _tx.commit().await?;
7364            ::core::result::Result::Ok(())
7365        }
7366    } else {
7367        quote!(Self::__rustango_bulk_insert_exec(rows, pool).await)
7368    };
7369    let refuse_bulk = refuse("bulk_insert_on");
7370    let bulk_insert_method = if fields.has_auto {
7371        let cols_no_auto = &fields.bulk_columns_no_auto;
7372        let cols_all = &fields.bulk_columns_all;
7373        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7374        let pushes_all = &fields.bulk_pushes_all;
7375        let returning_cols = &fields.returning_cols;
7376        let uniformity = &fields.bulk_auto_uniformity;
7377        let fill_in_place = &fields.bulk_fill_in_place;
7378        // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7379        let first_auto_ident = fields
7380            .first_db_auto
7381            .as_ref()
7382            .map(|(ident, _)| ident)
7383            .or(fields.first_auto_ident.as_ref())
7384            .expect("has_auto implies first_auto_ident is Some");
7385        quote! {
7386            /// Bulk-insert `rows`: one statement, or one per chunk in one
7387            /// transaction past the bind cap. Every row's `Auto<T>` PK must
7388            /// be `Auto::Unset` (the database fills it) or every one `Set`.
7389            /// Each row's `Auto` fields are read back from `RETURNING`; MySQL
7390            /// has none, so it leaves them unset. An empty slice is a no-op.
7391            ///
7392            /// # Errors
7393            /// Returns [`#root::sql::ExecError`] for validation,
7394            /// SQL-writing, mixed-Auto rejection, or driver failures.
7395            pub async fn bulk_insert(
7396                rows: &mut [Self],
7397                pool: &#root::sql::Pool,
7398            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7399                #bulk_pool_body
7400            }
7401
7402            /// [`Self::bulk_insert`] on any [`#root::sql::SqlExecutor`]. An
7403            /// audited model refuses it: use [`Self::bulk_insert`].
7404            ///
7405            /// # Errors
7406            /// As [`Self::bulk_insert`].
7407            pub async fn bulk_insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7408                rows: &mut [Self],
7409                _executor: _E,
7410            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7411                #refuse_bulk
7412                Self::__rustango_bulk_insert_exec(rows, _executor).await
7413            }
7414
7415            #[doc(hidden)]
7416            pub async fn __rustango_bulk_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7417                rows: &mut [Self],
7418                _executor: _E,
7419            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7420                if rows.is_empty() {
7421                    return ::core::result::Result::Ok(());
7422                }
7423                for _row in rows.iter_mut() {
7424                    #( #fill_in_place )*
7425                }
7426                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7427                let _query = #root::core::BulkInsertQuery::new(
7428                    <Self as #root::core::Model>::SCHEMA,
7429                    _columns,
7430                    _all_rows,
7431                )
7432                .returning(::std::vec![ #( #returning_cols ),* ]);
7433                let _returned =
7434                    #root::sql::__macro_internals::bulk_insert_owned_on(_executor, _query).await?;
7435                if _returned.is_empty() {
7436                    return ::core::result::Result::Ok(());
7437                }
7438                if _returned.len() != rows.len() {
7439                    return ::core::result::Result::Err(#root::sql::ExecError::Sql(
7440                        #root::sql::SqlError::BulkInsertReturningMismatch {
7441                            expected: rows.len(),
7442                            actual: _returned.len(),
7443                        },
7444                    ));
7445                }
7446                for (_row, _row_mut) in _returned.into_iter().zip(rows.iter_mut()) {
7447                    #root::sql::apply_auto_pk(_row, _row_mut)?;
7448                }
7449                ::core::result::Result::Ok(())
7450            }
7451
7452            /// Columns and per-row values for every bulk write. A database-filled
7453            /// `Auto` is left out when every row leaves it `Unset`, written when
7454            /// every row sets it; a mix is `BulkAutoMixed`.
7455            #[doc(hidden)]
7456            pub fn __rustango_bulk_rows(
7457                rows: &[Self],
7458            ) -> ::core::result::Result<
7459                (
7460                    ::std::vec::Vec<&'static str>,
7461                    ::std::vec::Vec<::std::vec::Vec<#root::core::SqlValue>>,
7462                ),
7463                #root::sql::ExecError,
7464            > {
7465                let _first_unset = match rows.first() {
7466                    ::core::option::Option::Some(_r) => {
7467                        matches!(_r.#first_auto_ident, #root::sql::Auto::Unset)
7468                    }
7469                    ::core::option::Option::None => true,
7470                };
7471                #( #uniformity )*
7472                let mut _all_rows: ::std::vec::Vec<
7473                    ::std::vec::Vec<#root::core::SqlValue>,
7474                > = ::std::vec::Vec::with_capacity(rows.len());
7475                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7476                    for _row in rows.iter() {
7477                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7478                            ::std::vec::Vec::new();
7479                        #( #pushes_no_auto )*
7480                        _all_rows.push(_row_vals);
7481                    }
7482                    ::std::vec![ #( #cols_no_auto ),* ]
7483                } else {
7484                    for _row in rows.iter() {
7485                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7486                            ::std::vec::Vec::new();
7487                        #( #pushes_all )*
7488                        _all_rows.push(_row_vals);
7489                    }
7490                    ::std::vec![ #( #cols_all ),* ]
7491                };
7492                ::core::result::Result::Ok((_columns, _all_rows))
7493            }
7494        }
7495    } else {
7496        let cols_all = &fields.bulk_columns_all;
7497        let pushes_all = &fields.bulk_pushes_all;
7498        quote! {
7499            /// Bulk-insert `rows`: one statement, or one per chunk in one
7500            /// transaction past the bind cap. An empty slice is a no-op.
7501            ///
7502            /// # Errors
7503            /// Returns [`#root::sql::ExecError`] for validation,
7504            /// SQL-writing, or driver failures.
7505            pub async fn bulk_insert(
7506                rows: &[Self],
7507                pool: &#root::sql::Pool,
7508            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7509                #bulk_pool_body
7510            }
7511
7512            /// [`Self::bulk_insert`] on any [`#root::sql::SqlExecutor`]. An
7513            /// audited model refuses it: use [`Self::bulk_insert`].
7514            ///
7515            /// # Errors
7516            /// As [`Self::bulk_insert`].
7517            pub async fn bulk_insert_on<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7518                rows: &[Self],
7519                _executor: _E,
7520            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7521                #refuse_bulk
7522                Self::__rustango_bulk_insert_exec(rows, _executor).await
7523            }
7524
7525            #[doc(hidden)]
7526            pub async fn __rustango_bulk_insert_exec<'_c, _E: #root::sql::SqlExecutor<'_c>>(
7527                rows: &[Self],
7528                _executor: _E,
7529            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7530                if rows.is_empty() {
7531                    return ::core::result::Result::Ok(());
7532                }
7533                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7534                let _query = #root::core::BulkInsertQuery::new(
7535                    <Self as #root::core::Model>::SCHEMA,
7536                    _columns,
7537                    _all_rows,
7538                );
7539                #root::sql::__macro_internals::bulk_insert_owned_on(_executor, _query)
7540                    .await
7541                    .map(drop)
7542            }
7543
7544            /// Columns and per-row values for every bulk write.
7545            #[doc(hidden)]
7546            pub fn __rustango_bulk_rows(
7547                rows: &[Self],
7548            ) -> ::core::result::Result<
7549                (
7550                    ::std::vec::Vec<&'static str>,
7551                    ::std::vec::Vec<::std::vec::Vec<#root::core::SqlValue>>,
7552                ),
7553                #root::sql::ExecError,
7554            > {
7555                let mut _all_rows: ::std::vec::Vec<
7556                    ::std::vec::Vec<#root::core::SqlValue>,
7557                > = ::std::vec::Vec::with_capacity(rows.len());
7558                for _row in rows.iter() {
7559                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7560                        ::std::vec::Vec::new();
7561                    #( #pushes_all )*
7562                    _all_rows.push(_row_vals);
7563                }
7564                ::core::result::Result::Ok((::std::vec![ #( #cols_all ),* ], _all_rows))
7565            }
7566        }
7567    };
7568
7569    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7570    // (no postgres-feature gate); routes through the existing
7571    // `bulk_insert_pool` + per-dialect conflict writer. Its columns come
7572    // from `__rustango_bulk_rows`, as `bulk_insert`'s do.
7573    // Audited models audit each written row; MySQL, with no RETURNING,
7574    // is refused there (#1747, #1795).
7575    let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7576        (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7577        (true, true) => quote! {
7578            #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7579                _r.__rustango_audit_entry(_op)
7580            })
7581            .await
7582        },
7583        (true, false) => quote! {
7584            ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7585                table: <Self as #root::core::Model>::SCHEMA.table,
7586            })
7587        },
7588    };
7589    let bulk_upsert_pool_method = {
7590        let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7591            fields.uuid_v7_fields.iter().cloned().unzip();
7592        quote! {
7593            /// Tri-dialect bulk insert that updates on conflict — the
7594            /// "import a batch idempotently" shape. Issue #267 / T1.5.
7595            ///
7596            /// Per-row values are extracted and lowered into a
7597            /// [`#root::core::BulkInsertQuery`] with
7598            /// `on_conflict = DoUpdate { target, update_columns }`. The
7599            /// writer dispatches per-dialect:
7600            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7601            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7602            ///   `target` is not the only unique key, since MySQL matches every UNIQUE index
7603            ///
7604            /// `target` names the column(s) whose unique constraint
7605            /// defines the conflict (typically a `unique` or
7606            /// `unique_together` natural-key column, NOT the `Auto<T>`
7607            /// PK). `update_cols` names the columns to overwrite on
7608            /// conflict — every other column is left untouched on the
7609            /// existing row.
7610            ///
7611            /// A database-filled `Auto` PK follows [`Self::bulk_insert`]: all
7612            /// `Unset` leaves it to the database, all `Set` writes it (so
7613            /// `target = ["id"]` updates by id), a mix is `BulkAutoMixed`.
7614            /// Rows repeating a `target` key: the last one wins.
7615            /// Empty slice is a no-op. On an audited model each written
7616            /// row gets a `create` or `update` audit row; MySQL refuses it.
7617            ///
7618            /// # Errors
7619            /// Returns [`#root::sql::ExecError`] for validation,
7620            /// SQL-writing, or driver failures.
7621            pub async fn bulk_upsert_pool(
7622                rows: &[Self],
7623                target: &[&'static str],
7624                update_cols: &[&'static str],
7625                pool: &#root::sql::Pool,
7626            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7627                if rows.is_empty() {
7628                    return ::core::result::Result::Ok(());
7629                }
7630                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7631                #[allow(unused_mut)]
7632                let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7633                #(
7634                    if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7635                        _generated.push(#gen_cols);
7636                    }
7637                )*
7638                let _query = #root::core::BulkInsertQuery::new(
7639                    <Self as #root::core::Model>::SCHEMA,
7640                    _columns,
7641                    _all_rows,
7642                )
7643                .on_conflict_do_update(target, update_cols)
7644                .generated(_generated);
7645                #run_conflict_bulk_insert
7646            }
7647
7648            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7649            /// skip rows that would violate a unique constraint. Issue
7650            /// #267 / T1.5. Same per-dialect dispatch as
7651            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7652            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7653            /// <pivot> = <pivot>` (MySQL no-op write).
7654            ///
7655            /// # Errors
7656            /// As [`Self::bulk_upsert_pool`].
7657            pub async fn bulk_insert_or_ignore_pool(
7658                rows: &[Self],
7659                pool: &#root::sql::Pool,
7660            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7661                if rows.is_empty() {
7662                    return ::core::result::Result::Ok(());
7663                }
7664                let (_columns, _all_rows) = Self::__rustango_bulk_rows(rows)?;
7665                let _query = #root::core::BulkInsertQuery::new(
7666                    <Self as #root::core::Model>::SCHEMA,
7667                    _columns,
7668                    _all_rows,
7669                )
7670                .on_conflict_do_nothing();
7671                #run_conflict_bulk_insert
7672            }
7673        }
7674    };
7675
7676    // Ergonomic `Model::bulk_update(objs, fields)`.
7677    // The SQL/IR/executor stack
7678    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7679    // `write_bulk_update_*` writers) already existed; what was missing
7680    // was the per-model constructor that maps `&[Self]` + a runtime
7681    // column list into rows of `[pk, col_vals…]` so callers don't
7682    // hand-build the IR. Emitted only when the model has a primary key
7683    // (the PK is the join key and can't itself be updated).
7684    let bulk_update_rows_run = if audited_fields.is_some() {
7685        quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7686            _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7687        })
7688        .await)
7689    } else {
7690        quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7691    };
7692    let bulk_update_method = match &fields.primary_key {
7693        None => quote! {},
7694        Some((pk_ident, pk_col)) => {
7695            // One pair of match arms per non-PK column: a validation arm
7696            // resolving the runtime name to its `&'static str` column,
7697            // and a value arm pushing that field off the row.
7698            let mut col_arms: Vec<TokenStream2> = Vec::new();
7699            let mut val_arms: Vec<TokenStream2> = Vec::new();
7700            for entry in &fields.column_entries {
7701                if &entry.column == pk_col {
7702                    continue;
7703                }
7704                let col = &entry.column;
7705                let ident = &entry.ident;
7706                col_arms.push(quote! { #col => #col, });
7707                val_arms.push(quote! {
7708                    #col => _row_vals.push(
7709                        ::core::convert::Into::<#root::core::SqlValue>::into(
7710                            ::core::clone::Clone::clone(&_o.#ident)
7711                        )
7712                    ),
7713                });
7714            }
7715            quote! {
7716                /// Write per-row-different values for the named `fields`
7717                /// across every object in `objs`, matched by primary key.
7718                /// Past the backend's bind cap the rows go in batches inside
7719                /// one transaction, so the update is all or none.
7720                ///
7721                /// `fields` names the **columns** to update. The primary
7722                /// key identifies each row and cannot itself be updated
7723                /// (pass it and you get
7724                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7725                /// Empty `objs` or `fields` is a no-op returning `0`.
7726                /// Objects whose PK matches no row are simply not updated.
7727                /// Returns the number of rows affected.
7728                ///
7729                /// Tri-dialect: lowers to a
7730                /// [`#root::core::BulkUpdateQuery`] and dispatches
7731                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7732                /// a CTE + correlated subquery on SQLite, an inner
7733                /// `JOIN (VALUES …)` on MySQL.
7734                ///
7735                /// # Errors
7736                /// [`#root::core::QueryError::UnknownField`] for a name
7737                /// that isn't a column on this model,
7738                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7739                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7740                /// SQL-writing / driver failures.
7741                pub async fn bulk_update(
7742                    objs: &[Self],
7743                    fields: &[&str],
7744                    pool: &#root::sql::Pool,
7745                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7746                    if objs.is_empty() || fields.is_empty() {
7747                        return ::core::result::Result::Ok(0);
7748                    }
7749                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7750                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7751                        ::std::vec::Vec::with_capacity(fields.len());
7752                    for &_f in fields {
7753                        let _col: &'static str = match _f {
7754                            #pk_col => {
7755                                return ::core::result::Result::Err(
7756                                    ::core::convert::Into::into(
7757                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7758                                            model: _model_name,
7759                                            field: ::std::string::ToString::to_string(_f),
7760                                        }
7761                                    )
7762                                );
7763                            }
7764                            #( #col_arms )*
7765                            _ => {
7766                                return ::core::result::Result::Err(
7767                                    ::core::convert::Into::into(
7768                                        #root::core::QueryError::UnknownField {
7769                                            model: _model_name,
7770                                            field: ::std::string::ToString::to_string(_f),
7771                                        }
7772                                    )
7773                                );
7774                            }
7775                        };
7776                        // A model whose only field is the primary key emits no
7777                        // non-PK arms, so every arm above diverges and this push
7778                        // is genuinely unreachable — correct, since such a model
7779                        // has nothing to bulk-update. Scoped to the statement
7780                        // rather than the fn so a real unreachability elsewhere
7781                        // in this body still warns. Without it every PK-only
7782                        // model warns in the *user's* crate, which they cannot
7783                        // act on (#1290).
7784                        #[allow(unreachable_code)]
7785                        _update_columns.push(_col);
7786                    }
7787                    let mut _rows: ::std::vec::Vec<
7788                        ::std::vec::Vec<#root::core::SqlValue>,
7789                    > = ::std::vec::Vec::with_capacity(objs.len());
7790                    for _o in objs.iter() {
7791                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7792                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7793                        // PK first — the writers expect `[pk, …update cols]`.
7794                        _row_vals.push(
7795                            ::core::convert::Into::<#root::core::SqlValue>::into(
7796                                ::core::clone::Clone::clone(&_o.#pk_ident)
7797                            )
7798                        );
7799                        for &_f in fields {
7800                            match _f {
7801                                #( #val_arms )*
7802                                // Unreachable: every name was validated
7803                                // against the same arm set above.
7804                                _ => {}
7805                            }
7806                        }
7807                        _rows.push(_row_vals);
7808                    }
7809                    let _query = #root::core::BulkUpdateQuery::new(
7810                        <Self as #root::core::Model>::SCHEMA,
7811                        _update_columns,
7812                        _rows,
7813                    );
7814                    #bulk_update_rows_run
7815                }
7816            }
7817        }
7818    };
7819
7820    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7821        quote! {
7822            /// Hidden runtime accessor for the primary-key value as a
7823            /// [`SqlValue`]. Used by reverse-relation helpers
7824            /// (`<parent>::<child>_set`) emitted from sibling models'
7825            /// FK fields. Not part of the public API.
7826            #[doc(hidden)]
7827            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7828                ::core::convert::Into::<#root::core::SqlValue>::into(
7829                    ::core::clone::Clone::clone(&self.#pk_ident)
7830                )
7831            }
7832        }
7833    });
7834
7835    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7836        let column_arms = fields.column_entries.iter().map(|entry| {
7837            let col = &entry.column;
7838            let ident = &entry.ident;
7839            quote! {
7840                #col => ::core::option::Option::Some(
7841                    ::core::convert::Into::<#root::core::SqlValue>::into(
7842                        ::core::clone::Clone::clone(&self.#ident)
7843                    )
7844                ),
7845            }
7846        });
7847        quote! {
7848            impl #root::sql::HasPkValue for #struct_name {
7849                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7850                    ::core::convert::Into::<#root::core::SqlValue>::into(
7851                        ::core::clone::Clone::clone(&self.#pk_ident)
7852                    )
7853                }
7854                fn __rustango_column_value(
7855                    &self,
7856                    column: &str,
7857                ) -> ::core::option::Option<#root::core::SqlValue> {
7858                    match column {
7859                        #( #column_arms )*
7860                        _ => ::core::option::Option::None,
7861                    }
7862                }
7863            }
7864        }
7865    });
7866
7867    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7868
7869    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7870    // dispatch to the right per-backend body without the macro emitting
7871    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7872    // so audited models with non-Auto PKs (which still go through
7873    // `insert_one_with_audit` → `apply_auto_pk`) link.
7874    let assign_auto_pk_pool_impl = {
7875        let auto_assigns = &fields.auto_assigns;
7876        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7877        // so the body is structurally identical to `auto_assigns` —
7878        // only the helper name swaps from `try_get_returning` to
7879        // `try_get_returning_sqlite` so the closure typechecks against
7880        // a `SqliteRow` instead of a `PgRow`.
7881        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7882            .auto_field_idents
7883            .iter()
7884            .map(|(ident, column, uuid)| {
7885                // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
7886                if *uuid {
7887                    return quote! {
7888                        self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
7889                            _returning_row, #column
7890                        )?);
7891                    };
7892                }
7893                quote! {
7894                    self.#ident = #root::sql::try_get_returning_sqlite(
7895                        _returning_row, #column
7896                    )?;
7897                }
7898            })
7899            .collect();
7900        // #1028 — decode each `generated_as` column from the same
7901        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7902        // own type drives the decode (no `Auto<T>` wrapper).
7903        let generated_assigns: Vec<TokenStream2> = fields
7904            .generated_field_idents
7905            .iter()
7906            .map(|(ident, column)| {
7907                quote! {
7908                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7909                }
7910            })
7911            .collect();
7912        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7913            .generated_field_idents
7914            .iter()
7915            .map(|(ident, column)| {
7916                quote! {
7917                    self.#ident = #root::sql::try_get_returning_sqlite(
7918                        _returning_row, #column
7919                    )?;
7920                }
7921            })
7922            .collect();
7923        let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
7924            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7925            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7926            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7927            // those models can't use AUTO_INCREMENT). The trait is only
7928            // touched on the MySQL arm at runtime, so PG-only consumers
7929            // never see the bound failure.
7930            //
7931            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7932            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7933            // runtime with "multi-column RETURNING". MySQL has no
7934            // multi-column RETURNING semantic and a follow-up SELECT
7935            // would need cross-trait plumbing. Pragmatic shape: succeed
7936            // with the FIRST Auto field populated from LAST_INSERT_ID();
7937            // any other Auto fields stay `Auto::Unset`. Callers that
7938            // need the DB-defaulted timestamp / UUID can re-fetch the
7939            // row by PK after `save_pool`. Fixes the cookbook chapter
7940            // 12 dialect divergence.
7941            quote! {
7942                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7943                    ::rustango_from_mysql_auto_id(_id)?;
7944                self.#first = #root::sql::Auto::Set(_converted);
7945                ::core::result::Result::Ok(())
7946            }
7947        } else {
7948            quote! {
7949                let _ = _id;
7950                ::core::result::Result::Ok(())
7951            }
7952        };
7953        quote! {
7954            impl #root::sql::AssignAutoPkPool for #struct_name {
7955                fn __rustango_assign_from_pg_row(
7956                    &mut self,
7957                    _returning_row: &#root::sql::PgReturningRow,
7958                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7959                    #( #auto_assigns )*
7960                    #( #generated_assigns )*
7961                    ::core::result::Result::Ok(())
7962                }
7963                fn __rustango_assign_from_mysql_id(
7964                    &mut self,
7965                    _id: i64,
7966                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7967                    #mysql_body
7968                }
7969                fn __rustango_assign_from_sqlite_row(
7970                    &mut self,
7971                    _returning_row: &#root::sql::SqliteReturningRow,
7972                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7973                    #( #auto_assigns_sqlite )*
7974                    #( #generated_assigns_sqlite )*
7975                    ::core::result::Result::Ok(())
7976                }
7977            }
7978        }
7979    };
7980
7981    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7982    let aliased_row_helper = quote! {
7983        /// Decode a row's aliased target columns (produced by
7984        /// `select_related`'s LEFT JOIN) into a fresh instance of
7985        /// this model. Reads each column via
7986        /// `format!("{prefix}__{col}")`, matching the alias the
7987        /// SELECT writer emitted. Slice 9.0d.
7988        #[doc(hidden)]
7989        #[cfg(feature = "postgres")]
7990        pub fn __rustango_from_aliased_row(
7991            row: &#root::sql::sqlx::postgres::PgRow,
7992            prefix: &str,
7993        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7994            ::core::result::Result::Ok(Self {
7995                #( #from_aliased_row_inits ),*
7996            })
7997        }
7998    };
7999    // v0.23.0-batch8 — MySQL counterpart, gated through the
8000    // cfg-aware macro_rules so PG-only builds expand to nothing.
8001    let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
8002    let aliased_row_helper_my = quote! {
8003        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
8004            #( #from_aliased_row_inits_my ),*
8005        });
8006    };
8007
8008    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
8009    // pattern + cfg gate on the `sqlite` feature.
8010    let aliased_row_helper_sqlite = quote! {
8011        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
8012            #( #from_aliased_row_inits ),*
8013        });
8014    };
8015
8016    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
8017    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
8018    let load_related_impl_sqlite =
8019        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
8020
8021    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
8022    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
8023    // default `Self::objects()`. Each accessor returns a fresh
8024    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
8025    // methods the user defined.
8026    let extra_manager_fns: Vec<TokenStream2> = manager_fns
8027        .iter()
8028        .map(|fn_ident| {
8029            let model_name_str = struct_name.to_string();
8030            let fn_name_str = fn_ident.to_string();
8031            let doc = format!(
8032                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
8033                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
8034                 equivalent to `Self::objects()`. Chains with any \
8035                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
8036                 extension methods."
8037            );
8038            quote! {
8039                #[doc = #doc]
8040                #[must_use]
8041                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
8042                    #root::query::QuerySet::new()
8043                }
8044            }
8045        })
8046        .collect();
8047
8048    quote! {
8049        impl #struct_name {
8050            /// Start a new `QuerySet` over this model.
8051            #[must_use]
8052            pub fn objects() -> #root::query::QuerySet<#struct_name> {
8053                #root::query::QuerySet::new()
8054            }
8055
8056            /// Eloquent-shape alias of [`Self::objects`]. Returns
8057            /// a fresh `QuerySet<Self>` ready for `.filter()` /
8058            /// `.where_()` / etc. Matches Laravel muscle-memory:
8059            ///
8060            /// ```ignore
8061            /// // Eloquent:    Post::query()->where('published', true)
8062            /// // rustango:    Post::query().filter("published", true)
8063            /// //         or:  Post::objects().filter("published", true)
8064            /// ```
8065            ///
8066            /// Both names point at the same underlying constructor;
8067            /// neither is preferred.
8068            #[must_use]
8069            pub fn query() -> #root::query::QuerySet<#struct_name> {
8070                #root::query::QuerySet::new()
8071            }
8072
8073            #( #extra_manager_fns )*
8074
8075            #insert_method
8076
8077            #bulk_insert_method
8078
8079            #bulk_upsert_pool_method
8080
8081            #bulk_update_method
8082
8083            #save_method
8084
8085            #pk_methods
8086
8087            #pk_value_helper
8088
8089            #aliased_row_helper
8090
8091            #column_consts
8092        }
8093
8094        #aliased_row_helper_my
8095
8096        #aliased_row_helper_sqlite
8097
8098        #load_related_impl
8099
8100        #load_related_impl_my
8101
8102        #load_related_impl_sqlite
8103
8104        #has_pk_value_impl
8105
8106        #fk_pk_access_impl
8107
8108        #assign_auto_pk_pool_impl
8109    }
8110}
8111
8112/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8113fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8114    let lines = entries.iter().map(|e| {
8115        let ident = &e.ident;
8116        let col_ty = column_type_ident(ident);
8117        // Documented so apps with `#![deny(missing_docs)]` build.
8118        let doc = format!("Typed column for the `{}` field.", e.name);
8119        quote! {
8120            #[doc = #doc]
8121            #[allow(non_upper_case_globals)]
8122            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8123        }
8124    });
8125    quote! { #(#lines)* }
8126}
8127
8128/// Emit a hidden per-model module carrying one zero-sized type per field,
8129/// each with a `Column` impl pointing back at the model.
8130fn column_module_tokens(
8131    module_ident: &syn::Ident,
8132    struct_name: &syn::Ident,
8133    entries: &[ColumnEntry],
8134) -> TokenStream2 {
8135    let root = rustango_root();
8136    let items = entries.iter().map(|e| {
8137        let col_ty = column_type_ident(&e.ident);
8138        let value_ty = &e.value_ty;
8139        let name = &e.name;
8140        let column = &e.column;
8141        let field_type_tokens = &e.field_type_tokens;
8142        quote! {
8143            #[derive(::core::clone::Clone, ::core::marker::Copy)]
8144            pub struct #col_ty;
8145
8146            impl #root::core::Column for #col_ty {
8147                type Model = super::#struct_name;
8148                type Value = #value_ty;
8149                const NAME: &'static str = #name;
8150                const COLUMN: &'static str = #column;
8151                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8152            }
8153        }
8154    });
8155    quote! {
8156        #[doc(hidden)]
8157        #[allow(non_camel_case_types, non_snake_case)]
8158        pub mod #module_ident {
8159            // Re-import the parent scope so field types referencing
8160            // sibling models (e.g. `ForeignKey<Author>`) resolve
8161            // inside this submodule. Without this we'd hit
8162            // `proc_macro_derive_resolution_fallback` warnings.
8163            #[allow(unused_imports)]
8164            use super::*;
8165            #(#items)*
8166        }
8167    }
8168}
8169
8170fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8171    let name = ident_name(field_ident);
8172    syn::Ident::new(&format!("{name}_col"), field_ident.span())
8173}
8174
8175/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8176/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8177fn restore_auto_now_unless(
8178    idents: &[syn::Ident],
8179    ret: &TokenStream2,
8180    keep: &TokenStream2,
8181    body: &TokenStream2,
8182) -> TokenStream2 {
8183    if idents.is_empty() {
8184        return body.clone();
8185    }
8186    let prev: Vec<syn::Ident> = (0..idents.len())
8187        .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8188        .collect();
8189    quote! {
8190        #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8191        let _saved: #ret = async { #body }.await;
8192        if !(#keep) {
8193            #( self.#idents = #prev; )*
8194        }
8195        _saved
8196    }
8197}
8198
8199/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8200fn ident_name(ident: &syn::Ident) -> String {
8201    syn::ext::IdentExt::unraw(ident).to_string()
8202}
8203
8204/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8205fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8206    match syn::parse_str::<syn::Ident>(name) {
8207        Ok(mut ident) => {
8208            ident.set_span(span);
8209            ident
8210        }
8211        Err(_) => syn::Ident::new_raw(name, span),
8212    }
8213}
8214
8215fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8216    syn::Ident::new(
8217        &format!("__rustango_cols_{struct_name}"),
8218        struct_name.span(),
8219    )
8220}
8221
8222fn from_row_impl_tokens(
8223    struct_name: &syn::Ident,
8224    from_row_inits: &[TokenStream2],
8225    from_row_inits_my: &[TokenStream2],
8226) -> TokenStream2 {
8227    let root = rustango_root();
8228    // The Postgres impl is always emitted — every rustango build pulls in
8229    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8230    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8231    // macro_rules whose body collapses to nothing when rustango is built
8232    // without the `mysql` feature. No user-facing feature shim required.
8233    //
8234    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8235    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8236    // back into a comma-separated list inside square brackets. Since each
8237    // entry is already in `field: expr` shape, the existing tokens slot in.
8238    quote! {
8239        #[cfg(feature = "postgres")]
8240        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8241            for #struct_name
8242        {
8243            fn from_row(
8244                row: &'r #root::sql::sqlx::postgres::PgRow,
8245            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8246                ::core::result::Result::Ok(Self {
8247                    #( #from_row_inits ),*
8248                })
8249            }
8250        }
8251
8252        #root::__impl_my_from_row!(#struct_name, |row| {
8253            #( #from_row_inits_my ),*
8254        });
8255
8256        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8257            #( #from_row_inits ),*
8258        });
8259    }
8260}
8261
8262struct ContainerAttrs {
8263    table: Option<String>,
8264    display: Option<(String, proc_macro2::Span)>,
8265    /// Explicit app label from `#[rustango(app = "blog")]`.
8266    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8267    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8268    /// at runtime — this attribute is the override for cases where
8269    /// the inference is wrong (e.g. a model that conceptually belongs
8270    /// to one app but is physically in another module).
8271    app: Option<String>,
8272    /// Per-model admin knobs from
8273    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8274    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8275    /// admin code falls back to `AdminConfig::DEFAULT`.
8276    admin: Option<AdminAttrs>,
8277    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8278    /// `None` when the model isn't audited — write paths emit no
8279    /// audit entries. When present, single-row writes capture
8280    /// before/after for the listed fields and bulk writes batch
8281    /// snapshots into one INSERT into `rustango_audit_log`.
8282    audit: Option<AuditAttrs>,
8283    /// `true` when `#[rustango(permissions)]` is present. Signals that
8284    /// `auto_create_permissions` should seed the four CRUD codenames for
8285    /// this model.
8286    permissions: bool,
8287    /// Many-to-many relations declared via
8288    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8289    ///                 src = "post_id", dst = "tag_id"))]`.
8290    m2m: Vec<M2MAttr>,
8291    /// Polymorphic M2M relations declared via
8292    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8293    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8294    ///   related_column = "tag_id"))]` (issue #818).
8295    generic_m2m: Vec<GenericM2MAttr>,
8296    /// Composite indexes declared via
8297    /// `#[rustango(index("col1, col2"))]` or
8298    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8299    /// Single-column indexes from `#[rustango(index)]` on fields are
8300    /// accumulated here during field collection.
8301    indexes: Vec<IndexAttr>,
8302    /// Table-level CHECK constraints declared via
8303    /// `#[rustango(check(name = "…", expr = "…"))]`.
8304    checks: Vec<CheckAttr>,
8305    /// Table-level PG `EXCLUDE` constraints declared via
8306    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8307    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8308    /// migration writer renders them on Postgres and skips with a
8309    /// warning on MySQL/SQLite. Issue #319.
8310    excludes: Vec<ExcludeAttr>,
8311    /// Composite (multi-column) FKs declared via
8312    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8313    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8314    composite_fks: Vec<CompositeFkAttr>,
8315    /// Generic ("any model") FKs declared via
8316    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8317    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8318    generic_fks: Vec<GenericFkAttr>,
8319    /// Where this model lives in a tenancy deployment, declared via
8320    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8321    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8322    /// to partition diff output between registry-scoped and
8323    /// tenant-scoped migration files.
8324    scope: Option<String>,
8325    /// `#[rustango(access = "open" | "admin_only" | "never")]`: which
8326    /// generic surfaces may serve the table. Unset emits `None`.
8327    access: Option<String>,
8328    /// Extra QuerySet accessor names from
8329    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8330    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8331    /// the default `Self::objects()`. Multiple attributes allowed.
8332    manager_fns: Vec<syn::Ident>,
8333    /// Default ordering declared via `#[rustango(default_order =
8334    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8335    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8336    /// `-` prefix means descending; the `+` prefix or no prefix means
8337    /// ascending.
8338    default_order: Vec<(String, bool, proc_macro2::Span)>,
8339    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8340    /// Routes the emitted schema's `is_view = true` so the migration
8341    /// snapshot skips this model (its underlying SQL view is operator-
8342    /// managed, not rustango-managed).
8343    is_view: bool,
8344    /// `#[rustango(managed = false)]` — who owns the table.
8345    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8346    /// the migration snapshot skips this model so `makemigrations` /
8347    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8348    /// TABLE` against it (operator-managed schema).
8349    managed: bool,
8350    /// Name of the model's base manager, from
8351    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8352    /// `ModelSchema::base_manager_name`. Declarative-only today.
8353    base_manager_name: Option<String>,
8354    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8355    /// field this model's instances are ordered relative to.
8356    /// Declarative-only today; threaded onto
8357    /// `ModelSchema::order_with_respect_to`.
8358    order_with_respect_to: Option<String>,
8359    /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8360    /// Marks the model as a proxy that
8361    /// shares its DB table with another struct. Threaded into
8362    /// `ModelSchema::proxy` so future codegen can skip table-owning
8363    /// behavior for proxies.
8364    proxy: bool,
8365    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8366    /// Each comma-separated capability token surfaces on
8367    /// `ModelSchema::required_db_features` so `manage check --deploy`
8368    /// can warn when the active dialect lacks one.
8369    required_db_features: Vec<String>,
8370    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8371    /// Normalized to the dialect name `manage check --deploy`
8372    /// compares against `Settings.database.backend`. Aliases
8373    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8374    /// stored under the canonical name.
8375    required_db_vendor: Option<String>,
8376    /// `#[rustango(default_related_name = "...")]`. Threaded into
8377    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8378    /// name to use when an FK / M2M field doesn't override it.
8379    /// Today rustango doesn't auto-emit reverse managers; the
8380    /// metadata is the foundation for that work.
8381    default_related_name: Option<String>,
8382    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8383    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8384    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8385    /// inline `COMMENT='...'`, SQLite: no-op).
8386    db_table_comment: Option<String>,
8387    /// `#[rustango(get_latest_by = "created_at")]` /
8388    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8389    /// `(column, descending)` where `descending = true` when the
8390    /// attribute value starts with `-`. Threaded into
8391    /// `ModelSchema::get_latest_by`.
8392    get_latest_by: Option<(String, bool, proc_macro2::Span)>,
8393    /// Extra `(codename, label)` permissions
8394    /// from `#[rustango(extra_permissions = "approve:Can approve,
8395    /// archive:Can archive")]`. Comma-separated `codename:label`
8396    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8397    extra_permissions: Vec<(String, String)>,
8398    /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8399    /// `"view"`) the framework
8400    /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8401    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8402    /// Validated at parse time; unknown actions fail with a span-pointing
8403    /// error.
8404    default_permissions: Vec<String>,
8405    /// `#[rustango(verbose_name = "blog post")]` — the
8406    /// human-readable singular label for the model. Threaded into
8407    /// `ModelSchema::verbose_name` so admin section headers /
8408    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8409    /// over the Rust struct identifier.
8410    verbose_name: Option<String>,
8411    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8412    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8413    /// When unset, `display_label_plural()` auto-suffixes `s`.
8414    verbose_name_plural: Option<String>,
8415    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8416    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8417    /// a name (used by `QuerySet::without_global_scope`) with a
8418    /// `fn() -> WhereExpr` path that the macro emits into
8419    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8420    global_scopes: Vec<GlobalScopeAttr>,
8421    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8422    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8423    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8424    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8425    /// entry emits an inherent `<name>_through(&self)` accessor that
8426    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8427    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8428    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8429    through_relations: Vec<ThroughAttr>,
8430    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8431    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8432    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8433    /// Each entry emits two associated functions on the parent —
8434    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8435    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8436    /// over a correlated subquery against the child table. Users
8437    /// drop the result into `QuerySet::where_raw(...)`.
8438    reverse_has_relations: Vec<ReverseHasAttr>,
8439    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8440    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8441    /// entry so the relation-existence family resolves polymorphic
8442    /// children by name.
8443    generic_has_relations: Vec<GenericHasAttr>,
8444}
8445
8446/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8447/// declaration. Each entry becomes one `core::GlobalScope` in the
8448/// emitted schema literal; `apply` resolves at macro-expand time
8449/// against the consumer's scope so the function must be in scope at
8450/// the use site. Issue #820.
8451struct GlobalScopeAttr {
8452    name: String,
8453    apply: syn::Path,
8454}
8455
8456/// Parsed `#[rustango(through(...))]` declaration. Issue
8457/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8458/// `hasManyThrough` / `hasOneThrough` parity.
8459///
8460/// `Country hasManyThrough Post via User` declares as:
8461///
8462/// ```ignore
8463/// #[rustango(through(
8464///     name                   = "posts",
8465///     far                    = "Post",
8466///     far_fk_column          = "author_id",
8467///     intermediate           = "User",
8468///     intermediate_fk_column = "country_id",
8469/// ))]
8470/// struct Country { ... }
8471/// ```
8472///
8473/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8474/// which returns a queryset filtered via a correlated subquery —
8475/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8476/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8477/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8478/// because the subquery lives inside a `WhereExpr::InSubquery` node
8479/// and the outer queryset's pending list stays empty.
8480///
8481/// All four required arguments use **SQL column / table names**
8482/// (not Rust field names) to sidestep the multi-hop-filter substrate
8483/// gap. Once that substrate lands, a higher-level Rust-field-name
8484/// shorthand can be added without breaking this surface.
8485struct ThroughAttr {
8486    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8487    name: String,
8488    /// Far model type identifier. `far = "Post"` → returns
8489    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8490    /// the derive expands.
8491    far: syn::Ident,
8492    /// SQL column on the far model's table that references the
8493    /// intermediate model's primary key. For `Post`'s
8494    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8495    /// default `<field>_id` convention) or whatever the user
8496    /// declared via `#[rustango(db_column = "...")]`.
8497    far_fk_column: String,
8498    /// Intermediate model type identifier. Needed to look up its
8499    /// `SCHEMA` so the subquery's `FROM` clause points at the
8500    /// intermediate table. `intermediate = "User"`.
8501    intermediate: syn::Ident,
8502    /// SQL column on the intermediate's table that references the
8503    /// source (this) model's primary key. For `User`'s
8504    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8505    intermediate_fk_column: String,
8506    /// SQL primary-key column on the intermediate's table — the
8507    /// column the subquery projects. Optional; defaults to `"id"`
8508    /// (rustango's default PK column name). Override when the
8509    /// intermediate declares a custom PK column.
8510    intermediate_pk_column: String,
8511}
8512
8513/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8514/// child_fk_column = "..."))]` declaration. Issue
8515/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8516/// `whereHas` / `whereDoesntHave` parity.
8517///
8518/// `Post hasMany Comment` declares as:
8519///
8520/// ```ignore
8521/// #[rustango(reverse_has(
8522///     name             = "comments",
8523///     child            = "Comment",
8524///     child_fk_column  = "post_id",
8525/// ))]
8526/// struct Post { ... }
8527/// ```
8528///
8529/// The macro emits two associated functions on `Post`:
8530///
8531/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8532///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8533///   <outer>.<self_pk_column>)`.
8534/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8535///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8536///
8537/// User code:
8538///
8539/// ```ignore
8540/// // Posts with at least one comment:
8541/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8542/// // Posts with no comments:
8543/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8544/// ```
8545///
8546/// As with #817, all column / table identifiers are **SQL names**
8547/// (not Rust field names) so the substrate is independent of the
8548/// outstanding multi-hop filter resolver gap. The emitted
8549/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8550/// SQL-emit time via the writer's scope stack.
8551struct ReverseHasAttr {
8552    /// Accessor name. `name = "comments"` → emits
8553    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8554    name: String,
8555    /// Child model type identifier. `child = "Comment"` — needed to
8556    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8557    /// points at the child table.
8558    child: syn::Ident,
8559    /// SQL column on the child's table that references this model's
8560    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8561    /// column is `"post_id"`.
8562    child_fk_column: String,
8563    /// Column on **this** model's table the child FK references — the
8564    /// `OuterRef` target. Defaults to the PK column (`"id"` without one).
8565    self_pk_column: Option<String>,
8566    /// Where an explicit `self_pk_column` was written, for its error.
8567    self_pk_span: Option<proc_macro2::Span>,
8568}
8569
8570impl ReverseHasAttr {
8571    fn self_pk_column(&self) -> &str {
8572        self.self_pk_column.as_deref().unwrap_or("id")
8573    }
8574}
8575
8576/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8577/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8578/// relation-existence family (issue #830). The child is a polymorphic,
8579/// content-type-discriminated model; emits a `Model::
8580/// generic_reverse_relations()` entry the queryset resolves by name.
8581struct GenericHasAttr {
8582    /// Accessor name, e.g. `name = "tags"`.
8583    name: String,
8584    /// Child model type identifier (`child = "Tag"`) — looked up for its
8585    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8586    child: syn::Ident,
8587    /// Column on the child table holding the parent's content-type id.
8588    /// Optional; defaults to `"content_type_id"`.
8589    ct_column: String,
8590    /// Column on the child table holding the parent's PK value.
8591    /// Optional; defaults to `"object_pk"`.
8592    pk_column: String,
8593    /// SQL primary-key column on **this** (parent) model's table.
8594    /// Defaults to the PK column (`"id"` without one).
8595    self_pk_column: Option<String>,
8596    /// Where an explicit `self_pk_column` was written, for its error.
8597    self_pk_span: Option<proc_macro2::Span>,
8598}
8599
8600impl GenericHasAttr {
8601    fn self_pk_column(&self) -> &str {
8602        self.self_pk_column.as_deref().unwrap_or("id")
8603    }
8604}
8605
8606/// Parsed form of one index declaration (field-level or container-level).
8607struct IndexAttr {
8608    /// The declaring attribute, for diagnostics.
8609    span: proc_macro2::Span,
8610    /// Index name; auto-derived when `None` at parse time.
8611    name: Option<String>,
8612    /// Column names in the index.
8613    columns: Vec<String>,
8614    /// `true` for `CREATE UNIQUE INDEX`.
8615    unique: bool,
8616    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8617    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8618    /// `"btree"` when the attribute is absent — the DDL writer omits
8619    /// the `USING` clause and the backend uses its own default
8620    /// (btree on every supported dialect).
8621    method: String,
8622    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8623    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8624    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8625    where_clause: Option<String>,
8626    /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8627    /// Empty `Vec` (the default) means "no covering columns".
8628    include: Vec<String>,
8629}
8630
8631/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8632struct CheckAttr {
8633    name: String,
8634    expr: String,
8635}
8636
8637/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8638/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8639/// PG-only — surfaced on every backend in the macro emit; the migration
8640/// writer skips the op on MySQL/SQLite. Issue #319.
8641struct ExcludeAttr {
8642    /// Constraint name (free-form Rust identifier).
8643    name: String,
8644    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8645    using: String,
8646    /// Comma-separated `(column, operator)` pairs, in declaration
8647    /// order. Parsed from `"col WITH op, col WITH op"`.
8648    elements: Vec<(String, String)>,
8649    /// Optional `WHERE` predicate (raw SQL).
8650    where_clause: Option<String>,
8651}
8652
8653/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8654/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8655/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8656/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8657/// the model, not the field.
8658struct CompositeFkAttr {
8659    /// Logical relation name (free-form Rust identifier).
8660    name: String,
8661    /// SQL table name of the target.
8662    to: String,
8663    /// Source-side column names, in declaration order.
8664    from: Vec<String>,
8665    /// Target-side column names, same length / order as `from`.
8666    on: Vec<String>,
8667}
8668
8669/// Parsed form of one `#[rustango(generic_fk(name = "target",
8670/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8671/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8672/// generic ("any model") FKs live on the model, not the field.
8673struct GenericFkAttr {
8674    /// Logical relation name (free-form Rust identifier).
8675    name: String,
8676    /// Source-side column carrying the `content_type_id` value.
8677    ct_column: String,
8678    /// Source-side column carrying the target row's primary key.
8679    pk_column: String,
8680}
8681
8682/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8683struct M2MAttr {
8684    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8685    name: String,
8686    /// Target table (e.g. `"app_tags"`).
8687    to: String,
8688    /// Junction table (e.g. `"post_tags"`).
8689    through: String,
8690    /// Source FK column in the junction table (e.g. `"post_id"`).
8691    src: String,
8692    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8693    dst: String,
8694    /// Whether the migration writer should auto-create the junction
8695    /// table. Default `true`. Set `auto_create = false` (#324) when
8696    /// the operator declares the through table as its own
8697    /// `#[derive(Model)]` struct with extra columns.
8698    auto_create: bool,
8699}
8700
8701/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8702/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8703/// junction carries a ContentType discriminator so unrelated models
8704/// share one pivot + related set.
8705struct GenericM2MAttr {
8706    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8707    name: String,
8708    /// Polymorphic junction table (e.g. `"taggables"`).
8709    through: String,
8710    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8711    pk_column: String,
8712    /// Junction column holding the owning model's `content_type_id`
8713    /// discriminator (e.g. `"taggable_type"`).
8714    ct_column: String,
8715    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8716    related_column: String,
8717}
8718
8719/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8720/// "user"))]`. `track` is a comma-separated list of field names whose
8721/// before/after values land in the JSONB `changes` column. `source`
8722/// is informational only — it pins a default source when the model
8723/// is written outside any `audit::with_source(...)` scope (rare).
8724#[derive(Default)]
8725struct AuditAttrs {
8726    /// Field names to capture in the `changes` JSONB. Validated
8727    /// against declared scalar fields at compile time. Empty means
8728    /// "track every scalar field".
8729    track: Option<(Vec<String>, proc_macro2::Span)>,
8730}
8731
8732/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8733/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8734/// comma-separated strings; we validate each ident against the model's
8735/// declared fields at compile time.
8736#[derive(Default)]
8737struct AdminAttrs {
8738    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8739    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8740    list_per_page: Option<usize>,
8741    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8742    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8743    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8744    /// Bulk action names. No field-validation against model fields —
8745    /// these are action handlers, not column references.
8746    actions: Option<(Vec<String>, proc_macro2::Span)>,
8747    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8748    /// sections, comma-separated fields per section, optional
8749    /// `Title:` prefix. Empty title omits the `<legend>`.
8750    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8751    /// `admin(list_display_links = "title")`. Names
8752    /// from `list_display` whose cells should link to detail/edit.
8753    /// Issue #350.
8754    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8755    /// `admin(search_help_text = "...")`. Short
8756    /// caption rendered beside the admin list view's search box.
8757    /// Issue #353.
8758    search_help_text: Option<String>,
8759    /// `admin(actions_on_top = false)`. Hides the
8760    /// action-bar above the table. Default `true`. Issue #354.
8761    actions_on_top: Option<bool>,
8762    /// `admin(actions_on_bottom = true)`. Renders an
8763    /// additional action-bar below the table. Default `false`.
8764    /// Issue #354.
8765    actions_on_bottom: Option<bool>,
8766    /// `admin(date_hierarchy = "created_at")`. Name of
8767    /// a date / datetime field whose values render as a clickable
8768    /// year / month / day drill-down strip above the list table.
8769    /// Empty / unset disables the strip. Issue #355.
8770    date_hierarchy: Option<String>,
8771    /// `admin(prepopulated_fields = "slug:title")`.
8772    /// Each entry is `target:source[+source2]`; multiple entries are
8773    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8774    /// The admin change-form emits JS that slugifies the source values
8775    /// into the target field on every keystroke. Issue #356.
8776    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8777    /// `admin(raw_id_fields = "parent, owner")`. Names
8778    /// of FK fields whose change-form widget renders a lookup link
8779    /// next to the input. Issue #357.
8780    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8781    /// `admin(autocomplete_fields = "author_id")`.
8782    /// Names of FK fields whose change-form widget renders an
8783    /// Ajax-driven typeahead populated from a `__autocomplete`
8784    /// endpoint on the target model. Issue #358.
8785    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8786    /// `admin(list_select_related = "all" | "none" | "author, …")`
8787    /// — tunes the admin list view's FK auto-JOIN
8788    /// policy. Default `"all"` matches rustango's join-everything
8789    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8790    list_select_related: Option<String>,
8791    /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8792    /// Each entry is `field_name:widget_name`; multiple
8793    /// entries comma-separated. Empty / unset → no overrides. The
8794    /// list of widget names supported is documented on
8795    /// `AdminConfig::formfield_overrides`. Issue #359.
8796    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8797}
8798
8799/// The names `#[derive(Model)]` reads its attributes under: `rgo` is a
8800/// short alias of `rustango` with the same meaning.
8801#[derive(Clone, Copy, PartialEq, Eq)]
8802enum ModelAttrName {
8803    Rustango,
8804    Rgo,
8805}
8806
8807impl ModelAttrName {
8808    fn of(attr: &syn::Attribute) -> Option<Self> {
8809        let path = attr.path();
8810        if path.is_ident("rustango") {
8811            Some(Self::Rustango)
8812        } else if path.is_ident("rgo") {
8813            Some(Self::Rgo)
8814        } else {
8815            None
8816        }
8817    }
8818
8819    fn as_str(self) -> &'static str {
8820        match self {
8821            Self::Rustango => "rustango",
8822            Self::Rgo => "rgo",
8823        }
8824    }
8825}
8826
8827/// The model attributes in `attrs`, under either name. One item or field
8828/// uses one name, so a mix reads as a mistake rather than a merge.
8829fn model_attrs(attrs: &[syn::Attribute]) -> syn::Result<Vec<&syn::Attribute>> {
8830    let mut first: Option<ModelAttrName> = None;
8831    let mut out = Vec::new();
8832    for attr in attrs {
8833        let Some(name) = ModelAttrName::of(attr) else {
8834            continue;
8835        };
8836        match first {
8837            Some(seen) if seen != name => {
8838                return Err(syn::Error::new_spanned(
8839                    attr.path(),
8840                    format!(
8841                        "`#[{}(...)]` and `#[{}(...)]` are the same attribute; \
8842                         use one name on an item or field",
8843                        seen.as_str(),
8844                        name.as_str(),
8845                    ),
8846                ));
8847            }
8848            _ => first = Some(name),
8849        }
8850        out.push(attr);
8851    }
8852    Ok(out)
8853}
8854
8855fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8856    let mut out = ContainerAttrs {
8857        table: None,
8858        display: None,
8859        app: None,
8860        admin: None,
8861        audit: None,
8862        // Default `permissions = true` so every `#[derive(Model)]`
8863        // gets the four CRUD codenames seeded by `auto_create_permissions`
8864        // and is visible to non-superusers in the tenant admin without
8865        // manual per-model annotation. Models that intentionally don't
8866        // want permission rows (registry-internal types, framework
8867        // tables operators shouldn't manage directly) opt out via
8868        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8869        // out-of-the-box admin invisibility regression (#62).
8870        permissions: true,
8871        m2m: Vec::new(),
8872        generic_m2m: Vec::new(),
8873        indexes: Vec::new(),
8874        checks: Vec::new(),
8875        excludes: Vec::new(),
8876        composite_fks: Vec::new(),
8877        generic_fks: Vec::new(),
8878        scope: None,
8879        access: None,
8880        manager_fns: Vec::new(),
8881        default_order: Vec::new(),
8882        is_view: false,
8883        managed: true,
8884        verbose_name: None,
8885        verbose_name_plural: None,
8886        base_manager_name: None,
8887        order_with_respect_to: None,
8888        proxy: false,
8889        required_db_features: Vec::new(),
8890        required_db_vendor: None,
8891        default_related_name: None,
8892        db_table_comment: None,
8893        get_latest_by: None,
8894        extra_permissions: Vec::new(),
8895        default_permissions: Vec::new(),
8896        global_scopes: Vec::new(),
8897        through_relations: Vec::new(),
8898        reverse_has_relations: Vec::new(),
8899        generic_has_relations: Vec::new(),
8900    };
8901    for attr in model_attrs(&input.attrs)? {
8902        // The name the user wrote, for hints that quote the attribute.
8903        let written = ModelAttrName::of(attr).map_or("rustango", ModelAttrName::as_str);
8904        attr.parse_nested_meta(|meta| {
8905            if meta.path.is_ident("table") {
8906                let s: LitStr = meta.value()?.parse()?;
8907                let name = s.value();
8908                // v0.27.3 (#65) — macro-time guard against table names
8909                // that compile but break SQL downstream. Hyphens are
8910                // the common footgun: PostgreSQL accepts a quoted
8911                // `"intermediate-region"` in CREATE TABLE, but the
8912                // FK / index name derivation in `migrate::ddl`
8913                // emits `intermediate-region_field_fkey` unquoted,
8914                // which then fails the SQL parser. Same shape rule
8915                // as Postgres regular identifiers so the safe path
8916                // is the only path.
8917                validate_table_name(&name, s.span())?;
8918                out.table = Some(name);
8919                return Ok(());
8920            }
8921            if meta.path.is_ident("display") {
8922                let s: LitStr = meta.value()?.parse()?;
8923                out.display = Some((s.value(), s.span()));
8924                return Ok(());
8925            }
8926            if meta.path.is_ident("app") {
8927                let s: LitStr = meta.value()?.parse()?;
8928                out.app = Some(s.value());
8929                return Ok(());
8930            }
8931            if meta.path.is_ident("scope") {
8932                let s: LitStr = meta.value()?.parse()?;
8933                let val = s.value();
8934                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8935                    return Err(meta.error(format!(
8936                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8937                    )));
8938                }
8939                out.scope = Some(val);
8940                return Ok(());
8941            }
8942            if meta.path.is_ident("access") {
8943                let s: LitStr = meta.value()?.parse()?;
8944                let val = s.value();
8945                if !matches!(val.as_str(), "open" | "admin_only" | "never") {
8946                    return Err(meta.error(format!(
8947                        "`access` must be \"open\", \"admin_only\" or \"never\", got {val:?}"
8948                    )));
8949                }
8950                out.access = Some(val);
8951                return Ok(());
8952            }
8953            if meta.path.is_ident("admin") {
8954                let mut admin = AdminAttrs::default();
8955                meta.parse_nested_meta(|inner| {
8956                    if inner.path.is_ident("list_display") {
8957                        let s: LitStr = inner.value()?.parse()?;
8958                        admin.list_display =
8959                            Some((split_field_list(&s.value()), s.span()));
8960                        return Ok(());
8961                    }
8962                    if inner.path.is_ident("search_fields") {
8963                        let s: LitStr = inner.value()?.parse()?;
8964                        admin.search_fields =
8965                            Some((split_field_list(&s.value()), s.span()));
8966                        return Ok(());
8967                    }
8968                    if inner.path.is_ident("readonly_fields") {
8969                        let s: LitStr = inner.value()?.parse()?;
8970                        admin.readonly_fields =
8971                            Some((split_field_list(&s.value()), s.span()));
8972                        return Ok(());
8973                    }
8974                    if inner.path.is_ident("list_per_page") {
8975                        let lit: syn::LitInt = inner.value()?.parse()?;
8976                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8977                        return Ok(());
8978                    }
8979                    if inner.path.is_ident("ordering") {
8980                        let s: LitStr = inner.value()?.parse()?;
8981                        admin.ordering = Some((
8982                            parse_ordering_list(&s.value()),
8983                            s.span(),
8984                        ));
8985                        return Ok(());
8986                    }
8987                    if inner.path.is_ident("list_filter") {
8988                        let s: LitStr = inner.value()?.parse()?;
8989                        admin.list_filter =
8990                            Some((split_field_list(&s.value()), s.span()));
8991                        return Ok(());
8992                    }
8993                    if inner.path.is_ident("actions") {
8994                        let s: LitStr = inner.value()?.parse()?;
8995                        admin.actions =
8996                            Some((split_field_list(&s.value()), s.span()));
8997                        return Ok(());
8998                    }
8999                    if inner.path.is_ident("fieldsets") {
9000                        let s: LitStr = inner.value()?.parse()?;
9001                        admin.fieldsets =
9002                            Some((parse_fieldset_list(&s.value()), s.span()));
9003                        return Ok(());
9004                    }
9005                    if inner.path.is_ident("list_display_links") {
9006                        let s: LitStr = inner.value()?.parse()?;
9007                        admin.list_display_links =
9008                            Some((split_field_list(&s.value()), s.span()));
9009                        return Ok(());
9010                    }
9011                    if inner.path.is_ident("search_help_text") {
9012                        let s: LitStr = inner.value()?.parse()?;
9013                        admin.search_help_text = Some(s.value());
9014                        return Ok(());
9015                    }
9016                    if inner.path.is_ident("actions_on_top") {
9017                        let lit: syn::LitBool = inner.value()?.parse()?;
9018                        admin.actions_on_top = Some(lit.value);
9019                        return Ok(());
9020                    }
9021                    if inner.path.is_ident("actions_on_bottom") {
9022                        let lit: syn::LitBool = inner.value()?.parse()?;
9023                        admin.actions_on_bottom = Some(lit.value);
9024                        return Ok(());
9025                    }
9026                    if inner.path.is_ident("date_hierarchy") {
9027                        let s: LitStr = inner.value()?.parse()?;
9028                        admin.date_hierarchy = Some(s.value());
9029                        return Ok(());
9030                    }
9031                    if inner.path.is_ident("prepopulated_fields") {
9032                        let s: LitStr = inner.value()?.parse()?;
9033                        admin.prepopulated_fields =
9034                            Some((parse_prepopulated_list(&s.value()), s.span()));
9035                        return Ok(());
9036                    }
9037                    if inner.path.is_ident("raw_id_fields") {
9038                        let s: LitStr = inner.value()?.parse()?;
9039                        admin.raw_id_fields =
9040                            Some((split_field_list(&s.value()), s.span()));
9041                        return Ok(());
9042                    }
9043                    if inner.path.is_ident("autocomplete_fields") {
9044                        let s: LitStr = inner.value()?.parse()?;
9045                        admin.autocomplete_fields =
9046                            Some((split_field_list(&s.value()), s.span()));
9047                        return Ok(());
9048                    }
9049                    if inner.path.is_ident("list_select_related") {
9050                        let s: LitStr = inner.value()?.parse()?;
9051                        admin.list_select_related = Some(s.value());
9052                        return Ok(());
9053                    }
9054                    if inner.path.is_ident("formfield_overrides") {
9055                        let s: LitStr = inner.value()?.parse()?;
9056                        admin.formfield_overrides =
9057                            Some((parse_formfield_overrides(&s.value()), s.span()));
9058                        return Ok(());
9059                    }
9060                    Err(inner.error(
9061                        "unknown admin attribute (supported: \
9062                         `list_display`, `list_display_links`, \
9063                         `search_fields`, `search_help_text`, \
9064                         `readonly_fields`, \
9065                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
9066                         `actions_on_top`, `actions_on_bottom`, \
9067                         `date_hierarchy`, \
9068                         `prepopulated_fields`, \
9069                         `raw_id_fields`, \
9070                         `autocomplete_fields`, \
9071                         `list_select_related`, \
9072                         `formfield_overrides`, \
9073                         `fieldsets`)",
9074                    ))
9075                })?;
9076                out.admin = Some(admin);
9077                return Ok(());
9078            }
9079            if meta.path.is_ident("manager") {
9080                // Its empty trait could not take methods (#2132).
9081                return Err(meta.error(
9082                    "`manager(ext = ...)` was removed (#2132): declare your own \
9083                     `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
9084                ));
9085            }
9086            if meta.path.is_ident("manager_fn") {
9087                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
9088                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
9089                // next to the default `Self::objects()`. Multiple
9090                // attributes accumulate.
9091                let s: LitStr = meta.value()?.parse()?;
9092                let name = s.value();
9093                if name.is_empty() {
9094                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
9095                }
9096                if name == "objects" {
9097                    return Err(meta.error(
9098                        "`manager_fn = \"objects\"` collides with the default \
9099                         accessor — pick a different name",
9100                    ));
9101                }
9102                let ident = syn::Ident::new(&name, s.span());
9103                if out.manager_fns.iter().any(|prev| *prev == ident) {
9104                    return Err(meta.error(format!(
9105                        "duplicate `manager_fn = \"{name}\"`"
9106                    )));
9107                }
9108                out.manager_fns.push(ident);
9109                return Ok(());
9110            }
9111            if meta.path.is_ident("default_order") {
9112                // `#[rustango(default_order = "-created_at, status")]`
9113                // — issue #291 / T2.5. Comma-separated list; `-prefix`
9114                // means descending, `+prefix` or bare name means ascending.
9115                // Per-query opt-in via `QuerySet::with_default_order()`.
9116                let s: LitStr = meta.value()?.parse()?;
9117                let raw = s.value();
9118                let span = s.span();
9119                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9120                    Vec::new();
9121                for entry in raw.split(',') {
9122                    let trimmed = entry.trim();
9123                    if trimmed.is_empty() {
9124                        return Err(syn::Error::new(
9125                            span,
9126                            "`default_order = \"...\"` has an empty entry — \
9127                             check for a stray comma",
9128                        ));
9129                    }
9130                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9131                        (true, rest.trim().to_owned())
9132                    } else if let Some(rest) = trimmed.strip_prefix('+') {
9133                        (false, rest.trim().to_owned())
9134                    } else {
9135                        (false, trimmed.to_owned())
9136                    };
9137                    if name.is_empty() {
9138                        return Err(syn::Error::new(
9139                            span,
9140                            "`default_order` entry has no column name after the prefix",
9141                        ));
9142                    }
9143                    if parsed.iter().any(|(n, _, _)| *n == name) {
9144                        return Err(syn::Error::new(
9145                            span,
9146                            format!("duplicate column `{name}` in `default_order`"),
9147                        ));
9148                    }
9149                    parsed.push((name, desc, span));
9150                }
9151                if parsed.is_empty() {
9152                    return Err(syn::Error::new(
9153                        span,
9154                        "`default_order = \"...\"` cannot be empty",
9155                    ));
9156                }
9157                out.default_order = parsed;
9158                return Ok(());
9159            }
9160            if meta.path.is_ident("global_scope") {
9161                // `#[rustango(global_scope(name = "active", apply =
9162                //  path::to::fn))]` — issue #820. The apply function
9163                // path resolves at macro-expand time in the consumer's
9164                // scope; the macro re-emits it verbatim into the
9165                // `ModelSchema::global_scopes` slice literal.
9166                let span = meta.path.span();
9167                let mut scope_name: Option<String> = None;
9168                let mut apply_path: Option<syn::Path> = None;
9169                meta.parse_nested_meta(|inner| {
9170                    if inner.path.is_ident("name") {
9171                        let s: LitStr = inner.value()?.parse()?;
9172                        let raw = s.value();
9173                        if raw.trim().is_empty() {
9174                            return Err(syn::Error::new(
9175                                s.span(),
9176                                "`global_scope(name = \"...\")` must not be empty",
9177                            ));
9178                        }
9179                        scope_name = Some(raw);
9180                        return Ok(());
9181                    }
9182                    if inner.path.is_ident("apply") {
9183                        let p: syn::Path = inner.value()?.parse()?;
9184                        apply_path = Some(p);
9185                        return Ok(());
9186                    }
9187                    Err(inner.error(
9188                        "unknown `global_scope` attribute (supported: \
9189                         `name`, `apply`)",
9190                    ))
9191                })?;
9192                let Some(name) = scope_name else {
9193                    return Err(syn::Error::new(
9194                        span,
9195                        "`global_scope` requires `name = \"...\"`",
9196                    ));
9197                };
9198                let Some(apply) = apply_path else {
9199                    return Err(syn::Error::new(
9200                        span,
9201                        "`global_scope` requires `apply = fn_path`",
9202                    ));
9203                };
9204                if out.global_scopes.iter().any(|s| s.name == name) {
9205                    return Err(syn::Error::new(
9206                        span,
9207                        format!(
9208                            "duplicate global scope name `{name}` — \
9209                             pick a unique identifier so \
9210                             `QuerySet::without_global_scope(\"{name}\")` \
9211                             is unambiguous"
9212                        ),
9213                    ));
9214                }
9215                out.global_scopes.push(GlobalScopeAttr { name, apply });
9216                return Ok(());
9217            }
9218            if meta.path.is_ident("through") {
9219                // `#[rustango(through(name = "posts", far = "Post",
9220                //  far_fk_column = "author_id", intermediate = "User",
9221                //  intermediate_fk_column = "country_id"
9222                //  [, intermediate_pk_column = "id"]))]` — issue #817.
9223                let span = meta.path.span();
9224                let mut accessor_name: Option<String> = None;
9225                let mut far_ident: Option<syn::Ident> = None;
9226                let mut far_fk_column: Option<String> = None;
9227                let mut intermediate_ident: Option<syn::Ident> = None;
9228                let mut intermediate_fk_column: Option<String> = None;
9229                let mut intermediate_pk_column: Option<String> = None;
9230                fn parse_nonempty_string(
9231                    inner: &syn::meta::ParseNestedMeta<'_>,
9232                    field: &str,
9233                ) -> syn::Result<String> {
9234                    let s: LitStr = inner.value()?.parse()?;
9235                    let raw = s.value();
9236                    let trimmed = raw.trim();
9237                    if trimmed.is_empty() {
9238                        return Err(syn::Error::new(
9239                            s.span(),
9240                            format!("`through({field} = \"...\")` must not be empty"),
9241                        ));
9242                    }
9243                    Ok(trimmed.to_owned())
9244                }
9245                meta.parse_nested_meta(|inner| {
9246                    if inner.path.is_ident("name") {
9247                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9248                        return Ok(());
9249                    }
9250                    if inner.path.is_ident("far") {
9251                        // Far model name accepted as a string literal
9252                        // so the attribute fits inside the existing
9253                        // `parse_nested_meta` shape; parsed back into a
9254                        // `syn::Ident` so the emitted accessor can
9255                        // reference the type directly.
9256                        let s: LitStr = inner.value()?.parse()?;
9257                        let raw = s.value();
9258                        let trimmed = raw.trim();
9259                        if trimmed.is_empty() {
9260                            return Err(syn::Error::new(
9261                                s.span(),
9262                                "`through(far = \"...\")` must not be empty",
9263                            ));
9264                        }
9265                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
9266                        return Ok(());
9267                    }
9268                    if inner.path.is_ident("far_fk_column") {
9269                        far_fk_column =
9270                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
9271                        return Ok(());
9272                    }
9273                    if inner.path.is_ident("intermediate") {
9274                        let s: LitStr = inner.value()?.parse()?;
9275                        let raw = s.value();
9276                        let trimmed = raw.trim();
9277                        if trimmed.is_empty() {
9278                            return Err(syn::Error::new(
9279                                s.span(),
9280                                "`through(intermediate = \"...\")` must not be empty",
9281                            ));
9282                        }
9283                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9284                        return Ok(());
9285                    }
9286                    if inner.path.is_ident("intermediate_fk_column") {
9287                        intermediate_fk_column =
9288                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9289                        return Ok(());
9290                    }
9291                    if inner.path.is_ident("intermediate_pk_column") {
9292                        intermediate_pk_column =
9293                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9294                        return Ok(());
9295                    }
9296                    Err(inner.error(
9297                        "unknown `through` attribute (supported: \
9298                         `name`, `far`, `far_fk_column`, \
9299                         `intermediate`, `intermediate_fk_column`, \
9300                         `intermediate_pk_column`)",
9301                    ))
9302                })?;
9303                let Some(name) = accessor_name else {
9304                    return Err(syn::Error::new(
9305                        span,
9306                        "`through` requires `name = \"...\"`",
9307                    ));
9308                };
9309                let Some(far) = far_ident else {
9310                    return Err(syn::Error::new(
9311                        span,
9312                        "`through` requires `far = \"FarModelType\"`",
9313                    ));
9314                };
9315                let Some(far_fk_column) = far_fk_column else {
9316                    return Err(syn::Error::new(
9317                        span,
9318                        "`through` requires `far_fk_column = \"<column>\"`",
9319                    ));
9320                };
9321                let Some(intermediate) = intermediate_ident else {
9322                    return Err(syn::Error::new(
9323                        span,
9324                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9325                    ));
9326                };
9327                let Some(intermediate_fk_column) = intermediate_fk_column else {
9328                    return Err(syn::Error::new(
9329                        span,
9330                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9331                    ));
9332                };
9333                let intermediate_pk_column =
9334                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9335                if out.through_relations.iter().any(|t| t.name == name) {
9336                    return Err(syn::Error::new(
9337                        span,
9338                        format!(
9339                            "duplicate `through(name = \"{name}\")` — \
9340                             pick a unique accessor name"
9341                        ),
9342                    ));
9343                }
9344                out.through_relations.push(ThroughAttr {
9345                    name,
9346                    far,
9347                    far_fk_column,
9348                    intermediate,
9349                    intermediate_fk_column,
9350                    intermediate_pk_column,
9351                });
9352                return Ok(());
9353            }
9354            if meta.path.is_ident("reverse_has") {
9355                // `#[rustango(reverse_has(name = "comments",
9356                //  child = "Comment", child_fk_column = "post_id"
9357                //  [, self_pk_column = "id"]))]` — issue #830.
9358                let span = meta.path.span();
9359                let mut accessor_name: Option<String> = None;
9360                let mut child_ident: Option<syn::Ident> = None;
9361                let mut child_fk_column: Option<String> = None;
9362                let mut self_pk_column: Option<String> = None;
9363                let mut self_pk_span: Option<proc_macro2::Span> = None;
9364                meta.parse_nested_meta(|inner| {
9365                    if inner.path.is_ident("name") {
9366                        let s: LitStr = inner.value()?.parse()?;
9367                        let raw = s.value();
9368                        if raw.trim().is_empty() {
9369                            return Err(syn::Error::new(
9370                                s.span(),
9371                                "`reverse_has(name = \"...\")` must not be empty",
9372                            ));
9373                        }
9374                        accessor_name = Some(raw);
9375                        return Ok(());
9376                    }
9377                    if inner.path.is_ident("child") {
9378                        let s: LitStr = inner.value()?.parse()?;
9379                        let raw = s.value();
9380                        let trimmed = raw.trim();
9381                        if trimmed.is_empty() {
9382                            return Err(syn::Error::new(
9383                                s.span(),
9384                                "`reverse_has(child = \"...\")` must not be empty",
9385                            ));
9386                        }
9387                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9388                        return Ok(());
9389                    }
9390                    if inner.path.is_ident("child_fk_column") {
9391                        let s: LitStr = inner.value()?.parse()?;
9392                        let raw = s.value();
9393                        let trimmed = raw.trim();
9394                        if trimmed.is_empty() {
9395                            return Err(syn::Error::new(
9396                                s.span(),
9397                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9398                            ));
9399                        }
9400                        child_fk_column = Some(trimmed.to_owned());
9401                        return Ok(());
9402                    }
9403                    if inner.path.is_ident("self_pk_column") {
9404                        let s: LitStr = inner.value()?.parse()?;
9405                        let raw = s.value();
9406                        let trimmed = raw.trim();
9407                        if trimmed.is_empty() {
9408                            return Err(syn::Error::new(
9409                                s.span(),
9410                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9411                            ));
9412                        }
9413                        self_pk_column = Some(trimmed.to_owned());
9414                        self_pk_span = Some(s.span());
9415                        return Ok(());
9416                    }
9417                    Err(inner.error(
9418                        "unknown `reverse_has` attribute (supported: \
9419                         `name`, `child`, `child_fk_column`, \
9420                         `self_pk_column`)",
9421                    ))
9422                })?;
9423                let Some(name) = accessor_name else {
9424                    return Err(syn::Error::new(
9425                        span,
9426                        "`reverse_has` requires `name = \"...\"`",
9427                    ));
9428                };
9429                let Some(child) = child_ident else {
9430                    return Err(syn::Error::new(
9431                        span,
9432                        "`reverse_has` requires `child = \"ChildModelType\"`",
9433                    ));
9434                };
9435                let Some(child_fk_column) = child_fk_column else {
9436                    return Err(syn::Error::new(
9437                        span,
9438                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9439                    ));
9440                };
9441                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9442                    return Err(syn::Error::new(
9443                        span,
9444                        format!(
9445                            "duplicate `reverse_has(name = \"{name}\")` — \
9446                             pick a unique accessor name"
9447                        ),
9448                    ));
9449                }
9450                out.reverse_has_relations.push(ReverseHasAttr {
9451                    name,
9452                    child,
9453                    child_fk_column,
9454                    self_pk_column,
9455                    self_pk_span,
9456                });
9457                return Ok(());
9458            }
9459            if meta.path.is_ident("generic_has") {
9460                // `#[rustango(generic_has(name = "tags",
9461                //  child = "Tag", ct_column = "content_type_id",
9462                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9463                //  — issue #830, the reverse generic-FK (GFK) arm of the
9464                //  relation-existence family.
9465                let span = meta.path.span();
9466                let mut accessor_name: Option<String> = None;
9467                let mut child_ident: Option<syn::Ident> = None;
9468                let mut ct_column: Option<String> = None;
9469                let mut pk_column: Option<String> = None;
9470                let mut self_pk_column: Option<String> = None;
9471                let mut self_pk_span: Option<proc_macro2::Span> = None;
9472                meta.parse_nested_meta(|inner| {
9473                    if inner.path.is_ident("name") {
9474                        let s: LitStr = inner.value()?.parse()?;
9475                        let raw = s.value();
9476                        if raw.trim().is_empty() {
9477                            return Err(syn::Error::new(
9478                                s.span(),
9479                                "`generic_has(name = \"...\")` must not be empty",
9480                            ));
9481                        }
9482                        accessor_name = Some(raw);
9483                        return Ok(());
9484                    }
9485                    if inner.path.is_ident("child") {
9486                        let s: LitStr = inner.value()?.parse()?;
9487                        let trimmed = s.value().trim().to_owned();
9488                        if trimmed.is_empty() {
9489                            return Err(syn::Error::new(
9490                                s.span(),
9491                                "`generic_has(child = \"...\")` must not be empty",
9492                            ));
9493                        }
9494                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9495                        return Ok(());
9496                    }
9497                    if inner.path.is_ident("ct_column") {
9498                        let s: LitStr = inner.value()?.parse()?;
9499                        let trimmed = s.value().trim().to_owned();
9500                        if trimmed.is_empty() {
9501                            return Err(syn::Error::new(
9502                                s.span(),
9503                                "`generic_has(ct_column = \"...\")` must not be empty",
9504                            ));
9505                        }
9506                        ct_column = Some(trimmed);
9507                        return Ok(());
9508                    }
9509                    if inner.path.is_ident("pk_column") {
9510                        let s: LitStr = inner.value()?.parse()?;
9511                        let trimmed = s.value().trim().to_owned();
9512                        if trimmed.is_empty() {
9513                            return Err(syn::Error::new(
9514                                s.span(),
9515                                "`generic_has(pk_column = \"...\")` must not be empty",
9516                            ));
9517                        }
9518                        pk_column = Some(trimmed);
9519                        return Ok(());
9520                    }
9521                    if inner.path.is_ident("self_pk_column") {
9522                        let s: LitStr = inner.value()?.parse()?;
9523                        let trimmed = s.value().trim().to_owned();
9524                        if trimmed.is_empty() {
9525                            return Err(syn::Error::new(
9526                                s.span(),
9527                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9528                            ));
9529                        }
9530                        self_pk_column = Some(trimmed);
9531                        self_pk_span = Some(s.span());
9532                        return Ok(());
9533                    }
9534                    Err(inner.error(
9535                        "unknown `generic_has` attribute (supported: \
9536                         `name`, `child`, `ct_column`, `pk_column`, \
9537                         `self_pk_column`)",
9538                    ))
9539                })?;
9540                let Some(name) = accessor_name else {
9541                    return Err(syn::Error::new(
9542                        span,
9543                        "`generic_has` requires `name = \"...\"`",
9544                    ));
9545                };
9546                let Some(child) = child_ident else {
9547                    return Err(syn::Error::new(
9548                        span,
9549                        "`generic_has` requires `child = \"ChildModelType\"`",
9550                    ));
9551                };
9552                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9553                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9554                if out.generic_has_relations.iter().any(|r| r.name == name) {
9555                    return Err(syn::Error::new(
9556                        span,
9557                        format!(
9558                            "duplicate `generic_has(name = \"{name}\")` — \
9559                             pick a unique accessor name"
9560                        ),
9561                    ));
9562                }
9563                out.generic_has_relations.push(GenericHasAttr {
9564                    name,
9565                    child,
9566                    ct_column,
9567                    pk_column,
9568                    self_pk_column,
9569                    self_pk_span,
9570                });
9571                return Ok(());
9572            }
9573            if meta.path.is_ident("audit") {
9574                let mut audit = AuditAttrs::default();
9575                meta.parse_nested_meta(|inner| {
9576                    if inner.path.is_ident("track") {
9577                        let s: LitStr = inner.value()?.parse()?;
9578                        audit.track =
9579                            Some((split_field_list(&s.value()), s.span()));
9580                        return Ok(());
9581                    }
9582                    Err(inner.error(
9583                        "unknown audit attribute (supported: `track`)",
9584                    ))
9585                })?;
9586                out.audit = Some(audit);
9587                return Ok(());
9588            }
9589            if meta.path.is_ident("permissions") {
9590                // Two forms accepted:
9591                //   #[rustango(permissions)]          — flag form, true
9592                //   #[rustango(permissions = false)]  — explicit opt-out
9593                //   #[rustango(permissions = true)]   — explicit opt-in
9594                if let Ok(v) = meta.value() {
9595                    let lit: syn::LitBool = v.parse()?;
9596                    out.permissions = lit.value;
9597                } else {
9598                    out.permissions = true;
9599                }
9600                return Ok(());
9601            }
9602            if meta.path.is_ident("view") {
9603                // Issue #293 / T2.10. Two forms accepted, matching
9604                // the `permissions` flag pattern:
9605                //   #[rustango(view)]          — flag form, true
9606                //   #[rustango(view = false)]  — explicit opt-out
9607                //   #[rustango(view = true)]   — explicit opt-in
9608                if let Ok(v) = meta.value() {
9609                    let lit: syn::LitBool = v.parse()?;
9610                    out.is_view = lit.value;
9611                } else {
9612                    out.is_view = true;
9613                }
9614                return Ok(());
9615            }
9616            if meta.path.is_ident("managed") {
9617                // Who owns the table. Issue #321.
9618                //   #[rustango(managed = false)]  — operator-managed table
9619                //   #[rustango(managed = true)]   — rustango-managed (the default)
9620                // Bare-flag form is intentionally not accepted: writing
9621                // `#[rustango(managed)]` reads as "yes please manage it"
9622                // which is already the default. The opt-out is the only
9623                // useful state, so it must be explicit.
9624                let v = meta.value()?;
9625                let lit: syn::LitBool = v.parse()?;
9626                out.managed = lit.value;
9627                return Ok(());
9628            }
9629            if meta.path.is_ident("verbose_name") {
9630                let s: LitStr = meta.value()?.parse()?;
9631                out.verbose_name = Some(s.value());
9632                return Ok(());
9633            }
9634            if meta.path.is_ident("verbose_name_plural") {
9635                let s: LitStr = meta.value()?.parse()?;
9636                out.verbose_name_plural = Some(s.value());
9637                return Ok(());
9638            }
9639            if meta.path.is_ident("db_table_comment") {
9640                // Free-form table-level comment attached to the DB
9641                // catalog.
9642                let s: LitStr = meta.value()?.parse()?;
9643                out.db_table_comment = Some(s.value());
9644                return Ok(());
9645            }
9646            if meta.path.is_ident("proxy") {
9647                // Declarative flag
9648                // marking the struct as a proxy of another model that
9649                // shares its DB table. Stored on `ModelSchema::proxy`
9650                // so future codegen can skip `CreateTable` emission
9651                // for proxies (parent owns the table) and route
9652                // per-instance method resolution to the proxy class.
9653                //
9654                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9655                let value = if meta.input.peek(syn::Token![=]) {
9656                    meta.value()?.parse::<syn::LitBool>()?.value
9657                } else {
9658                    true
9659                };
9660                out.proxy = value;
9661                return Ok(());
9662            }
9663            if meta.path.is_ident("order_with_respect_to") {
9664                // `order_with_respect_to = "parent_fk"` — the model's
9665                // instances are intrinsically ordered relative to
9666                // their parent FK.
9667                //
9668                // rustango stores the FK field name on
9669                // `ModelSchema::order_with_respect_to`. Declarative-only
9670                // today: the migration writer + admin surfaces still
9671                // treat every model identically. Future codegen will
9672                // key off the metadata to auto-emit the `_order`
9673                // column and reorder helpers.
9674                //
9675                // Validated as a Rust-shape identifier so the macro
9676                // can reject typos at derive time.
9677                let s: LitStr = meta.value()?.parse()?;
9678                let raw = s.value();
9679                if raw.is_empty() {
9680                    return Err(syn::Error::new(
9681                        s.span(),
9682                        "`order_with_respect_to` must be a non-empty FK field name",
9683                    ));
9684                }
9685                let valid = raw
9686                    .chars()
9687                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9688                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9689                if !valid {
9690                    return Err(syn::Error::new(
9691                        s.span(),
9692                        format!(
9693                            "`order_with_respect_to` must be a valid Rust \
9694                             identifier (letters / digits / underscores, \
9695                             not starting with a digit); got `{raw}`"
9696                        ),
9697                    ));
9698                }
9699                out.order_with_respect_to = Some(raw);
9700                return Ok(());
9701            }
9702            if meta.path.is_ident("required_db_features") {
9703                // Capability tokens the model needs (e.g.
9704                // `"json_extract"`, `"window_functions"`,
9705                // `"row_security"`). Comma-separated.
9706                // `manage check --deploy` walks every model and warns
9707                // when the active backend doesn't advertise the
9708                // capability.
9709                //
9710                // rustango ships a small registry of capability tokens
9711                // each dialect supports — see `Dialect::supports`.
9712                // Unknown tokens still parse (they end up on the
9713                // schema and show up in the warning) so projects can
9714                // declare aspirational capabilities and the check
9715                // verb will keep nagging until the dialect implements
9716                // them.
9717                let s: LitStr = meta.value()?.parse()?;
9718                let raw = s.value();
9719                let features: Vec<String> = raw
9720                    .split(',')
9721                    .map(str::trim)
9722                    .filter(|s| !s.is_empty())
9723                    .map(str::to_owned)
9724                    .collect();
9725                if features.is_empty() {
9726                    return Err(syn::Error::new(
9727                        s.span(),
9728                        "`required_db_features` must list at least one \
9729                         comma-separated capability token",
9730                    ));
9731                }
9732                out.required_db_features = features;
9733                return Ok(());
9734            }
9735            if meta.path.is_ident("required_db_vendor") {
9736                // The model is only meant to run against the named DB
9737                // backend. `manage check --deploy` flags a mismatch so
9738                // ops catches "I forgot to switch DATABASE_URL" at
9739                // deploy time rather than runtime.
9740                //
9741                // Restricted to the three backends rustango ships
9742                // dialects for so the check verb can compare reliably.
9743                let s: LitStr = meta.value()?.parse()?;
9744                let raw = s.value().to_ascii_lowercase();
9745                match raw.as_str() {
9746                    "postgresql" | "postgres" | "pg" => {
9747                        out.required_db_vendor = Some("postgres".to_owned());
9748                    }
9749                    "mysql" | "mariadb" => {
9750                        out.required_db_vendor = Some("mysql".to_owned());
9751                    }
9752                    "sqlite" | "sqlite3" => {
9753                        out.required_db_vendor = Some("sqlite".to_owned());
9754                    }
9755                    _ => {
9756                        return Err(syn::Error::new(
9757                            s.span(),
9758                            format!(
9759                                "unknown required_db_vendor `{raw}` — \
9760                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9761                                 `mysql` (alias: `mariadb`), or `sqlite` \
9762                                 (alias: `sqlite3`)"
9763                            ),
9764                        ));
9765                    }
9766                }
9767                return Ok(());
9768            }
9769            if meta.path.is_ident("base_manager_name") {
9770                // Name of the manager that `<instance>.<relation>_set`
9771                // uses when resolving reverse-relation managers.
9772                // Distinct from `default_manager_name` (what
9773                // `Self::objects()` returns). Stored on
9774                // `ModelSchema::base_manager_name`.
9775                //
9776                // Validated as a Rust identifier so it stays safe to
9777                // re-emit as code in future reverse-manager codegen.
9778                let s: LitStr = meta.value()?.parse()?;
9779                let raw = s.value();
9780                if raw.is_empty() {
9781                    return Err(syn::Error::new(
9782                        s.span(),
9783                        "`base_manager_name` must be a non-empty string",
9784                    ));
9785                }
9786                let valid = raw
9787                    .chars()
9788                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9789                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9790                if !valid {
9791                    return Err(syn::Error::new(
9792                        s.span(),
9793                        format!(
9794                            "`base_manager_name` must be a valid Rust \
9795                             identifier (letters / digits / underscores, \
9796                             not starting with a digit); got `{raw}`"
9797                        ),
9798                    ));
9799                }
9800                out.base_manager_name = Some(raw);
9801                return Ok(());
9802            }
9803            if meta.path.is_ident("default_related_name") {
9804                // The name reverse-relation accessors use when callers
9805                // don't override `related_name = "..."` on the FK / M2M
9806                // field. Stored on `ModelSchema::default_related_name`
9807                // so future reverse-manager codegen / schema emit /
9808                // admin templates can pick the right accessor name
9809                // (today rustango doesn't auto-emit reverse managers;
9810                // the metadata is the foundation for that work).
9811                //
9812                // Enforced non-empty + ASCII identifier-shape so the
9813                // string is safe to use as a Rust ident later.
9814                let s: LitStr = meta.value()?.parse()?;
9815                let raw = s.value();
9816                if raw.is_empty() {
9817                    return Err(syn::Error::new(
9818                        s.span(),
9819                        "`default_related_name` must be a non-empty string",
9820                    ));
9821                }
9822                let valid = raw
9823                    .chars()
9824                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9825                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9826                if !valid {
9827                    return Err(syn::Error::new(
9828                        s.span(),
9829                        format!(
9830                            "`default_related_name` must be snake_case ASCII \
9831                             (lowercase letters / digits / underscores, not \
9832                             starting with a digit); got `{raw}`"
9833                        ),
9834                    ));
9835                }
9836                out.default_related_name = Some(raw);
9837                return Ok(());
9838            }
9839            if meta.path.is_ident("extra_permissions") {
9840                // Comma-separated `codename:label` pairs.
9841                let s: LitStr = meta.value()?.parse()?;
9842                let raw = s.value();
9843                let mut pairs = Vec::new();
9844                for entry in raw.split(',') {
9845                    let entry = entry.trim();
9846                    if entry.is_empty() {
9847                        continue;
9848                    }
9849                    let (codename, label) = match entry.split_once(':') {
9850                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9851                        None => (entry.to_owned(), entry.to_owned()),
9852                    };
9853                    if codename.is_empty() {
9854                        return Err(meta.error(
9855                            "`extra_permissions` entries must be `codename:label` pairs",
9856                        ));
9857                    }
9858                    pairs.push((codename, label));
9859                }
9860                if pairs.is_empty() {
9861                    return Err(meta
9862                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9863                }
9864                out.extra_permissions = pairs;
9865                return Ok(());
9866            }
9867            if meta.path.is_ident("default_permissions") {
9868                // Comma-separated subset of the CRUD action set.
9869                // Empty means all four (the framework default).
9870                let s: LitStr = meta.value()?.parse()?;
9871                let raw = s.value();
9872                let mut actions: Vec<String> = Vec::new();
9873                for entry in raw.split(',') {
9874                    let action = entry.trim().to_ascii_lowercase();
9875                    if action.is_empty() {
9876                        continue;
9877                    }
9878                    match action.as_str() {
9879                        "add" | "change" | "delete" | "view" => {}
9880                        other => {
9881                            return Err(syn::Error::new(
9882                                s.span(),
9883                                format!(
9884                                    "unknown default_permissions action `{other}` — \
9885                                     expected one of `add`, `change`, `delete`, `view`"
9886                                ),
9887                            ));
9888                        }
9889                    }
9890                    if !actions.contains(&action) {
9891                        actions.push(action);
9892                    }
9893                }
9894                if actions.is_empty() {
9895                    return Err(syn::Error::new(
9896                        s.span(),
9897                        "`default_permissions = \"…\"` must list at least one action; \
9898                         use `permissions = false` on the container if you want NO \
9899                         permissions seeded for this model.",
9900                    ));
9901                }
9902                out.default_permissions = actions;
9903                return Ok(());
9904            }
9905            if meta.path.is_ident("get_latest_by") {
9906                // The `-` prefix selects descending order.
9907                let s: LitStr = meta.value()?.parse()?;
9908                let raw = s.value();
9909                let trimmed = raw.trim();
9910                if trimmed.is_empty() {
9911                    return Err(meta.error("`get_latest_by` must name a column"));
9912                }
9913                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9914                    (stripped.to_owned(), true)
9915                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9916                    (stripped.to_owned(), false)
9917                } else {
9918                    (trimmed.to_owned(), false)
9919                };
9920                if col.is_empty() {
9921                    return Err(meta.error("`get_latest_by` must name a column"));
9922                }
9923                out.get_latest_by = Some((col, desc, s.span()));
9924                return Ok(());
9925            }
9926            if meta.path.is_ident("unique_together") {
9927                // Composite UNIQUE index. Two syntaxes:
9928                //
9929                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9930                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9931                //
9932                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9933                // (col1, col2)`, where <name> defaults to
9934                // `<table>_<col1>_<col2>_uq` when not supplied.
9935                let (columns, name) = parse_together_attr(&meta, "unique_together", written)?;
9936                out.indexes.push(IndexAttr {
9937                    span: meta.path.span(),
9938                    name,
9939                    columns,
9940                    unique: true,
9941                    method: "btree".to_owned(),
9942                    where_clause: None,
9943                include: Vec::new(),
9944                });
9945                return Ok(());
9946            }
9947            if meta.path.is_ident("index_together") {
9948                // Composite (non-unique) index. Two syntaxes
9949                // mirroring `unique_together`.
9950                //
9951                //   #[rustango(index_together = "created_at, status")]
9952                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9953                let (columns, name) = parse_together_attr(&meta, "index_together", written)?;
9954                out.indexes.push(IndexAttr {
9955                    span: meta.path.span(),
9956                    name,
9957                    columns,
9958                    unique: false,
9959                    method: "btree".to_owned(),
9960                    where_clause: None,
9961                include: Vec::new(),
9962                });
9963                return Ok(());
9964            }
9965            if meta.path.is_ident("unique_when") {
9966                // Partial unique index. Issue #265 / T1.3.
9967                //
9968                //   #[rustango(unique_when(
9969                //       columns   = "email",
9970                //       condition = "deleted_at IS NULL",
9971                //       name      = "unique_active_email"
9972                //   ))]
9973                //
9974                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9975                // on PG / SQLite (both ship partial indexes natively).
9976                // MySQL falls back to a plain UNIQUE index — the
9977                // condition is lost; document the limitation in the
9978                // generated migration.
9979                let mut columns: Option<Vec<String>> = None;
9980                let mut condition: Option<String> = None;
9981                let mut name: Option<String> = None;
9982                let mut include: Vec<String> = Vec::new();
9983                meta.parse_nested_meta(|inner| {
9984                    if inner.path.is_ident("columns") {
9985                        let s: LitStr = inner.value()?.parse()?;
9986                        columns = Some(split_field_list(&s.value()));
9987                        return Ok(());
9988                    }
9989                    if inner.path.is_ident("condition") {
9990                        let s: LitStr = inner.value()?.parse()?;
9991                        condition = Some(s.value());
9992                        return Ok(());
9993                    }
9994                    if inner.path.is_ident("name") {
9995                        let s: LitStr = inner.value()?.parse()?;
9996                        name = Some(s.value());
9997                        return Ok(());
9998                    }
9999                    if inner.path.is_ident("include") {
10000                        // PG 11+ covering-index columns. Non-key columns
10001                        // travel with the index leaf for index-only
10002                        // scans. Dropped on MySQL/SQLite by the writer.
10003                        let s: LitStr = inner.value()?.parse()?;
10004                        include = split_field_list(&s.value());
10005                        return Ok(());
10006                    }
10007                    Err(inner.error(
10008                        "unknown unique_when attribute (supported: \
10009                         `columns = \"...\"`, `condition = \"...\"`, \
10010                         `name = \"...\"`, `include = \"...\"`)",
10011                    ))
10012                })?;
10013                let columns = columns.ok_or_else(|| {
10014                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
10015                })?;
10016                let condition = condition.ok_or_else(|| {
10017                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
10018                })?;
10019                if columns.is_empty() {
10020                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
10021                }
10022                out.indexes.push(IndexAttr {
10023                    span: meta.path.span(),
10024                    name,
10025                    columns,
10026                    unique: true,
10027                    method: "btree".to_owned(),
10028                    where_clause: Some(condition),
10029                    include,
10030                });
10031                return Ok(());
10032            }
10033            if meta.path.is_ident("index_when") {
10034                // Non-unique partial index. Sibling of `unique_when`
10035                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
10036                //
10037                //   #[rustango(index_when(
10038                //       columns   = "status, created_at",
10039                //       condition = "deleted_at IS NULL",
10040                //       name      = "active_status_created_idx"
10041                //   ))]
10042                //
10043                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
10044                // on PG / SQLite (both ship partial indexes natively).
10045                // MySQL has no native partial-index support — the writer
10046                // emits a plain CREATE INDEX and the condition is lost;
10047                // operators wanting that selectivity on MySQL should
10048                // declare a covering index plus an application-level
10049                // filter.
10050                let mut columns: Option<Vec<String>> = None;
10051                let mut condition: Option<String> = None;
10052                let mut name: Option<String> = None;
10053                let mut method: String = "btree".to_owned();
10054                let mut include: Vec<String> = Vec::new();
10055                meta.parse_nested_meta(|inner| {
10056                    if inner.path.is_ident("columns") {
10057                        let s: LitStr = inner.value()?.parse()?;
10058                        columns = Some(split_field_list(&s.value()));
10059                        return Ok(());
10060                    }
10061                    if inner.path.is_ident("condition") {
10062                        let s: LitStr = inner.value()?.parse()?;
10063                        condition = Some(s.value());
10064                        return Ok(());
10065                    }
10066                    if inner.path.is_ident("name") {
10067                        let s: LitStr = inner.value()?.parse()?;
10068                        name = Some(s.value());
10069                        return Ok(());
10070                    }
10071                    if inner.path.is_ident("method") {
10072                        let s: LitStr = inner.value()?.parse()?;
10073                        method = s.value();
10074                        return Ok(());
10075                    }
10076                    if inner.path.is_ident("include") {
10077                        // PG 11+ covering-index columns; non-key
10078                        // columns travel with the index leaf. Dropped
10079                        // on MySQL/SQLite.
10080                        let s: LitStr = inner.value()?.parse()?;
10081                        include = split_field_list(&s.value());
10082                        return Ok(());
10083                    }
10084                    Err(inner.error(
10085                        "unknown index_when attribute (supported: \
10086                         `columns = \"...\"`, `condition = \"...\"`, \
10087                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
10088                         `include = \"...\"`)",
10089                    ))
10090                })?;
10091                let columns = columns
10092                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
10093                let condition = condition.ok_or_else(|| {
10094                    meta.error("`index_when(...)` requires `condition = \"...\"`")
10095                })?;
10096                if columns.is_empty() {
10097                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
10098                }
10099                out.indexes.push(IndexAttr {
10100                    span: meta.path.span(),
10101                    name,
10102                    columns,
10103                    unique: false,
10104                    method,
10105                    where_clause: Some(condition),
10106                    include,
10107                });
10108                return Ok(());
10109            }
10110            if meta.path.is_ident("index") {
10111                // Container-level composite index. Two syntaxes:
10112                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
10113                //   #[rustango(index("col1, col2"))]    — call form (same result)
10114                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
10115                // The call form takes the column list as a leading string
10116                // literal, then optional `unique` / `name = "..."` /
10117                // `method = "..."` flags (a leading literal can't compose
10118                // under `parse_nested_meta`, so the paren body is parsed by
10119                // hand). `unique_together` / `index_together` remain
10120                // aliases for the same feature.
10121                let cols_lit: LitStr;
10122                let mut unique = false;
10123                let mut name: Option<String> = None;
10124                let mut method = "btree".to_owned();
10125                if meta.input.peek(syn::token::Paren) {
10126                    let content;
10127                    syn::parenthesized!(content in meta.input);
10128                    cols_lit = content.parse()?;
10129                    while content.peek(syn::Token![,]) {
10130                        content.parse::<syn::Token![,]>()?;
10131                        if content.is_empty() {
10132                            break;
10133                        }
10134                        let flag: syn::Ident = content.parse()?;
10135                        if flag == "unique" {
10136                            unique = true;
10137                        } else if flag == "name" {
10138                            content.parse::<syn::Token![=]>()?;
10139                            let s: LitStr = content.parse()?;
10140                            name = Some(s.value());
10141                        } else if flag == "method" {
10142                            content.parse::<syn::Token![=]>()?;
10143                            let s: LitStr = content.parse()?;
10144                            let v = s.value();
10145                            match v.as_str() {
10146                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10147                                | "bloom" => method = v,
10148                                other => {
10149                                    return Err(syn::Error::new(
10150                                        s.span(),
10151                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10152                                    ));
10153                                }
10154                            }
10155                        } else {
10156                            return Err(syn::Error::new(
10157                                flag.span(),
10158                                "unknown index flag (supported: `unique`, `name`, `method`)",
10159                            ));
10160                        }
10161                    }
10162                } else {
10163                    cols_lit = meta.value()?.parse()?;
10164                }
10165                let columns = split_field_list(&cols_lit.value());
10166                out.indexes.push(IndexAttr {
10167                    span: meta.path.span(),
10168                    name,
10169                    columns,
10170                    unique,
10171                    method,
10172                    where_clause: None,
10173                    include: Vec::new(),
10174                });
10175                return Ok(());
10176            }
10177            if meta.path.is_ident("check") {
10178                // #[rustango(check(name = "…", expr = "…"))]
10179                let mut name: Option<String> = None;
10180                let mut expr: Option<String> = None;
10181                meta.parse_nested_meta(|inner| {
10182                    if inner.path.is_ident("name") {
10183                        let s: LitStr = inner.value()?.parse()?;
10184                        name = Some(s.value());
10185                        return Ok(());
10186                    }
10187                    if inner.path.is_ident("expr") {
10188                        let s: LitStr = inner.value()?.parse()?;
10189                        expr = Some(s.value());
10190                        return Ok(());
10191                    }
10192                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10193                })?;
10194                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10195                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10196                out.checks.push(CheckAttr { name, expr });
10197                return Ok(());
10198            }
10199            if meta.path.is_ident("exclude") {
10200                // #[rustango(exclude(name = "…", using = "gist",
10201                //                    elements = "col WITH op, col WITH op",
10202                //                    where = "…"))]
10203                let mut name: Option<String> = None;
10204                let mut using: Option<String> = None;
10205                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10206                let mut where_clause: Option<String> = None;
10207                meta.parse_nested_meta(|inner| {
10208                    if inner.path.is_ident("name") {
10209                        let s: LitStr = inner.value()?.parse()?;
10210                        name = Some(s.value());
10211                        return Ok(());
10212                    }
10213                    if inner.path.is_ident("using") {
10214                        let s: LitStr = inner.value()?.parse()?;
10215                        using = Some(s.value());
10216                        return Ok(());
10217                    }
10218                    if inner.path.is_ident("elements") {
10219                        let s: LitStr = inner.value()?.parse()?;
10220                        elements_raw = Some((s.value(), s.span()));
10221                        return Ok(());
10222                    }
10223                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10224                        let s: LitStr = inner.value()?.parse()?;
10225                        where_clause = Some(s.value());
10226                        return Ok(());
10227                    }
10228                    Err(inner.error(
10229                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10230                    ))
10231                })?;
10232                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10233                let using = using.unwrap_or_else(|| "gist".to_owned());
10234                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10235                    meta.error(
10236                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
10237                    )
10238                })?;
10239                // Parse `col WITH op` pairs separated by commas.
10240                let mut elements: Vec<(String, String)> = Vec::new();
10241                for pair in elements_str.split(',') {
10242                    let pair = pair.trim();
10243                    if pair.is_empty() {
10244                        continue;
10245                    }
10246                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10247                    let col = split.next().unwrap_or("").trim();
10248                    let rest = split.next().unwrap_or("").trim();
10249                    // `WITH op` — case-insensitive on `WITH`, then op.
10250                    let rest_lc = rest.to_ascii_lowercase();
10251                    let op = rest_lc
10252                        .strip_prefix("with")
10253                        .map(|r| r.trim_start())
10254                        .filter(|r| !r.is_empty())
10255                        .map(|_| {
10256                            // Pull the original-case op from `rest` after the
10257                            // `WITH ` token (5 chars).
10258                            rest[4..].trim_start().to_owned()
10259                        });
10260                    let Some(op) = op else {
10261                        return Err(syn::Error::new(
10262                            elements_span,
10263                            format!(
10264                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10265                                 (e.g. `room_id WITH =` or `during WITH &&`)"
10266                            ),
10267                        ));
10268                    };
10269                    if col.is_empty() || op.is_empty() {
10270                        return Err(syn::Error::new(
10271                            elements_span,
10272                            format!(
10273                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10274                                 (both sides non-empty)"
10275                            ),
10276                        ));
10277                    }
10278                    elements.push((col.to_owned(), op));
10279                }
10280                if elements.is_empty() {
10281                    return Err(syn::Error::new(
10282                        elements_span,
10283                        "exclude requires at least one `col WITH op` element",
10284                    ));
10285                }
10286                out.excludes.push(ExcludeAttr {
10287                    name,
10288                    using,
10289                    elements,
10290                    where_clause,
10291                });
10292                return Ok(());
10293            }
10294            if meta.path.is_ident("generic_fk") {
10295                let mut gfk = GenericFkAttr {
10296                    name: String::new(),
10297                    ct_column: String::new(),
10298                    pk_column: String::new(),
10299                };
10300                meta.parse_nested_meta(|inner| {
10301                    if inner.path.is_ident("name") {
10302                        let s: LitStr = inner.value()?.parse()?;
10303                        gfk.name = s.value();
10304                        return Ok(());
10305                    }
10306                    if inner.path.is_ident("ct_column") {
10307                        let s: LitStr = inner.value()?.parse()?;
10308                        gfk.ct_column = s.value();
10309                        return Ok(());
10310                    }
10311                    if inner.path.is_ident("pk_column") {
10312                        let s: LitStr = inner.value()?.parse()?;
10313                        gfk.pk_column = s.value();
10314                        return Ok(());
10315                    }
10316                    Err(inner.error(
10317                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10318                    ))
10319                })?;
10320                if gfk.name.is_empty() {
10321                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10322                }
10323                if gfk.ct_column.is_empty() {
10324                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10325                }
10326                if gfk.pk_column.is_empty() {
10327                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10328                }
10329                out.generic_fks.push(gfk);
10330                return Ok(());
10331            }
10332            if meta.path.is_ident("fk_composite") {
10333                let mut fk = CompositeFkAttr {
10334                    name: String::new(),
10335                    to: String::new(),
10336                    from: Vec::new(),
10337                    on: Vec::new(),
10338                };
10339                meta.parse_nested_meta(|inner| {
10340                    if inner.path.is_ident("name") {
10341                        let s: LitStr = inner.value()?.parse()?;
10342                        fk.name = s.value();
10343                        return Ok(());
10344                    }
10345                    if inner.path.is_ident("to") {
10346                        let s: LitStr = inner.value()?.parse()?;
10347                        fk.to = s.value();
10348                        return Ok(());
10349                    }
10350                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10351                    // comma-list of string literals.
10352                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10353                        let value = inner.value()?;
10354                        let content;
10355                        syn::parenthesized!(content in value);
10356                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10357                            content.parse_terminated(
10358                                |p| p.parse::<syn::LitStr>(),
10359                                syn::Token![,],
10360                            )?;
10361                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10362                        if inner.path.is_ident("on") {
10363                            fk.on = cols;
10364                        } else {
10365                            fk.from = cols;
10366                        }
10367                        return Ok(());
10368                    }
10369                    Err(inner.error(
10370                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10371                    ))
10372                })?;
10373                if fk.name.is_empty() {
10374                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10375                }
10376                if fk.to.is_empty() {
10377                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10378                }
10379                if fk.from.is_empty() || fk.on.is_empty() {
10380                    return Err(meta.error(
10381                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10382                    ));
10383                }
10384                if fk.from.len() != fk.on.len() {
10385                    return Err(meta.error(format!(
10386                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10387                        fk.from.len(),
10388                        fk.on.len(),
10389                    )));
10390                }
10391                out.composite_fks.push(fk);
10392                return Ok(());
10393            }
10394            if meta.path.is_ident("m2m") {
10395                let mut m2m = M2MAttr {
10396                    name: String::new(),
10397                    to: String::new(),
10398                    through: String::new(),
10399                    src: String::new(),
10400                    dst: String::new(),
10401                    auto_create: true,
10402                };
10403                meta.parse_nested_meta(|inner| {
10404                    if inner.path.is_ident("name") {
10405                        let s: LitStr = inner.value()?.parse()?;
10406                        m2m.name = s.value();
10407                        return Ok(());
10408                    }
10409                    if inner.path.is_ident("to") {
10410                        let s: LitStr = inner.value()?.parse()?;
10411                        m2m.to = s.value();
10412                        return Ok(());
10413                    }
10414                    if inner.path.is_ident("through") {
10415                        let s: LitStr = inner.value()?.parse()?;
10416                        m2m.through = s.value();
10417                        return Ok(());
10418                    }
10419                    if inner.path.is_ident("src") {
10420                        let s: LitStr = inner.value()?.parse()?;
10421                        m2m.src = s.value();
10422                        return Ok(());
10423                    }
10424                    if inner.path.is_ident("dst") {
10425                        let s: LitStr = inner.value()?.parse()?;
10426                        m2m.dst = s.value();
10427                        return Ok(());
10428                    }
10429                    if inner.path.is_ident("auto_create") {
10430                        let lit: syn::LitBool = inner.value()?.parse()?;
10431                        m2m.auto_create = lit.value;
10432                        return Ok(());
10433                    }
10434                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10435                })?;
10436                if m2m.name.is_empty() {
10437                    return Err(meta.error("m2m requires `name = \"...\"`"));
10438                }
10439                if m2m.to.is_empty() {
10440                    return Err(meta.error("m2m requires `to = \"...\"`"));
10441                }
10442                if m2m.through.is_empty() {
10443                    return Err(meta.error("m2m requires `through = \"...\"`"));
10444                }
10445                if m2m.src.is_empty() {
10446                    return Err(meta.error("m2m requires `src = \"...\"`"));
10447                }
10448                if m2m.dst.is_empty() {
10449                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10450                }
10451                out.m2m.push(m2m);
10452                return Ok(());
10453            }
10454            if meta.path.is_ident("generic_m2m") {
10455                let mut gm = GenericM2MAttr {
10456                    name: String::new(),
10457                    through: String::new(),
10458                    pk_column: String::new(),
10459                    ct_column: String::new(),
10460                    related_column: String::new(),
10461                };
10462                meta.parse_nested_meta(|inner| {
10463                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10464                        let s: LitStr = inner.value()?.parse()?;
10465                        Ok(s.value())
10466                    };
10467                    if inner.path.is_ident("name") {
10468                        gm.name = field(&inner)?;
10469                        return Ok(());
10470                    }
10471                    if inner.path.is_ident("through") {
10472                        gm.through = field(&inner)?;
10473                        return Ok(());
10474                    }
10475                    if inner.path.is_ident("pk_column") {
10476                        gm.pk_column = field(&inner)?;
10477                        return Ok(());
10478                    }
10479                    if inner.path.is_ident("ct_column") {
10480                        gm.ct_column = field(&inner)?;
10481                        return Ok(());
10482                    }
10483                    if inner.path.is_ident("related_column") {
10484                        gm.related_column = field(&inner)?;
10485                        return Ok(());
10486                    }
10487                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10488                })?;
10489                for (val, label) in [
10490                    (&gm.name, "name"),
10491                    (&gm.through, "through"),
10492                    (&gm.pk_column, "pk_column"),
10493                    (&gm.ct_column, "ct_column"),
10494                    (&gm.related_column, "related_column"),
10495                ] {
10496                    if val.is_empty() {
10497                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10498                    }
10499                }
10500                out.generic_m2m.push(gm);
10501                return Ok(());
10502            }
10503            Err(unknown_key_error(&meta, "container", CONTAINER_ATTR_KEYS))
10504        })?;
10505    }
10506    Ok(out)
10507}
10508
10509/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10510/// owned field names, trimming whitespace and skipping empty entries.
10511/// Field-name validation against the model is done by the caller.
10512fn split_field_list(raw: &str) -> Vec<String> {
10513    raw.split(',')
10514        .map(str::trim)
10515        .filter(|s| !s.is_empty())
10516        .map(str::to_owned)
10517        .collect()
10518}
10519
10520/// Shared parser for `unique_together` and `index_together` container
10521/// attrs. Accepts both shapes:
10522///
10523///   * `attr = "col1, col2"`              — auto-derived index name.
10524///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10525///
10526/// Returns `(columns, name)`.
10527fn parse_together_attr(
10528    meta: &syn::meta::ParseNestedMeta<'_>,
10529    attr: &str,
10530    written: &str,
10531) -> syn::Result<(Vec<String>, Option<String>)> {
10532    // Disambiguate by whether the next token is `=` (key-value) or
10533    // `(` (parenthesized).
10534    if meta.input.peek(syn::Token![=]) {
10535        let cols_lit: LitStr = meta.value()?.parse()?;
10536        let columns = split_field_list(&cols_lit.value());
10537        check_together_columns(meta, attr, written, &columns)?;
10538        return Ok((columns, None));
10539    }
10540    let mut columns: Option<Vec<String>> = None;
10541    let mut name: Option<String> = None;
10542    meta.parse_nested_meta(|inner| {
10543        if inner.path.is_ident("columns") {
10544            let s: LitStr = inner.value()?.parse()?;
10545            columns = Some(split_field_list(&s.value()));
10546            return Ok(());
10547        }
10548        if inner.path.is_ident("name") {
10549            let s: LitStr = inner.value()?.parse()?;
10550            name = Some(s.value());
10551            return Ok(());
10552        }
10553        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10554    })?;
10555    let columns = columns.ok_or_else(|| {
10556        meta.error(format!(
10557            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10558        ))
10559    })?;
10560    check_together_columns(meta, attr, written, &columns)?;
10561    Ok((columns, name))
10562}
10563
10564fn check_together_columns(
10565    meta: &syn::meta::ParseNestedMeta<'_>,
10566    attr: &str,
10567    written: &str,
10568    columns: &[String],
10569) -> syn::Result<()> {
10570    if columns.len() < 2 {
10571        let flag = if attr == "unique_together" {
10572            "unique"
10573        } else {
10574            "index"
10575        };
10576        let single = format!("#[{written}({flag})] on the field");
10577        return Err(meta.error(format!(
10578            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10579        )));
10580    }
10581    Ok(())
10582}
10583
10584/// Parse the fieldsets DSL: pipe-separated sections, optional
10585/// `"Title:"` prefix on each, comma-separated field names after.
10586/// Examples:
10587/// * `"name, office"` → one untitled section with two fields
10588/// * `"Identity: name, office | Metadata: created_at"` → two titled
10589///   sections
10590///
10591/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10592fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10593    raw.split('|')
10594        .map(str::trim)
10595        .filter(|s| !s.is_empty())
10596        .map(|section| {
10597            // Split off an optional `Title:` prefix (first colon).
10598            let (title, rest) = match section.split_once(':') {
10599                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10600                _ => (String::new(), section),
10601            };
10602            let fields = split_field_list(rest);
10603            (title, fields)
10604        })
10605        .collect()
10606}
10607
10608/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10609/// comma-separated entry maps a target field to one or more source
10610/// fields joined with `+`. Whitespace around tokens is trimmed.
10611/// Entries missing `:` or with empty target/source lists are dropped.
10612fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10613    raw.split(',')
10614        .map(str::trim)
10615        .filter(|s| !s.is_empty())
10616        .filter_map(|entry| {
10617            let (target, sources_raw) = entry.split_once(':')?;
10618            let target = target.trim().to_owned();
10619            if target.is_empty() {
10620                return None;
10621            }
10622            let sources: Vec<String> = sources_raw
10623                .split('+')
10624                .map(|s| s.trim().to_owned())
10625                .filter(|s| !s.is_empty())
10626                .collect();
10627            if sources.is_empty() {
10628                return None;
10629            }
10630            Some((target, sources))
10631        })
10632        .collect()
10633}
10634
10635/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10636/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10637/// and empty halves drop silently — the macro layer only enforces shape,
10638/// not field-name vs. widget-name validity (those checks happen at
10639/// `AdminConfig` consumption time). Issue #359.
10640fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10641    raw.split(',')
10642        .map(str::trim)
10643        .filter(|s| !s.is_empty())
10644        .filter_map(|entry| {
10645            let (field, widget) = entry.split_once(':')?;
10646            let field = field.trim().to_owned();
10647            let widget = widget.trim().to_owned();
10648            if field.is_empty() || widget.is_empty() {
10649                return None;
10650            }
10651            Some((field, widget))
10652        })
10653        .collect()
10654}
10655
10656/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10657/// Returns `(field_name, desc)` pairs in the same order as the input.
10658fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10659    raw.split(',')
10660        .map(str::trim)
10661        .filter(|s| !s.is_empty())
10662        .map(|spec| {
10663            spec.strip_prefix('-')
10664                .map_or((spec.to_owned(), false), |rest| {
10665                    (rest.trim().to_owned(), true)
10666                })
10667        })
10668        .collect()
10669}
10670
10671struct FieldAttrs {
10672    column: Option<String>,
10673    primary_key: bool,
10674    fk: Option<String>,
10675    o2o: Option<String>,
10676    on: Option<String>,
10677    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10678    /// "set_default" | "no_action")]` — what happens to this row when
10679    /// the row it points at is deleted.
10680    /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10681    /// set; the macro errors at compile time if applied to a
10682    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10683    /// DDL writer renders `ON DELETE <action>` after the constraint
10684    /// clause when this is `Some`; `None` falls back to the database
10685    /// default (NO ACTION on every backend rustango supports).
10686    on_delete: Option<String>,
10687    /// `#[rustango(related_name = "...")]` — per-FK
10688    /// reverse-accessor override. When set, the derive emits
10689    /// `Parent::<related_name>[_pool]` instead of the container-level
10690    /// `default_related_name` or the `<child_snake>_set[_pool]`
10691    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10692    /// silently ignored on non-FK fields. Follow-up to #816.
10693    /// `"+"` emits none, for a parent defined in another crate.
10694    related_name: Option<String>,
10695    max_length: Option<u32>,
10696    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10697    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10698    /// unconstrained `vector` column.
10699    vector_dims: Option<u32>,
10700    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10701    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10702    /// unconstrained `geometry(Point)` column (SRID 0).
10703    geometry_srid: Option<u32>,
10704    min: Option<i64>,
10705    max: Option<i64>,
10706    default: Option<String>,
10707    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10708    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10709    /// "gen_random_uuid()"`. The Rust field type must be
10710    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10711    /// INSERTs so the DB DEFAULT fires.
10712    auto_uuid: bool,
10713    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10714    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10715    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10716    /// the field is `Auto::Unset`, then bound as a normal parameter
10717    /// rather than relying on a per-dialect DB function. Issue #823
10718    /// (Eloquent `HasUuids`).
10719    ///
10720    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10721    /// Composes with every backend (PG / MySQL / SQLite) — no
10722    /// `gen_random_uuid()` requirement on the database.
10723    default_uuid_v7: bool,
10724    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10725    /// Server-set on insert, immutable from app code afterwards.
10726    /// Implies `auto + default = "now()"`. Field type must be
10727    /// `DateTime<Utc>`.
10728    auto_now_add: bool,
10729    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10730    /// every insert AND every update. Implies `auto + default =
10731    /// "now()"`; the macro additionally rewrites `update_on` /
10732    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10733    /// field value.
10734    auto_now: bool,
10735    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10736    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10737    /// `soft_delete_on(executor)` and `restore_on(executor)`
10738    /// methods on the model.
10739    soft_delete: bool,
10740    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10741    /// the column in the generated DDL.
10742    unique: bool,
10743    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10744    /// generates a `CREATE INDEX` for this column. `unique` here means
10745    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10746    index: bool,
10747    index_unique: bool,
10748    index_name: Option<String>,
10749    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10750    /// `"btree"`. Issue #34.
10751    index_method: String,
10752    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10753    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10754    /// the macro skips this column from every INSERT and UPDATE
10755    /// path, so the database always recomputes the value from
10756    /// `EXPR`. Backlog item #35.
10757    generated_as: Option<String>,
10758    /// `#[rustango(help_text = "…")]` — help text
10759    /// rendered below the admin form's input. Threaded into
10760    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10761    /// layers can read it.
10762    help_text: Option<String>,
10763    /// `#[rustango(choices = "value:Label, value:Label")]` — the
10764    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10765    /// as a `&'static [(&'static str, &'static str)]` slice. When
10766    /// present, the admin form renders a `<select>` instead of `<input>`
10767    /// and the validator rejects values not in the list. Only meaningful
10768    /// for `FieldType::String`; the macro errors at compile time if
10769    /// applied to a non-string field.
10770    choices: Option<Vec<(String, String)>>,
10771    /// `#[rustango(db_comment = "…")]` — DB-side column
10772    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10773    /// the comment in CREATE TABLE; Postgres emits a separate
10774    /// `COMMENT ON COLUMN` statement after the table is created;
10775    /// SQLite silently drops the value (no native column comments).
10776    db_comment: Option<String>,
10777    /// `#[rustango(verbose_name = "…")]` — human-readable
10778    /// label for the field. Threaded into `FieldSchema::verbose_name`
10779    /// so admin column headers, form labels, and other display
10780    /// surfaces can prefer the friendly caption over the Rust
10781    /// identifier. `None` means renderers fall back to the field name.
10782    verbose_name: Option<String>,
10783    /// `#[rustango(editable = false)]` — opt out of
10784    /// auto-generated form rendering. Defaults to `true` so existing
10785    /// fields keep their current admin / form behavior; setting
10786    /// `false` removes the field from the admin change-form entirely
10787    /// (the value is still visible on detail / list views, just not
10788    /// editable).
10789    editable: bool,
10790    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10791    /// "form may submit empty even when DB is NOT NULL". Threaded into
10792    /// `FieldSchema::blank`. Defaults to `false`.
10793    blank: bool,
10794    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10795    /// case-insensitive text column. Threaded into
10796    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10797    /// fields; the macro errors at derive time if applied elsewhere.
10798    case_insensitive: bool,
10799    /// `#[rustango(validators = "email,url")]` — the
10800    /// model-level validator chain. Comma-separated names that
10801    /// dispatch to the `validators::*` family in `validate_value`.
10802    /// Empty by default; fires on every typed INSERT/UPDATE. Each is
10803    /// spanned on the attribute string, so an unknown name errors there.
10804    validators: Vec<syn::Ident>,
10805    /// `#[rustango(secret)]` / `secret = false`; `None` defers to the name rules.
10806    secret: Option<bool>,
10807}
10808
10809/// Keys `parse_field_attrs` accepts, listed in its unknown-key error.
10810const FIELD_ATTR_KEYS: &[&str] = &[
10811    "column",
10812    "primary_key",
10813    "fk",
10814    "o2o",
10815    "on",
10816    "on_delete",
10817    "related_name",
10818    "max_length",
10819    "vector",
10820    "geometry",
10821    "min",
10822    "max",
10823    "default",
10824    "generated_as",
10825    "help_text",
10826    "choices",
10827    "db_comment",
10828    "verbose_name",
10829    "editable",
10830    "secret",
10831    "blank",
10832    "citext",
10833    "validators",
10834    "auto_uuid",
10835    "default_uuid_v7",
10836    "auto_now_add",
10837    "auto_now",
10838    "soft_delete",
10839    "unique",
10840    "index",
10841];
10842
10843/// Keys `parse_container_attrs` accepts, listed in its unknown-key error.
10844const CONTAINER_ATTR_KEYS: &[&str] = &[
10845    "table",
10846    "display",
10847    "app",
10848    "scope",
10849    "access",
10850    "admin",
10851    "manager",
10852    "manager_fn",
10853    "default_order",
10854    "global_scope",
10855    "through",
10856    "reverse_has",
10857    "generic_has",
10858    "audit",
10859    "permissions",
10860    "view",
10861    "managed",
10862    "verbose_name",
10863    "verbose_name_plural",
10864    "db_table_comment",
10865    "proxy",
10866    "order_with_respect_to",
10867    "required_db_features",
10868    "required_db_vendor",
10869    "base_manager_name",
10870    "default_related_name",
10871    "extra_permissions",
10872    "default_permissions",
10873    "get_latest_by",
10874    "unique_together",
10875    "index_together",
10876    "unique_when",
10877    "index_when",
10878    "index",
10879    "check",
10880    "exclude",
10881    "generic_fk",
10882    "fk_composite",
10883    "m2m",
10884    "generic_m2m",
10885];
10886
10887/// A flag attribute: `#[rustango(key)]` or `#[rustango(key = true)]`.
10888/// `= false` is refused: off is the default, so the attribute is a no-op.
10889fn parse_flag(meta: &syn::meta::ParseNestedMeta<'_>, key: &str) -> syn::Result<()> {
10890    if meta.input.peek(syn::Token![=]) {
10891        let lit: syn::LitBool = meta.value()?.parse()?;
10892        if !lit.value {
10893            return Err(syn::Error::new(
10894                lit.span(),
10895                format!("`{key} = false` is the default; remove the attribute"),
10896            ));
10897        }
10898    }
10899    Ok(())
10900}
10901
10902/// "unknown rustango <what> attribute `key`", with the nearest key and the full list.
10903fn unknown_key_error(
10904    meta: &syn::meta::ParseNestedMeta<'_>,
10905    what: &str,
10906    keys: &[&str],
10907) -> syn::Error {
10908    let path = &meta.path;
10909    let got = path
10910        .get_ident()
10911        .map_or_else(|| quote!(#path).to_string(), ToString::to_string);
10912    let mut msg = format!("unknown rustango {what} attribute `{got}`");
10913    if let Some(near) = keys
10914        .iter()
10915        .map(|k| (edit_distance(&got, k), *k))
10916        .filter(|(d, k)| *d <= 2.max(k.len() / 4))
10917        .min_by_key(|(d, _)| *d)
10918        .map(|(_, k)| k)
10919    {
10920        msg.push_str(&format!("; did you mean `{near}`?"));
10921    }
10922    msg.push_str(&format!(" Supported: {}", keys.join(", ")));
10923    meta.error(msg)
10924}
10925
10926/// Levenshtein distance, for "did you mean" hints.
10927fn edit_distance(a: &str, b: &str) -> usize {
10928    let b: Vec<char> = b.chars().collect();
10929    let mut prev: Vec<usize> = (0..=b.len()).collect();
10930    for (i, ca) in a.chars().enumerate() {
10931        let mut cur = vec![i + 1];
10932        for (j, cb) in b.iter().enumerate() {
10933            let sub = prev[j] + usize::from(ca != *cb);
10934            cur.push(sub.min(prev[j + 1] + 1).min(cur[j] + 1));
10935        }
10936        prev = cur;
10937    }
10938    prev[b.len()]
10939}
10940
10941fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10942    let mut out = FieldAttrs {
10943        column: None,
10944        primary_key: false,
10945        fk: None,
10946        o2o: None,
10947        on: None,
10948        on_delete: None,
10949        related_name: None,
10950        max_length: None,
10951        vector_dims: None,
10952        geometry_srid: None,
10953        min: None,
10954        max: None,
10955        default: None,
10956        auto_uuid: false,
10957        default_uuid_v7: false,
10958        auto_now_add: false,
10959        auto_now: false,
10960        soft_delete: false,
10961        unique: false,
10962        index: false,
10963        index_unique: false,
10964        index_name: None,
10965        index_method: "btree".to_owned(),
10966        generated_as: None,
10967        help_text: None,
10968        choices: None,
10969        db_comment: None,
10970        verbose_name: None,
10971        editable: true,
10972        blank: false,
10973        case_insensitive: false,
10974        validators: Vec::new(),
10975        secret: None,
10976    };
10977    for attr in model_attrs(&field.attrs)? {
10978        attr.parse_nested_meta(|meta| {
10979            if meta.path.is_ident("column") {
10980                let s: LitStr = meta.value()?.parse()?;
10981                let name = s.value();
10982                validate_sql_identifier(&name, "column", s.span())?;
10983                out.column = Some(name);
10984                return Ok(());
10985            }
10986            if meta.path.is_ident("primary_key") {
10987                parse_flag(&meta, "primary_key")?;
10988                out.primary_key = true;
10989                return Ok(());
10990            }
10991            if meta.path.is_ident("fk") {
10992                let s: LitStr = meta.value()?.parse()?;
10993                out.fk = Some(s.value());
10994                return Ok(());
10995            }
10996            if meta.path.is_ident("o2o") {
10997                let s: LitStr = meta.value()?.parse()?;
10998                out.o2o = Some(s.value());
10999                return Ok(());
11000            }
11001            if meta.path.is_ident("on") {
11002                let s: LitStr = meta.value()?.parse()?;
11003                out.on = Some(s.value());
11004                return Ok(());
11005            }
11006            if meta.path.is_ident("on_delete") {
11007                let s: LitStr = meta.value()?.parse()?;
11008                let raw = s.value();
11009                let normalized = raw.trim().to_ascii_lowercase();
11010                // Validate at parse time so the user gets a clear span
11011                // rather than a downstream compile error in the emit.
11012                match normalized.as_str() {
11013                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
11014                    _ => {
11015                        return Err(syn::Error::new(
11016                            s.span(),
11017                            format!(
11018                                "unknown on_delete action `{raw}`; expected one of \
11019                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
11020                            ),
11021                        ));
11022                    }
11023                }
11024                out.on_delete = Some(normalized);
11025                return Ok(());
11026            }
11027            if meta.path.is_ident("related_name") {
11028                let s: LitStr = meta.value()?.parse()?;
11029                let raw = s.value();
11030                if raw.trim().is_empty() {
11031                    return Err(syn::Error::new(
11032                        s.span(),
11033                        "`related_name` must be a non-empty identifier",
11034                    ));
11035                }
11036                // Validate as a Rust-ident shape — the value becomes
11037                // a method name on the parent type. Same rule as
11038                // `default_related_name` so the two surfaces match.
11039                // `"+"` emits no accessor at all.
11040                let snake = raw
11041                    .chars()
11042                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
11043                    && !raw.starts_with(char::is_numeric);
11044                if !snake && raw != NO_REVERSE_ACCESSOR {
11045                    return Err(syn::Error::new(
11046                        s.span(),
11047                        "`related_name` must be snake_case ASCII (lowercase letters, \
11048                         digits, underscores; no leading digit), or \"+\" for none",
11049                    ));
11050                }
11051                out.related_name = Some(raw);
11052                return Ok(());
11053            }
11054            if meta.path.is_ident("max_length") {
11055                let lit: syn::LitInt = meta.value()?.parse()?;
11056                out.max_length = Some(lit.base10_parse::<u32>()?);
11057                return Ok(());
11058            }
11059            // `#[rustango(vector(dims = N))]` — pgvector column
11060            // dimension (#824). Nested-meta form so it reads like the
11061            // other typed-column attrs.
11062            if meta.path.is_ident("vector") {
11063                meta.parse_nested_meta(|inner| {
11064                    if inner.path.is_ident("dims") {
11065                        let lit: syn::LitInt = inner.value()?.parse()?;
11066                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
11067                        return Ok(());
11068                    }
11069                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
11070                })?;
11071                return Ok(());
11072            }
11073            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
11074            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
11075            if meta.path.is_ident("geometry") {
11076                meta.parse_nested_meta(|inner| {
11077                    if inner.path.is_ident("srid") {
11078                        let lit: syn::LitInt = inner.value()?.parse()?;
11079                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
11080                        return Ok(());
11081                    }
11082                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
11083                })?;
11084                return Ok(());
11085            }
11086            if meta.path.is_ident("min") {
11087                out.min = Some(parse_signed_i64(&meta)?);
11088                return Ok(());
11089            }
11090            if meta.path.is_ident("max") {
11091                out.max = Some(parse_signed_i64(&meta)?);
11092                return Ok(());
11093            }
11094            if meta.path.is_ident("default") {
11095                let s: LitStr = meta.value()?.parse()?;
11096                out.default = Some(s.value());
11097                return Ok(());
11098            }
11099            if meta.path.is_ident("generated_as") {
11100                let s: LitStr = meta.value()?.parse()?;
11101                out.generated_as = Some(s.value());
11102                return Ok(());
11103            }
11104            if meta.path.is_ident("help_text") {
11105                let s: LitStr = meta.value()?.parse()?;
11106                out.help_text = Some(s.value());
11107                return Ok(());
11108            }
11109            if meta.path.is_ident("choices") {
11110                let s: LitStr = meta.value()?.parse()?;
11111                let raw = s.value();
11112                let mut pairs: Vec<(String, String)> = Vec::new();
11113                for chunk in raw.split(',') {
11114                    let chunk = chunk.trim();
11115                    if chunk.is_empty() {
11116                        continue;
11117                    }
11118                    let (value, label) = match chunk.split_once(':') {
11119                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
11120                        None => (chunk.to_owned(), chunk.to_owned()),
11121                    };
11122                    if value.is_empty() {
11123                        return Err(syn::Error::new(
11124                            s.span(),
11125                            "`choices` entry has empty value before `:`",
11126                        ));
11127                    }
11128                    pairs.push((value, label));
11129                }
11130                if pairs.is_empty() {
11131                    return Err(syn::Error::new(
11132                        s.span(),
11133                        "`choices = \"…\"` must contain at least one value",
11134                    ));
11135                }
11136                out.choices = Some(pairs);
11137                return Ok(());
11138            }
11139            if meta.path.is_ident("db_comment") {
11140                let s: LitStr = meta.value()?.parse()?;
11141                out.db_comment = Some(s.value());
11142                return Ok(());
11143            }
11144            if meta.path.is_ident("verbose_name") {
11145                let s: LitStr = meta.value()?.parse()?;
11146                out.verbose_name = Some(s.value());
11147                return Ok(());
11148            }
11149            if meta.path.is_ident("secret") {
11150                // `#[rustango(secret)]` or `#[rustango(secret = false)]`.
11151                out.secret = Some(if let Ok(v) = meta.value() {
11152                    v.parse::<syn::LitBool>()?.value
11153                } else {
11154                    true
11155                });
11156                return Ok(());
11157            }
11158            if meta.path.is_ident("editable") {
11159                // Two forms accepted:
11160                //   #[rustango(editable = false)] / true — explicit
11161                //   #[rustango(editable)] — flag form (= true, the
11162                //   default, so harmless; included for symmetry)
11163                if let Ok(v) = meta.value() {
11164                    let lit: syn::LitBool = v.parse()?;
11165                    out.editable = lit.value;
11166                } else {
11167                    out.editable = true;
11168                }
11169                return Ok(());
11170            }
11171            if meta.path.is_ident("blank") {
11172                // Two forms accepted:
11173                //   #[rustango(blank)] — flag form, true
11174                //   #[rustango(blank = false)] / true — explicit
11175                if let Ok(v) = meta.value() {
11176                    let lit: syn::LitBool = v.parse()?;
11177                    out.blank = lit.value;
11178                } else {
11179                    out.blank = true;
11180                }
11181                return Ok(());
11182            }
11183            if meta.path.is_ident("citext") {
11184                // Case-insensitive text column (#344). Two forms:
11185                //   #[rustango(citext)]          — flag form, true
11186                //   #[rustango(citext = true)]   — explicit
11187                //   #[rustango(citext = false)]  — explicit opt-out
11188                // String-only validation lives in the field-type
11189                // emitter (the FieldType discriminant is computed in
11190                // `detect_type`); the macro records the flag and the
11191                // DDL writer emits dialect-specific COLLATE / CITEXT.
11192                if let Ok(v) = meta.value() {
11193                    let lit: syn::LitBool = v.parse()?;
11194                    out.case_insensitive = lit.value;
11195                } else {
11196                    out.case_insensitive = true;
11197                }
11198                return Ok(());
11199            }
11200            if meta.path.is_ident("validators") {
11201                let s: LitStr = meta.value()?.parse()?;
11202                let raw = s.value();
11203                out.validators = raw
11204                    .split(',')
11205                    .map(str::trim)
11206                    .filter(|s| !s.is_empty())
11207                    .map(|name| {
11208                        let mut ident = syn::parse_str::<syn::Ident>(name).map_err(|_| {
11209                            syn::Error::new(s.span(), format!("unknown validator `{name}`"))
11210                        })?;
11211                        ident.set_span(s.span());
11212                        Ok(ident)
11213                    })
11214                    .collect::<syn::Result<_>>()?;
11215                if out.validators.is_empty() {
11216                    return Err(syn::Error::new(
11217                        s.span(),
11218                        "`validators = \"…\"` must list at least one name",
11219                    ));
11220                }
11221                return Ok(());
11222            }
11223            if meta.path.is_ident("auto_uuid") {
11224                parse_flag(&meta, "auto_uuid")?;
11225                out.auto_uuid = true;
11226                // Implied: PK + auto + DEFAULT gen_random_uuid().
11227                // Each is also explicitly settable; the explicit
11228                // value wins if conflicting.
11229                out.primary_key = true;
11230                if out.default.is_none() {
11231                    out.default = Some("gen_random_uuid()".into());
11232                }
11233                return Ok(());
11234            }
11235            if meta.path.is_ident("default_uuid_v7") {
11236                parse_flag(&meta, "default_uuid_v7")?;
11237                // Backend-neutral counterpart of `auto_uuid` — issue #823.
11238                // No SQL DEFAULT (the macro fills the value Rust-side
11239                // before binding); just mark the field as PK + Auto
11240                // so the insert path is the `Auto::Unset → generate`
11241                // branch.
11242                out.default_uuid_v7 = true;
11243                out.primary_key = true;
11244                return Ok(());
11245            }
11246            if meta.path.is_ident("auto_now_add") {
11247                parse_flag(&meta, "auto_now_add")?;
11248                out.auto_now_add = true;
11249                if out.default.is_none() {
11250                    out.default = Some("now()".into());
11251                }
11252                return Ok(());
11253            }
11254            if meta.path.is_ident("auto_now") {
11255                parse_flag(&meta, "auto_now")?;
11256                out.auto_now = true;
11257                if out.default.is_none() {
11258                    out.default = Some("now()".into());
11259                }
11260                return Ok(());
11261            }
11262            if meta.path.is_ident("soft_delete") {
11263                parse_flag(&meta, "soft_delete")?;
11264                out.soft_delete = true;
11265                return Ok(());
11266            }
11267            if meta.path.is_ident("unique") {
11268                parse_flag(&meta, "unique")?;
11269                out.unique = true;
11270                return Ok(());
11271            }
11272            if meta.path.is_ident("index") {
11273                parse_flag(&meta, "index")?;
11274                out.index = true;
11275                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11276                if meta.input.peek(syn::token::Paren) {
11277                    meta.parse_nested_meta(|inner| {
11278                        if inner.path.is_ident("unique") {
11279                            out.index_unique = true;
11280                            return Ok(());
11281                        }
11282                        if inner.path.is_ident("name") {
11283                            let s: LitStr = inner.value()?.parse()?;
11284                            out.index_name = Some(s.value());
11285                            return Ok(());
11286                        }
11287                        if inner.path.is_ident("method") {
11288                            let s: LitStr = inner.value()?.parse()?;
11289                            let v = s.value();
11290                            match v.as_str() {
11291                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11292                                    out.index_method = v;
11293                                }
11294                                other => {
11295                                    return Err(inner.error(format!(
11296                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11297                                    )));
11298                                }
11299                            }
11300                            return Ok(());
11301                        }
11302                        Err(inner.error(
11303                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11304                        ))
11305                    })?;
11306                }
11307                return Ok(());
11308            }
11309            Err(unknown_key_error(&meta, "field", FIELD_ATTR_KEYS))
11310        })?;
11311    }
11312    Ok(out)
11313}
11314
11315/// Parse a signed integer literal, accepting optional leading `-`.
11316fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11317    let expr: syn::Expr = meta.value()?.parse()?;
11318    match expr {
11319        syn::Expr::Lit(syn::ExprLit {
11320            lit: syn::Lit::Int(lit),
11321            ..
11322        }) => lit.base10_parse::<i64>(),
11323        syn::Expr::Unary(syn::ExprUnary {
11324            op: syn::UnOp::Neg(_),
11325            expr,
11326            ..
11327        }) => {
11328            if let syn::Expr::Lit(syn::ExprLit {
11329                lit: syn::Lit::Int(lit),
11330                ..
11331            }) = *expr
11332            {
11333                let v: i64 = lit.base10_parse()?;
11334                Ok(-v)
11335            } else {
11336                Err(syn::Error::new_spanned(expr, "expected integer literal"))
11337            }
11338        }
11339        other => Err(syn::Error::new_spanned(
11340            other,
11341            "expected integer literal (signed)",
11342        )),
11343    }
11344}
11345
11346struct FieldInfo<'a> {
11347    ident: &'a syn::Ident,
11348    column: String,
11349    primary_key: bool,
11350    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11351    /// skip this column when `Auto::Unset` and emit it under
11352    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11353    auto: bool,
11354    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11355    /// the `Column::Value` associated type for typed-column tokens.
11356    value_ty: &'a Type,
11357    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11358    field_type_tokens: TokenStream2,
11359    schema: TokenStream2,
11360    from_row_init: TokenStream2,
11361    /// Variant of [`Self::from_row_init`] that reads the column via
11362    /// `format!("{prefix}__{col}")` so a model can be decoded out of
11363    /// the aliased columns of a JOINed row. Drives slice 9.0d's
11364    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11365    /// helper that `select_related` calls when stitching loaded FKs.
11366    from_aliased_row_init: TokenStream2,
11367    /// MySQL twins of the two inits above. They differ only for a UUID
11368    /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11369    from_row_init_my: TokenStream2,
11370    from_aliased_row_init_my: TokenStream2,
11371    /// A bare or optional `Uuid` field.
11372    uuid_column: bool,
11373    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11374    /// relation helper emit (`Author::<child>_set`) needs to know `T`
11375    /// to point the generated method at the right child model.
11376    fk_inner: Option<Type>,
11377    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11378    /// `kind` (since ForeignKey detection sets `kind` to K's
11379    /// underlying type) but stored separately for clarity at the
11380    /// `FkRelation` construction site, which only sees the FK's
11381    /// surface fields.
11382    fk_pk_kind: DetectedKind,
11383    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11384    /// the bare `ForeignKey<T, K>`. Routes the load_related and
11385    /// fk_pk_access emitters to wrap assignments / accessors in
11386    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11387    /// FK column compiles end-to-end. The DDL writer reads this off
11388    /// the field schema (`nullable` flag); the macro just needs to
11389    /// keep the Rust-side codegen consistent.
11390    nullable: bool,
11391    /// `true` when this column was marked `#[rustango(auto_now)]` —
11392    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11393    /// column instead of the user-supplied value, so `updated_at`
11394    /// always reflects the latest write without the caller having
11395    /// to remember to set it.
11396    auto_now: bool,
11397    /// `true` when this column was marked `#[rustango(auto_now_add)]`
11398    /// — the column is server-set on INSERT (DB DEFAULT) and
11399    /// **immutable** afterwards. `update_on` / `save_on` skip the
11400    /// column entirely so a stale `created_at` value in memory never
11401    /// rewrites the persisted timestamp.
11402    auto_now_add: bool,
11403    /// `true` when this column was marked `#[rustango(soft_delete)]`.
11404    /// Triggers emission of `soft_delete_on(executor)` and
11405    /// `restore_on(executor)` on the model's inherent impl. There is
11406    /// at most one such column per model — emission asserts this.
11407    soft_delete: bool,
11408    /// `Some` when this column was marked
11409    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11410    /// every INSERT and UPDATE path; the database recomputes the
11411    /// value from `EXPR`. Backlog item #35.
11412    generated_as: Option<String>,
11413    /// `true` when this column was marked
11414    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11415    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11416    /// with `Uuid::now_v7()` before binding, so the PK is generated
11417    /// Rust-side and the column is always present in the INSERT
11418    /// statement (no DB DEFAULT requirement). Issue #823.
11419    default_uuid_v7: bool,
11420    /// `Some` when this FK field carried `#[rustango(related_name =
11421    /// "...")]`. Threaded into [`FkRelation::related_name`] and
11422    /// consumed by [`reverse_helper_tokens`] to override the default
11423    /// `<child_snake>_set` accessor name. Follow-up to #816.
11424    related_name: Option<String>,
11425}
11426
11427/// Reject table names that won't survive SQL identifier
11428/// derivation downstream. Postgres' regular-identifier rule
11429/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11430/// through the framework's unquoted FK / index / constraint name
11431/// emission without surprises. We also disallow leading-digit and
11432/// the empty string for clarity.
11433///
11434/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11435/// here — those produce a runtime error from the SQL parser,
11436/// which is loud enough; statically enumerating reserved words
11437/// across the three supported dialects is more friction than help.
11438///
11439/// Backlog item #65.
11440fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11441    validate_sql_identifier(name, "table", span)
11442}
11443
11444/// Reject SQL identifiers that compile but break downstream SQL
11445/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11446/// `kind` is "table" / "column" — used for the error message so users
11447/// see which attribute caused the failure.
11448fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11449    if name.is_empty() {
11450        return Err(syn::Error::new(
11451            span,
11452            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11453        ));
11454    }
11455    let mut chars = name.chars();
11456    let first = chars.next().unwrap();
11457    if !(first.is_ascii_alphabetic() || first == '_') {
11458        return Err(syn::Error::new(
11459            span,
11460            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11461        ));
11462    }
11463    for c in chars {
11464        if !(c.is_ascii_alphanumeric() || c == '_') {
11465            return Err(syn::Error::new(
11466                span,
11467                format!(
11468                    "{kind} name `{name}` contains invalid character {c:?} — \
11469                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11470                     Hyphens in particular break FK / index name derivation \
11471                     downstream; use underscores instead (e.g. `{}`)",
11472                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11473                ),
11474            ));
11475        }
11476    }
11477    Ok(())
11478}
11479
11480fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11481    let root = rustango_root();
11482    let attrs = parse_field_attrs(field)?;
11483    let ident = field
11484        .ident
11485        .as_ref()
11486        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11487    let name = ident_name(ident);
11488    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11489    let primary_key = attrs.primary_key;
11490    let DetectedType {
11491        kind,
11492        nullable,
11493        auto: detected_auto,
11494        fk_inner,
11495    } = detect_type(&field.ty)?;
11496    check_bound_compatibility(field, &attrs, kind)?;
11497    let auto = detected_auto;
11498    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11499    // INSERT path: the user must wrap the field in `Auto<T>`, which
11500    // marks the column as DB-default-supplied. The mixin attrs then
11501    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11502    // for `auto_now`, force the value on UPDATE too.
11503    if attrs.auto_uuid {
11504        if kind != DetectedKind::Uuid {
11505            return Err(syn::Error::new_spanned(
11506                field,
11507                "`#[rustango(auto_uuid)]` requires the field type to be \
11508                 `Auto<uuid::Uuid>`",
11509            ));
11510        }
11511        if !detected_auto {
11512            return Err(syn::Error::new_spanned(
11513                field,
11514                "`#[rustango(auto_uuid)]` requires the field type to be \
11515                 wrapped in `Auto<...>` so the macro skips the column on \
11516                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11517            ));
11518        }
11519    }
11520    if attrs.default_uuid_v7 {
11521        if kind != DetectedKind::Uuid {
11522            return Err(syn::Error::new_spanned(
11523                field,
11524                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11525                 `Auto<uuid::Uuid>`",
11526            ));
11527        }
11528        if !detected_auto {
11529            return Err(syn::Error::new_spanned(
11530                field,
11531                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11532                 wrapped in `Auto<...>` so the macro can detect the \
11533                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11534            ));
11535        }
11536        if attrs.auto_uuid {
11537            return Err(syn::Error::new_spanned(
11538                field,
11539                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11540                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11541                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11542                 Pick one.",
11543            ));
11544        }
11545    }
11546    if attrs.auto_now_add || attrs.auto_now {
11547        if kind != DetectedKind::DateTime {
11548            return Err(syn::Error::new_spanned(
11549                field,
11550                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11551                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11552            ));
11553        }
11554        if !detected_auto {
11555            return Err(syn::Error::new_spanned(
11556                field,
11557                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11558                 the field type to be wrapped in `Auto<...>` so the macro skips \
11559                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11560            ));
11561        }
11562    }
11563    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11564        return Err(syn::Error::new_spanned(
11565            field,
11566            "`#[rustango(soft_delete)]` requires the field type to be \
11567             `Option<chrono::DateTime<chrono::Utc>>`",
11568        ));
11569    }
11570    let is_mixin_auto =
11571        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11572    if detected_auto && !primary_key && !is_mixin_auto {
11573        return Err(syn::Error::new_spanned(
11574            field,
11575            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11576             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11577             `auto_now`",
11578        ));
11579    }
11580    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11581        return Err(syn::Error::new_spanned(
11582            field,
11583            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11584             SERIAL / BIGSERIAL already supplies a default sequence.",
11585        ));
11586    }
11587    if fk_inner.is_some() && primary_key {
11588        return Err(syn::Error::new_spanned(
11589            field,
11590            "`ForeignKey<T>` is not allowed on a primary-key field — \
11591             a row's PK is its own identity, not a reference to a parent.",
11592        ));
11593    }
11594    if attrs.generated_as.is_some() {
11595        if primary_key {
11596            return Err(syn::Error::new_spanned(
11597                field,
11598                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11599                 primary-key field — a PK must be writable so the row \
11600                 has an identity at INSERT time.",
11601            ));
11602        }
11603        if attrs.default.is_some() {
11604            return Err(syn::Error::new_spanned(
11605                field,
11606                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11607                 `default = \"…\"` — Postgres rejects DEFAULT on \
11608                 generated columns. The expression IS the default.",
11609            ));
11610        }
11611        if detected_auto {
11612            return Err(syn::Error::new_spanned(
11613                field,
11614                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11615                 an `Auto<T>` field — generated columns are computed \
11616                 by the DB, not server-assigned via a sequence. Use a \
11617                 plain Rust type (e.g. `f64`).",
11618            ));
11619        }
11620        if fk_inner.is_some() {
11621            return Err(syn::Error::new_spanned(
11622                field,
11623                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11624                 ForeignKey field.",
11625            ));
11626        }
11627    }
11628    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11629    let column_lit = column.as_str();
11630    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11631    // dimension that the bare `Vector` Rust type can't carry, so emit
11632    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11633    // fallback of `Vector(0)`.
11634    let field_type_tokens = if kind == DetectedKind::Vector {
11635        let root = rustango_root();
11636        let dims = attrs.vector_dims.unwrap_or(0);
11637        quote!(#root::core::FieldType::Vector(#dims))
11638    } else if kind == DetectedKind::Geometry {
11639        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11640        // SRID that the bare `Point` Rust type can't carry, so emit
11641        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11642        let root = rustango_root();
11643        let srid = attrs.geometry_srid.unwrap_or(0);
11644        quote!(#root::core::FieldType::Geometry(#srid))
11645    } else {
11646        kind.variant_tokens()
11647    };
11648    let max_length = optional_u32(attrs.max_length);
11649    let min = optional_i64(attrs.min);
11650    let max = optional_i64(attrs.max);
11651    let default = optional_str(attrs.default.as_deref());
11652
11653    let unique = attrs.unique;
11654    let generated_as = optional_str(attrs.generated_as.as_deref());
11655    let help_text = optional_str(attrs.help_text.as_deref());
11656    let choices = optional_choices(attrs.choices.as_deref());
11657    let db_comment = optional_str(attrs.db_comment.as_deref());
11658    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11659    let editable = attrs.editable;
11660    let secret = match attrs.secret {
11661        Some(v) => quote!(::core::option::Option::Some(#v)),
11662        None => quote!(::core::option::Option::None),
11663    };
11664    let blank = attrs.blank;
11665    let case_insensitive = attrs.case_insensitive;
11666    // A path per name: one the runtime does not know fails to resolve.
11667    let validators = &attrs.validators;
11668    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11669    {
11670        return Err(syn::Error::new_spanned(
11671            field,
11672            "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11673             or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11674             has no meaning on a non-FK column.",
11675        ));
11676    }
11677    if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11678        return Err(syn::Error::new_spanned(
11679            field,
11680            "`on_delete = \"set_null\"` needs a nullable column: use \
11681             `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11682             alias for `Option` is not seen).",
11683        ));
11684    }
11685    let fk_on_delete = match attrs.on_delete.as_deref() {
11686        None => quote!(::core::option::Option::None),
11687        Some(action) => {
11688            let variant = match action {
11689                "cascade" => quote!(Cascade),
11690                "restrict" => quote!(Restrict),
11691                "set_null" => quote!(SetNull),
11692                "set_default" => quote!(SetDefault),
11693                "no_action" => quote!(NoAction),
11694                // parse_field_attrs already validated this — guard against future drift.
11695                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11696            };
11697            quote!(::core::option::Option::Some(
11698                #root::core::OnDeleteAction::#variant
11699            ))
11700        }
11701    };
11702    let auto_now_flag = attrs.auto_now;
11703    let cast = match cast_type(&field.ty) {
11704        Some(ty) => quote!(::core::option::Option::Some(<#ty>::FIELD_CAST)),
11705        None => quote!(::core::option::Option::None),
11706    };
11707    // A sealed cast is always a credential: `secret = false` cannot opt it out.
11708    let sealed_guard = match (attrs.secret, cast_type(&field.ty)) {
11709        (Some(false), Some(ty)) => quote! {
11710            const _: () = ::core::assert!(
11711                !::core::matches!(<#ty>::FIELD_CAST, #root::core::FieldCast::Sealed),
11712                "`#[rustango(secret = false)]` on a sealed cast: it is always a credential",
11713            );
11714        },
11715        _ => quote!(),
11716    };
11717    let schema = quote! {{
11718        #sealed_guard
11719        #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11720            name: #name,
11721            column: #column_lit,
11722            ty: #field_type_tokens,
11723            nullable: #nullable,
11724            primary_key: #primary_key,
11725            relation: #relation,
11726            max_length: #max_length,
11727            min: #min,
11728            max: #max,
11729            default: #default,
11730            auto: #auto,
11731            auto_now: #auto_now_flag,
11732            unique: #unique,
11733            generated_as: #generated_as,
11734            help_text: #help_text,
11735            choices: #choices,
11736            db_comment: #db_comment,
11737            verbose_name: #verbose_name,
11738            editable: #editable,
11739            blank: #blank,
11740            case_insensitive: #case_insensitive,
11741            fk_on_delete: #fk_on_delete,
11742            validators: &[ #( #root::core::__validator_names::#validators ),* ],
11743            cast: #cast,
11744            secret: #secret,
11745        })
11746    }};
11747
11748    // sqlx-sqlite has no `Decimal` decode: it goes through `FlatScalar`'s cell.
11749    let decimal_column = kind == DetectedKind::Decimal && !detected_auto && fk_inner.is_none();
11750    let (from_row_init, from_aliased_row_init) = if decimal_column {
11751        (
11752            quote! { #ident: #root::sql::try_get_flat(row, #column_lit)? },
11753            quote! {
11754                #ident: #root::sql::try_get_flat(
11755                    row,
11756                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11757                )?
11758            },
11759        )
11760    } else {
11761        (
11762            quote! { #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)? },
11763            quote! {
11764                #ident: #root::sql::sqlx::Row::try_get(
11765                    row,
11766                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11767                )?
11768            },
11769        )
11770    };
11771    // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11772    // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11773    let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11774    let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11775        (
11776            quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11777            quote! {
11778                #ident: #root::sql::try_get_flat_my(
11779                    row,
11780                    #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11781                )?
11782            },
11783        )
11784    } else {
11785        (from_row_init.clone(), from_aliased_row_init.clone())
11786    };
11787
11788    Ok(FieldInfo {
11789        ident,
11790        column,
11791        primary_key,
11792        auto,
11793        value_ty: &field.ty,
11794        field_type_tokens,
11795        schema,
11796        from_row_init,
11797        from_aliased_row_init,
11798        from_row_init_my,
11799        from_aliased_row_init_my,
11800        uuid_column,
11801        fk_inner: fk_inner.cloned(),
11802        fk_pk_kind: kind,
11803        nullable,
11804        auto_now: attrs.auto_now,
11805        auto_now_add: attrs.auto_now_add,
11806        soft_delete: attrs.soft_delete,
11807        generated_as: attrs.generated_as.clone(),
11808        default_uuid_v7: attrs.default_uuid_v7,
11809        related_name: attrs.related_name.clone(),
11810    })
11811}
11812
11813fn check_bound_compatibility(
11814    field: &syn::Field,
11815    attrs: &FieldAttrs,
11816    kind: DetectedKind,
11817) -> syn::Result<()> {
11818    if attrs.max_length.is_some() && kind != DetectedKind::String {
11819        return Err(syn::Error::new_spanned(
11820            field,
11821            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11822        ));
11823    }
11824    // These were silently dropped on the wrong type (#1937).
11825    if attrs.case_insensitive && kind != DetectedKind::String {
11826        return Err(syn::Error::new_spanned(
11827            field,
11828            "`citext` is only valid on `String` fields (or `Option<String>`)",
11829        ));
11830    }
11831    if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11832        return Err(syn::Error::new_spanned(
11833            field,
11834            "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11835        ));
11836    }
11837    if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11838        return Err(syn::Error::new_spanned(
11839            field,
11840            "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11841        ));
11842    }
11843    if attrs.choices.is_some() && kind != DetectedKind::String {
11844        return Err(syn::Error::new_spanned(
11845            field,
11846            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11847             integer-valued enumerations should be modeled with a Rust enum and \
11848             custom (de)serializer for now",
11849        ));
11850    }
11851    // They run only on string values, so elsewhere they would never fire.
11852    if !attrs.validators.is_empty() && kind != DetectedKind::String {
11853        return Err(syn::Error::new_spanned(
11854            field,
11855            "`validators` is only valid on `String` fields (or `Option<String>`)",
11856        ));
11857    }
11858    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11859        return Err(syn::Error::new_spanned(
11860            field,
11861            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11862        ));
11863    }
11864    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11865        if min > max {
11866            return Err(syn::Error::new_spanned(
11867                field,
11868                format!("`min` ({min}) is greater than `max` ({max})"),
11869            ));
11870        }
11871    }
11872    if let Some(default) = &attrs.default {
11873        check_default_expr(default, kind).map_err(|msg| syn::Error::new_spanned(field, msg))?;
11874    }
11875    Ok(())
11876}
11877
11878/// SQL words a `default` may be on its own; any other bare word is a
11879/// column reference, which Postgres and MySQL refuse in a DEFAULT.
11880const DEFAULT_KEYWORDS: &[&str] = &[
11881    "NULL",
11882    "TRUE",
11883    "FALSE",
11884    "CURRENT_TIMESTAMP",
11885    "CURRENT_DATE",
11886    "CURRENT_TIME",
11887    "LOCALTIMESTAMP",
11888    "LOCALTIME",
11889];
11890
11891/// `default` is raw SQL. Refuse the shapes only SQLite accepts: a bare
11892/// word (read as a column on PG/MySQL) and a non-numeric literal on a
11893/// numeric field.
11894fn check_default_expr(default: &str, kind: DetectedKind) -> Result<(), String> {
11895    let expr = default.trim();
11896    let is_word = expr.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
11897        && expr
11898            .chars()
11899            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ' ');
11900    let upper = expr.to_ascii_uppercase();
11901    if is_word && ["CURRENT_USER", "SESSION_USER", "USER"].contains(&upper.as_str()) {
11902        return Err(format!(
11903            "`default = \"{expr}\"` works on Postgres only: MySQL refuses it as a default \
11904             and SQLite stores the text `{expr}`; set the value in code instead"
11905        ));
11906    }
11907    if is_word && !DEFAULT_KEYWORDS.contains(&upper.as_str()) {
11908        return Err(format!(
11909            "`default = \"{expr}\"` is raw SQL, so `{expr}` is read as a column name; \
11910             for the text '{expr}' write `default = \"'{expr}'\"`"
11911        ));
11912    }
11913    let numeric = kind.is_integer()
11914        || matches!(
11915            kind,
11916            DetectedKind::F32 | DetectedKind::F64 | DetectedKind::Decimal
11917        );
11918    if numeric {
11919        let unquoted = expr
11920            .strip_prefix('\'')
11921            .and_then(|e| e.strip_suffix('\''))
11922            .unwrap_or(expr);
11923        let is_number = if kind.is_integer() {
11924            unquoted.parse::<i64>().is_ok()
11925        } else {
11926            unquoted.parse::<f64>().is_ok_and(f64::is_finite)
11927        };
11928        // Keep expressions (`(1 + 2)`, `abs(-1)`) and NULL; refuse a
11929        // literal that is no number. `""` renders as `''`.
11930        let is_literal = is_word || expr.is_empty() || expr.starts_with('\'');
11931        if is_literal && !is_number && upper != "NULL" {
11932            return Err(format!(
11933                "`default = \"{expr}\"` is not a number, but the field is numeric"
11934            ));
11935        }
11936    }
11937    Ok(())
11938}
11939
11940/// A `&'static [ty]` of `const fn` results. The slice needs its own
11941/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11942fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11943    quote!({
11944        const ITEMS: &[#ty] = &[ #(#items),* ];
11945        ITEMS
11946    })
11947}
11948
11949fn optional_u32(value: Option<u32>) -> TokenStream2 {
11950    if let Some(v) = value {
11951        quote!(::core::option::Option::Some(#v))
11952    } else {
11953        quote!(::core::option::Option::None)
11954    }
11955}
11956
11957fn optional_i64(value: Option<i64>) -> TokenStream2 {
11958    if let Some(v) = value {
11959        quote!(::core::option::Option::Some(#v))
11960    } else {
11961        quote!(::core::option::Option::None)
11962    }
11963}
11964
11965fn optional_str(value: Option<&str>) -> TokenStream2 {
11966    if let Some(v) = value {
11967        quote!(::core::option::Option::Some(#v))
11968    } else {
11969        quote!(::core::option::Option::None)
11970    }
11971}
11972
11973fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11974    let Some(pairs) = pairs else {
11975        return quote!(::core::option::Option::None);
11976    };
11977    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11978    quote!(::core::option::Option::Some(&[#(#entries),*]))
11979}
11980
11981fn relation_tokens(
11982    field: &syn::Field,
11983    attrs: &FieldAttrs,
11984    fk_inner: Option<&syn::Type>,
11985    table: &str,
11986) -> syn::Result<TokenStream2> {
11987    let root = rustango_root();
11988    if let Some(inner) = fk_inner {
11989        if attrs.fk.is_some() || attrs.o2o.is_some() {
11990            return Err(syn::Error::new_spanned(
11991                field,
11992                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11993                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11994            ));
11995        }
11996        // No `on`: the target's PK column, which may be renamed.
11997        let on = match attrs.on.as_deref() {
11998            Some(on) => quote!(#on),
11999            None => quote! {
12000                match <#inner as #root::core::Model>::SCHEMA.pk_column() {
12001                    ::core::option::Option::Some(c) => c,
12002                    ::core::option::Option::None => "id",
12003                }
12004            },
12005        };
12006        return Ok(quote! {
12007            ::core::option::Option::Some(#root::core::Relation::fk(
12008                <#inner as #root::core::Model>::SCHEMA.table,
12009                #on,
12010            ))
12011        });
12012    }
12013    match (&attrs.fk, &attrs.o2o) {
12014        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
12015            field,
12016            "`fk` and `o2o` are mutually exclusive",
12017        )),
12018        (Some(to), None) => {
12019            let on = attrs.on.as_deref().unwrap_or("id");
12020            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
12021            // the model's own table. Threaded as a literal string at
12022            // macro-expansion time to sidestep the const-eval cycle
12023            // that `Self::SCHEMA.table` would create when referenced
12024            // inside Self::SCHEMA's own initializer.
12025            let resolved = if to == "self" { table } else { to };
12026            Ok(quote! {
12027                ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
12028            })
12029        }
12030        (None, Some(to)) => {
12031            let on = attrs.on.as_deref().unwrap_or("id");
12032            let resolved = if to == "self" { table } else { to };
12033            Ok(quote! {
12034                ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
12035            })
12036        }
12037        (None, None) => {
12038            if attrs.on.is_some() {
12039                return Err(syn::Error::new_spanned(
12040                    field,
12041                    "`on` requires `fk` or `o2o`",
12042                ));
12043            }
12044            Ok(quote!(::core::option::Option::None))
12045        }
12046    }
12047}
12048
12049/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
12050/// about kinds without depending on `rustango-core` (which would require a
12051/// proc-macro/normal split it doesn't have today).
12052#[derive(Clone, Copy, PartialEq, Eq)]
12053enum DetectedKind {
12054    I16,
12055    I32,
12056    I64,
12057    F32,
12058    F64,
12059    Bool,
12060    String,
12061    DateTime,
12062    Date,
12063    Time,
12064    Uuid,
12065    Json,
12066    Decimal,
12067    Binary,
12068    /// `Array<String>` → PG `text[]` (#341).
12069    ArrayText,
12070    /// `Array<i32>` → PG `integer[]` (#341).
12071    ArrayInt,
12072    /// `Array<i64>` → PG `bigint[]` (#341).
12073    ArrayBigInt,
12074    /// `Range<i32>` → PG `int4range` (#343).
12075    RangeInt,
12076    /// `Range<i64>` → PG `int8range` (#343).
12077    RangeBigInt,
12078    /// `Range<Decimal>` → PG `numrange` (#343).
12079    RangeNumeric,
12080    /// `Range<NaiveDate>` → PG `daterange` (#343).
12081    RangeDate,
12082    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
12083    RangeDateTime,
12084    /// `HStore` → PG `hstore` (#342).
12085    HStore,
12086    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
12087    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
12088    /// in at the `FieldType` emission site (not carried on this enum).
12089    Vector,
12090    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
12091    /// from the `#[rustango(geometry(srid = N))]` field attribute,
12092    /// threaded in at the `FieldType` emission site.
12093    Geometry,
12094}
12095
12096impl DetectedKind {
12097    fn variant_tokens(self) -> TokenStream2 {
12098        let root = rustango_root();
12099        match self {
12100            Self::I16 => quote!(#root::core::FieldType::I16),
12101            Self::I32 => quote!(#root::core::FieldType::I32),
12102            Self::I64 => quote!(#root::core::FieldType::I64),
12103            Self::F32 => quote!(#root::core::FieldType::F32),
12104            Self::F64 => quote!(#root::core::FieldType::F64),
12105            Self::Bool => quote!(#root::core::FieldType::Bool),
12106            Self::String => quote!(#root::core::FieldType::String),
12107            Self::DateTime => quote!(#root::core::FieldType::DateTime),
12108            Self::Date => quote!(#root::core::FieldType::Date),
12109            Self::Time => quote!(#root::core::FieldType::Time),
12110            Self::Uuid => quote!(#root::core::FieldType::Uuid),
12111            Self::Json => quote!(#root::core::FieldType::Json),
12112            Self::Decimal => quote!(#root::core::FieldType::Decimal),
12113            Self::Binary => quote!(#root::core::FieldType::Binary),
12114            Self::ArrayText => {
12115                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
12116            }
12117            Self::ArrayInt => {
12118                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
12119            }
12120            Self::ArrayBigInt => {
12121                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
12122            }
12123            Self::RangeInt => {
12124                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
12125            }
12126            Self::RangeBigInt => {
12127                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
12128            }
12129            Self::RangeNumeric => {
12130                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
12131            }
12132            Self::RangeDate => {
12133                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
12134            }
12135            Self::RangeDateTime => {
12136                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
12137            }
12138            Self::HStore => quote!(#root::core::FieldType::HStore),
12139            // Dimension comes from the `vector(dims = N)` attribute,
12140            // applied at the emission site; `0` here is just a fallback.
12141            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
12142            // SRID comes from the `geometry(srid = N)` attribute, applied
12143            // at the emission site; `0` here is just a fallback.
12144            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
12145        }
12146    }
12147
12148    fn is_integer(self) -> bool {
12149        matches!(self, Self::I16 | Self::I32 | Self::I64)
12150    }
12151}
12152
12153/// Result of walking a field's Rust type. `kind` is the underlying
12154/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
12155/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
12156/// `Some(<T>)` when the field was `ForeignKey<T>` (or
12157/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
12158#[derive(Clone, Copy)]
12159struct DetectedType<'a> {
12160    kind: DetectedKind,
12161    nullable: bool,
12162    auto: bool,
12163    fk_inner: Option<&'a syn::Type>,
12164}
12165
12166/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
12167/// non-`Auto` types — the caller should already have routed Auto-only
12168/// codegen through this helper, so a `None` indicates a macro-internal
12169/// invariant break.
12170fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
12171    let Type::Path(TypePath { path, qself: None }) = ty else {
12172        return None;
12173    };
12174    let last = path.segments.last()?;
12175    if last.ident != "Auto" {
12176        return None;
12177    }
12178    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12179        return None;
12180    };
12181    args.args.iter().find_map(|a| match a {
12182        syn::GenericArgument::Type(t) => Some(t),
12183        _ => None,
12184    })
12185}
12186
12187/// The `Cast<C>` inside `ty` (bare or in an `Option`), for its schema record.
12188fn cast_type(ty: &syn::Type) -> Option<&syn::Type> {
12189    let Type::Path(TypePath { path, qself: None }) = ty else {
12190        return None;
12191    };
12192    let last = path.segments.last()?;
12193    if last.ident == "Cast" {
12194        return Some(ty);
12195    }
12196    if last.ident != "Option" {
12197        return None;
12198    }
12199    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12200        return None;
12201    };
12202    match args.args.first()? {
12203        syn::GenericArgument::Type(inner) => cast_type(inner).filter(|_| args.args.len() == 1),
12204        _ => None,
12205    }
12206}
12207
12208fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
12209    let Type::Path(TypePath { path, qself: None }) = ty else {
12210        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
12211    };
12212    let last = path
12213        .segments
12214        .last()
12215        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
12216
12217    if last.ident == "Option" {
12218        let inner = generic_inner(ty, &last.arguments, "Option")?;
12219        let inner_det = detect_type(inner)?;
12220        if inner_det.nullable {
12221            return Err(syn::Error::new_spanned(
12222                ty,
12223                "nested Option is not supported",
12224            ));
12225        }
12226        if inner_det.auto {
12227            return Err(syn::Error::new_spanned(
12228                ty,
12229                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12230            ));
12231        }
12232        return Ok(DetectedType {
12233            nullable: true,
12234            ..inner_det
12235        });
12236    }
12237
12238    if last.ident == "Auto" {
12239        let inner = generic_inner(ty, &last.arguments, "Auto")?;
12240        let inner_det = detect_type(inner)?;
12241        if inner_det.auto {
12242            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
12243        }
12244        if inner_det.nullable {
12245            return Err(syn::Error::new_spanned(
12246                ty,
12247                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12248            ));
12249        }
12250        if inner_det.fk_inner.is_some() {
12251            return Err(syn::Error::new_spanned(
12252                ty,
12253                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
12254            ));
12255        }
12256        if !matches!(
12257            inner_det.kind,
12258            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
12259        ) {
12260            return Err(syn::Error::new_spanned(
12261                ty,
12262                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
12263                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
12264                 (DEFAULT now())",
12265            ));
12266        }
12267        return Ok(DetectedType {
12268            auto: true,
12269            ..inner_det
12270        });
12271    }
12272
12273    if last.ident == "ForeignKey" {
12274        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
12275        // Resolve the FK column's underlying SQL type from `K`. When the
12276        // user wrote `ForeignKey<T>` without a key parameter, the type
12277        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
12278        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
12279        // recurse into K so the column DDL emits the right SQL type
12280        // (VARCHAR for String, UUID for Uuid, …) and the load_related
12281        // emitter knows which `SqlValue` variant to match.
12282        let kind = match key_ty {
12283            Some(k) => detect_type(k)?.kind,
12284            None => DetectedKind::I64,
12285        };
12286        return Ok(DetectedType {
12287            kind,
12288            nullable: false,
12289            auto: false,
12290            fk_inner: Some(inner),
12291        });
12292    }
12293
12294    let kind = match last.ident.to_string().as_str() {
12295        "i16" => DetectedKind::I16,
12296        "i32" => DetectedKind::I32,
12297        "i64" => DetectedKind::I64,
12298        "f32" => DetectedKind::F32,
12299        "f64" => DetectedKind::F64,
12300        "bool" => DetectedKind::Bool,
12301        "String" => DetectedKind::String,
12302        "DateTime" => DetectedKind::DateTime,
12303        "NaiveDate" => DetectedKind::Date,
12304        "NaiveTime" => DetectedKind::Time,
12305        "Uuid" => DetectedKind::Uuid,
12306        "Value" => DetectedKind::Json,
12307        "Decimal" => DetectedKind::Decimal,
12308        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
12309        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
12310        // — that would emit Binary DDL and decode-fail at runtime.
12311        "Vec" => {
12312            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
12313            if let Type::Path(TypePath { path, qself: None }) = inner {
12314                if let Some(seg) = path.segments.last() {
12315                    if seg.ident == "u8" && seg.arguments.is_empty() {
12316                        return Ok(DetectedType {
12317                            kind: DetectedKind::Binary,
12318                            nullable: false,
12319                            auto: false,
12320                            fk_inner: None,
12321                        });
12322                    }
12323                }
12324            }
12325            return Err(syn::Error::new_spanned(
12326                ty,
12327                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
12328                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
12329            ));
12330        }
12331        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
12332        // `integer[]` / `bigint[]` (#341).
12333        "Array" => {
12334            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
12335            let elem = match inner {
12336                Type::Path(TypePath { path, qself: None }) => {
12337                    path.segments.last().map(|s| s.ident.to_string())
12338                }
12339                _ => None,
12340            };
12341            let kind = match elem.as_deref() {
12342                Some("String") => DetectedKind::ArrayText,
12343                Some("i32") => DetectedKind::ArrayInt,
12344                Some("i64") => DetectedKind::ArrayBigInt,
12345                _ => {
12346                    return Err(syn::Error::new_spanned(
12347                        ty,
12348                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12349                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12350                    ));
12351                }
12352            };
12353            return Ok(DetectedType {
12354                kind,
12355                nullable: false,
12356                auto: false,
12357                fk_inner: None,
12358            });
12359        }
12360        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12361        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12362        // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12363        "Range" => {
12364            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12365            let elem = match inner {
12366                Type::Path(TypePath { path, qself: None }) => {
12367                    path.segments.last().map(|s| s.ident.to_string())
12368                }
12369                _ => None,
12370            };
12371            let kind = match elem.as_deref() {
12372                Some("i32") => DetectedKind::RangeInt,
12373                Some("i64") => DetectedKind::RangeBigInt,
12374                Some("Decimal") => DetectedKind::RangeNumeric,
12375                Some("NaiveDate") => DetectedKind::RangeDate,
12376                Some("DateTime") => DetectedKind::RangeDateTime,
12377                _ => {
12378                    return Err(syn::Error::new_spanned(
12379                        ty,
12380                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12381                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12382                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12383                         (→ tstzrange) are supported (#343)",
12384                    ));
12385                }
12386            };
12387            return Ok(DetectedType {
12388                kind,
12389                nullable: false,
12390                auto: false,
12391                fk_inner: None,
12392            });
12393        }
12394        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12395        // (the `CastValue` impl bridges logical↔stored); the field's own
12396        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12397        // schema just needs `FieldType::String`.
12398        "Cast" => {
12399            return Ok(DetectedType {
12400                kind: DetectedKind::String,
12401                nullable: false,
12402                auto: false,
12403                fk_inner: None,
12404            });
12405        }
12406        // `HStore` → PG `hstore` (#342). No generic
12407        // parameter — always a string→string map.
12408        "HStore" => {
12409            return Ok(DetectedType {
12410                kind: DetectedKind::HStore,
12411                nullable: false,
12412                auto: false,
12413                fk_inner: None,
12414            });
12415        }
12416        // `Vector` → pgvector `vector(N)` (#824). The dimension is
12417        // supplied by `#[rustango(vector(dims = N))]`, not the type.
12418        "Vector" => {
12419            return Ok(DetectedType {
12420                kind: DetectedKind::Vector,
12421                nullable: false,
12422                auto: false,
12423                fk_inner: None,
12424            });
12425        }
12426        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12427        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12428        "Point" => {
12429            return Ok(DetectedType {
12430                kind: DetectedKind::Geometry,
12431                nullable: false,
12432                auto: false,
12433                fk_inner: None,
12434            });
12435        }
12436        other => {
12437            return Err(syn::Error::new_spanned(
12438                ty,
12439                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)"),
12440            ));
12441        }
12442    };
12443    Ok(DetectedType {
12444        kind,
12445        nullable: false,
12446        auto: false,
12447        fk_inner: None,
12448    })
12449}
12450
12451fn generic_inner<'a>(
12452    ty: &'a Type,
12453    arguments: &'a PathArguments,
12454    wrapper: &str,
12455) -> syn::Result<&'a Type> {
12456    let PathArguments::AngleBracketed(args) = arguments else {
12457        return Err(syn::Error::new_spanned(
12458            ty,
12459            format!("{wrapper} requires a generic argument"),
12460        ));
12461    };
12462    args.args
12463        .iter()
12464        .find_map(|a| match a {
12465            GenericArgument::Type(t) => Some(t),
12466            _ => None,
12467        })
12468        .ok_or_else(|| {
12469            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12470        })
12471}
12472
12473/// Like [`generic_inner`] but pulls *two* type args — the first is
12474/// required, the second is optional. Used by the `ForeignKey<T, K>`
12475/// detection where K defaults to `i64` when omitted.
12476fn generic_pair<'a>(
12477    ty: &'a Type,
12478    arguments: &'a PathArguments,
12479    wrapper: &str,
12480) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12481    let PathArguments::AngleBracketed(args) = arguments else {
12482        return Err(syn::Error::new_spanned(
12483            ty,
12484            format!("{wrapper} requires a generic argument"),
12485        ));
12486    };
12487    let mut types = args.args.iter().filter_map(|a| match a {
12488        GenericArgument::Type(t) => Some(t),
12489        _ => None,
12490    });
12491    let first = types.next().ok_or_else(|| {
12492        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12493    })?;
12494    let second = types.next();
12495    Ok((first, second))
12496}
12497
12498fn to_snake_case(s: &str) -> String {
12499    let mut out = String::with_capacity(s.len() + 4);
12500    for (i, ch) in s.chars().enumerate() {
12501        if ch.is_ascii_uppercase() {
12502            if i > 0 {
12503                out.push('_');
12504            }
12505            out.push(ch.to_ascii_lowercase());
12506        } else {
12507            out.push(ch);
12508        }
12509    }
12510    out
12511}
12512
12513// ============================================================
12514//  #[derive(Form)]  —  slice 8.4B
12515// ============================================================
12516
12517/// Per-field `#[form(...)]` attributes recognised by the derive.
12518#[derive(Default)]
12519struct FormFieldAttrs {
12520    min: Option<i64>,
12521    max: Option<i64>,
12522    min_length: Option<u32>,
12523    max_length: Option<u32>,
12524    /// `#[form(clean = "fn_name")]` — per-field clean hook.
12525    /// The named static method on the form struct is called after the
12526    /// field's typed parse + length/range checks; it gets the parsed
12527    /// value by reference and returns `Result<<FieldType>, String>`.
12528    /// On Ok, the returned value replaces the parsed one; on Err, the
12529    /// message is attached to the field error list. Issue #372.
12530    clean: Option<syn::Ident>,
12531}
12532
12533/// Container-level `#[form(...)]` attributes. Currently only the
12534/// cross-field `validate` hook (issue #373).
12535#[derive(Default)]
12536struct FormContainerAttrs {
12537    /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12538    /// After every per-field parse succeeds, the named method on the
12539    /// form struct is called with `&self` and may return
12540    /// `Result<(), FormErrors>`. Errors merge into the field error
12541    /// list. Issue #373.
12542    validate: Option<syn::Ident>,
12543}
12544
12545/// Detected shape of a form field's Rust type.
12546#[derive(Clone, Copy)]
12547enum FormFieldKind {
12548    String,
12549    I16,
12550    I32,
12551    I64,
12552    F32,
12553    F64,
12554    Bool,
12555}
12556
12557impl FormFieldKind {
12558    fn parse_method(self) -> &'static str {
12559        match self {
12560            Self::I16 => "i16",
12561            Self::I32 => "i32",
12562            Self::I64 => "i64",
12563            Self::F32 => "f32",
12564            Self::F64 => "f64",
12565            // String + Bool don't go through `str::parse`; the codegen
12566            // handles them inline.
12567            Self::String | Self::Bool => "",
12568        }
12569    }
12570}
12571
12572/// A generated `derive(Form)` local; mixed-site so a same-named field can't shadow it.
12573fn form_local(name: &str) -> syn::Ident {
12574    syn::Ident::new(name, proc_macro2::Span::mixed_site())
12575}
12576
12577fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12578    let root = rustango_root();
12579    let (errs, cleaned, data_param) = (
12580        form_local("__errors"),
12581        form_local("__cleaned"),
12582        form_local("data"),
12583    );
12584    let struct_name = &input.ident;
12585
12586    let Data::Struct(data) = &input.data else {
12587        return Err(syn::Error::new_spanned(
12588            struct_name,
12589            "Form can only be derived on structs",
12590        ));
12591    };
12592    let Fields::Named(named) = &data.fields else {
12593        return Err(syn::Error::new_spanned(
12594            struct_name,
12595            "Form requires a struct with named fields",
12596        ));
12597    };
12598
12599    // #373 — container-level `#[form(validate = "fn")]` hook.
12600    let container = parse_form_container_attrs(input)?;
12601    let post_field_clean: Vec<TokenStream2> = Vec::new();
12602    let _ = post_field_clean;
12603
12604    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12605    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12606
12607    for field in &named.named {
12608        let ident = field
12609            .ident
12610            .as_ref()
12611            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12612        let attrs = parse_form_field_attrs(field)?;
12613        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12614        let is_string = matches!(kind, FormFieldKind::String);
12615        let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12616        if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12617            return Err(syn::Error::new(
12618                field.span(),
12619                "`min_length` / `max_length` need a String field",
12620            ));
12621        }
12622        if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12623            return Err(syn::Error::new(
12624                field.span(),
12625                "`min` / `max` need a numeric field",
12626            ));
12627        }
12628
12629        let name_lit = ident_name(ident);
12630        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12631        // #372 — append the per-field `clean_<field>` call right after
12632        // the parse block when the attribute is set. The clean fn
12633        // takes &T and returns Result<T, String>; on Err we attach
12634        // the message to the field error list without aborting, so
12635        // the caller sees every field error at once.
12636        let clean_block = if let Some(clean_fn) = &attrs.clean {
12637            quote! {
12638                if #errs.fields().get(#name_lit).is_none() {
12639                    match Self::#clean_fn(&#ident) {
12640                        ::core::result::Result::Ok(#cleaned) => { #ident = #cleaned; }
12641                        ::core::result::Result::Err(__msg) => {
12642                            #errs.add(#name_lit, __msg);
12643                        }
12644                    }
12645                }
12646            }
12647        } else {
12648            quote! {}
12649        };
12650        field_blocks.push(quote! {
12651            #parse_block
12652            #clean_block
12653        });
12654        field_idents.push(ident);
12655    }
12656
12657    // #373 — after every per-field parse + clean succeeds, call the
12658    // cross-field validator if declared. Errors merge into the
12659    // outgoing FormErrors via the existing `FormErrors::merge` helper
12660    // (same primitive the serializer cross-field hook uses).
12661    let cross_field_call = if let Some(validate_fn) = &container.validate {
12662        quote! {
12663            if #errs.is_empty() {
12664                let __candidate = Self { #( #field_idents ),* };
12665                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12666                    #errs.merge(__other);
12667                }
12668                if !#errs.is_empty() {
12669                    return ::core::result::Result::Err(#errs);
12670                }
12671                return ::core::result::Result::Ok(__candidate);
12672            }
12673        }
12674    } else {
12675        quote! {}
12676    };
12677
12678    Ok(quote! {
12679        impl #root::forms::Form for #struct_name {
12680            fn parse(
12681                #data_param: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12682            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12683                let mut #errs = #root::forms::FormErrors::default();
12684                #( #field_blocks )*
12685                #cross_field_call
12686                if !#errs.is_empty() {
12687                    return ::core::result::Result::Err(#errs);
12688                }
12689                ::core::result::Result::Ok(Self {
12690                    #( #field_idents ),*
12691                })
12692            }
12693        }
12694    })
12695}
12696
12697fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12698    let mut out = FormContainerAttrs::default();
12699    for attr in &input.attrs {
12700        if !attr.path().is_ident("form") {
12701            continue;
12702        }
12703        attr.parse_nested_meta(|meta| {
12704            if meta.path.is_ident("validate") {
12705                let s: LitStr = meta.value()?.parse()?;
12706                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12707                return Ok(());
12708            }
12709            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12710        })?;
12711    }
12712    Ok(out)
12713}
12714
12715fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12716    let mut out = FormFieldAttrs::default();
12717    for attr in &field.attrs {
12718        if !attr.path().is_ident("form") {
12719            continue;
12720        }
12721        attr.parse_nested_meta(|meta| {
12722            if meta.path.is_ident("min") {
12723                let lit: syn::LitInt = meta.value()?.parse()?;
12724                out.min = Some(lit.base10_parse::<i64>()?);
12725                return Ok(());
12726            }
12727            if meta.path.is_ident("max") {
12728                let lit: syn::LitInt = meta.value()?.parse()?;
12729                out.max = Some(lit.base10_parse::<i64>()?);
12730                return Ok(());
12731            }
12732            if meta.path.is_ident("min_length") {
12733                let lit: syn::LitInt = meta.value()?.parse()?;
12734                out.min_length = Some(lit.base10_parse::<u32>()?);
12735                return Ok(());
12736            }
12737            if meta.path.is_ident("max_length") {
12738                let lit: syn::LitInt = meta.value()?.parse()?;
12739                out.max_length = Some(lit.base10_parse::<u32>()?);
12740                return Ok(());
12741            }
12742            if meta.path.is_ident("clean") {
12743                let s: LitStr = meta.value()?.parse()?;
12744                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12745                return Ok(());
12746            }
12747            Err(meta.error(
12748                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12749            ))
12750        })?;
12751    }
12752    Ok(out)
12753}
12754
12755fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12756    let Type::Path(TypePath { path, qself: None }) = ty else {
12757        return Err(syn::Error::new(
12758            span,
12759            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12760        ));
12761    };
12762    let last = path
12763        .segments
12764        .last()
12765        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12766
12767    if last.ident == "Option" {
12768        let inner = generic_inner(ty, &last.arguments, "Option")?;
12769        let (kind, nested) = detect_form_field(inner, span)?;
12770        if nested {
12771            return Err(syn::Error::new(
12772                span,
12773                "nested Option in Form fields is not supported",
12774            ));
12775        }
12776        return Ok((kind, true));
12777    }
12778
12779    let kind = match last.ident.to_string().as_str() {
12780        "String" => FormFieldKind::String,
12781        "i16" => FormFieldKind::I16,
12782        "i32" => FormFieldKind::I32,
12783        "i64" => FormFieldKind::I64,
12784        "f32" => FormFieldKind::F32,
12785        "f64" => FormFieldKind::F64,
12786        "bool" => FormFieldKind::Bool,
12787        other => {
12788            return Err(syn::Error::new(
12789                span,
12790                format!(
12791                    "Form field type `{other}` is not supported in v0.8 — use String / \
12792                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12793                ),
12794            ));
12795        }
12796    };
12797    Ok((kind, false))
12798}
12799
12800#[allow(clippy::too_many_lines)]
12801fn render_form_field_parse(
12802    ident: &syn::Ident,
12803    name_lit: &str,
12804    kind: FormFieldKind,
12805    nullable: bool,
12806    attrs: &FormFieldAttrs,
12807) -> TokenStream2 {
12808    let (errs, data_param) = (form_local("__errors"), form_local("data"));
12809    // Pull the raw &str from the payload `Form::parse(data: &HashMap<…>)`.
12810    let lookup = quote! {
12811        let __raw: ::core::option::Option<&::std::string::String> = #data_param.get(#name_lit);
12812    };
12813
12814    let root = rustango_root();
12815    // Strict, as ModelForm: an unknown spelling is an error, not `true`.
12816    let checkbox = quote! {
12817        match #root::forms::checkbox(__s) {
12818            ::core::option::Option::Some(__b) => __b,
12819            ::core::option::Option::None => {
12820                #errs.add(#name_lit, "Enter true or false.");
12821                false
12822            }
12823        }
12824    };
12825    let parsed_value = match kind {
12826        // `Option<bool>`: absent or blank is `None`, as for the other types.
12827        FormFieldKind::Bool if nullable => quote! {
12828            let __v: ::core::option::Option<bool> = match __raw {
12829                ::core::option::Option::None => ::core::option::Option::None,
12830                ::core::option::Option::Some(__s) if __s.is_empty() => {
12831                    ::core::option::Option::None
12832                }
12833                ::core::option::Option::Some(__s) => ::core::option::Option::Some(#checkbox),
12834            };
12835        },
12836        FormFieldKind::Bool => quote! {
12837            let __v: bool = match __raw {
12838                ::core::option::Option::None => false,
12839                ::core::option::Option::Some(__s) => #checkbox,
12840            };
12841        },
12842        FormFieldKind::String => {
12843            if nullable {
12844                quote! {
12845                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12846                        ::core::option::Option::None => ::core::option::Option::None,
12847                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12848                            ::core::option::Option::None
12849                        }
12850                        ::core::option::Option::Some(__s) => {
12851                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12852                        }
12853                    };
12854                }
12855            } else {
12856                quote! {
12857                    let __v: ::std::string::String = match __raw {
12858                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12859                            ::core::clone::Clone::clone(__s)
12860                        }
12861                        _ => {
12862                            #errs.add(#name_lit, "This field is required.");
12863                            ::std::string::String::new()
12864                        }
12865                    };
12866                }
12867            }
12868        }
12869        FormFieldKind::I16
12870        | FormFieldKind::I32
12871        | FormFieldKind::I64
12872        | FormFieldKind::F32
12873        | FormFieldKind::F64 => {
12874            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12875            let ty_lit = kind.parse_method();
12876            let default_val = match kind {
12877                FormFieldKind::I16 => quote! { 0i16 },
12878                FormFieldKind::I32 => quote! { 0i32 },
12879                FormFieldKind::I64 => quote! { 0i64 },
12880                FormFieldKind::F32 => quote! { 0f32 },
12881                FormFieldKind::F64 => quote! { 0f64 },
12882                _ => quote! { Default::default() },
12883            };
12884            // NaN and inf parse, then pass every min/max compare.
12885            let finite = if matches!(kind, FormFieldKind::F32 | FormFieldKind::F64) {
12886                quote! {
12887                    .and_then(|__n| if __n.is_finite() {
12888                        ::core::result::Result::Ok(__n)
12889                    } else {
12890                        ::core::result::Result::Err(::std::string::String::from("not a finite number"))
12891                    })
12892                }
12893            } else {
12894                quote! {}
12895            };
12896            if nullable {
12897                quote! {
12898                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12899                        ::core::option::Option::None => ::core::option::Option::None,
12900                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12901                            ::core::option::Option::None
12902                        }
12903                        ::core::option::Option::Some(__s) => {
12904                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12905                                ::core::result::Result::Ok(__n) => {
12906                                    ::core::option::Option::Some(__n)
12907                                }
12908                                ::core::result::Result::Err(__e) => {
12909                                    #errs.add(
12910                                        #name_lit,
12911                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12912                                    );
12913                                    ::core::option::Option::None
12914                                }
12915                            }
12916                        }
12917                    };
12918                }
12919            } else {
12920                quote! {
12921                    let __v: #parse_ty = match __raw {
12922                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12923                            match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12924                                ::core::result::Result::Ok(__n) => __n,
12925                                ::core::result::Result::Err(__e) => {
12926                                    #errs.add(
12927                                        #name_lit,
12928                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12929                                    );
12930                                    #default_val
12931                                }
12932                            }
12933                        }
12934                        _ => {
12935                            #errs.add(#name_lit, "This field is required.");
12936                            #default_val
12937                        }
12938                    };
12939                }
12940            }
12941        }
12942    };
12943
12944    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12945
12946    quote! {
12947        // `mut` so the per-field `clean` hook (#372) can rewrite the
12948        // parsed value in-place when it returns Ok with a normalized
12949        // form (e.g. trim / lowercase).
12950        let mut #ident = {
12951            #lookup
12952            #parsed_value
12953            #validators
12954            __v
12955        };
12956    }
12957}
12958
12959fn render_form_validators(
12960    name_lit: &str,
12961    kind: FormFieldKind,
12962    nullable: bool,
12963    attrs: &FormFieldAttrs,
12964) -> TokenStream2 {
12965    let errs = form_local("__errors");
12966    let mut checks: Vec<TokenStream2> = Vec::new();
12967
12968    let val_ref = if nullable {
12969        quote! { __v.as_ref() }
12970    } else {
12971        quote! { ::core::option::Option::Some(&__v) }
12972    };
12973
12974    let is_string = matches!(kind, FormFieldKind::String);
12975    let is_numeric = matches!(
12976        kind,
12977        FormFieldKind::I16
12978            | FormFieldKind::I32
12979            | FormFieldKind::I64
12980            | FormFieldKind::F32
12981            | FormFieldKind::F64
12982    );
12983
12984    if is_string {
12985        if let Some(min_len) = attrs.min_length {
12986            let min_len_usize = min_len as usize;
12987            checks.push(quote! {
12988                if let ::core::option::Option::Some(__s) = #val_ref {
12989                    if __s.chars().count() < #min_len_usize {
12990                        #errs.add(
12991                            #name_lit,
12992                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12993                        );
12994                    }
12995                }
12996            });
12997        }
12998        if let Some(max_len) = attrs.max_length {
12999            let max_len_usize = max_len as usize;
13000            checks.push(quote! {
13001                if let ::core::option::Option::Some(__s) = #val_ref {
13002                    if __s.chars().count() > #max_len_usize {
13003                        #errs.add(
13004                            #name_lit,
13005                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
13006                        );
13007                    }
13008                }
13009            });
13010        }
13011    }
13012
13013    if is_numeric {
13014        if let Some(min) = attrs.min {
13015            checks.push(quote! {
13016                if let ::core::option::Option::Some(__n) = #val_ref {
13017                    if (*__n as f64) < (#min as f64) {
13018                        #errs.add(
13019                            #name_lit,
13020                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
13021                        );
13022                    }
13023                }
13024            });
13025        }
13026        if let Some(max) = attrs.max {
13027            checks.push(quote! {
13028                if let ::core::option::Option::Some(__n) = #val_ref {
13029                    if (*__n as f64) > (#max as f64) {
13030                        #errs.add(
13031                            #name_lit,
13032                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
13033                        );
13034                    }
13035                }
13036            });
13037        }
13038    }
13039
13040    quote! { #( #checks )* }
13041}
13042
13043// ============================================================
13044//  #[derive(ViewSet)]
13045// ============================================================
13046
13047struct ViewSetAttrs {
13048    model: syn::Path,
13049    /// `filter_backend = expr`, in order.
13050    filter_backends: Vec<syn::Expr>,
13051    fields: Vec<String>,
13052    filter_fields: Vec<String>,
13053    search_fields: Vec<String>,
13054    /// (field_name, desc)
13055    ordering: Vec<(String, bool)>,
13056    page_size: Option<usize>,
13057    read_only: bool,
13058    allow_anonymous: bool,
13059    perms: ViewSetPermsAttrs,
13060    /// `#[viewset(serializer = SomeSerializer)]` — render list /
13061    /// retrieve / create responses through this `#[derive(Serializer)]`
13062    /// type instead of the default field-level projection (requires the
13063    /// `serializer` feature). Tri-dialect.
13064    serializer: Option<syn::Path>,
13065}
13066
13067#[derive(Default)]
13068struct ViewSetPermsAttrs {
13069    list: Vec<String>,
13070    retrieve: Vec<String>,
13071    create: Vec<String>,
13072    update: Vec<String>,
13073    destroy: Vec<String>,
13074}
13075
13076fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
13077    let root = rustango_root();
13078    let struct_name = &input.ident;
13079
13080    // Must be a unit struct or an empty named struct.
13081    match &input.data {
13082        Data::Struct(s) => match &s.fields {
13083            Fields::Unit | Fields::Named(_) => {}
13084            Fields::Unnamed(_) => {
13085                return Err(syn::Error::new_spanned(
13086                    struct_name,
13087                    "ViewSet can only be derived on a unit struct or an empty named struct",
13088                ));
13089            }
13090        },
13091        _ => {
13092            return Err(syn::Error::new_spanned(
13093                struct_name,
13094                "ViewSet can only be derived on a struct",
13095            ));
13096        }
13097    }
13098
13099    let attrs = parse_viewset_attrs(input)?;
13100    let model_path = &attrs.model;
13101
13102    let field_lits = attrs.fields.iter().map(|f| f.as_str());
13103
13104    let filter_fields_call = if attrs.filter_fields.is_empty() {
13105        quote!()
13106    } else {
13107        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
13108        quote!(.filter_fields(&[ #(#lits),* ]))
13109    };
13110
13111    let search_fields_call = if attrs.search_fields.is_empty() {
13112        quote!()
13113    } else {
13114        let lits = attrs.search_fields.iter().map(|f| f.as_str());
13115        quote!(.search_fields(&[ #(#lits),* ]))
13116    };
13117
13118    let ordering_call = if attrs.ordering.is_empty() {
13119        quote!()
13120    } else {
13121        let pairs = attrs.ordering.iter().map(|(f, desc)| {
13122            let f = f.as_str();
13123            quote!((#f, #desc))
13124        });
13125        quote!(.ordering(&[ #(#pairs),* ]))
13126    };
13127
13128    let filter_backends = &attrs.filter_backends;
13129    let filter_backend_calls = quote!( #( .filter_backend(#filter_backends) )* );
13130
13131    let page_size_call = if let Some(n) = attrs.page_size {
13132        quote!(.page_size(#n))
13133    } else {
13134        quote!()
13135    };
13136
13137    let read_only_call = if attrs.read_only {
13138        quote!(.read_only())
13139    } else {
13140        quote!()
13141    };
13142    let allow_anonymous_call = if attrs.allow_anonymous {
13143        quote!(.allow_anonymous())
13144    } else {
13145        quote!()
13146    };
13147
13148    // `.serializer::<S>()` — reshape responses through a derived
13149    // serializer. Requires the downstream crate to enable the
13150    // `serializer` feature (the method is gated on it).
13151    let serializer_call = if let Some(ref ser) = attrs.serializer {
13152        quote!(.serializer::<#ser>())
13153    } else {
13154        quote!()
13155    };
13156
13157    let perms = &attrs.perms;
13158    let perms_call = if perms.list.is_empty()
13159        && perms.retrieve.is_empty()
13160        && perms.create.is_empty()
13161        && perms.update.is_empty()
13162        && perms.destroy.is_empty()
13163    {
13164        quote!()
13165    } else {
13166        let list_lits = perms.list.iter().map(|s| s.as_str());
13167        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
13168        let create_lits = perms.create.iter().map(|s| s.as_str());
13169        let update_lits = perms.update.iter().map(|s| s.as_str());
13170        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
13171        quote! {
13172            .permissions(#root::viewset::ViewSetPerms {
13173                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
13174                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
13175                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
13176                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
13177                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
13178            })
13179        }
13180    };
13181
13182    Ok(quote! {
13183        impl #struct_name {
13184            /// Build an `axum::Router` with the six standard REST endpoints
13185            /// for this ViewSet, mounted at `prefix`.
13186            ///
13187            /// Accepts any backend's pool — a `sqlx::PgPool`,
13188            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
13189            /// enum — because they all `Into<Pool>` (#1273). This is why
13190            /// the derive no longer names `PgPool`, which would not
13191            /// exist on a project built without the `postgres` feature.
13192            pub fn router(
13193                prefix: &str,
13194                pool: impl ::core::convert::Into<#root::sql::Pool>,
13195            ) -> #root::__axum::Router {
13196                #root::viewset::ViewSet::for_model(
13197                    <#model_path as #root::core::Model>::SCHEMA,
13198                    &[ #(#field_lits),* ],
13199                )
13200                    #filter_fields_call
13201                    #filter_backend_calls
13202                    #search_fields_call
13203                    #ordering_call
13204                    #page_size_call
13205                    #perms_call
13206                    #read_only_call
13207                    #allow_anonymous_call
13208                    #serializer_call
13209                    .router(prefix, pool.into())
13210            }
13211        }
13212    })
13213}
13214
13215fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
13216    let mut model: Option<syn::Path> = None;
13217    let mut fields: Option<Vec<String>> = None;
13218    let mut filter_fields: Vec<String> = Vec::new();
13219    let mut search_fields: Vec<String> = Vec::new();
13220    let mut ordering: Vec<(String, bool)> = Vec::new();
13221    let mut page_size: Option<usize> = None;
13222    let mut read_only = false;
13223    let mut allow_anonymous = false;
13224    let mut perms = ViewSetPermsAttrs::default();
13225    let mut serializer: Option<syn::Path> = None;
13226    let mut filter_backends: Vec<syn::Expr> = Vec::new();
13227
13228    for attr in &input.attrs {
13229        if !attr.path().is_ident("viewset") {
13230            continue;
13231        }
13232        attr.parse_nested_meta(|meta| {
13233            if meta.path.is_ident("model") {
13234                let path: syn::Path = meta.value()?.parse()?;
13235                model = Some(path);
13236                return Ok(());
13237            }
13238            if meta.path.is_ident("serializer") {
13239                let path: syn::Path = meta.value()?.parse()?;
13240                serializer = Some(path);
13241                return Ok(());
13242            }
13243            if meta.path.is_ident("fields") {
13244                let s: LitStr = meta.value()?.parse()?;
13245                fields = Some(split_field_list(&s.value()));
13246                return Ok(());
13247            }
13248            if meta.path.is_ident("filter_backend") {
13249                filter_backends.push(meta.value()?.parse()?);
13250                return Ok(());
13251            }
13252            if meta.path.is_ident("filter_fields") {
13253                let s: LitStr = meta.value()?.parse()?;
13254                filter_fields = split_field_list(&s.value());
13255                return Ok(());
13256            }
13257            if meta.path.is_ident("search_fields") {
13258                let s: LitStr = meta.value()?.parse()?;
13259                search_fields = split_field_list(&s.value());
13260                return Ok(());
13261            }
13262            if meta.path.is_ident("ordering") {
13263                let s: LitStr = meta.value()?.parse()?;
13264                ordering = parse_ordering_list(&s.value());
13265                return Ok(());
13266            }
13267            if meta.path.is_ident("page_size") {
13268                let lit: syn::LitInt = meta.value()?.parse()?;
13269                page_size = Some(lit.base10_parse::<usize>()?);
13270                return Ok(());
13271            }
13272            if meta.path.is_ident("read_only") {
13273                read_only = true;
13274                return Ok(());
13275            }
13276            if meta.path.is_ident("allow_anonymous") {
13277                allow_anonymous = true;
13278                return Ok(());
13279            }
13280            if meta.path.is_ident("permissions") {
13281                meta.parse_nested_meta(|inner| {
13282                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
13283                        let s: LitStr = inner.value()?.parse()?;
13284                        Ok(split_field_list(&s.value()))
13285                    };
13286                    if inner.path.is_ident("list") {
13287                        perms.list = parse_codenames(&inner)?;
13288                    } else if inner.path.is_ident("retrieve") {
13289                        perms.retrieve = parse_codenames(&inner)?;
13290                    } else if inner.path.is_ident("create") {
13291                        perms.create = parse_codenames(&inner)?;
13292                    } else if inner.path.is_ident("update") {
13293                        perms.update = parse_codenames(&inner)?;
13294                    } else if inner.path.is_ident("destroy") {
13295                        perms.destroy = parse_codenames(&inner)?;
13296                    } else {
13297                        return Err(inner.error(
13298                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
13299                        ));
13300                    }
13301                    Ok(())
13302                })?;
13303                return Ok(());
13304            }
13305            Err(meta.error(
13306                "unknown viewset attribute (supported: model, fields, filter_fields, \
13307                 filter_backend, search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
13308                 permissions(...))",
13309            ))
13310        })?;
13311    }
13312
13313    let model = model.ok_or_else(|| {
13314        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
13315    })?;
13316    // The field list is the write allow-list, so it is never implied.
13317    let fields = fields.ok_or_else(|| {
13318        syn::Error::new_spanned(
13319            &input.ident,
13320            "`#[viewset(fields = \"id, title\")]` is required: the fields the API renders and writes",
13321        )
13322    })?;
13323
13324    Ok(ViewSetAttrs {
13325        model,
13326        filter_backends,
13327        fields,
13328        filter_fields,
13329        search_fields,
13330        ordering,
13331        page_size,
13332        read_only,
13333        allow_anonymous,
13334        perms,
13335        serializer,
13336    })
13337}
13338
13339// ============================================================ #[derive(Serializer)]
13340
13341struct SerializerContainerAttrs {
13342    model: syn::Path,
13343    /// `#[serializer(validate = "fn_name")]` on the struct — the
13344    /// cross-field validation hook (#436). The named inherent method
13345    /// must take `&self` and return
13346    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
13347    /// `validate()` runs every per-field validator first; then calls
13348    /// the cross-field method if declared; aggregates all errors into
13349    /// one `FormErrors`.
13350    cross_validate: Option<syn::Ident>,
13351}
13352
13353#[derive(Default)]
13354struct SerializerFieldAttrs {
13355    read_only: bool,
13356    write_only: bool,
13357    source: Option<String>,
13358    skip: bool,
13359    /// `#[serializer(method = "fn_name")]` — computed field. The macro
13360    /// emits a `from_model` initializer that calls
13361    /// `Self::fn_name(&model)` and stores the return value.
13362    method: Option<String>,
13363    /// `#[serializer(validate = "fn_name")]` — per-field validator
13364    /// callable run by `Self::validate(&self)`. Must return
13365    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
13366    /// the field name.
13367    validate: Option<String>,
13368    /// `#[serializer(nested)]` on a field whose type is another
13369    /// `Serializer` — the macro emits `from_model` initializer that
13370    /// reads the parent via `model.<source>.value()` then calls the
13371    /// child serializer's `from_model(parent)`. When the FK is
13372    /// unloaded the field falls back to `Default::default()` (does
13373    /// NOT panic) so a missing prefetch in prod degrades gracefully.
13374    /// Source field on the model defaults to the field name; override
13375    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
13376    /// panic-on-unloaded behavior for tests.
13377    nested: bool,
13378    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
13379    /// strict behavior: panic when the FK isn't loaded. Useful in
13380    /// test code where forgetting select_related must trip a hard
13381    /// failure rather than render a blank nested object.
13382    nested_strict: bool,
13383    /// `#[serializer(many = TagSerializer)]` — declare the field as
13384    /// a list of nested serializers. Field type must be `Vec<S>`
13385    /// where `S` is the inner serializer. The macro initializes the
13386    /// field to `Vec::new()` in `from_model` and emits a typed
13387    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13388    /// maps each model row through `S::from_model`. Auto-load isn't
13389    /// possible (the M2M / one-to-many accessor is async); callers
13390    /// fetch the children + call the setter post-from_model.
13391    many: Option<syn::Type>,
13392    /// `#[serializer(slug = "name")]` — render a relation as one of
13393    /// its fields.
13394    /// Source field on the model must be a `ForeignKey<T>`; the
13395    /// macro emits `from_model` glue that walks
13396    /// `model.<source>.value()?.<slug>` and clones it. Field type on
13397    /// the serializer is typically `String` (whatever type the slug
13398    /// column has). When the FK is unloaded the field falls back to
13399    /// `Default::default()`, same graceful-degrade contract as
13400    /// `nested`. Source defaults to the field name; override with
13401    /// `source = "..."`. v0.44.
13402    slug: Option<String>,
13403    /// `#[serializer(max_length = N)]` — caps
13404    /// the character count of a string field on write. Overrides the
13405    /// model's `max_length`; when absent the model value is inherited.
13406    max_length: Option<u64>,
13407    /// `#[serializer(min_length = N)]` — minimum character count.
13408    /// Serializer-only (the model has no `min_length` column).
13409    min_length: Option<u64>,
13410    /// `#[serializer(min = N)]` — inclusive
13411    /// integer lower bound; overrides the model's `min` when given.
13412    min: Option<i64>,
13413    /// `#[serializer(max = N)]` — inclusive
13414    /// integer upper bound; overrides the model's `max` when given.
13415    max: Option<i64>,
13416}
13417
13418fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13419    let mut model: Option<syn::Path> = None;
13420    let mut cross_validate: Option<syn::Ident> = None;
13421    for attr in &input.attrs {
13422        if !attr.path().is_ident("serializer") {
13423            continue;
13424        }
13425        attr.parse_nested_meta(|meta| {
13426            if meta.path.is_ident("model") {
13427                let _eq: syn::Token![=] = meta.input.parse()?;
13428                model = Some(meta.input.parse()?);
13429                return Ok(());
13430            }
13431            if meta.path.is_ident("validate") {
13432                // #436 — container-level `validate = "fn_name"` for
13433                // cross-field validation. Field-level
13434                // `#[serializer(validate = "...")]` on a field is
13435                // parsed separately in `parse_serializer_field_attrs`.
13436                let s: LitStr = meta.value()?.parse()?;
13437                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13438                return Ok(());
13439            }
13440            Err(meta.error(
13441                "unknown serializer container attribute \
13442                 (supported: `model`, `validate`)",
13443            ))
13444        })?;
13445    }
13446    let model = model.ok_or_else(|| {
13447        syn::Error::new_spanned(
13448            &input.ident,
13449            "`#[serializer(model = SomeModel)]` is required",
13450        )
13451    })?;
13452    Ok(SerializerContainerAttrs {
13453        model,
13454        cross_validate,
13455    })
13456}
13457
13458/// The `#[serde(...)]` keys the Serializer derive honours. Its own
13459/// `Serialize` impl would otherwise drop them silently.
13460#[derive(Default)]
13461struct SerdeWireAttrs {
13462    rename_ser: Option<String>,
13463    rename_de: Option<String>,
13464    skip_ser: bool,
13465    skip_de: bool,
13466    skip_ser_if: Option<syn::ExprPath>,
13467}
13468
13469/// A serde `rename_all` rule, applied to a `snake_case` field name.
13470#[derive(Clone, Copy)]
13471enum SerdeRenameRule {
13472    Lower,
13473    Upper,
13474    Pascal,
13475    Camel,
13476    ScreamingSnake,
13477    Kebab,
13478    ScreamingKebab,
13479}
13480
13481impl SerdeRenameRule {
13482    fn parse(s: &LitStr) -> syn::Result<Self> {
13483        Ok(match s.value().as_str() {
13484            "lowercase" | "snake_case" => Self::Lower,
13485            "UPPERCASE" => Self::Upper,
13486            "PascalCase" => Self::Pascal,
13487            "camelCase" => Self::Camel,
13488            "SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
13489            "kebab-case" => Self::Kebab,
13490            "SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
13491            _ => {
13492                return Err(syn::Error::new_spanned(
13493                    s,
13494                    "unknown serde `rename_all` rule",
13495                ))
13496            }
13497        })
13498    }
13499
13500    /// Same output as serde's own field renaming.
13501    fn apply(self, field: &str) -> String {
13502        match self {
13503            Self::Lower => field.to_owned(),
13504            Self::Upper | Self::ScreamingSnake => field.to_ascii_uppercase(),
13505            Self::Pascal | Self::Camel => {
13506                let mut out = String::new();
13507                let mut cap = matches!(self, Self::Pascal);
13508                for ch in field.chars() {
13509                    if ch == '_' {
13510                        cap = !out.is_empty() || matches!(self, Self::Pascal);
13511                    } else if cap {
13512                        out.push(ch.to_ascii_uppercase());
13513                        cap = false;
13514                    } else {
13515                        out.push(ch);
13516                    }
13517                }
13518                out
13519            }
13520            Self::Kebab => field.replace('_', "-"),
13521            Self::ScreamingKebab => field.to_ascii_uppercase().replace('_', "-"),
13522        }
13523    }
13524}
13525
13526/// Consume the value of a `serde` key the derive does not act on.
13527fn skip_serde_meta_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
13528    if meta.input.peek(syn::Token![=]) {
13529        let _: syn::Expr = meta.value()?.parse()?;
13530    } else if meta.input.peek(syn::token::Paren) {
13531        let _: proc_macro2::Group = meta.input.parse()?;
13532    }
13533    Ok(())
13534}
13535
13536/// `rename = "x"` or `rename(serialize = "a", deserialize = "b")`.
13537fn parse_serde_rename(
13538    meta: &syn::meta::ParseNestedMeta,
13539    mut set: impl FnMut(bool, LitStr) -> syn::Result<()>,
13540) -> syn::Result<()> {
13541    if meta.input.peek(syn::Token![=]) {
13542        let s: LitStr = meta.value()?.parse()?;
13543        set(true, s.clone())?;
13544        return set(false, s);
13545    }
13546    meta.parse_nested_meta(|inner| {
13547        let is_ser = inner.path.is_ident("serialize");
13548        if !is_ser && !inner.path.is_ident("deserialize") {
13549            return Err(inner.error("expected `serialize` or `deserialize`"));
13550        }
13551        set(is_ser, inner.value()?.parse()?)
13552    })
13553}
13554
13555/// Container `rename_all` rules: (serialize, deserialize).
13556fn parse_serde_container_rules(
13557    input: &DeriveInput,
13558) -> syn::Result<(Option<SerdeRenameRule>, Option<SerdeRenameRule>)> {
13559    let (mut ser, mut de) = (None, None);
13560    for attr in input.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13561        attr.parse_nested_meta(|meta| {
13562            if meta.path.is_ident("rename_all") {
13563                return parse_serde_rename(&meta, |is_ser, s| {
13564                    let rule = Some(SerdeRenameRule::parse(&s)?);
13565                    if is_ser {
13566                        ser = rule;
13567                    } else {
13568                        de = rule;
13569                    }
13570                    Ok(())
13571                });
13572            }
13573            skip_serde_meta_value(&meta)
13574        })?;
13575    }
13576    Ok((ser, de))
13577}
13578
13579fn parse_serde_field_attrs(field: &syn::Field) -> syn::Result<SerdeWireAttrs> {
13580    let mut out = SerdeWireAttrs::default();
13581    for attr in field.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13582        attr.parse_nested_meta(|meta| {
13583            if meta.path.is_ident("rename") {
13584                return parse_serde_rename(&meta, |is_ser, s| {
13585                    if is_ser {
13586                        out.rename_ser = Some(s.value());
13587                    } else {
13588                        out.rename_de = Some(s.value());
13589                    }
13590                    Ok(())
13591                });
13592            }
13593            if meta.path.is_ident("skip") {
13594                out.skip_ser = true;
13595                out.skip_de = true;
13596                return Ok(());
13597            }
13598            if meta.path.is_ident("skip_serializing") {
13599                out.skip_ser = true;
13600                return Ok(());
13601            }
13602            if meta.path.is_ident("skip_deserializing") {
13603                out.skip_de = true;
13604                return Ok(());
13605            }
13606            if meta.path.is_ident("skip_serializing_if") {
13607                let s: LitStr = meta.value()?.parse()?;
13608                out.skip_ser_if = Some(s.parse()?);
13609                return Ok(());
13610            }
13611            skip_serde_meta_value(&meta)
13612        })?;
13613    }
13614    Ok(out)
13615}
13616
13617fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13618    let mut out = SerializerFieldAttrs::default();
13619    for attr in &field.attrs {
13620        if !attr.path().is_ident("serializer") {
13621            continue;
13622        }
13623        attr.parse_nested_meta(|meta| {
13624            if meta.path.is_ident("read_only") {
13625                out.read_only = true;
13626                return Ok(());
13627            }
13628            if meta.path.is_ident("write_only") {
13629                out.write_only = true;
13630                return Ok(());
13631            }
13632            if meta.path.is_ident("skip") {
13633                out.skip = true;
13634                return Ok(());
13635            }
13636            if meta.path.is_ident("source") {
13637                let s: LitStr = meta.value()?.parse()?;
13638                out.source = Some(s.value());
13639                return Ok(());
13640            }
13641            if meta.path.is_ident("method") {
13642                let s: LitStr = meta.value()?.parse()?;
13643                out.method = Some(s.value());
13644                return Ok(());
13645            }
13646            if meta.path.is_ident("validate") {
13647                let s: LitStr = meta.value()?.parse()?;
13648                out.validate = Some(s.value());
13649                return Ok(());
13650            }
13651            if meta.path.is_ident("many") {
13652                let _eq: syn::Token![=] = meta.input.parse()?;
13653                out.many = Some(meta.input.parse()?);
13654                return Ok(());
13655            }
13656            if meta.path.is_ident("nested") {
13657                out.nested = true;
13658                // Optional strict flag inside parentheses:
13659                //   #[serializer(nested(strict))]
13660                if meta.input.peek(syn::token::Paren) {
13661                    meta.parse_nested_meta(|inner| {
13662                        if inner.path.is_ident("strict") {
13663                            out.nested_strict = true;
13664                            return Ok(());
13665                        }
13666                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13667                    })?;
13668                }
13669                return Ok(());
13670            }
13671            if meta.path.is_ident("slug") {
13672                let s: LitStr = meta.value()?.parse()?;
13673                out.slug = Some(s.value());
13674                return Ok(());
13675            }
13676            if meta.path.is_ident("max_length") {
13677                let lit: syn::LitInt = meta.value()?.parse()?;
13678                out.max_length = Some(lit.base10_parse::<u64>()?);
13679                return Ok(());
13680            }
13681            if meta.path.is_ident("min_length") {
13682                let lit: syn::LitInt = meta.value()?.parse()?;
13683                out.min_length = Some(lit.base10_parse::<u64>()?);
13684                return Ok(());
13685            }
13686            if meta.path.is_ident("min") {
13687                let lit: syn::LitInt = meta.value()?.parse()?;
13688                out.min = Some(lit.base10_parse::<i64>()?);
13689                return Ok(());
13690            }
13691            if meta.path.is_ident("max") {
13692                let lit: syn::LitInt = meta.value()?.parse()?;
13693                out.max = Some(lit.base10_parse::<i64>()?);
13694                return Ok(());
13695            }
13696            Err(meta.error(
13697                "unknown serializer field attribute (supported: \
13698                 `read_only`, `write_only`, `source`, `skip`, `method`, \
13699                 `validate`, `nested`, `many`, `slug`, `max_length`, \
13700                 `min_length`, `min`, `max`)",
13701            ))
13702        })?;
13703    }
13704    // Validate: read_only + write_only is nonsensical
13705    if out.read_only && out.write_only {
13706        return Err(syn::Error::new_spanned(
13707            field,
13708            "a field cannot be both `read_only` and `write_only`",
13709        ));
13710    }
13711    if out.method.is_some() && out.source.is_some() {
13712        return Err(syn::Error::new_spanned(
13713            field,
13714            "`method` and `source` are mutually exclusive — `method` computes \
13715             the value from a method, `source` reads it from a different model field",
13716        ));
13717    }
13718    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13719        return Err(syn::Error::new_spanned(
13720            field,
13721            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13722             — pick one strategy for populating the field",
13723        ));
13724    }
13725    Ok(out)
13726}
13727
13728fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13729    let root = rustango_root();
13730    let struct_name = &input.ident;
13731    let struct_name_lit = struct_name.to_string();
13732
13733    let Data::Struct(data) = &input.data else {
13734        return Err(syn::Error::new_spanned(
13735            struct_name,
13736            "Serializer can only be derived on structs",
13737        ));
13738    };
13739    let Fields::Named(named) = &data.fields else {
13740        return Err(syn::Error::new_spanned(
13741            struct_name,
13742            "Serializer requires a struct with named fields",
13743        ));
13744    };
13745
13746    let container = parse_serializer_container_attrs(input)?;
13747    let model_path = &container.model;
13748
13749    // Classify each field. `ty` is only consumed by the
13750    // `#[cfg(feature = "openapi")]` block below, but we always
13751    // capture it to keep the field-info build a single pass.
13752    #[allow(dead_code)]
13753    struct FieldInfo {
13754        ident: syn::Ident,
13755        ty: syn::Type,
13756        attrs: SerializerFieldAttrs,
13757        serde: SerdeWireAttrs,
13758        /// JSON key in the output / in a request body.
13759        ser_name: String,
13760        de_name: String,
13761    }
13762    let (ser_rule, de_rule) = parse_serde_container_rules(input)?;
13763    let mut fields_info: Vec<FieldInfo> = Vec::new();
13764    for field in &named.named {
13765        let ident = field.ident.clone().expect("named field has ident");
13766        let attrs = parse_serializer_field_attrs(field)?;
13767        let serde = parse_serde_field_attrs(field)?;
13768        let name = ident_name(&ident);
13769        let wire = |rename: &Option<String>, rule: Option<SerdeRenameRule>| {
13770            rename
13771                .clone()
13772                .unwrap_or_else(|| rule.map_or_else(|| name.clone(), |r| r.apply(&name)))
13773        };
13774        fields_info.push(FieldInfo {
13775            ser_name: wire(&serde.rename_ser, ser_rule),
13776            de_name: wire(&serde.rename_de, de_rule),
13777            ident,
13778            ty: field.ty.clone(),
13779            attrs,
13780            serde,
13781        });
13782    }
13783
13784    // Generate from_model body: struct literal with each field assigned.
13785    let from_model_fields = fields_info.iter().map(|fi| {
13786        let ident = &fi.ident;
13787        if let Some(_inner) = &fi.attrs.many {
13788            // Many — collection field. Initialize empty; caller
13789            // populates via the macro-emitted set_<field> helper
13790            // after fetching the M2M children.
13791            quote! { #ident: ::std::vec::Vec::new() }
13792        } else if let Some(method) = &fi.attrs.method {
13793            // SerializerMethodField: call Self::<method>(&model) to
13794            // compute the value. Method signature must be
13795            // `fn <method>(model: &T) -> <field type>`.
13796            let method_ident = syn::Ident::new(method, ident.span());
13797            quote! { #ident: Self::#method_ident(model) }
13798        } else if let Some(slug_field) = &fi.attrs.slug {
13799            // v0.44 — SlugRelatedField. Source defaults to the field
13800            // name on this struct; override via `source = "..."`. The
13801            // source field on the model is expected to be a
13802            // `ForeignKey<T>`; the slug field on the parent is named
13803            // by the attribute value. When the FK is unloaded the
13804            // field falls back to `Default::default()` — same
13805            // graceful-degrade contract as `nested`; a NULL
13806            // `Option<ForeignKey>` renders `None`.
13807            let src_name = fi
13808                .attrs
13809                .source
13810                .as_deref()
13811                .unwrap_or(&ident_name(&fi.ident))
13812                .to_owned();
13813            let src_ident = ident_from_name(&src_name, ident.span());
13814            let slug_ident = syn::Ident::new(slug_field, ident.span());
13815            quote! {
13816                #ident: #root::serializer::LoadedRelation::render(
13817                    &model.#src_ident,
13818                    |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
13819                    ::core::default::Default::default,
13820                )
13821            }
13822        } else if fi.attrs.nested {
13823            // Nested serializer. Source defaults to the field name on
13824            // this struct; override via `source = "..."`. The source
13825            // field on the model is a `ForeignKey<T>` or
13826            // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
13827            //
13828            // Behavior matrix (tweakable per-field):
13829            //   * FK loaded   → nested object materializes via
13830            //                   ChildSerializer::from_model(parent).
13831            //   * FK unloaded → fall back to ChildSerializer::default()
13832            //                   (so prod doesn't crash on a missing
13833            //                   prefetch — just renders a blank nested
13834            //                   object). Add `#[serializer(nested(
13835            //                   strict))]` to keep the v0.18.1
13836            //                   panic-on-unloaded behavior for tests
13837            //                   that want hard guardrails.
13838            //   * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
13839            //                   field (JSON null), strict or not.
13840            let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13841            let src_ident = ident_from_name(&src_name, ident.span());
13842            let unloaded = if fi.attrs.nested_strict {
13843                let panic_msg = format!(
13844                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13845                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13846                );
13847                quote! { || ::core::panic!("{}", #panic_msg) }
13848            } else {
13849                quote! { ::core::default::Default::default }
13850            };
13851            quote! {
13852                #ident: #root::serializer::LoadedRelation::render(
13853                    &model.#src_ident,
13854                    |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
13855                    #unloaded,
13856                )
13857            }
13858        } else if fi.attrs.write_only || fi.attrs.skip {
13859            // Not read from model — use default
13860            quote! { #ident: ::core::default::Default::default() }
13861        } else if let Some(src) = &fi.attrs.source {
13862            let src_ident = ident_from_name(src, ident.span());
13863            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13864        } else {
13865            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13866        }
13867    });
13868
13869    // is_writable predicate (also used by writable_lits /
13870    // writable_source_lits below).
13871    let is_writable = |fi: &&FieldInfo| {
13872        !fi.attrs.read_only
13873            && !fi.attrs.skip
13874            && !fi.serde.skip_de
13875            && fi.attrs.method.is_none()
13876            && !fi.attrs.nested
13877            && fi.attrs.many.is_none()
13878            && fi.attrs.slug.is_none()
13879    };
13880
13881    // Declarative field constraints: one block
13882    // per writable field, run inside `validate()`. Each resolves its
13883    // bounds as the serializer attr when given (`#[serializer(max_length
13884    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13885    // `max` / `choices`), then dispatches on the field's JSON value via
13886    // `forms::validators::check_value` (string → length/choices, integer
13887    // → min/max).
13888    let opt_usize = |v: Option<u64>| match v {
13889        Some(n) => {
13890            let n = n as usize;
13891            quote!(::core::option::Option::Some(#n))
13892        }
13893        None => quote!(::core::option::Option::None),
13894    };
13895    #[cfg(feature = "openapi")]
13896    let opt_u64 = |v: Option<u64>| match v {
13897        Some(n) => quote!(::core::option::Option::Some(#n)),
13898        None => quote!(::core::option::Option::None),
13899    };
13900    let opt_i64 = |v: Option<i64>| match v {
13901        Some(n) => quote!(::core::option::Option::Some(#n)),
13902        None => quote!(::core::option::Option::None),
13903    };
13904    let constraint_blocks: Vec<_> = fields_info
13905        .iter()
13906        .filter(is_writable)
13907        .map(|fi| {
13908            let ident = &fi.ident;
13909            let fname = &fi.de_name;
13910            let mname = fi
13911                .attrs
13912                .source
13913                .clone()
13914                .unwrap_or_else(|| ident_name(&fi.ident));
13915            let attr_max_len = opt_usize(fi.attrs.max_length);
13916            let attr_min_len = opt_usize(fi.attrs.min_length);
13917            let attr_min = opt_i64(fi.attrs.min);
13918            let attr_max = opt_i64(fi.attrs.max);
13919            // A bound the field's type cannot take is a compile error, not a no-op.
13920            let ty = &fi.ty;
13921            let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
13922                .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
13923            let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
13924                .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
13925            quote! {
13926                #len_check
13927                #num_check
13928                if __sent(#fname) {
13929                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13930                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13931                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13932                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13933                    let __min: ::core::option::Option<i64> =
13934                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13935                    let __max: ::core::option::Option<i64> =
13936                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13937                    let __choices = __sf.and_then(|__f| __f.choices);
13938                    let __v = #root::__serde_json::to_value(&self.#ident)
13939                        .unwrap_or(#root::__serde_json::Value::Null);
13940                    #root::forms::validators::check_value(
13941                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13942                        &mut __errors,
13943                    );
13944                }
13945            }
13946        })
13947        .collect();
13948    let has_constraints = !constraint_blocks.is_empty();
13949
13950    // Per-field validators. Emit a
13951    // `validate(&self)` method that runs each user-defined validator
13952    // and aggregates errors into `FormErrors`.
13953    let validator_calls: Vec<_> = fields_info
13954        .iter()
13955        .filter_map(|fi| {
13956            let ident = &fi.ident;
13957            let name_lit = &fi.de_name;
13958            let method = fi.attrs.validate.as_ref()?;
13959            let method_ident = syn::Ident::new(method, ident.span());
13960            Some(quote! {
13961                if __sent(#name_lit) {
13962                    if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13963                        __errors.add(#name_lit.to_owned(), __e);
13964                    }
13965                }
13966            })
13967        })
13968        .collect();
13969    // #436 — cross-field `validate(&self)`. If the
13970    // container declared `#[serializer(validate = "fn_name")]`,
13971    // the macro-generated `validate(&self)` runs every per-field
13972    // validator first, then calls the user's cross-field method,
13973    // merging its `FormErrors` into the per-field errors. Either
13974    // alone is enough to emit the wrapper.
13975    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13976        quote! {
13977            // Merge cross-field errors into the per-field bucket so
13978            // a single .validate() call surfaces both layers.
13979            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13980                __errors.merge(__cross);
13981            }
13982        }
13983    });
13984    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13985    // Anything to run? Declarative constraints (for any writable field) +
13986    // per-field validators + cross-field hook.
13987    let has_run_validations = has_validators || has_constraints;
13988    // Shared body: declarative field constraints first (length / range /
13989    // choices, serializer-attr-or-model), then per-field validators, then
13990    // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13991    let checks = quote! {
13992        let mut __errors = #root::forms::FormErrors::default();
13993        #( #constraint_blocks )*
13994        #( #validator_calls )*
13995        #cross_validate_call
13996        if __errors.is_empty() {
13997            ::core::result::Result::Ok(())
13998        } else {
13999            ::core::result::Result::Err(__errors)
14000        }
14001    };
14002    let validate_body = quote! {
14003        let __sent = |_: &str| true;
14004        #checks
14005    };
14006    // PATCH: only the keys the body carries.
14007    let validate_patch_body = quote! {
14008        let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
14009        #checks
14010    };
14011    // Inherent `validate(&self)` — kept for back-compat with direct
14012    // `serializer.validate()` calls that don't import `ModelSerializer`.
14013    // Emitted only when the serializer declares per-field/cross-field
14014    // validators (unchanged rule) so it never collides with a
14015    // hand-written inherent `validate`.
14016    let validate_method = if has_validators {
14017        quote! {
14018            impl #struct_name {
14019                /// Run the declarative field constraints, every
14020                /// `#[serializer(validate = "...")]` per-field validator,
14021                /// and (when declared) the container-level cross-field
14022                /// validator. Aggregates errors into `FormErrors` keyed by
14023                /// the field name. Returns `Ok(())` when all pass.
14024                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
14025                    #validate_body
14026                }
14027            }
14028        }
14029    } else {
14030        quote! {}
14031    };
14032    // Trait-method override so the type-erased ViewSet write path can
14033    // dispatch `ModelSerializer::validate` generically. Emitted whenever
14034    // there's anything to run (declarative constraints inherited from the
14035    // model count); otherwise the trait's default no-op is used.
14036    let trait_validate_override = if has_run_validations {
14037        quote! {
14038            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
14039                #validate_body
14040            }
14041        }
14042    } else {
14043        quote! {}
14044    };
14045
14046    // For every `#[serializer(many = S)]` field, emit a
14047    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
14048    // helper that maps the parents through `S::from_model`.
14049    let many_setters: Vec<_> = fields_info
14050        .iter()
14051        .filter_map(|fi| {
14052            let many_ty = fi.attrs.many.as_ref()?;
14053            let ident = &fi.ident;
14054            let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
14055            Some(quote! {
14056                /// Populate this `many` field by mapping each parent model
14057                /// through the inner serializer's `from_model`. Call after
14058                /// fetching the M2M / one-to-many children since
14059                /// `from_model` itself can't await an SQL query.
14060                pub fn #setter(
14061                    &mut self,
14062                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
14063                ) -> &mut Self {
14064                    self.#ident = models.iter()
14065                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
14066                        .collect();
14067                    self
14068                }
14069            })
14070        })
14071        .collect();
14072    let many_setters_impl = if many_setters.is_empty() {
14073        quote! {}
14074    } else {
14075        quote! {
14076            impl #struct_name {
14077                #( #many_setters )*
14078            }
14079        }
14080    };
14081
14082    // Generate custom Serialize: skip write_only and serde-skipped fields
14083    let output_fields: Vec<_> = fields_info
14084        .iter()
14085        .filter(|fi| !fi.attrs.write_only && !fi.serde.skip_ser)
14086        .collect();
14087    // `skip_serializing_if` fields count only when rendered.
14088    let output_field_count = output_fields.iter().map(|fi| {
14089        let ident = &fi.ident;
14090        if let Some(pred) = &fi.serde.skip_ser_if {
14091            quote! { + if #pred(&self.#ident) { 0 } else { 1 } }
14092        } else {
14093            quote! { + 1 }
14094        }
14095    });
14096    let output_field_count = quote! { 0usize #( #output_field_count )* };
14097    let serialize_fields = output_fields.iter().map(|fi| {
14098        let ident = &fi.ident;
14099        let name_lit = &fi.ser_name;
14100        if let Some(pred) = &fi.serde.skip_ser_if {
14101            quote! {
14102                if #pred(&self.#ident) {
14103                    __state.skip_field(#name_lit)?;
14104                } else {
14105                    __state.serialize_field(#name_lit, &self.#ident)?;
14106                }
14107            }
14108        } else {
14109            quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
14110        }
14111    });
14112
14113    // writable_fields: normal + write_only.
14114    // Exclude:
14115    //   - `read_only` — server-computed.
14116    //   - `skip` — caller sets manually post-from_model.
14117    //   - `method` — computed from a Self::fn(&model) call; accepting
14118    //     it on write is meaningless.
14119    //   - `nested` / `many` — populated from related-model data, not
14120    //     from a field on the wire body.
14121    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
14122    // `many` in `writable_fields()`, which made the ViewSet write
14123    // path accept those fields from the JSON body and try to bind
14124    // them to the SQL UPDATE — a silent no-op at best, a type
14125    // mismatch at worst.
14126    // (`is_writable` is defined above, near the constraint codegen.)
14127    let writable_lits: Vec<_> = fields_info
14128        .iter()
14129        .filter(is_writable)
14130        .map(|fi| fi.de_name.clone())
14131        .collect();
14132
14133    // `writable_source_fields`: the MODEL field names of writable
14134    // serializer fields (source-resolved). The ViewSet write path skips
14135    // every model column NOT in this set, so `read_only` / `method` /
14136    // computed fields a client posts are ignored instead of written.
14137    let writable_source_lits: Vec<String> = fields_info
14138        .iter()
14139        .filter(is_writable)
14140        .map(|fi| {
14141            fi.attrs
14142                .source
14143                .clone()
14144                .unwrap_or_else(|| ident_name(&fi.ident))
14145        })
14146        .collect();
14147
14148    // `readable_source_fields`: model columns the JSON output copies
14149    // straight from the model, so `?ordering=` cannot sort on a column
14150    // the API never shows (#1845).
14151    let readable: Vec<&FieldInfo> = fields_info
14152        .iter()
14153        .filter(|fi| {
14154            !fi.attrs.write_only
14155                && !fi.serde.skip_ser
14156                && !fi.attrs.skip
14157                && fi.attrs.method.is_none()
14158                && !fi.attrs.nested
14159                && fi.attrs.many.is_none()
14160                && fi.attrs.slug.is_none()
14161        })
14162        .collect();
14163    let readable_source_lits: Vec<String> = readable
14164        .iter()
14165        .map(|fi| {
14166            fi.attrs
14167                .source
14168                .clone()
14169                .unwrap_or_else(|| ident_name(&fi.ident))
14170        })
14171        .collect();
14172    // Their JSON keys, so the ViewSet finds a renamed cursor column.
14173    let readable_lits: Vec<String> = readable.iter().map(|fi| fi.ser_name.clone()).collect();
14174
14175    // `from_writable_json`: build a partial instance for input
14176    // validation. Writable fields are parsed from the JSON body (keyed
14177    // by serializer field name); every other field defaults. Per-field
14178    // type errors collect into `FormErrors` keyed by the field name.
14179    // Construction is field-by-field (not `Self::default()`) so it works
14180    // for serializers regardless of a struct-level `Default`.
14181    let from_writable_json_inits: Vec<_> = fields_info
14182        .iter()
14183        .map(|fi| {
14184            let ident = &fi.ident;
14185            let fname = &fi.de_name;
14186            let ty = &fi.ty;
14187            if is_writable(&fi) {
14188                quote! {
14189                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
14190                        ::core::option::Option::Some(__v) => {
14191                            match #root::__serde_json::from_value::<#ty>(
14192                                ::core::clone::Clone::clone(__v),
14193                            ) {
14194                                ::core::result::Result::Ok(__x) => __x,
14195                                ::core::result::Result::Err(__e) => {
14196                                    __errors.add(#fname.to_owned(), __e.to_string());
14197                                    ::core::default::Default::default()
14198                                }
14199                            }
14200                        }
14201                        ::core::option::Option::None => ::core::default::Default::default(),
14202                    }
14203                }
14204            } else {
14205                quote! { #ident: ::core::default::Default::default() }
14206            }
14207        })
14208        .collect();
14209    // `validate_patch`: overlay each sent writable field on the stored row.
14210    let patch_overlays: Vec<_> = fields_info
14211        .iter()
14212        .filter(is_writable)
14213        .map(|fi| {
14214            let ident = &fi.ident;
14215            let fname = &fi.de_name;
14216            let ty = &fi.ty;
14217            quote! {
14218                if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
14219                    match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
14220                        ::core::result::Result::Ok(__x) => __merged.#ident = __x,
14221                        ::core::result::Result::Err(__e) => {
14222                            __errors.add(#fname.to_owned(), __e.to_string());
14223                        }
14224                    }
14225                }
14226            }
14227        })
14228        .collect();
14229    let patch_validate_call = if has_run_validations {
14230        quote! { __merged.__rustango_validate_patch(__obj) }
14231    } else {
14232        quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
14233    };
14234    let patch_validate_helper = if has_run_validations {
14235        quote! {
14236            impl #struct_name {
14237                #[doc(hidden)]
14238                fn __rustango_validate_patch(
14239                    &self,
14240                    __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
14241                ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14242                    #validate_patch_body
14243                }
14244            }
14245        }
14246    } else {
14247        quote! {}
14248    };
14249
14250    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
14251    // Only includes fields shown in JSON output (skips write_only). For each
14252    // `Option<T>` field, omit from `required` and add `.nullable()`.
14253    let openapi_impl = {
14254        #[cfg(feature = "openapi")]
14255        {
14256            let property_calls = output_fields.iter().map(|fi| {
14257                let name_lit = &fi.ser_name;
14258                let ty = &fi.ty;
14259                let nullable_call = if is_option(ty) {
14260                    quote! { .nullable() }
14261                } else {
14262                    quote! {}
14263                };
14264                // The bounds `validate()` enforces on a written field.
14265                let limits_call = is_writable(fi).then(|| {
14266                    let mname = fi
14267                        .attrs
14268                        .source
14269                        .clone()
14270                        .unwrap_or_else(|| ident_name(&fi.ident));
14271                    let (a, b, c, d) = (
14272                        opt_u64(fi.attrs.max_length),
14273                        opt_u64(fi.attrs.min_length),
14274                        opt_i64(fi.attrs.min),
14275                        opt_i64(fi.attrs.max),
14276                    );
14277                    quote! {
14278                        .field_limits(
14279                            <#model_path as #root::core::Model>::SCHEMA.field(#mname),
14280                            #a, #b, #c, #d,
14281                        )
14282                    }
14283                });
14284                quote! {
14285                    .property(
14286                        #name_lit,
14287                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
14288                            #limits_call
14289                            #nullable_call,
14290                    )
14291                }
14292            });
14293            let required_lits: Vec<_> = output_fields
14294                .iter()
14295                .filter(|fi| !is_option(&fi.ty) && fi.serde.skip_ser_if.is_none())
14296                .map(|fi| fi.ser_name.clone())
14297                .collect();
14298            quote! {
14299                impl #root::openapi::OpenApiSchema for #struct_name {
14300                    fn openapi_schema() -> #root::openapi::Schema {
14301                        #root::openapi::Schema::object()
14302                            #( #property_calls )*
14303                            .required([ #( #required_lits ),* ])
14304                    }
14305                }
14306            }
14307        }
14308        #[cfg(not(feature = "openapi"))]
14309        {
14310            quote! {}
14311        }
14312    };
14313
14314    // The other tables a `nested`, `many` or `slug` field renders rows of.
14315    let related_tables = fields_info.iter().filter_map(|fi| {
14316        let name = &fi.ser_name;
14317        if let Some(inner) = &fi.attrs.many {
14318            return Some(quote! {
14319                (#name, <<#inner as #root::serializer::ModelSerializer>::Model
14320                    as #root::core::Model>::SCHEMA.table)
14321            });
14322        }
14323        if !(fi.attrs.nested || fi.attrs.slug.is_some()) {
14324            return None;
14325        }
14326        let src_name = fi
14327            .attrs
14328            .source
14329            .as_deref()
14330            .unwrap_or(&ident_name(&fi.ident))
14331            .to_owned();
14332        let src_ident = ident_from_name(&src_name, fi.ident.span());
14333        Some(quote! {
14334            (#name, #root::serializer::relation_table(|__m: &#model_path| &__m.#src_ident))
14335        })
14336    });
14337
14338    Ok(quote! {
14339        impl #root::serializer::ModelSerializer for #struct_name {
14340            type Model = #model_path;
14341
14342            fn related_tables() -> ::std::vec::Vec<(&'static str, &'static str)> {
14343                ::std::vec![ #( #related_tables ),* ]
14344            }
14345
14346            fn from_model(model: &Self::Model) -> Self {
14347                Self {
14348                    #( #from_model_fields ),*
14349                }
14350            }
14351
14352            fn writable_fields() -> &'static [&'static str] {
14353                &[ #( #writable_lits ),* ]
14354            }
14355
14356            fn writable_source_fields() -> &'static [&'static str] {
14357                &[ #( #writable_source_lits ),* ]
14358            }
14359
14360            fn readable_source_fields() -> &'static [&'static str] {
14361                &[ #( #readable_source_lits ),* ]
14362            }
14363
14364            fn readable_fields() -> &'static [&'static str] {
14365                &[ #( #readable_lits ),* ]
14366            }
14367
14368            fn from_writable_json(
14369                __body: &#root::__serde_json::Value,
14370            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
14371                let mut __errors = #root::forms::FormErrors::default();
14372                let __obj = __body.as_object();
14373                let __out = Self {
14374                    #( #from_writable_json_inits ),*
14375                };
14376                if __errors.is_empty() {
14377                    ::core::result::Result::Ok(__out)
14378                } else {
14379                    ::core::result::Result::Err(__errors)
14380                }
14381            }
14382
14383            fn validate_patch(
14384                __stored: &Self::Model,
14385                __body: &#root::__serde_json::Value,
14386            ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14387                let mut __errors = #root::forms::FormErrors::default();
14388                let __obj = __body.as_object();
14389                #[allow(unused_mut)]
14390                let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
14391                #( #patch_overlays )*
14392                if !__errors.is_empty() {
14393                    return ::core::result::Result::Err(__errors);
14394                }
14395                #patch_validate_call
14396            }
14397
14398            #trait_validate_override
14399        }
14400
14401        #patch_validate_helper
14402
14403        impl #root::__serde::Serialize for #struct_name {
14404            fn serialize<S>(&self, serializer: S)
14405                -> ::core::result::Result<S::Ok, S::Error>
14406            where
14407                S: #root::__serde::Serializer,
14408            {
14409                use #root::__serde::ser::SerializeStruct;
14410                let mut __state = serializer.serialize_struct(
14411                    #struct_name_lit,
14412                    #output_field_count,
14413                )?;
14414                #( #serialize_fields )*
14415                __state.end()
14416            }
14417        }
14418
14419        #openapi_impl
14420
14421        #validate_method
14422
14423        #many_setters_impl
14424    })
14425}
14426
14427/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
14428/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
14429/// when the feature is off.
14430#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
14431fn is_option(ty: &syn::Type) -> bool {
14432    if let syn::Type::Path(p) = ty {
14433        if let Some(last) = p.path.segments.last() {
14434            return last.ident == "Option";
14435        }
14436    }
14437    false
14438}
14439
14440#[cfg(test)]
14441mod main_attr_tests {
14442    use super::*;
14443
14444    fn expand(args: &str) -> String {
14445        let body = quote! { async fn main() { run().await } };
14446        expand_main(args.parse().unwrap(), body)
14447            .expect("expansion failed")
14448            .to_string()
14449    }
14450
14451    /// Asserts on the expansion rather than on `parse_main_args`,
14452    /// because the defect was a subscriber reaching the output, not
14453    /// an argument being misread (#1465).
14454    #[test]
14455    fn logging_false_emits_no_subscriber() {
14456        let out = expand("logging = false");
14457        assert!(
14458            !out.contains("logging :: Setup"),
14459            "`logging = false` must not install a subscriber:\n{out}",
14460        );
14461        assert!(
14462            out.contains("block_on"),
14463            "the runtime must still be built:\n{out}",
14464        );
14465    }
14466
14467    #[test]
14468    fn default_still_installs_a_subscriber() {
14469        // The control. Without it, an `expand_main` that dropped the
14470        // prologue unconditionally would pass the test above while
14471        // leaving every default project with no logging at all.
14472        let out = expand("");
14473        assert!(
14474            out.contains("logging :: Setup"),
14475            "the default entrypoint must still install one:\n{out}",
14476        );
14477        // `./app serve` or `./app --port 8080` must keep logging to stdout:
14478        // only `Cli`, which knows its verbs, moves logs to stderr.
14479        assert!(
14480            !out.contains("args ()"),
14481            "the entrypoint must not route logs by argv:\n{out}",
14482        );
14483    }
14484
14485    #[test]
14486    fn logging_false_composes_with_flavor() {
14487        let out = expand("flavor = \"current_thread\", logging = false");
14488        assert!(!out.contains("logging :: Setup"), "{out}");
14489        assert!(out.contains("new_current_thread"), "{out}");
14490    }
14491
14492    /// The body runs on the big-stack thread; `stack_size` overrides it.
14493    #[test]
14494    fn body_runs_on_a_big_stack_thread() {
14495        let out = expand("");
14496        assert!(out.contains("run_on_main_thread"), "{out}");
14497        assert!(out.contains("MAIN_STACK_SIZE"), "{out}");
14498        let out = expand("stack_size = 16777216, logging = false");
14499        assert!(out.contains("16777216usize"), "{out}");
14500        assert!(!out.contains("MAIN_STACK_SIZE"), "{out}");
14501        assert!(
14502            out.contains("thread_stack_size"),
14503            "workers get it too:\n{out}"
14504        );
14505    }
14506
14507    /// A stack too small to run anything is a compile error.
14508    #[test]
14509    fn a_tiny_stack_size_is_refused() {
14510        let body = quote! { async fn main() {} };
14511        let err = expand_main("stack_size = 4096".parse().unwrap(), body)
14512            .expect_err("4 KiB")
14513            .to_string();
14514        assert!(err.contains("at least"), "{err}");
14515    }
14516
14517    /// Lint attributes reach the inner body fn; others do not.
14518    #[test]
14519    fn lint_attributes_reach_the_body() {
14520        let body = quote! {
14521            #[allow(unused_variables)]
14522            #[doc = "entry"]
14523            async fn main() { let x = 1; }
14524        };
14525        let out = expand_main(quote!(), body).unwrap().to_string();
14526        assert_eq!(out.matches("allow (unused_variables)").count(), 2, "{out}");
14527        assert_eq!(out.matches("doc = \"entry\"").count(), 1, "{out}");
14528    }
14529
14530    #[test]
14531    fn logging_true_is_the_default_shape() {
14532        assert!(
14533            parse_main_args(&"logging = true".parse().unwrap())
14534                .unwrap()
14535                .logging
14536        );
14537        assert!(
14538            !parse_main_args(&"logging=false".parse().unwrap())
14539                .unwrap()
14540                .logging
14541        );
14542    }
14543}
14544
14545#[cfg(test)]
14546mod main_arg_tests {
14547    use super::*;
14548
14549    fn expand(args: &str) -> syn::Result<TokenStream2> {
14550        let body = quote! { async fn main() { run().await } };
14551        expand_main(args.parse().unwrap(), body)
14552    }
14553
14554    /// A typo or unknown key is a compile error, not silently ignored.
14555    #[test]
14556    fn unknown_or_misspelt_args_are_errors() {
14557        for args in [
14558            "logging = flase",
14559            "multi_thread",
14560            "workers = 4",
14561            r#"flavor = "curent_thread""#,
14562            "worker_threads = 4",
14563        ] {
14564            let err = expand(args).expect_err(args).to_string();
14565            assert!(err.contains("#[rustango::main]"), "{args}: {err}");
14566        }
14567    }
14568
14569    #[test]
14570    fn supported_args_expand() {
14571        for args in [
14572            "",
14573            r#"flavor = "current_thread""#,
14574            r#"flavor = "multi_thread", logging = false"#,
14575            "logging = true",
14576        ] {
14577            assert!(expand(args).is_ok(), "{args}");
14578        }
14579    }
14580}
14581
14582#[cfg(test)]
14583mod embed_migrations_tests {
14584    use super::*;
14585
14586    fn expand(path: &str) -> String {
14587        expand_embed_migrations(quote! { #path })
14588            .expect("expansion failed")
14589            .to_string()
14590    }
14591
14592    #[test]
14593    fn missing_dir_warns() {
14594        let out = expand("./no_such_migrations_dir");
14595        assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
14596        assert!(out.contains("no_such_migrations_dir"), "{out}");
14597    }
14598
14599    #[test]
14600    fn existing_dir_does_not_warn() {
14601        let out = expand("./src");
14602        assert!(!out.contains("deprecated"), "{out}");
14603    }
14604}
14605
14606#[cfg(test)]
14607mod field_attr_tests {
14608    use super::*;
14609
14610    fn error_for(field: syn::Field) -> Option<String> {
14611        let attrs = parse_field_attrs(&field).unwrap();
14612        let kind = detect_type(&field.ty).unwrap().kind;
14613        check_bound_compatibility(&field, &attrs, kind)
14614            .err()
14615            .map(|e| e.to_string())
14616    }
14617
14618    /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
14619    #[test]
14620    fn type_specific_attrs_reject_other_types() {
14621        let f: syn::FieldsNamed = syn::parse_quote!({
14622            #[rustango(citext)] a: i64,
14623            #[rustango(vector(dims = 3))] b: String,
14624            #[rustango(geometry(srid = 4326))] c: String,
14625            #[rustango(citext)] d: String,
14626        });
14627        let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
14628        assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
14629        assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
14630        assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
14631        assert_eq!(errs[3], None);
14632    }
14633
14634    /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
14635    #[test]
14636    fn manager_ext_is_refused() {
14637        let input: DeriveInput = syn::parse_quote! {
14638            #[rustango(table = "p", manager(ext = "PostManagerExt"))]
14639            struct Post { id: i64 }
14640        };
14641        let err = parse_container_attrs(&input).err().expect("refused");
14642        assert!(err.to_string().contains("#2132"), "{err}");
14643    }
14644
14645    /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
14646    #[test]
14647    fn set_null_needs_an_optional_foreign_key() {
14648        let input: DeriveInput = syn::parse_quote! {
14649            #[rustango(table = "c")]
14650            struct Child {
14651                #[rustango(primary_key)] id: i64,
14652                #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
14653            }
14654        };
14655        let err = expand(&input).err().expect("refused");
14656        assert!(err.to_string().contains("set_null"), "{err}");
14657        let input: DeriveInput = syn::parse_quote! {
14658            #[rustango(table = "c")]
14659            struct Child {
14660                #[rustango(primary_key)] id: i64,
14661                #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
14662            }
14663        };
14664        assert!(expand(&input).is_ok());
14665    }
14666
14667    /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
14668    #[test]
14669    fn set_null_needs_a_nullable_fk_column() {
14670        let input: DeriveInput = syn::parse_quote! {
14671            #[rustango(table = "c")]
14672            struct Child {
14673                #[rustango(primary_key)] id: i64,
14674                #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
14675                #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
14676            }
14677        };
14678        let err = expand(&input).err().expect("refused");
14679        assert!(err.to_string().contains("set_null"), "{err}");
14680        let input: DeriveInput = syn::parse_quote! {
14681            #[rustango(table = "c")]
14682            struct Child {
14683                #[rustango(primary_key)] id: i64,
14684                #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
14685                #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
14686                #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
14687            }
14688        };
14689        assert!(expand(&input).is_ok());
14690    }
14691
14692    /// An index or `get_latest_by` naming no column is a compile error,
14693    /// one per name.
14694    #[test]
14695    fn unknown_index_columns_are_errors() {
14696        let input: DeriveInput = syn::parse_quote! {
14697            #[rustango(
14698                table = "w",
14699                unique_together = "a, nope",
14700                index_together = "a, b_typo",
14701                index("missing"),
14702                get_latest_by = "-when_typo"
14703            )]
14704            struct W { #[rustango(primary_key)] id: i64, a: i64, b: i64 }
14705        };
14706        let err = expand(&input).expect_err("must not compile");
14707        let all: Vec<String> = err.into_iter().map(|e| e.to_string()).collect();
14708        for name in ["nope", "b_typo", "missing", "when_typo"] {
14709            assert!(
14710                all.iter().any(|m| m.contains(&format!("`{name}`"))),
14711                "{name}: {all:?}"
14712            );
14713        }
14714    }
14715
14716    /// Real columns, field names for `get_latest_by` and expression
14717    /// indexes compile.
14718    #[test]
14719    fn known_index_columns_compile() {
14720        let input: DeriveInput = syn::parse_quote! {
14721            #[rustango(
14722                table = "w",
14723                unique_together = "a, b_col",
14724                index("lower(a)"),
14725                get_latest_by = "-b"
14726            )]
14727            struct W {
14728                #[rustango(primary_key)] id: i64,
14729                a: i64,
14730                #[rustango(column = "b_col")] b: i64,
14731            }
14732        };
14733        expand(&input).expect("compiles");
14734    }
14735
14736    /// Two FKs to one parent would emit the same reverse accessor twice.
14737    #[test]
14738    fn reverse_accessor_clash_names_related_name() {
14739        let input: DeriveInput = syn::parse_quote! {
14740            #[rustango(table = "m")]
14741            struct Message {
14742                #[rustango(primary_key)] id: i64,
14743                sender: ForeignKey<User>,
14744                recipient: ForeignKey<User>,
14745            }
14746        };
14747        let err = expand(&input).err().expect("refused");
14748        assert!(err.to_string().contains("related_name"), "{err}");
14749    }
14750
14751    /// `related_name = "+"` emits no reverse accessor, so a parent from
14752    /// another crate, which cannot take an inherent impl, still derives.
14753    #[test]
14754    fn related_name_plus_skips_the_reverse_accessor() {
14755        let input: DeriveInput = syn::parse_quote! {
14756            #[rustango(table = "m")]
14757            struct Message {
14758                #[rustango(primary_key)] id: i64,
14759                sender: ForeignKey<User>,
14760                #[rustango(related_name = "+")] recipient: ForeignKey<User>,
14761                #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
14762            }
14763        };
14764        let out = expand(&input).expect("derives").to_string();
14765        assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
14766        assert!(!out.contains("impl other :: Account {"), "{out}");
14767    }
14768
14769    fn field_error(src: &str) -> Option<String> {
14770        let f: syn::FieldsNamed = syn::parse_str(&format!("{{ {src} }}")).unwrap();
14771        let field = f.named.into_iter().next().unwrap();
14772        match parse_field_attrs(&field) {
14773            Err(e) => Some(e.to_string()),
14774            Ok(_) => error_for(field),
14775        }
14776    }
14777
14778    /// Flag attrs take `= true` like `blank`; `= false` is refused by name.
14779    #[test]
14780    fn flag_attrs_accept_eq_true() {
14781        for key in [
14782            "primary_key",
14783            "unique",
14784            "soft_delete",
14785            "index",
14786            "auto_now",
14787            "auto_now_add",
14788        ] {
14789            let f: syn::FieldsNamed =
14790                syn::parse_str(&format!("{{ #[rustango({key} = true)] a: i64 }}")).unwrap();
14791            let field = f.named.into_iter().next().unwrap();
14792            assert!(parse_field_attrs(&field).is_ok(), "{key} = true");
14793            let err = field_error(&format!("#[rustango({key} = false)] a: i64")).unwrap();
14794            assert!(err.contains(&format!("`{key} = false`")), "{err}");
14795        }
14796        let f: syn::Field =
14797            syn::parse_quote!(#[rustango(primary_key = true, unique = true)] a: i64);
14798        let attrs = parse_field_attrs(&f).unwrap();
14799        assert!(attrs.primary_key && attrs.unique);
14800    }
14801
14802    /// An unknown key is named, the nearest key suggested and the list shown.
14803    #[test]
14804    fn unknown_key_is_named_with_a_suggestion() {
14805        let err = field_error("#[rustango(max_lenght = 5)] a: String").unwrap();
14806        assert!(err.contains("`max_lenght`"), "{err}");
14807        assert!(err.contains("did you mean `max_length`"), "{err}");
14808        assert!(err.contains("Supported: column"), "{err}");
14809        let input: DeriveInput = syn::parse_quote! {
14810            #[rustango(tabel = "p")]
14811            struct P { id: i64 }
14812        };
14813        let err = parse_container_attrs(&input).err().unwrap().to_string();
14814        assert!(
14815            err.contains("`tabel`") && err.contains("did you mean `table`"),
14816            "{err}"
14817        );
14818        let err = field_error("#[rustango(zzzzzz)] a: i64").unwrap();
14819        assert!(!err.contains("did you mean"), "{err}");
14820    }
14821
14822    /// The listed keys are the parsed keys; a stale list would mislead.
14823    #[test]
14824    fn listed_keys_are_all_known() {
14825        for key in FIELD_ATTR_KEYS {
14826            let err = field_error(&format!("#[rustango({key})] a: i64")).unwrap_or_default();
14827            assert!(!err.contains("unknown rustango field"), "{key}: {err}");
14828        }
14829        for key in CONTAINER_ATTR_KEYS {
14830            let input: DeriveInput =
14831                syn::parse_str(&format!("#[rustango({key})] struct P {{ id: i64 }}")).unwrap();
14832            let err = parse_container_attrs(&input)
14833                .err()
14834                .map(|e| e.to_string())
14835                .unwrap_or_default();
14836            assert!(!err.contains("unknown rustango container"), "{key}: {err}");
14837        }
14838    }
14839
14840    /// `default` is raw SQL: a bare word or a non-number on a numeric field is refused.
14841    #[test]
14842    fn default_shapes_only_sqlite_takes_are_refused() {
14843        for bad in [
14844            r#"#[rustango(default = "draft")] s: String"#,
14845            r#"#[rustango(default = "draft")] s: Option<String>"#,
14846            r#"#[rustango(default = "abc")] n: i32"#,
14847            r#"#[rustango(default = "'abc'")] n: i64"#,
14848            r#"#[rustango(default = "true")] n: i32"#,
14849            r#"#[rustango(default = "")] n: i32"#,
14850            r#"#[rustango(default = "'x'")] n: f64"#,
14851        ] {
14852            assert!(field_error(bad).is_some(), "{bad} must be refused");
14853        }
14854        let err = field_error(r#"#[rustango(default = "draft")] s: String"#).unwrap();
14855        assert!(err.contains(r#"default = "'draft'""#), "{err}");
14856        // Valid on Postgres alone, so the message says that, not "a column".
14857        let err = field_error(r#"#[rustango(default = "current_user")] s: String"#).unwrap();
14858        assert!(
14859            err.contains("Postgres only") && !err.contains("column"),
14860            "{err}"
14861        );
14862        for good in [
14863            r#"#[rustango(default = "'draft'")] s: String"#,
14864            r#"#[rustango(default = "")] s: String"#,
14865            r#"#[rustango(default = "now()")] s: String"#,
14866            r#"#[rustango(default = "CURRENT_TIMESTAMP")] s: String"#,
14867            r#"#[rustango(default = "NULL")] s: Option<String>"#,
14868            r#"#[rustango(default = "0")] n: i32"#,
14869            r#"#[rustango(default = "-1")] n: i64"#,
14870            r#"#[rustango(default = "'5'")] n: i64"#,
14871            r#"#[rustango(default = "(1 + 2)")] n: i64"#,
14872            r#"#[rustango(default = "1.5")] n: f64"#,
14873            r#"#[rustango(default = "true")] b: bool"#,
14874        ] {
14875            assert_eq!(field_error(good), None, "{good}");
14876        }
14877    }
14878
14879    /// A raw ident derives names without `r#`; this used to panic (#1937).
14880    #[test]
14881    fn raw_idents_derive_plain_names() {
14882        let ident: syn::Ident = syn::parse_quote!(r#type);
14883        assert_eq!(column_type_ident(&ident).to_string(), "type_col");
14884        assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
14885    }
14886}
14887
14888#[cfg(test)]
14889mod model_attr_name_tests {
14890    use super::*;
14891
14892    fn container_error(item: proc_macro2::TokenStream) -> Option<String> {
14893        let input: DeriveInput = syn::parse2(item).unwrap();
14894        parse_container_attrs(&input).err().map(|e| e.to_string())
14895    }
14896
14897    fn field_error(field: proc_macro2::TokenStream) -> Option<String> {
14898        let f: syn::FieldsNamed = syn::parse2(quote!({ #field })).unwrap();
14899        let field = f.named.into_iter().next().unwrap();
14900        parse_field_attrs(&field).err().map(|e| e.to_string())
14901    }
14902
14903    /// `#[rgo(...)]` reads exactly as `#[rustango(...)]`.
14904    #[test]
14905    fn rgo_is_read_like_rustango() {
14906        let long: DeriveInput = syn::parse_quote! {
14907            #[rustango(table = "t", display = "name")] struct S;
14908        };
14909        let short: DeriveInput = syn::parse_quote! {
14910            #[rgo(table = "t", display = "name")] struct S;
14911        };
14912        let (long, short) = (
14913            parse_container_attrs(&long).unwrap(),
14914            parse_container_attrs(&short).unwrap(),
14915        );
14916        assert_eq!(long.table, short.table);
14917        assert_eq!(short.table.as_deref(), Some("t"));
14918        assert_eq!(long.display.map(|d| d.0), short.display.map(|d| d.0));
14919    }
14920
14921    #[test]
14922    fn mixing_names_on_one_item_or_field_is_refused() {
14923        let err = container_error(quote! {
14924            #[rustango(table = "t")] #[rgo(display = "name")] struct S;
14925        })
14926        .expect("a mix on an item must fail");
14927        assert!(
14928            err.contains("`#[rustango(...)]` and `#[rgo(...)]`"),
14929            "{err}"
14930        );
14931        let err = field_error(quote! { #[rgo(primary_key)] #[rustango(column = "c")] a: i64 })
14932            .expect("a mix on a field must fail");
14933        assert!(
14934            err.contains("`#[rgo(...)]` and `#[rustango(...)]`"),
14935            "{err}"
14936        );
14937        // Either name repeated alone is fine.
14938        assert_eq!(
14939            container_error(quote! { #[rgo(table = "t")] #[rgo(display = "name")] struct S; }),
14940            None
14941        );
14942        assert_eq!(
14943            field_error(quote! { #[rustango(primary_key)] #[rustango(column = "c")] a: i64 }),
14944            None
14945        );
14946    }
14947
14948    /// A hint quotes the attribute under the name the user wrote.
14949    #[test]
14950    fn hints_use_the_written_name() {
14951        let err = container_error(quote! { #[rgo(table = "t", unique_together = "a")] struct S; })
14952            .expect("one column must fail");
14953        assert!(err.contains("#[rgo(unique)]"), "{err}");
14954        let err =
14955            container_error(quote! { #[rustango(table = "t", index_together = "a")] struct S; })
14956                .expect("one column must fail");
14957        assert!(err.contains("#[rustango(index)]"), "{err}");
14958    }
14959}
14960
14961/// A migration file's text: a UTF-8 BOM dropped, UTF-16 LE / BE decoded by
14962/// its BOM. A copy of `rustango::text_file`'s decoder, which this crate
14963/// cannot depend on.
14964fn decode_text(bytes: Vec<u8>) -> Result<String, String> {
14965    fn utf16(bytes: &[u8], unit: fn([u8; 2]) -> u16) -> Result<String, String> {
14966        if bytes.len() % 2 != 0 {
14967            return Err("odd-length UTF-16".into());
14968        }
14969        let units = bytes.chunks_exact(2).map(|c| unit([c[0], c[1]]));
14970        char::decode_utf16(units)
14971            .collect::<Result<String, _>>()
14972            .map_err(|e| e.to_string())
14973    }
14974    match bytes.as_slice() {
14975        [0xFF, 0xFE, rest @ ..] => utf16(rest, u16::from_le_bytes),
14976        [0xFE, 0xFF, rest @ ..] => utf16(rest, u16::from_be_bytes),
14977        _ => {
14978            let mut bytes = bytes;
14979            if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
14980                bytes.drain(..3);
14981            }
14982            String::from_utf8(bytes).map_err(|e| e.to_string())
14983        }
14984    }
14985}
14986
14987#[cfg(test)]
14988mod decode_text_tests {
14989    use super::decode_text;
14990
14991    #[test]
14992    fn a_bom_or_utf16_migration_decodes() {
14993        let text = r#"{"name": "0001_é"}"#;
14994        let mut bom = vec![0xEF, 0xBB, 0xBF];
14995        bom.extend(text.bytes());
14996        let mut le = vec![0xFF, 0xFE];
14997        le.extend(text.encode_utf16().flat_map(u16::to_le_bytes));
14998        let mut be = vec![0xFE, 0xFF];
14999        be.extend(text.encode_utf16().flat_map(u16::to_be_bytes));
15000        for bytes in [text.as_bytes().to_vec(), bom, le, be] {
15001            assert_eq!(decode_text(bytes).unwrap(), text);
15002        }
15003        assert!(decode_text(vec![0xFF, 0xFE, 0x41]).is_err());
15004    }
15005}