Skip to main content

rustango_macros/
lib.rs

1//! Proc-macros for rustango.
2//!
3//! v0.1 ships `#[derive(Model)]`, which emits:
4//! * a `Model` impl carrying a static `ModelSchema`,
5//! * an `inventory::submit!` so the model is discoverable from the registry,
6//! * an inherent `objects()` returning a `QuerySet<Self>`,
7//! * a `sqlx::FromRow` impl so query results decode into the struct.
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13    parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14    PathArguments, Type, TypePath,
15};
16
17/// Resolve the consumer's local name for the `rustango` crate so the
18/// macro-emitted code keeps compiling when a downstream `Cargo.toml`
19/// renames the dep (`[dependencies] orm = { package = "rustango", … }`)
20/// or when the standalone `rustango-orm` crate ships in a future
21/// slice of the [orm-extract epic](https://github.com/ujeenet/rustango/issues/149).
22///
23/// Returns one of:
24/// - `quote!(::rustango)` — the consumer IS the `rustango` crate
25///   itself. We emit the absolute path `::rustango` (NOT `crate`)
26///   because examples and integration tests inside the rustango
27///   package compile as separate binaries — their own `crate::`
28///   namespace is the example/test file, not rustango's lib root.
29///   The absolute `::rustango` resolves correctly in both contexts
30///   (rustango lib code AND rustango/examples/*.rs).
31/// - `quote!(::ident)` — the consumer renamed the dep; emit the
32///   user's chosen ident.
33/// - `quote!(::rustango)` — fallback when `proc-macro-crate` can't
34///   read the manifest (rare; preserves today's behavior).
35///
36/// Issue [#142](https://github.com/ujeenet/rustango/issues/142).
37fn rustango_root() -> TokenStream2 {
38    use proc_macro_crate::{crate_name, FoundCrate};
39    match crate_name("rustango") {
40        Ok(FoundCrate::Itself) => quote!(::rustango),
41        Ok(FoundCrate::Name(name)) => {
42            let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
43            quote!(::#ident)
44        }
45        Err(_) => quote!(::rustango),
46    }
47}
48
49/// Derive a `Model` impl. See crate docs for the supported attributes.
50#[proc_macro_derive(Model, attributes(rustango))]
51pub fn derive_model(input: TokenStream) -> TokenStream {
52    let input = parse_macro_input!(input as DeriveInput);
53    expand(&input)
54        .unwrap_or_else(syn::Error::into_compile_error)
55        .into()
56}
57
58/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
59/// marker struct, wiring the full CRUD ViewSet in one annotation.
60///
61/// ```ignore
62/// #[derive(ViewSet)]
63/// #[viewset(
64///     model        = Post,
65///     fields       = "id, title, body, author_id",
66///     filter_fields = "author_id",
67///     search_fields = "title, body",
68///     ordering     = "-published_at",
69///     page_size    = 20,
70/// )]
71/// pub struct PostViewSet;
72///
73/// // Mount into your app:
74/// let app = Router::new()
75///     .merge(PostViewSet::router("/api/posts", pool.clone()));
76/// ```
77///
78/// Attributes:
79/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
80///   `SCHEMA` constant drives the endpoints.
81/// * `fields = "a, b, c"` — scalar fields included in list/retrieve JSON
82///   and accepted on create/update (default: all scalar fields).
83/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
84/// * `search_fields = "a, b"` — fields searched by `?search=...`.
85/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
86/// * `page_size = N` — default page size (default: 20, max: 1000).
87/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
88/// * `permissions(list = "...", retrieve = "...", create = "...",
89///   update = "...", destroy = "...")` — codenames required per action.
90#[proc_macro_derive(ViewSet, attributes(viewset))]
91pub fn derive_viewset(input: TokenStream) -> TokenStream {
92    let input = parse_macro_input!(input as DeriveInput);
93    expand_viewset(&input)
94        .unwrap_or_else(syn::Error::into_compile_error)
95        .into()
96}
97
98/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
99/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
100/// that walks every named field and:
101///
102/// * Parses the string value into the field's Rust type (`String`,
103///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
104///   nullable case).
105/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
106///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
107///   validators in declaration order, returning `FormError::Parse`
108///   on the first failure.
109///
110/// Example:
111///
112/// ```ignore
113/// #[derive(Form)]
114/// pub struct CreateItemForm {
115///     #[form(min_length = 1, max_length = 64)]
116///     pub name: String,
117///     #[form(min = 0, max = 150)]
118///     pub age: i32,
119///     pub active: bool,
120///     pub email: Option<String>,
121/// }
122///
123/// let parsed = CreateItemForm::parse(&form_map)?;
124/// ```
125#[proc_macro_derive(Form, attributes(form))]
126pub fn derive_form(input: TokenStream) -> TokenStream {
127    let input = parse_macro_input!(input as DeriveInput);
128    expand_form(&input)
129        .unwrap_or_else(syn::Error::into_compile_error)
130        .into()
131}
132
133/// Derive `rustango::serializer::ModelSerializer` for a struct.
134/// (intra-doc link disabled — the macro crate doesn't depend on
135/// `rustango` itself, so rustdoc can't resolve the path.)
136///
137/// # Container attribute (required)
138/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
139///
140/// # Field attributes
141/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
142/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
143/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
144/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
145/// - `#[serializer(method = "fn_name")]` — DRF `SerializerMethodField`: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
146/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
147/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
148/// - `#[serializer(slug = "name")]` — DRF `SlugRelatedField`: clones `model.<source>.value()?.name`; excluded from `writable_fields()` (v0.44)
149/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
150///
151/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
152#[proc_macro_derive(Serializer, attributes(serializer))]
153pub fn derive_serializer(input: TokenStream) -> TokenStream {
154    let input = parse_macro_input!(input as DeriveInput);
155    expand_serializer(&input)
156        .unwrap_or_else(syn::Error::into_compile_error)
157        .into()
158}
159
160/// Bake every `*.json` migration file in a directory into the binary
161/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
162/// of `(name, json_content)` pairs, lex-sorted by file stem.
163///
164/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
165/// behaviour as `migrate(pool, dir)` but with no filesystem access.
166/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
167/// (i.e. the crate that invokes the macro). Default is
168/// `"./migrations"` if no argument is supplied.
169///
170/// ```ignore
171/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
172/// // or:
173/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
174///
175/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
176/// ```
177///
178/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
179/// file's `name` field must equal its file stem, every `prev`
180/// reference must point to another migration in the same directory,
181/// and the JSON must parse. A broken chain — orphan `prev`, missing
182/// predecessor, malformed file — fails at macro-expansion time with
183/// a clear `compile_error!`. *No other Django-shape Rust framework
184/// validates migration chains at compile time*: Cot's migrations are
185/// imperative Rust code (no static chain), Loco's are SeaORM
186/// up/down (same), Rwf's are raw SQL (no chain at all).
187///
188/// Each migration is included via `include_str!` so cargo's rebuild
189/// detection picks up file *content* changes. **Caveat:** cargo
190/// doesn't watch directory listings, so adding or removing a
191/// migration file inside the dir won't auto-trigger a rebuild — run
192/// `cargo clean` (or just bump any other source file) when you add
193/// new migrations during embedded development.
194#[proc_macro]
195pub fn embed_migrations(input: TokenStream) -> TokenStream {
196    expand_embed_migrations(input.into())
197        .unwrap_or_else(syn::Error::into_compile_error)
198        .into()
199}
200
201/// `Q!()` — Django-shape filter syntax compile-time-resolved against
202/// typed columns. Issue #269 / T1.7.
203///
204/// Each invocation lowers to the equivalent typed-column method call:
205///
206/// ```ignore
207/// // These expand identically:
208/// Q!(User.email__icontains = "alice")
209/// User::email.ilike("%alice%")
210/// ```
211///
212/// Field-name typos fail the build (the macro emits `User::no_such_field`
213/// which doesn't exist) — the headline ergonomic win of this slice over
214/// Django's stringly-typed `__lookup` filters.
215///
216/// # Supported lookup suffixes
217///
218/// * bare `=` / `__exact` → `.eq(value)`
219/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
220/// * `__ne` → `.ne(value)`
221/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
222/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
223/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
224/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
225/// * `__in` → `.is_in(iterable)`
226/// * `__not_in` → `.not_in(iterable)`
227/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
228/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
229/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
230///
231/// Unknown suffixes fail the build with a `compile_error!` pointing at
232/// the lookup token.
233///
234/// # Combine
235///
236/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
237/// `.and()` / `.or()` / `.not()` methods:
238///
239/// ```ignore
240/// User::objects()
241///     .where_(
242///         Q!(User.active = true)
243///             .and(Q!(User.email__icontains = "alice"))
244///     )
245///     .fetch_pool(&pool).await?;
246/// ```
247///
248/// All emitted code routes through existing per-dialect writers — no new
249/// SQL emission machinery. Tri-dialect support is inherent.
250#[allow(non_snake_case)]
251#[proc_macro]
252pub fn Q(input: TokenStream) -> TokenStream {
253    expand_q(input.into())
254        .unwrap_or_else(syn::Error::into_compile_error)
255        .into()
256}
257
258/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
259/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
260/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
261/// functions are zero-boilerplate:
262///
263/// ```ignore
264/// #[rustango::main]
265/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
266///     rustango::server::Builder::from_env().await?
267///         .migrate("migrations").await?
268///         .api(my_app::urls::api())
269///         .seed_with(my_app::seed::run).await?
270///         .serve("0.0.0.0:8080").await
271/// }
272/// ```
273///
274/// Optional `flavor = "current_thread"` passes through to
275/// `#[tokio::main]`; default is the multi-threaded runtime.
276///
277/// Pulls `tracing-subscriber` into the rustango crate behind the
278/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
279/// out get plain `#[tokio::main]` ergonomics without the dependency.
280#[proc_macro_attribute]
281pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
282    expand_main(args.into(), item.into())
283        .unwrap_or_else(syn::Error::into_compile_error)
284        .into()
285}
286
287fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
288    let mut input: syn::ItemFn = syn::parse2(item)?;
289    if input.sig.asyncness.is_none() {
290        return Err(syn::Error::new(
291            input.sig.ident.span(),
292            "`#[rustango::main]` must wrap an `async fn`",
293        ));
294    }
295
296    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
297    // to `#[tokio::main]`. Tokio's proc-macro internally emits
298    // `::tokio::*` paths that resolve against the user crate's deps,
299    // so calling it through the rustango re-export still requires the
300    // user to add tokio to their own Cargo.toml. Building the
301    // runtime ourselves keeps the dep transitive through the
302    // `runtime` feature on rustango.
303    //
304    // Parse optional `flavor = "current_thread"` / `flavor =
305    // "multi_thread"` from the attribute args. Unknown args are
306    // tolerated (forward-compat with tokio's own arg surface).
307    let root = rustango_root();
308    let flavor = parse_flavor(&args);
309    let builder_call = match flavor {
310        Flavor::CurrentThread => quote! {
311            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
312        },
313        Flavor::MultiThread => quote! {
314            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
315        },
316    };
317
318    // Detach the user body and rewrite `main` as a sync fn that
319    // builds the runtime and blocks on the async body.
320    let user_body = input.block.clone();
321    input.sig.asyncness = None;
322    input.block = syn::parse2(quote! {{
323        {
324            use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
325            // `try_init` so duplicate installers (e.g. tests already
326            // holding a subscriber) don't panic.
327            let _ = tracing_subscriber::fmt()
328                .with_env_filter(
329                    EnvFilter::try_from_default_env()
330                        .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
331                )
332                .try_init();
333        }
334        let __rt = #builder_call
335            .enable_all()
336            .build()
337            .expect("failed to build tokio runtime");
338        __rt.block_on(async move #user_body)
339    }})?;
340
341    Ok(quote! {
342        #input
343    })
344}
345
346enum Flavor {
347    MultiThread,
348    CurrentThread,
349}
350
351fn parse_flavor(args: &TokenStream2) -> Flavor {
352    // Cheap parser: look for the literal token sequence
353    // `flavor = "current_thread"`. Everything else (including
354    // bare `multi_thread` or no args) defaults to multi-thread.
355    let s = args.to_string();
356    if s.contains("current_thread") {
357        Flavor::CurrentThread
358    } else {
359        Flavor::MultiThread
360    }
361}
362
363/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
364struct QInput {
365    base_path: syn::Path,
366    field: syn::Ident,
367    value: syn::Expr,
368}
369
370impl syn::parse::Parse for QInput {
371    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
372        let base_path: syn::Path = input.parse()?;
373        input.parse::<syn::Token![.]>()?;
374        let field: syn::Ident = input.parse()?;
375        input.parse::<syn::Token![=]>()?;
376        let value: syn::Expr = input.parse()?;
377        Ok(QInput {
378            base_path,
379            field,
380            value,
381        })
382    }
383}
384
385fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
386    let q: QInput = syn::parse2(input)?;
387    let root = rustango_root();
388    let field_str = q.field.to_string();
389    let field_span = q.field.span();
390    let (base, suffix) = match field_str.find("__") {
391        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
392        None => (field_str.as_str(), ""),
393    };
394    if base.is_empty() {
395        return Err(syn::Error::new(
396            field_span,
397            "Q!(): field name is empty before `__` suffix",
398        ));
399    }
400    let base_ident = syn::Ident::new(base, field_span);
401    let value = &q.value;
402    let path = &q.base_path;
403
404    // Most suffixes map directly to a Column method with the value
405    // forwarded unchanged. Some need value-shape massaging (wildcards
406    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
407    // ISNULL). Unknown suffixes fail the build.
408    let expanded = match suffix {
409        "" | "exact" => quote! {
410            #root::core::Column::eq(#path::#base_ident, #value)
411        },
412        "ne" => quote! {
413            #root::core::Column::ne(#path::#base_ident, #value)
414        },
415        "gt" => quote! {
416            #root::core::Column::gt(#path::#base_ident, #value)
417        },
418        "gte" => quote! {
419            #root::core::Column::gte(#path::#base_ident, #value)
420        },
421        "lt" => quote! {
422            #root::core::Column::lt(#path::#base_ident, #value)
423        },
424        "lte" => quote! {
425            #root::core::Column::lte(#path::#base_ident, #value)
426        },
427        "iexact" => quote! {
428            // Django emulates `__iexact` as case-insensitive equality.
429            // The non-wildcard `ILIKE value` is semantically identical
430            // for plain strings; LIKE-metachars `%` `_` in the rhs would
431            // accidentally match more — document the caveat.
432            #root::core::Column::ilike(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
433        },
434        "contains" => quote! {
435            #root::core::Column::like(
436                #path::#base_ident,
437                ::std::format!("%{}%", #value),
438            )
439        },
440        "icontains" => quote! {
441            #root::core::Column::ilike(
442                #path::#base_ident,
443                ::std::format!("%{}%", #value),
444            )
445        },
446        "startswith" => quote! {
447            #root::core::Column::like(
448                #path::#base_ident,
449                ::std::format!("{}%", #value),
450            )
451        },
452        "istartswith" => quote! {
453            #root::core::Column::ilike(
454                #path::#base_ident,
455                ::std::format!("{}%", #value),
456            )
457        },
458        "endswith" => quote! {
459            #root::core::Column::like(
460                #path::#base_ident,
461                ::std::format!("%{}", #value),
462            )
463        },
464        "iendswith" => quote! {
465            #root::core::Column::ilike(
466                #path::#base_ident,
467                ::std::format!("%{}", #value),
468            )
469        },
470        "in" => quote! {
471            #root::core::Column::is_in(#path::#base_ident, #value)
472        },
473        "not_in" => quote! {
474            #root::core::Column::not_in(#path::#base_ident, #value)
475        },
476        "isnull" => {
477            // Must be a bool literal at macro time so we can route to
478            // is_null vs is_not_null without a runtime branch.
479            let b = match value {
480                syn::Expr::Lit(syn::ExprLit {
481                    lit: syn::Lit::Bool(b),
482                    ..
483                }) => b.value(),
484                _ => {
485                    return Err(syn::Error::new_spanned(
486                        value,
487                        "Q!(): `__isnull` requires a `true` or `false` literal",
488                    ));
489                }
490            };
491            if b {
492                quote! { #root::core::Column::is_null(#path::#base_ident) }
493            } else {
494                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
495            }
496        }
497        "between" => {
498            // Accept a tuple literal `(lo, hi)`.
499            let tuple = match value {
500                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
501                _ => {
502                    return Err(syn::Error::new_spanned(
503                        value,
504                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
505                    ));
506                }
507            };
508            let lo = &tuple.elems[0];
509            let hi = &tuple.elems[1];
510            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
511        }
512        "regex" => quote! {
513            #root::core::Column::regex(#path::#base_ident, #value)
514        },
515        "iregex" => quote! {
516            #root::core::Column::iregex(#path::#base_ident, #value)
517        },
518        _ => {
519            return Err(syn::Error::new(
520                field_span,
521                format!(
522                    "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",
523                    suffix
524                ),
525            ));
526        }
527    };
528    Ok(expanded)
529}
530
531fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
532    // Default to "./migrations" if invoked without args.
533    let path_str = if input.is_empty() {
534        "./migrations".to_string()
535    } else {
536        let lit: LitStr = syn::parse2(input)?;
537        lit.value()
538    };
539
540    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
541        syn::Error::new(
542            proc_macro2::Span::call_site(),
543            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
544        )
545    })?;
546    let abs = std::path::Path::new(&manifest).join(&path_str);
547
548    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
549    if abs.is_dir() {
550        let read = std::fs::read_dir(&abs).map_err(|e| {
551            syn::Error::new(
552                proc_macro2::Span::call_site(),
553                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
554            )
555        })?;
556        for entry in read.flatten() {
557            let path = entry.path();
558            if !path.is_file() {
559                continue;
560            }
561            if path.extension().and_then(|s| s.to_str()) != Some("json") {
562                continue;
563            }
564            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
565                continue;
566            };
567            entries.push((stem.to_owned(), path));
568        }
569    }
570    entries.sort_by(|a, b| a.0.cmp(&b.0));
571
572    // Compile-time chain validation: read each migration's JSON,
573    // pull `name` and `prev` (file-stem-keyed for the chain check),
574    // and verify every `prev` points to another migration in the
575    // slice. Mismatches between the file stem and the embedded
576    // `name` field — and broken `prev` chains — fail at MACRO
577    // EXPANSION time so a misshapen migration set never compiles.
578    //
579    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
580    // migrations + a Rust proc-macro that reads them is the unique
581    // combo nothing else in the Django-shape Rust camp can match
582    // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
583    // Rwf's are raw SQL — none have a static chain to validate).
584    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
585    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
586    for (stem, path) in &entries {
587        let raw = std::fs::read_to_string(path).map_err(|e| {
588            syn::Error::new(
589                proc_macro2::Span::call_site(),
590                format!(
591                    "embed_migrations!: cannot read {} for chain validation: {e}",
592                    path.display()
593                ),
594            )
595        })?;
596        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
597            syn::Error::new(
598                proc_macro2::Span::call_site(),
599                format!(
600                    "embed_migrations!: {} is not valid JSON: {e}",
601                    path.display()
602                ),
603            )
604        })?;
605        let name = json
606            .get("name")
607            .and_then(|v| v.as_str())
608            .ok_or_else(|| {
609                syn::Error::new(
610                    proc_macro2::Span::call_site(),
611                    format!(
612                        "embed_migrations!: {} is missing the `name` field",
613                        path.display()
614                    ),
615                )
616            })?
617            .to_owned();
618        if name != *stem {
619            return Err(syn::Error::new(
620                proc_macro2::Span::call_site(),
621                format!(
622                    "embed_migrations!: file stem `{stem}` does not match the migration's \
623                     `name` field `{name}` — rename the file or fix the JSON",
624                ),
625            ));
626        }
627        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
628        chain_names.push(name.clone());
629        prev_refs.push((name, prev));
630    }
631
632    let name_set: std::collections::HashSet<&str> =
633        chain_names.iter().map(String::as_str).collect();
634    for (name, prev) in &prev_refs {
635        if let Some(p) = prev {
636            if !name_set.contains(p.as_str()) {
637                return Err(syn::Error::new(
638                    proc_macro2::Span::call_site(),
639                    format!(
640                        "embed_migrations!: broken migration chain — `{name}` declares \
641                         prev=`{p}` but no migration with that name exists in {}",
642                        abs.display()
643                    ),
644                ));
645            }
646        }
647    }
648
649    let pairs: Vec<TokenStream2> = entries
650        .iter()
651        .map(|(name, path)| {
652            let path_lit = path.display().to_string();
653            quote! { (#name, ::core::include_str!(#path_lit)) }
654        })
655        .collect();
656
657    Ok(quote! {
658        {
659            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
660            __RUSTANGO_EMBEDDED
661        }
662    })
663}
664
665fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
666    let root = rustango_root();
667    let struct_name = &input.ident;
668
669    let Data::Struct(data) = &input.data else {
670        return Err(syn::Error::new_spanned(
671            struct_name,
672            "Model can only be derived on structs",
673        ));
674    };
675    let Fields::Named(named) = &data.fields else {
676        return Err(syn::Error::new_spanned(
677            struct_name,
678            "Model requires a struct with named fields",
679        ));
680    };
681
682    let container = parse_container_attrs(input)?;
683    let table = container
684        .table
685        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
686    let model_name = struct_name.to_string();
687
688    let collected = collect_fields(named, &table)?;
689
690    // Validate that #[rustango(display = "…")] names a real field.
691    if let Some((ref display, span)) = container.display {
692        if !collected.field_names.iter().any(|n| n == display) {
693            return Err(syn::Error::new(
694                span,
695                format!("`display = \"{display}\"` does not match any field on this struct"),
696            ));
697        }
698    }
699    let display = container.display.map(|(name, _)| name);
700    let app_label = container.app.clone();
701
702    // Validate admin field-name lists against declared field names.
703    // Note: `list_display` is intentionally NOT validated here. As of
704    // v0.32 it may also reference inventory-registered computed
705    // fields (via `register_admin_computed!`) whose existence the
706    // macro can't see at compile time — they're submitted from any
707    // crate that depends on rustango. The runtime list-view resolves
708    // unknown names against the inventory + silently drops the
709    // truly-bogus ones, which is the cheaper trade-off versus
710    // forcing a per-Model attr to opt out.
711    if let Some(admin) = &container.admin {
712        for (label, list) in [
713            ("search_fields", &admin.search_fields),
714            ("readonly_fields", &admin.readonly_fields),
715            ("list_filter", &admin.list_filter),
716        ] {
717            if let Some((names, span)) = list {
718                for name in names {
719                    if !collected.field_names.iter().any(|n| n == name) {
720                        return Err(syn::Error::new(
721                            *span,
722                            format!(
723                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
724                            ),
725                        ));
726                    }
727                }
728            }
729        }
730        if let Some((pairs, span)) = &admin.ordering {
731            for (name, _) in pairs {
732                if !collected.field_names.iter().any(|n| n == name) {
733                    return Err(syn::Error::new(
734                        *span,
735                        format!(
736                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
737                        ),
738                    ));
739                }
740            }
741        }
742        if let Some((groups, span)) = &admin.fieldsets {
743            for (_, fields) in groups {
744                for name in fields {
745                    if !collected.field_names.iter().any(|n| n == name) {
746                        return Err(syn::Error::new(
747                            *span,
748                            format!(
749                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
750                            ),
751                        ));
752                    }
753                }
754            }
755        }
756    }
757    if let Some(audit) = &container.audit {
758        if let Some((names, span)) = &audit.track {
759            for name in names {
760                if !collected.field_names.iter().any(|n| n == name) {
761                    return Err(syn::Error::new(
762                        *span,
763                        format!(
764                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
765                        ),
766                    ));
767                }
768            }
769        }
770    }
771
772    // Issue #291 / T2.5 — validate each `default_order` column name
773    // against the model's collected fields. Typos fail at macro-expand
774    // time, not at the database.
775    for (col, _desc, span) in &container.default_order {
776        if !collected.field_names.iter().any(|n| n == col) {
777            return Err(syn::Error::new(
778                *span,
779                format!(
780                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
781                ),
782            ));
783        }
784    }
785
786    // Build the audit_track list for ModelSchema: None when no audit attr,
787    // Some(empty) when audit present without track, Some(names) when explicit.
788    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
789        audit
790            .track
791            .as_ref()
792            .map(|(names, _)| names.clone())
793            .unwrap_or_default()
794    });
795
796    // Merge field-level indexes into the container's index list.
797    let mut all_indexes: Vec<IndexAttr> = container.indexes;
798    for field in &named.named {
799        let ident = field.ident.as_ref().expect("named");
800        let col = to_snake_case(&ident.to_string()); // column name fallback
801                                                     // Re-parse field attrs to check for index flag
802        if let Ok(fa) = parse_field_attrs(field) {
803            if fa.index {
804                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
805                let auto_name = if fa.index_unique {
806                    format!("{table}_{col_name}_uq_idx")
807                } else {
808                    format!("{table}_{col_name}_idx")
809                };
810                all_indexes.push(IndexAttr {
811                    name: fa.index_name.or(Some(auto_name)),
812                    columns: vec![col_name],
813                    unique: fa.index_unique,
814                    method: fa.index_method,
815                    where_clause: None,
816                    include: Vec::new(),
817                });
818            }
819        }
820    }
821
822    let model_impl = model_impl_tokens(
823        struct_name,
824        &model_name,
825        &table,
826        display.as_deref(),
827        app_label.as_deref(),
828        container.admin.as_ref(),
829        &container.default_order,
830        &collected.field_schemas,
831        collected.soft_delete_column.as_deref(),
832        container.permissions,
833        audit_track_names.as_deref(),
834        &container.m2m,
835        &all_indexes,
836        &container.checks,
837        &container.excludes,
838        &container.composite_fks,
839        &container.generic_fks,
840        container.scope.as_deref(),
841        container.is_view,
842        container.verbose_name.as_deref(),
843        container.verbose_name_plural.as_deref(),
844        container.managed,
845        container.base_manager_name.as_deref(),
846        container.order_with_respect_to.as_deref(),
847        container.proxy,
848        &container.required_db_features,
849        container.required_db_vendor.as_deref(),
850        container.default_related_name.as_deref(),
851        container.db_table_comment.as_deref(),
852        container
853            .get_latest_by
854            .as_ref()
855            .map(|(c, d)| (c.as_str(), *d)),
856        &container.extra_permissions,
857        &container.default_permissions,
858        &container.global_scopes,
859        &container.reverse_has_relations,
860        &container.generic_has_relations,
861    );
862    let module_ident = column_module_ident(struct_name);
863    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
864    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
865        let track_set: Option<std::collections::HashSet<&str>> = audit
866            .track
867            .as_ref()
868            .map(|(names, _)| names.iter().map(String::as_str).collect());
869        collected
870            .column_entries
871            .iter()
872            .filter(|c| {
873                track_set
874                    .as_ref()
875                    .map_or(true, |s| s.contains(c.name.as_str()))
876            })
877            .collect()
878    });
879    let inherent_impl = inherent_impl_tokens(
880        struct_name,
881        &collected,
882        collected.primary_key.as_ref(),
883        &column_consts,
884        audited_fields.as_deref(),
885        &all_indexes,
886        &container.manager_fns,
887    );
888    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
889    let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
890    let reverse_helpers = reverse_helper_tokens(
891        struct_name,
892        &collected.fk_relations,
893        container.default_related_name.as_deref(),
894    );
895    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
896    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
897    // Issue #817 — `#[rustango(through(...))]` accessors.
898    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
899    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
900    let reverse_has_accessors =
901        reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
902    let generic_fk_accessors = generic_fk_accessor_tokens(
903        struct_name,
904        &container.generic_fks,
905        &collected.column_entries,
906    );
907
908    // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
909    // emits an empty extension trait so users can add methods via
910    // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
911    // trait declaration. See `crates/rustango/src/manager.rs` for the
912    // pattern this replaces.
913    let manager_trait = container.manager_ext.as_ref().map(|name| {
914        let model_name_str = struct_name.to_string();
915        let doc = format!(
916            "Custom-Manager extension trait for [`{model_name_str}`]. \
917             Generated by `#[rustango(manager(ext = ...))]`. Add methods \
918             via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
919        );
920        quote! {
921            #[doc = #doc]
922            pub trait #name: ::core::marker::Sized {}
923        }
924    });
925
926    Ok(quote! {
927        #model_impl
928        #inherent_impl
929        #from_row_impl
930        #column_module
931        #reverse_helpers
932        #m2m_accessors
933        #generic_m2m_accessors
934        #through_accessors
935        #reverse_has_accessors
936        #generic_fk_accessors
937        #manager_trait
938
939        #root::core::inventory::submit! {
940            #root::core::ModelEntry {
941                schema: <#struct_name as #root::core::Model>::SCHEMA,
942                // `module_path!()` evaluates at the registration site,
943                // so a Model declared in `crate::blog::models` records
944                // `"<crate>::blog::models"` and `resolved_app_label()`
945                // can infer "blog" without an explicit attribute.
946                module_path: ::core::module_path!(),
947            }
948        }
949    })
950}
951
952/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
953/// matches `field_name` against the model's FK fields and, for a
954/// match, decodes the FK target via the parent's macro-generated
955/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
956/// `ForeignKey::Loaded` on `self`.
957///
958/// Always emitted (with empty arms for FK-less models, which
959/// return `Ok(false)` for any field name) so the `T: LoadRelated`
960/// trait bound on `fetch_on` is universally satisfied — users
961/// never have to think about implementing it.
962fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
963    let root = rustango_root();
964    let arms = fk_relations.iter().map(|rel| {
965        let parent_ty = &rel.parent_type;
966        let fk_col = rel.fk_column.as_str();
967        // FK field's Rust ident matches its SQL column name in v0.8
968        // (no `column = "..."` rename ships on FK fields).
969        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
970        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
971        let assign = if rel.nullable {
972            quote! {
973                self.#field_ident = ::core::option::Option::Some(
974                    #root::sql::ForeignKey::loaded(_pk, _parent),
975                );
976            }
977        } else {
978            quote! {
979                self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
980            }
981        };
982        quote! {
983            #fk_col => {
984                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
985                // Audit #451 — multi-hop `select_related("a__b__c")`:
986                // stitch the deeper relation onto this parent first,
987                // decoding it at the accumulated `__next_alias`. The
988                // parent type also impls `LoadRelated`, so this recurses
989                // the FK chain to arbitrary depth.
990                if let ::core::option::Option::Some(__r) = __rest {
991                    let _ = #root::sql::LoadRelated::__rustango_load_related(
992                        &mut _parent, row, __r, &__next_alias,
993                    )?;
994                }
995                // Loud-in-debug, default-in-release: a divergence
996                // between the FK field's declared `K` (drives the
997                // expected `SqlValue::<Variant>`) and the parent's
998                // `__rustango_pk_value` output is a macro-internal
999                // invariant break — surfacing the panic in dev
1000                // catches it before users hit silent PK=0 corruption.
1001                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1002                    #root::core::SqlValue::#variant_ident(v) => v,
1003                    _other => {
1004                        ::core::debug_assert!(
1005                            false,
1006                            "rustango macro bug: load_related on FK `{}` expected \
1007                             SqlValue::{} from parent's __rustango_pk_value but got \
1008                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1009                            #fk_col,
1010                            ::core::stringify!(#variant_ident),
1011                            _other,
1012                        );
1013                        #default_expr
1014                    }
1015                };
1016                #assign
1017                ::core::result::Result::Ok(true)
1018            }
1019        }
1020    });
1021    quote! {
1022        #[cfg(feature = "postgres")]
1023        impl #root::sql::LoadRelated for #struct_name {
1024            #[allow(unused_variables)]
1025            fn __rustango_load_related(
1026                &mut self,
1027                row: &#root::sql::sqlx::postgres::PgRow,
1028                field_name: &str,
1029                alias: &str,
1030            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1031                // Audit #451 — split the multi-hop path: `base` is the FK
1032                // on THIS model, `__rest` (if any) is the remaining chain
1033                // to stitch onto the loaded parent. `__next_alias` is the
1034                // accumulated join alias the parent's columns live under
1035                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1036                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1037                    match field_name.split_once("__") {
1038                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1039                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1040                    };
1041                let __next_alias: ::std::string::String = match __rest {
1042                    ::core::option::Option::Some(__r) => {
1043                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1044                        ::std::format!("{}__{}", alias, __rb)
1045                    }
1046                    ::core::option::Option::None => ::std::string::String::new(),
1047                };
1048                match __base {
1049                    #( #arms )*
1050                    _ => ::core::result::Result::Ok(false),
1051                }
1052            }
1053        }
1054    }
1055}
1056
1057/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1058/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1059/// which expands to a `LoadRelatedMy` impl when rustango is built with
1060/// the `mysql` feature, and to nothing otherwise. The decoded parent
1061/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1062/// decoder, also batch8) so the dual emission is symmetric across
1063/// backends.
1064fn load_related_impl_my_tokens(
1065    struct_name: &syn::Ident,
1066    fk_relations: &[FkRelation],
1067) -> TokenStream2 {
1068    let root = rustango_root();
1069    let arms = fk_relations.iter().map(|rel| {
1070        let parent_ty = &rel.parent_type;
1071        let fk_col = rel.fk_column.as_str();
1072        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1073        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1074        let assign = if rel.nullable {
1075            quote! {
1076                __self.#field_ident = ::core::option::Option::Some(
1077                    #root::sql::ForeignKey::loaded(_pk, _parent),
1078                );
1079            }
1080        } else {
1081            quote! {
1082                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1083            }
1084        };
1085        // `self` IS hygiene-tracked through macro_rules — emitted from
1086        // a different context than the `&mut self` parameter inside
1087        // the macro_rules-expanded fn. Pass it through as `__self`
1088        // and let the macro_rules rebind it to the receiver.
1089        quote! {
1090            #fk_col => {
1091                let mut _parent: #parent_ty =
1092                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1093                // Audit #451 — multi-hop: stitch the deeper relation onto
1094                // the parent at the accumulated alias (see PG twin).
1095                if let ::core::option::Option::Some(__r) = __rest {
1096                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1097                        &mut _parent, row, __r, &__next_alias,
1098                    )?;
1099                }
1100                // See note in `load_related_impl_tokens` (PG twin) —
1101                // the same loud-in-debug invariant guard.
1102                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1103                    #root::core::SqlValue::#variant_ident(v) => v,
1104                    _other => {
1105                        ::core::debug_assert!(
1106                            false,
1107                            "rustango macro bug: load_related on FK `{}` expected \
1108                             SqlValue::{} from parent's __rustango_pk_value but got \
1109                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1110                            #fk_col,
1111                            ::core::stringify!(#variant_ident),
1112                            _other,
1113                        );
1114                        #default_expr
1115                    }
1116                };
1117                #assign
1118                ::core::result::Result::Ok(true)
1119            }
1120        }
1121    });
1122    quote! {
1123        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1124            #( #arms )*
1125        });
1126    }
1127}
1128
1129/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1130/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1131/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1132fn load_related_impl_sqlite_tokens(
1133    struct_name: &syn::Ident,
1134    fk_relations: &[FkRelation],
1135) -> TokenStream2 {
1136    let root = rustango_root();
1137    let arms = fk_relations.iter().map(|rel| {
1138        let parent_ty = &rel.parent_type;
1139        let fk_col = rel.fk_column.as_str();
1140        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1141        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1142        let assign = if rel.nullable {
1143            quote! {
1144                __self.#field_ident = ::core::option::Option::Some(
1145                    #root::sql::ForeignKey::loaded(_pk, _parent),
1146                );
1147            }
1148        } else {
1149            quote! {
1150                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1151            }
1152        };
1153        quote! {
1154            #fk_col => {
1155                let mut _parent: #parent_ty =
1156                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1157                // Audit #451 — multi-hop: stitch the deeper relation onto
1158                // the parent at the accumulated alias (see PG twin).
1159                if let ::core::option::Option::Some(__r) = __rest {
1160                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1161                        &mut _parent, row, __r, &__next_alias,
1162                    )?;
1163                }
1164                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1165                    #root::core::SqlValue::#variant_ident(v) => v,
1166                    _other => {
1167                        ::core::debug_assert!(
1168                            false,
1169                            "rustango macro bug: load_related on FK `{}` expected \
1170                             SqlValue::{} from parent's __rustango_pk_value but got \
1171                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1172                            #fk_col,
1173                            ::core::stringify!(#variant_ident),
1174                            _other,
1175                        );
1176                        #default_expr
1177                    }
1178                };
1179                #assign
1180                ::core::result::Result::Ok(true)
1181            }
1182        }
1183    });
1184    quote! {
1185        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1186            #( #arms )*
1187        });
1188    }
1189}
1190
1191/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1192/// matches `field_name` against the model's FK fields and returns
1193/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1194/// group children by parent PK.
1195///
1196/// Always emitted (with `_ => None` for FK-less models) so the
1197/// trait bound on `fetch_with_prefetch` is universally satisfied.
1198fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1199    let root = rustango_root();
1200    let arms = fk_relations.iter().map(|rel| {
1201        let fk_col = rel.fk_column.as_str();
1202        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1203        if rel.pk_kind == DetectedKind::I64 {
1204            // i64 FK — return the stored PK so prefetch_related can
1205            // group children by it. Nullable variant unwraps via
1206            // `as_ref().map(...)`: an unset (NULL) FK column yields
1207            // `None` and that child sits out of the grouping (correct
1208            // semantics — it has no parent to attach to).
1209            if rel.nullable {
1210                quote! {
1211                    #fk_col => self.#field_ident
1212                        .as_ref()
1213                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1214                }
1215            } else {
1216                quote! {
1217                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1218                }
1219            }
1220        } else {
1221            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1222            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1223            // i64-keyed grouping path — the trait signature is
1224            // `Option<i64>` and a non-i64 PK can't lower into it.
1225            // The FK still works for everything else (CRUD, lazy
1226            // load via `.get()`, select_related JOINs); only the
1227            // bulk prefetch grouper needs the integer key.
1228            quote! {
1229                #fk_col => ::core::option::Option::None,
1230            }
1231        }
1232    });
1233    // PK-type-agnostic version: every FK arm emits an
1234    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1235    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1236    // opt OUT of the legacy i64 method (it returns None) but opt IN
1237    // here.
1238    let value_arms = fk_relations.iter().map(|rel| {
1239        let fk_col = rel.fk_column.as_str();
1240        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1241        if rel.nullable {
1242            quote! {
1243                #fk_col => self.#field_ident
1244                    .as_ref()
1245                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1246                        #root::sql::ForeignKey::pk(fk)
1247                    )),
1248            }
1249        } else {
1250            quote! {
1251                #fk_col => ::core::option::Option::Some(
1252                    ::core::convert::Into::<#root::core::SqlValue>::into(
1253                        self.#field_ident.pk()
1254                    )
1255                ),
1256            }
1257        }
1258    });
1259    quote! {
1260        impl #root::sql::FkPkAccess for #struct_name {
1261            #[allow(unused_variables)]
1262            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1263                match field_name {
1264                    #( #arms )*
1265                    _ => ::core::option::Option::None,
1266                }
1267            }
1268            #[allow(unused_variables)]
1269            fn __rustango_fk_pk_value(
1270                &self,
1271                field_name: &str,
1272            ) -> ::core::option::Option<#root::core::SqlValue> {
1273                match field_name {
1274                    #( #value_arms )*
1275                    _ => ::core::option::Option::None,
1276                }
1277            }
1278        }
1279    }
1280}
1281
1282/// For every `ForeignKey<Parent>` field on `Child`, emit
1283/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1284/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1285/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1286/// — the canonical reverse-FK fetch. One round trip, no N+1.
1287///
1288/// **PG-only emission**: the accessor is bounded on
1289/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1290/// both of which are gated behind the `postgres` cargo feature. The
1291/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1292/// model derive itself compiles on tri-dialect / sqlite-only
1293/// downstream builds — the accessor just isn't materialised. A tri-
1294/// dialect `_set_pool` variant is a separate follow-up.
1295fn reverse_helper_tokens(
1296    child_ident: &syn::Ident,
1297    fk_relations: &[FkRelation],
1298    default_related_name: Option<&str>,
1299) -> TokenStream2 {
1300    let root = rustango_root();
1301    if fk_relations.is_empty() {
1302        return TokenStream2::new();
1303    }
1304    // Method-name resolution per FK (issue #816 + follow-up):
1305    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1306    //      itself — wins over everything else. Django's
1307    //      `ForeignKey(related_name="...")`.
1308    //   2. Container-level `default_related_name = "..."` on the
1309    //      child — Django's `class Meta: default_related_name`.
1310    //      Applies to every FK on this model that didn't override.
1311    //   3. Fallback: `<child_snake>_set` — Django's `<child>_set`
1312    //      convention. `Post` → `post_set`, `BlogComment` →
1313    //      `blog_comment_set`. Avoids English-plural edge cases.
1314    //
1315    // The PG-on-executor variant keeps the resolved name; the
1316    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1317    // framework's convention for the `&Pool` flavor of every helper).
1318    let default_pg_suffix = default_related_name
1319        .map(str::to_owned)
1320        .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1321    let impls = fk_relations.iter().map(|rel| {
1322        let pg_suffix = rel
1323            .related_name
1324            .clone()
1325            .unwrap_or_else(|| default_pg_suffix.clone());
1326        let pool_suffix = format!("{}_pool", pg_suffix);
1327        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1328        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1329        let parent_ty = &rel.parent_type;
1330        let fk_col = rel.fk_column.as_str();
1331        let doc = format!(
1332            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1333             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1334             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1335             further `{child_ident}::objects()` filters via direct queryset use."
1336        );
1337        let pool_doc = format!(
1338            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1339             [`#root::sql::Pool`] and dispatches per backend so the \
1340             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1341             Use this from framework code that holds a `&Pool` (admin, \
1342             tenancy resolver, viewset handlers); reach for the executor- \
1343             bound variant when you already have a typed `sqlx::Executor`."
1344        );
1345        quote! {
1346            #[cfg(feature = "postgres")]
1347            impl #parent_ty {
1348                #[doc = #doc]
1349                ///
1350                /// # Errors
1351                /// Returns [`#root::sql::ExecError`] for SQL-writing
1352                /// or driver failures.
1353                pub async fn #pg_method_ident<'_c, _E>(
1354                    &self,
1355                    _executor: _E,
1356                ) -> ::core::result::Result<
1357                    ::std::vec::Vec<#child_ident>,
1358                    #root::sql::ExecError,
1359                >
1360                where
1361                    _E: #root::sql::sqlx::Executor<
1362                        '_c,
1363                        Database = #root::sql::sqlx::Postgres,
1364                    >,
1365                {
1366                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1367                    #root::query::QuerySet::<#child_ident>::new()
1368                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1369                        .fetch_on(_executor)
1370                        .await
1371                }
1372            }
1373
1374            impl #parent_ty {
1375                #[doc = #pool_doc]
1376                ///
1377                /// # Errors
1378                /// Returns [`#root::sql::ExecError`] for SQL-writing
1379                /// or driver failures.
1380                pub async fn #pool_method_ident(
1381                    &self,
1382                    pool: &#root::sql::Pool,
1383                ) -> ::core::result::Result<
1384                    ::std::vec::Vec<#child_ident>,
1385                    #root::sql::ExecError,
1386                > {
1387                    use #root::sql::FetcherPool as _;
1388                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1389                    #root::query::QuerySet::<#child_ident>::new()
1390                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1391                        .fetch_pool(pool)
1392                        .await
1393                }
1394            }
1395        }
1396    });
1397    quote! { #( #impls )* }
1398}
1399
1400/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1401/// relation declared on the model.
1402/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1403/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1404///
1405/// Closes #239 + #240 — the Django-shape `comment.content_object` /
1406/// `comment.content_object = post` ergonomics on top of the existing
1407/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1408///
1409/// `column_entries` is passed so we can resolve each `ct_column` /
1410/// `pk_column` SQL name back to its Rust field ident — the macro
1411/// only sees the column-side strings in the attribute, but the
1412/// emitted accessor needs to read the actual struct field.
1413fn generic_fk_accessor_tokens(
1414    struct_name: &syn::Ident,
1415    generic_fks: &[GenericFkAttr],
1416    column_entries: &[ColumnEntry],
1417) -> TokenStream2 {
1418    let root = rustango_root();
1419    if generic_fks.is_empty() {
1420        return TokenStream2::new();
1421    }
1422    let methods = generic_fks.iter().filter_map(|gfk| {
1423        // Resolve `ct_column` + `pk_column` to the struct's Rust
1424        // field idents. A typo (column name doesn't match any field)
1425        // emits no method for that registration — the user will see
1426        // the compiler reject the SCHEMA literal anyway, so there's
1427        // a clear error path without us double-reporting.
1428        let ct_ident = column_entries
1429            .iter()
1430            .find(|c| c.column == gfk.ct_column)
1431            .map(|c| c.ident.clone())?;
1432        let pk_ident = column_entries
1433            .iter()
1434            .find(|c| c.column == gfk.pk_column)
1435            .map(|c| c.ident.clone())?;
1436
1437        let accessor_ident =
1438            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1439        let setter_ident =
1440            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1441        let name_literal = gfk.name.as_str();
1442
1443        Some(quote! {
1444            #[doc = concat!(
1445                "Resolve the polymorphic `",
1446                #name_literal,
1447                "` relation. Reads `self.",
1448                stringify!(#ct_ident),
1449                "` + `self.",
1450                stringify!(#pk_ident),
1451                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1452                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1453                #name_literal,
1454                "\", ...))]`."
1455            )]
1456            pub async fn #accessor_ident(
1457                &self,
1458                pool: &#root::sql::Pool,
1459            ) -> ::core::result::Result<
1460                ::core::option::Option<#root::__serde_json::Value>,
1461                #root::sql::ExecError,
1462            > {
1463                let gfk = #root::contenttypes::GenericForeignKey::new(
1464                    self.#ct_ident as i64,
1465                    self.#pk_ident as i64,
1466                );
1467                gfk.get_object(pool).await
1468            }
1469
1470            #[doc = concat!(
1471                "Set the polymorphic `",
1472                #name_literal,
1473                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1474                stringify!(#ct_ident),
1475                "` and `self.",
1476                stringify!(#pk_ident),
1477                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1478                #name_literal,
1479                "\", ...))]`."
1480            )]
1481            pub async fn #setter_ident<T: #root::core::Model>(
1482                &mut self,
1483                pool: &#root::sql::Pool,
1484                target_pk: i64,
1485            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1486                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1487                    pool,
1488                    target_pk,
1489                ).await?;
1490                self.#ct_ident = gfk.content_type_id as _;
1491                self.#pk_ident = gfk.object_pk as _;
1492                ::core::result::Result::Ok(())
1493            }
1494        })
1495    });
1496    quote! {
1497        impl #struct_name {
1498            #( #methods )*
1499        }
1500    }
1501}
1502
1503fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1504    let root = rustango_root();
1505    if m2m_relations.is_empty() {
1506        return TokenStream2::new();
1507    }
1508    let methods = m2m_relations.iter().map(|rel| {
1509        let method_name = format!("{}_m2m", rel.name);
1510        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1511        let through = rel.through.as_str();
1512        let src_col = rel.src.as_str();
1513        let dst_col = rel.dst.as_str();
1514        quote! {
1515            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1516                #root::sql::M2MManager {
1517                    src_pk: self.__rustango_pk_value(),
1518                    through: #through,
1519                    src_col: #src_col,
1520                    dst_col: #dst_col,
1521                }
1522            }
1523        }
1524    });
1525    quote! {
1526        impl #struct_name {
1527            #( #methods )*
1528        }
1529    }
1530}
1531
1532/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1533/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1534fn generic_m2m_accessor_tokens(
1535    struct_name: &syn::Ident,
1536    relations: &[GenericM2MAttr],
1537) -> TokenStream2 {
1538    let root = rustango_root();
1539    if relations.is_empty() {
1540        return TokenStream2::new();
1541    }
1542    let methods = relations.iter().map(|rel| {
1543        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1544        let through = rel.through.as_str();
1545        let pk_col = rel.pk_column.as_str();
1546        let ct_col = rel.ct_column.as_str();
1547        let related_col = rel.related_column.as_str();
1548        quote! {
1549            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1550                #root::sql::GenericM2MManager {
1551                    src_pk: self.__rustango_pk_value(),
1552                    src_schema: <Self as #root::core::Model>::SCHEMA,
1553                    through: #through,
1554                    pk_col: #pk_col,
1555                    ct_col: #ct_col,
1556                    dst_col: #related_col,
1557                }
1558            }
1559        }
1560    });
1561    quote! {
1562        impl #struct_name {
1563            #( #methods )*
1564        }
1565    }
1566}
1567
1568/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1569/// associated functions for each `#[rustango(reverse_has(...))]`
1570/// attribute. Issue #830.
1571///
1572/// The two emitted functions return ready-to-use `WhereExpr` nodes
1573/// that downstream callers drop into
1574/// `QuerySet::<Self>::where_raw(...)`:
1575///
1576/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1577///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1578///   `whereHas` parity (without the closure-style sub-predicate
1579///   refinement — that's a follow-up; users can layer additional
1580///   predicates by constructing the SelectQuery themselves and
1581///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1582/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1583///   `whereDoesntHave` parity.
1584///
1585/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1586/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1587/// machinery threads the outer-table reference through automatically
1588/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1589fn reverse_has_accessor_tokens(
1590    struct_name: &syn::Ident,
1591    reverse_has_relations: &[ReverseHasAttr],
1592) -> TokenStream2 {
1593    let root = rustango_root();
1594    if reverse_has_relations.is_empty() {
1595        return TokenStream2::new();
1596    }
1597    let methods = reverse_has_relations.iter().map(|rel| {
1598        let exists_name = format!("{}_exists_expr", rel.name);
1599        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1600        let count_name = format!("{}_count", rel.name);
1601        let fetch_name = format!("{}_fetch", rel.name);
1602        let first_name = format!("{}_first", rel.name);
1603        let pluck_name = format!("{}_pluck", rel.name);
1604        let accessor_name = rel.name.as_str();
1605        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1606        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1607        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1608        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1609        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1610        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1611        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1612        let child = &rel.child;
1613        let child_fk_column = rel.child_fk_column.as_str();
1614        let self_pk_column = rel.self_pk_column.as_str();
1615        let exists_doc = format!(
1616            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1617             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1618             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1619             filter to {struct_name}s with at least one matching child.",
1620        );
1621        let not_exists_doc = format!(
1622            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1623             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1624             <outer>.{self_pk_column})`. Drop into \
1625             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1626             {struct_name}s with **no** matching child.",
1627        );
1628        let count_doc = format!(
1629            "Eloquent `$model->{name}->count()` analog — returns \
1630             the number of `{child}` rows whose `{child_fk_column}` \
1631             matches this `{struct_name}` instance's primary key. \
1632             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1633             {child_fk_column} = <self.pk>`.",
1634            name = rel.name,
1635        );
1636        let accessor_doc = format!(
1637            "Eloquent `$model->{name}` accessor — returns a \
1638             `QuerySet<{child}>` filtered to rows whose \
1639             `{child_fk_column}` matches this `{struct_name}` \
1640             instance's primary key. **Chainable**: compose `.filter()` \
1641             / `.order_by()` / `.limit()` etc. on top, then call \
1642             `.fetch_pool(&pool)` (the QuerySet trait method) when \
1643             done. For the simple \"fetch all\" hot path with no \
1644             further composition, prefer the bare-name \
1645             `{name}_fetch(&pool)` companion.",
1646            name = rel.name,
1647        );
1648        let fetch_doc = format!(
1649            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1650             over `{name}(&self).fetch_pool(&pool)`. Use this when \
1651             you don't need further `.filter()` / `.order_by()` \
1652             composition; falls back to the chainable accessor when \
1653             you do. Avoids the `_pool` suffix on the most common \
1654             call-site shape.",
1655            name = rel.name,
1656        );
1657        quote! {
1658            #[doc = #accessor_doc]
1659            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1660                #root::query::QuerySet::<#child>::new()
1661                    .filter(#child_fk_column, self.__rustango_pk_value())
1662            }
1663
1664            #[doc = #fetch_doc]
1665            pub async fn #fetch_ident(
1666                &self,
1667                pool: &#root::sql::Pool,
1668            ) -> ::core::result::Result<
1669                ::std::vec::Vec<#child>,
1670                #root::sql::ExecError,
1671            > {
1672                use #root::sql::FetcherPool as _;
1673                self.#accessor_ident().fetch_pool(pool).await
1674            }
1675
1676            /// Eloquent `$model->relation->first()` / `hasOne`
1677            /// semantics — bare-name shortcut over
1678            /// `self.<name>().first(&pool)`. Returns `None` when no
1679            /// child rows match. Useful when the relation is
1680            /// nominally many-to-one in shape but at most one row is
1681            /// expected (latest comment, primary tag, etc.).
1682            pub async fn #first_ident(
1683                &self,
1684                pool: &#root::sql::Pool,
1685            ) -> ::core::result::Result<
1686                ::core::option::Option<#child>,
1687                #root::sql::ExecError,
1688            > {
1689                self.#accessor_ident().first(pool).await
1690            }
1691
1692            /// Eloquent `$model->relation->pluck($col)` — project a
1693            /// single column from the child rows into a `Vec<U>`.
1694            /// Skips the typed `Child` decode, which is cheaper when
1695            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1696            pub async fn #pluck_ident<U>(
1697                &self,
1698                col: &'static str,
1699                pool: &#root::sql::Pool,
1700            ) -> ::core::result::Result<
1701                ::std::vec::Vec<U>,
1702                #root::sql::ExecError,
1703            >
1704            where
1705                U: #root::sql::MaybePgScalar
1706                    + #root::sql::MaybeMyScalar
1707                    + #root::sql::MaybeSqliteScalar
1708                    + ::core::marker::Send
1709                    + ::core::marker::Unpin,
1710            {
1711                self.#accessor_ident()
1712                    .values_list_flat(col)
1713                    .fetch::<U>(pool)
1714                    .await
1715            }
1716
1717            #[doc = #exists_doc]
1718            pub fn #exists_ident() -> #root::core::WhereExpr {
1719                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1720                let child_schema =
1721                    <#child as #root::core::Model>::SCHEMA;
1722                let inner = SelectQuery {
1723                    where_clause: WhereExpr::ExprCompare {
1724                        lhs: Expr::Column(#child_fk_column),
1725                        op: Op::Eq,
1726                        rhs: Expr::OuterRef(#self_pk_column),
1727                    },
1728                    ..SelectQuery::new(child_schema)
1729                };
1730                WhereExpr::Exists(::std::boxed::Box::new(inner))
1731            }
1732
1733            #[doc = #not_exists_doc]
1734            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1735                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1736                let child_schema =
1737                    <#child as #root::core::Model>::SCHEMA;
1738                let inner = SelectQuery {
1739                    where_clause: WhereExpr::ExprCompare {
1740                        lhs: Expr::Column(#child_fk_column),
1741                        op: Op::Eq,
1742                        rhs: Expr::OuterRef(#self_pk_column),
1743                    },
1744                    ..SelectQuery::new(child_schema)
1745                };
1746                WhereExpr::NotExists(::std::boxed::Box::new(inner))
1747            }
1748
1749            #[doc = #count_doc]
1750            pub async fn #count_ident(
1751                &self,
1752                pool: &#root::sql::Pool,
1753            ) -> ::core::result::Result<
1754                i64,
1755                #root::sql::ExecError,
1756            > {
1757                use #root::sql::CounterPool as _;
1758                #root::query::QuerySet::<#child>::new()
1759                    .filter(#child_fk_column, self.__rustango_pk_value())
1760                    .count_pool(pool)
1761                    .await
1762            }
1763        }
1764    });
1765    quote! {
1766        impl #struct_name {
1767            #( #methods )*
1768        }
1769    }
1770}
1771
1772/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1773/// `#[rustango(through(...))]` attribute. Issue #817.
1774///
1775/// Each accessor builds a correlated subquery via
1776/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1777/// intermediate table, filters on the FK column pointing at this
1778/// model, and projects the intermediate PK. The outer queryset
1779/// filters the far model by its FK-to-intermediate column being in
1780/// that set.
1781///
1782/// The returned `QuerySet<Far>` is **chainable** — the subquery
1783/// lives inside a `where_raw` clause so the user's later
1784/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1785/// disturb it.
1786///
1787/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1788/// SQLite — no LATERAL or backend-specific syntax involved.
1789fn through_accessor_tokens(
1790    struct_name: &syn::Ident,
1791    through_relations: &[ThroughAttr],
1792) -> TokenStream2 {
1793    let root = rustango_root();
1794    if through_relations.is_empty() {
1795        return TokenStream2::new();
1796    }
1797    let methods = through_relations.iter().map(|rel| {
1798        let method_name = format!("{}_through", rel.name);
1799        let count_name = format!("{}_through_count", rel.name);
1800        let fetch_name = format!("{}_through_fetch", rel.name);
1801        let first_name = format!("{}_through_first", rel.name);
1802        let pluck_name = format!("{}_through_pluck", rel.name);
1803        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1804        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1805        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1806        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1807        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1808        let far = &rel.far;
1809        let intermediate = &rel.intermediate;
1810        let far_fk_column = rel.far_fk_column.as_str();
1811        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1812        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1813        let doc = format!(
1814            "Eloquent `hasManyThrough` accessor — returns a \
1815             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1816             instance through the intermediate `{intermediate}` table. \
1817             Generated SQL shape: \
1818             `… WHERE {far_fk_column} IN (SELECT \
1819             {intermediate_pk_column} FROM <{intermediate}> WHERE \
1820             {intermediate_fk_column} = self.pk)`. Chainable like any \
1821             other QuerySet — compose `.filter()` / `.order_by()` / \
1822             `.limit()` etc. on top.",
1823        );
1824        let count_doc = format!(
1825            "Eloquent `$model->{name}->count()` analog for the \
1826             through-relation — returns the number of `{far}` rows \
1827             reachable through `{intermediate}`. Equivalent to \
1828             `self.{name}_through().count_pool(pool)` but spelled \
1829             as a bare instance method for parity with the \
1830             `reverse_has` `<name>_count` shape.",
1831            name = rel.name,
1832        );
1833        quote! {
1834            #[doc = #doc]
1835            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1836                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1837                let intermediate_schema =
1838                    <#intermediate as #root::core::Model>::SCHEMA;
1839                let sub = SelectQuery {
1840                    where_clause: WhereExpr::Predicate(Filter {
1841                        column: #intermediate_fk_column,
1842                        op: Op::Eq,
1843                        value: self.__rustango_pk_value(),
1844                    }),
1845                    projection: ::core::option::Option::Some(
1846                        ::std::vec![#intermediate_pk_column],
1847                    ),
1848                    ..SelectQuery::new(intermediate_schema)
1849                };
1850                #root::query::QuerySet::<#far>::new().where_raw(
1851                    WhereExpr::InSubquery {
1852                        column: #far_fk_column,
1853                        negated: false,
1854                        subquery: ::std::boxed::Box::new(sub),
1855                    },
1856                )
1857            }
1858
1859            #[doc = #count_doc]
1860            pub async fn #count_ident(
1861                &self,
1862                pool: &#root::sql::Pool,
1863            ) -> ::core::result::Result<
1864                i64,
1865                #root::sql::ExecError,
1866            > {
1867                use #root::sql::CounterPool as _;
1868                self.#method_ident().count_pool(pool).await
1869            }
1870
1871            /// Eloquent `$model->relation->get()` for the
1872            /// through-relation — bare-name hot-path over
1873            /// `self.<name>_through().fetch_pool(&pool)`. Use this
1874            /// when you don't need further `.filter()` /
1875            /// `.order_by()` composition; falls back to the
1876            /// chainable accessor when you do. Avoids the `_pool`
1877            /// suffix on the most common call-site shape.
1878            pub async fn #fetch_ident(
1879                &self,
1880                pool: &#root::sql::Pool,
1881            ) -> ::core::result::Result<
1882                ::std::vec::Vec<#far>,
1883                #root::sql::ExecError,
1884            > {
1885                use #root::sql::FetcherPool as _;
1886                self.#method_ident().fetch_pool(pool).await
1887            }
1888
1889            /// Eloquent `hasOneThrough` analog — bare-name shortcut
1890            /// over `self.<name>_through().first(&pool)`. Returns
1891            /// `None` when no far rows are reachable through the
1892            /// intermediate. Useful when at most one row is
1893            /// expected (latest comment by country, primary tag,
1894            /// etc.).
1895            pub async fn #first_ident(
1896                &self,
1897                pool: &#root::sql::Pool,
1898            ) -> ::core::result::Result<
1899                ::core::option::Option<#far>,
1900                #root::sql::ExecError,
1901            > {
1902                self.#method_ident().first(pool).await
1903            }
1904
1905            /// Pluck a single column from the far rows into `Vec<U>`
1906            /// — cheaper than the typed `<Far>` decode when only one
1907            /// scalar column is needed.
1908            pub async fn #pluck_ident<U>(
1909                &self,
1910                col: &'static str,
1911                pool: &#root::sql::Pool,
1912            ) -> ::core::result::Result<
1913                ::std::vec::Vec<U>,
1914                #root::sql::ExecError,
1915            >
1916            where
1917                U: #root::sql::MaybePgScalar
1918                    + #root::sql::MaybeMyScalar
1919                    + #root::sql::MaybeSqliteScalar
1920                    + ::core::marker::Send
1921                    + ::core::marker::Unpin,
1922            {
1923                self.#method_ident()
1924                    .values_list_flat(col)
1925                    .fetch::<U>(pool)
1926                    .await
1927            }
1928        }
1929    });
1930    quote! {
1931        impl #struct_name {
1932            #( #methods )*
1933        }
1934    }
1935}
1936
1937struct ColumnEntry {
1938    /// The struct field ident, used both for the inherent const name on
1939    /// the model and for the inner column type's name.
1940    ident: syn::Ident,
1941    /// The struct's field type, used as `Column::Value`.
1942    value_ty: Type,
1943    /// Rust-side field name (e.g. `"id"`).
1944    name: String,
1945    /// SQL-side column name (e.g. `"user_id"`).
1946    column: String,
1947    /// `#root::core::FieldType::I64` etc.
1948    field_type_tokens: TokenStream2,
1949}
1950
1951struct CollectedFields {
1952    field_schemas: Vec<TokenStream2>,
1953    from_row_inits: Vec<TokenStream2>,
1954    /// Aliased counterparts of `from_row_inits` — read columns via
1955    /// `format!("{prefix}__{col}")` aliases so a Model can be
1956    /// decoded from a JOINed row's projected target columns.
1957    from_aliased_row_inits: Vec<TokenStream2>,
1958    /// Static column-name list — used by the simple insert path
1959    /// (no `Auto<T>` fields). Aligned with `insert_values`.
1960    insert_columns: Vec<TokenStream2>,
1961    /// Static `Into<SqlValue>` expressions, one per field. Aligned
1962    /// with `insert_columns`. Used by the simple insert path only.
1963    insert_values: Vec<TokenStream2>,
1964    /// Per-field push expressions for the dynamic (Auto-aware)
1965    /// insert path. Each statement either unconditionally pushes
1966    /// `(column, value)` or, for an `Auto<T>` field, conditionally
1967    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1968    insert_pushes: Vec<TokenStream2>,
1969    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1970    /// when `has_auto == false`.
1971    returning_cols: Vec<TokenStream2>,
1972    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1973    /// field. Run after `insert_returning` to populate the model.
1974    auto_assigns: Vec<TokenStream2>,
1975    /// `(ident, column_literal)` pairs for every Auto field. Used by
1976    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1977    /// instead of `self`.
1978    auto_field_idents: Vec<(syn::Ident, String)>,
1979    /// Inner `T` of the first `Auto<T>` field, for the MySQL
1980    /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1981    first_auto_value_ty: Option<Type>,
1982    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
1983    /// by the all-Auto-Unset bulk path (Auto cols dropped from
1984    /// `columns`).
1985    bulk_pushes_no_auto: Vec<TokenStream2>,
1986    /// Bulk-insert per-row pushes for **all fields including Auto**.
1987    /// Used by the all-Auto-Set bulk path (Auto col included with the
1988    /// caller-supplied value).
1989    bulk_pushes_all: Vec<TokenStream2>,
1990    /// Column-name literals for non-Auto fields only (paired with
1991    /// `bulk_pushes_no_auto`).
1992    bulk_columns_no_auto: Vec<TokenStream2>,
1993    /// Column-name literals for every field including Auto (paired
1994    /// with `bulk_pushes_all`).
1995    bulk_columns_all: Vec<TokenStream2>,
1996    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
1997    /// + the loop that asserts every row matches. One pair per Auto
1998    /// field. Empty when `has_auto == false`.
1999    bulk_auto_uniformity: Vec<TokenStream2>,
2000    /// Identifier of the first Auto field, used as the witness for
2001    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2002    first_auto_ident: Option<syn::Ident>,
2003    /// `true` if any field on the struct is `Auto<T>`.
2004    has_auto: bool,
2005    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2006    /// Gates `save()` codegen — only Auto PKs let us infer
2007    /// insert-vs-update from the in-memory value.
2008    pk_is_auto: bool,
2009    /// `Assignment` constructors for every non-PK column. Drives the
2010    /// UPDATE branch of `save()`.
2011    update_assignments: Vec<TokenStream2>,
2012    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2013    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2014    upsert_update_columns: Vec<TokenStream2>,
2015    primary_key: Option<(syn::Ident, String)>,
2016    column_entries: Vec<ColumnEntry>,
2017    /// Rust-side field names, in declaration order. Used to validate
2018    /// container attributes like `display = "…"`.
2019    field_names: Vec<String>,
2020    /// FK fields on this child model. Drives the reverse-relation
2021    /// helper emit — for each FK, the macro adds an inherent
2022    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2023    /// method on the parent type.
2024    fk_relations: Vec<FkRelation>,
2025    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2026    /// the model has one. Drives emission of the `soft_delete_on` /
2027    /// `restore_on` inherent methods. At most one such column per
2028    /// model is allowed; collect_fields rejects duplicates.
2029    soft_delete_column: Option<String>,
2030    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2031    /// companion to `soft_delete_column` for emitting predicates
2032    /// that need to read the field off `&self` (e.g. `trashed()`).
2033    soft_delete_field_ident: Option<syn::Ident>,
2034}
2035
2036#[derive(Clone)]
2037struct FkRelation {
2038    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2039    /// helper is emitted as `impl <ParentType> { … }`.
2040    parent_type: Type,
2041    /// SQL column name on the child table for this FK (e.g. `"author"`).
2042    /// Used in the generated `WHERE <fk_column> = $1` clause.
2043    fk_column: String,
2044    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2045    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2046    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2047    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2048    pk_kind: DetectedKind,
2049    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2050    /// FK column). Drives the `Some(...)` wrapping in load_related
2051    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2052    /// the codegen matches the field's declared shape.
2053    nullable: bool,
2054    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2055    /// override. When set, the reverse helper picks this name instead
2056    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2057    /// #816 (issue's "Related" note re: per-FK override).
2058    related_name: Option<String>,
2059}
2060
2061fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2062    let root = rustango_root();
2063    let cap = named.named.len();
2064    let mut out = CollectedFields {
2065        field_schemas: Vec::with_capacity(cap),
2066        from_row_inits: Vec::with_capacity(cap),
2067        from_aliased_row_inits: Vec::with_capacity(cap),
2068        insert_columns: Vec::with_capacity(cap),
2069        insert_values: Vec::with_capacity(cap),
2070        insert_pushes: Vec::with_capacity(cap),
2071        returning_cols: Vec::new(),
2072        auto_assigns: Vec::new(),
2073        auto_field_idents: Vec::new(),
2074        first_auto_value_ty: None,
2075        bulk_pushes_no_auto: Vec::with_capacity(cap),
2076        bulk_pushes_all: Vec::with_capacity(cap),
2077        bulk_columns_no_auto: Vec::with_capacity(cap),
2078        bulk_columns_all: Vec::with_capacity(cap),
2079        bulk_auto_uniformity: Vec::new(),
2080        first_auto_ident: None,
2081        has_auto: false,
2082        pk_is_auto: false,
2083        update_assignments: Vec::with_capacity(cap),
2084        upsert_update_columns: Vec::with_capacity(cap),
2085        primary_key: None,
2086        column_entries: Vec::with_capacity(cap),
2087        field_names: Vec::with_capacity(cap),
2088        fk_relations: Vec::new(),
2089        soft_delete_column: None,
2090        soft_delete_field_ident: None,
2091    };
2092
2093    for field in &named.named {
2094        let info = process_field(field, table)?;
2095        out.field_names.push(info.ident.to_string());
2096        out.field_schemas.push(info.schema);
2097        out.from_row_inits.push(info.from_row_init);
2098        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2099        if let Some(parent_ty) = info.fk_inner.clone() {
2100            out.fk_relations.push(FkRelation {
2101                parent_type: parent_ty,
2102                fk_column: info.column.clone(),
2103                pk_kind: info.fk_pk_kind,
2104                nullable: info.nullable,
2105                related_name: info.related_name.clone(),
2106            });
2107        }
2108        if info.soft_delete {
2109            if out.soft_delete_column.is_some() {
2110                return Err(syn::Error::new_spanned(
2111                    field,
2112                    "only one field may be marked `#[rustango(soft_delete)]`",
2113                ));
2114            }
2115            out.soft_delete_column = Some(info.column.clone());
2116            out.soft_delete_field_ident = Some(info.ident.clone());
2117        }
2118        let column = info.column.as_str();
2119        let ident = info.ident;
2120        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2121        // skip every write path — Postgres recomputes the value
2122        // from EXPR. Push only the column-entry record (so typed
2123        // column constants still exist for filtering / projection)
2124        // and the schema literal (already pushed above) and move
2125        // on. No insert_columns/values, no insert_pushes, no
2126        // bulk_*, no update_assignments, no upsert_update_columns,
2127        // no returning_cols.
2128        if info.generated_as.is_some() {
2129            out.column_entries.push(ColumnEntry {
2130                ident: ident.clone(),
2131                value_ty: info.value_ty.clone(),
2132                name: ident.to_string(),
2133                column: info.column.clone(),
2134                field_type_tokens: info.field_type_tokens,
2135            });
2136            continue;
2137        }
2138        out.insert_columns.push(quote!(#column));
2139        out.insert_values.push(quote! {
2140            ::core::convert::Into::<#root::core::SqlValue>::into(
2141                ::core::clone::Clone::clone(&self.#ident)
2142            )
2143        });
2144        if info.auto {
2145            out.has_auto = true;
2146            if out.first_auto_ident.is_none() {
2147                out.first_auto_ident = Some(ident.clone());
2148                out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2149            }
2150            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2151            // before binding, so the value is already in
2152            // `self.#ident` after the insert_push — RETURNING is
2153            // redundant. Skip adding this column to returning_cols /
2154            // auto_assigns / auto_field_idents to avoid (a) an
2155            // unnecessary RETURNING column on every dialect, and (b)
2156            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2157            // integer PK.
2158            if !info.default_uuid_v7 {
2159                out.returning_cols.push(quote!(#column));
2160                out.auto_field_idents
2161                    .push((ident.clone(), info.column.clone()));
2162                out.auto_assigns.push(quote! {
2163                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2164                });
2165            }
2166            if info.default_uuid_v7 {
2167                // Rust-side UUIDv7 generation (issue #823, Eloquent
2168                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2169                // and bind; Auto::Set → bind the user's value. The
2170                // column is ALWAYS present in the INSERT statement —
2171                // no RETURNING / no DB DEFAULT needed.
2172                out.insert_pushes.push(quote! {
2173                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2174                        self.#ident = #root::sql::Auto::Set(
2175                            #root::__uuid::Uuid::now_v7(),
2176                        );
2177                    }
2178                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2179                        _columns.push(#column);
2180                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2181                            ::core::clone::Clone::clone(_v)
2182                        ));
2183                    }
2184                });
2185            } else {
2186                out.insert_pushes.push(quote! {
2187                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2188                        _columns.push(#column);
2189                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2190                            ::core::clone::Clone::clone(_v)
2191                        ));
2192                    }
2193                });
2194            }
2195            // Bulk: Auto fields appear only in the all-Set path,
2196            // never in the Unset path (we drop them from `columns`).
2197            out.bulk_columns_all.push(quote!(#column));
2198            out.bulk_pushes_all.push(quote! {
2199                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2200                    ::core::clone::Clone::clone(&_row.#ident)
2201                ));
2202            });
2203            // Uniformity check: every row's Auto state must match the
2204            // first row's. Mixed Set/Unset within one bulk_insert is
2205            // rejected here so the column list stays consistent.
2206            let ident_clone = ident.clone();
2207            out.bulk_auto_uniformity.push(quote! {
2208                for _r in rows.iter().skip(1) {
2209                    if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2210                        return ::core::result::Result::Err(
2211                            #root::sql::ExecError::Sql(
2212                                #root::sql::SqlError::BulkAutoMixed
2213                            )
2214                        );
2215                    }
2216                }
2217            });
2218        } else {
2219            out.insert_pushes.push(quote! {
2220                _columns.push(#column);
2221                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2222                    ::core::clone::Clone::clone(&self.#ident)
2223                ));
2224            });
2225            // Bulk: non-Auto fields appear in BOTH paths.
2226            out.bulk_columns_no_auto.push(quote!(#column));
2227            out.bulk_columns_all.push(quote!(#column));
2228            let push_expr = quote! {
2229                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2230                    ::core::clone::Clone::clone(&_row.#ident)
2231                ));
2232            };
2233            out.bulk_pushes_no_auto.push(push_expr.clone());
2234            out.bulk_pushes_all.push(push_expr);
2235        }
2236        if info.primary_key {
2237            if out.primary_key.is_some() {
2238                return Err(syn::Error::new_spanned(
2239                    field,
2240                    "only one field may be marked `#[rustango(primary_key)]`",
2241                ));
2242            }
2243            out.primary_key = Some((ident.clone(), info.column.clone()));
2244            if info.auto {
2245                out.pk_is_auto = true;
2246            }
2247        } else if info.auto_now_add {
2248            // Immutable post-insert: skip from UPDATE entirely.
2249        } else if info.auto_now {
2250            // `auto_now` columns: bind `chrono::Utc::now()` on every
2251            // UPDATE so the column is always overridden with the
2252            // wall-clock at write time, regardless of what value the
2253            // user left in the struct field.
2254            out.update_assignments.push(quote! {
2255                #root::core::Assignment {
2256                    column: #column,
2257                    value: ::core::convert::Into::<#root::core::Expr>::into(
2258                        ::core::convert::Into::<#root::core::SqlValue>::into(
2259                            #root::__chrono::Utc::now()
2260                        )
2261                    ),
2262                }
2263            });
2264            out.upsert_update_columns.push(quote!(#column));
2265        } else {
2266            out.update_assignments.push(quote! {
2267                #root::core::Assignment {
2268                    column: #column,
2269                    value: ::core::convert::Into::<#root::core::Expr>::into(
2270                        ::core::convert::Into::<#root::core::SqlValue>::into(
2271                            ::core::clone::Clone::clone(&self.#ident)
2272                        )
2273                    ),
2274                }
2275            });
2276            out.upsert_update_columns.push(quote!(#column));
2277        }
2278        out.column_entries.push(ColumnEntry {
2279            ident: ident.clone(),
2280            value_ty: info.value_ty.clone(),
2281            name: ident.to_string(),
2282            column: info.column.clone(),
2283            field_type_tokens: info.field_type_tokens,
2284        });
2285    }
2286    Ok(out)
2287}
2288
2289fn model_impl_tokens(
2290    struct_name: &syn::Ident,
2291    model_name: &str,
2292    table: &str,
2293    display: Option<&str>,
2294    app_label: Option<&str>,
2295    admin: Option<&AdminAttrs>,
2296    default_order: &[(String, bool, proc_macro2::Span)],
2297    field_schemas: &[TokenStream2],
2298    soft_delete_column: Option<&str>,
2299    permissions: bool,
2300    audit_track: Option<&[String]>,
2301    m2m_relations: &[M2MAttr],
2302    indexes: &[IndexAttr],
2303    checks: &[CheckAttr],
2304    excludes: &[ExcludeAttr],
2305    composite_fks: &[CompositeFkAttr],
2306    generic_fks: &[GenericFkAttr],
2307    scope: Option<&str>,
2308    is_view: bool,
2309    verbose_name: Option<&str>,
2310    verbose_name_plural: Option<&str>,
2311    managed: bool,
2312    base_manager_name: Option<&str>,
2313    order_with_respect_to: Option<&str>,
2314    proxy: bool,
2315    required_db_features: &[String],
2316    required_db_vendor: Option<&str>,
2317    default_related_name: Option<&str>,
2318    db_table_comment: Option<&str>,
2319    get_latest_by: Option<(&str, bool)>,
2320    extra_permissions: &[(String, String)],
2321    default_permissions: &[String],
2322    global_scopes: &[GlobalScopeAttr],
2323    reverse_has_relations: &[ReverseHasAttr],
2324    generic_has_relations: &[GenericHasAttr],
2325) -> TokenStream2 {
2326    let root = rustango_root();
2327    let display_tokens = if let Some(name) = display {
2328        quote!(::core::option::Option::Some(#name))
2329    } else {
2330        quote!(::core::option::Option::None)
2331    };
2332    let app_label_tokens = if let Some(name) = app_label {
2333        quote!(::core::option::Option::Some(#name))
2334    } else {
2335        quote!(::core::option::Option::None)
2336    };
2337    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2338        quote!(::core::option::Option::Some(#col))
2339    } else {
2340        quote!(::core::option::Option::None)
2341    };
2342    let audit_track_tokens = match audit_track {
2343        None => quote!(::core::option::Option::None),
2344        Some(names) => {
2345            let lits = names.iter().map(|n| n.as_str());
2346            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2347        }
2348    };
2349    let admin_tokens = admin_config_tokens(admin);
2350    // Default `tenant` so single-tenant projects (no `scope` attr
2351    // anywhere) keep the v0.24.x behavior. Container-attr parser
2352    // already validated the value is "registry" or "tenant".
2353    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2354        Some("registry") => quote!(#root::core::ModelScope::Registry),
2355        _ => quote!(#root::core::ModelScope::Tenant),
2356    };
2357    let verbose_name_tokens = optional_str(verbose_name);
2358    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2359    let base_manager_name_tokens = optional_str(base_manager_name);
2360    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2361    let required_db_features_lits: Vec<&str> =
2362        required_db_features.iter().map(String::as_str).collect();
2363    let required_db_vendor_tokens = optional_str(required_db_vendor);
2364    let default_related_name_tokens = optional_str(default_related_name);
2365    let db_table_comment_tokens = optional_str(db_table_comment);
2366    let get_latest_by_tokens = match get_latest_by {
2367        Some((col, desc)) => {
2368            quote!(::core::option::Option::Some((#col, #desc)))
2369        }
2370        None => quote!(::core::option::Option::None),
2371    };
2372    let extra_permission_tokens: Vec<_> = extra_permissions
2373        .iter()
2374        .map(|(c, l)| quote!((#c, #l)))
2375        .collect();
2376    let default_permission_tokens: Vec<_> = default_permissions
2377        .iter()
2378        .map(|action| quote!(#action))
2379        .collect();
2380    let indexes_tokens = indexes.iter().map(|idx| {
2381        let name = idx.name.as_deref().unwrap_or("unnamed_index");
2382        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2383        let unique = idx.unique;
2384        // Map the parsed method string onto the IndexMethod enum
2385        // variant — kept at the codegen layer so the IR doesn't
2386        // carry the string form.
2387        let method_variant = match idx.method.as_str() {
2388            "gin" => quote!(#root::core::IndexMethod::Gin),
2389            "gist" => quote!(#root::core::IndexMethod::Gist),
2390            "brin" => quote!(#root::core::IndexMethod::Brin),
2391            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2392            "hash" => quote!(#root::core::IndexMethod::Hash),
2393            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2394            _ => quote!(#root::core::IndexMethod::BTree),
2395        };
2396        let where_clause = match &idx.where_clause {
2397            Some(s) => quote!(::core::option::Option::Some(#s)),
2398            None => quote!(::core::option::Option::None),
2399        };
2400        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2401        quote! {
2402            #root::core::IndexSchema {
2403                name: #name,
2404                columns: &[ #(#cols),* ],
2405                unique: #unique,
2406                method: #method_variant,
2407                where_clause: #where_clause,
2408                include: &[ #(#include_lits),* ],
2409            }
2410        }
2411    });
2412    let checks_tokens = checks.iter().map(|c| {
2413        let name = c.name.as_str();
2414        let expr = c.expr.as_str();
2415        quote! {
2416            #root::core::CheckConstraint {
2417                name: #name,
2418                expr: #expr,
2419            }
2420        }
2421    });
2422    let excludes_tokens = excludes.iter().map(|e| {
2423        let name = e.name.as_str();
2424        let using = e.using.as_str();
2425        let element_tokens = e.elements.iter().map(|(col, op)| {
2426            let col_s = col.as_str();
2427            let op_s = op.as_str();
2428            quote!((#col_s, #op_s))
2429        });
2430        let where_tokens = match e.where_clause.as_deref() {
2431            Some(w) => quote!(::core::option::Option::Some(#w)),
2432            None => quote!(::core::option::Option::None),
2433        };
2434        quote! {
2435            #root::core::ExclusionConstraint {
2436                name: #name,
2437                using: #using,
2438                elements: &[ #(#element_tokens),* ],
2439                where_clause: #where_tokens,
2440            }
2441        }
2442    });
2443    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2444        let name = rel.name.as_str();
2445        let to = rel.to.as_str();
2446        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2447        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2448        quote! {
2449            #root::core::CompositeFkRelation {
2450                name: #name,
2451                to: #to,
2452                from: &[ #(#from_cols),* ],
2453                on: &[ #(#on_cols),* ],
2454            }
2455        }
2456    });
2457    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2458        let name = rel.name.as_str();
2459        let ct_col = rel.ct_column.as_str();
2460        let pk_col = rel.pk_column.as_str();
2461        quote! {
2462            #root::core::GenericRelation {
2463                name: #name,
2464                ct_column: #ct_col,
2465                pk_column: #pk_col,
2466            }
2467        }
2468    });
2469    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2470    // no `#[rustango(default_order = "...")]` attribute was supplied.
2471    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2472        let col_lit = col.as_str();
2473        quote! { (#col_lit, #desc) }
2474    });
2475
2476    // Issue #820 — `global_scopes` slice literal. Empty when no
2477    // `#[rustango(global_scope(...))]` attribute was supplied. The
2478    // `apply` path is re-emitted verbatim so it resolves in the
2479    // consumer's scope; the name is stored as a string literal.
2480    let global_scope_tokens = global_scopes.iter().map(|s| {
2481        let name = s.name.as_str();
2482        let apply = &s.apply;
2483        quote! {
2484            #root::core::GlobalScope {
2485                name: #name,
2486                apply: #apply,
2487            }
2488        }
2489    });
2490
2491    let m2m_tokens = m2m_relations.iter().map(|rel| {
2492        let name = rel.name.as_str();
2493        let to = rel.to.as_str();
2494        let through = rel.through.as_str();
2495        let src = rel.src.as_str();
2496        let dst = rel.dst.as_str();
2497        let auto_create = rel.auto_create;
2498        quote! {
2499            #root::core::M2MRelation {
2500                name: #name,
2501                to: #to,
2502                through: #through,
2503                src_col: #src,
2504                dst_col: #dst,
2505                auto_create: #auto_create,
2506            }
2507        }
2508    });
2509    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2510    // when the model declares `#[rustango(reverse_has(...))]`. Each
2511    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2512    // a const expression. Models without reverse_has fall through to
2513    // the trait's empty default — no override emitted.
2514    let reverse_relations_override = if reverse_has_relations.is_empty() {
2515        quote!()
2516    } else {
2517        let entries = reverse_has_relations.iter().map(|rel| {
2518            let name = rel.name.as_str();
2519            let child = &rel.child;
2520            let child_fk_column = rel.child_fk_column.as_str();
2521            let self_pk_column = rel.self_pk_column.as_str();
2522            quote! {
2523                #root::core::ReverseRelation {
2524                    name: #name,
2525                    child_schema: <#child as #root::core::Model>::SCHEMA,
2526                    child_fk_column: #child_fk_column,
2527                    self_pk_column: #self_pk_column,
2528                }
2529            }
2530        });
2531        quote! {
2532            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2533                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2534                RELS
2535            }
2536        }
2537    };
2538    // Issue #830 — `Model::generic_reverse_relations()` override for
2539    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2540    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2541        quote!()
2542    } else {
2543        let entries = generic_has_relations.iter().map(|rel| {
2544            let name = rel.name.as_str();
2545            let child = &rel.child;
2546            let ct_column = rel.ct_column.as_str();
2547            let pk_column = rel.pk_column.as_str();
2548            let self_pk_column = rel.self_pk_column.as_str();
2549            quote! {
2550                #root::core::GenericReverseRelation {
2551                    name: #name,
2552                    child_schema: <#child as #root::core::Model>::SCHEMA,
2553                    ct_column: #ct_column,
2554                    pk_column: #pk_column,
2555                    self_pk_column: #self_pk_column,
2556                }
2557            }
2558        });
2559        quote! {
2560            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2561                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2562                RELS
2563            }
2564        }
2565    };
2566    quote! {
2567        impl #root::core::Model for #struct_name {
2568            const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2569                name: #model_name,
2570                table: #table,
2571                fields: &[ #(#field_schemas),* ],
2572                display: #display_tokens,
2573                app_label: #app_label_tokens,
2574                admin: #admin_tokens,
2575                soft_delete_column: #soft_delete_tokens,
2576                permissions: #permissions,
2577                audit_track: #audit_track_tokens,
2578                m2m: &[ #(#m2m_tokens),* ],
2579                indexes: &[ #(#indexes_tokens),* ],
2580                check_constraints: &[ #(#checks_tokens),* ],
2581                exclusion_constraints: &[ #(#excludes_tokens),* ],
2582                composite_relations: &[ #(#composite_fk_tokens),* ],
2583                generic_relations: &[ #(#generic_fk_tokens),* ],
2584                scope: #scope_tokens,
2585                default_order: &[ #(#default_order_tokens),* ],
2586                is_view: #is_view,
2587                verbose_name: #verbose_name_tokens,
2588                verbose_name_plural: #verbose_name_plural_tokens,
2589                managed: #managed,
2590                base_manager_name: #base_manager_name_tokens,
2591                order_with_respect_to: #order_with_respect_to_tokens,
2592                proxy: #proxy,
2593                required_db_features: &[ #(#required_db_features_lits),* ],
2594                required_db_vendor: #required_db_vendor_tokens,
2595                default_related_name: #default_related_name_tokens,
2596                db_table_comment: #db_table_comment_tokens,
2597                get_latest_by: #get_latest_by_tokens,
2598                extra_permissions: &[ #(#extra_permission_tokens),* ],
2599                default_permissions: &[ #(#default_permission_tokens),* ],
2600                global_scopes: &[ #(#global_scope_tokens),* ],
2601            };
2602
2603            #reverse_relations_override
2604            #generic_reverse_relations_override
2605        }
2606    }
2607}
2608
2609/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2610/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2611/// otherwise a static reference to a populated `AdminConfig`.
2612fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2613    let root = rustango_root();
2614    let Some(admin) = admin else {
2615        return quote!(::core::option::Option::None);
2616    };
2617
2618    let list_display = admin
2619        .list_display
2620        .as_ref()
2621        .map(|(v, _)| v.as_slice())
2622        .unwrap_or(&[]);
2623    let list_display_lits = list_display.iter().map(|s| s.as_str());
2624
2625    let search_fields = admin
2626        .search_fields
2627        .as_ref()
2628        .map(|(v, _)| v.as_slice())
2629        .unwrap_or(&[]);
2630    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2631
2632    let readonly_fields = admin
2633        .readonly_fields
2634        .as_ref()
2635        .map(|(v, _)| v.as_slice())
2636        .unwrap_or(&[]);
2637    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2638
2639    let list_filter = admin
2640        .list_filter
2641        .as_ref()
2642        .map(|(v, _)| v.as_slice())
2643        .unwrap_or(&[]);
2644    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2645
2646    let actions = admin
2647        .actions
2648        .as_ref()
2649        .map(|(v, _)| v.as_slice())
2650        .unwrap_or(&[]);
2651    let actions_lits = actions.iter().map(|s| s.as_str());
2652
2653    let fieldsets = admin
2654        .fieldsets
2655        .as_ref()
2656        .map(|(v, _)| v.as_slice())
2657        .unwrap_or(&[]);
2658    let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2659        let title = title.as_str();
2660        let field_lits = fields.iter().map(|s| s.as_str());
2661        quote!(#root::core::Fieldset {
2662            title: #title,
2663            fields: &[ #( #field_lits ),* ],
2664        })
2665    });
2666
2667    let list_display_links = admin
2668        .list_display_links
2669        .as_ref()
2670        .map(|(v, _)| v.as_slice())
2671        .unwrap_or(&[]);
2672    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2673
2674    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2675    let actions_on_top = admin.actions_on_top.unwrap_or(true);
2676    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2677    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2678
2679    let prepopulated = admin
2680        .prepopulated_fields
2681        .as_ref()
2682        .map(|(v, _)| v.as_slice())
2683        .unwrap_or(&[]);
2684    let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2685        let target = target.as_str();
2686        let source_lits = sources.iter().map(|s| s.as_str());
2687        quote!(#root::core::PrepopulatedField {
2688            target: #target,
2689            sources: &[ #( #source_lits ),* ],
2690        })
2691    });
2692
2693    let raw_id_fields = admin
2694        .raw_id_fields
2695        .as_ref()
2696        .map(|(v, _)| v.as_slice())
2697        .unwrap_or(&[]);
2698    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2699
2700    let autocomplete_fields = admin
2701        .autocomplete_fields
2702        .as_ref()
2703        .map(|(v, _)| v.as_slice())
2704        .unwrap_or(&[]);
2705    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2706
2707    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2708    let list_select_related_tokens = match admin.list_select_related.as_deref() {
2709        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2710        Some("none") => quote!(#root::core::ListSelectRelated::None),
2711        Some(raw) => {
2712            let names: Vec<&str> = raw
2713                .split(',')
2714                .map(str::trim)
2715                .filter(|s| !s.is_empty())
2716                .collect();
2717            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2718        }
2719    };
2720
2721    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2722    // a Vec<(String, String)>. Empty / unset → no overrides.
2723    let formfield_pairs: Vec<(&str, &str)> = admin
2724        .formfield_overrides
2725        .as_ref()
2726        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2727        .unwrap_or_default();
2728    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2729        let field = *field;
2730        let widget = *widget;
2731        quote!((#field, #widget))
2732    });
2733
2734    let list_per_page = admin.list_per_page.unwrap_or(0);
2735
2736    let ordering_pairs = admin
2737        .ordering
2738        .as_ref()
2739        .map(|(v, _)| v.as_slice())
2740        .unwrap_or(&[]);
2741    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2742        let name = name.as_str();
2743        let desc = *desc;
2744        quote!((#name, #desc))
2745    });
2746
2747    quote! {
2748        ::core::option::Option::Some(&#root::core::AdminConfig {
2749            list_display: &[ #( #list_display_lits ),* ],
2750            search_fields: &[ #( #search_fields_lits ),* ],
2751            list_per_page: #list_per_page,
2752            ordering: &[ #( #ordering_tokens ),* ],
2753            readonly_fields: &[ #( #readonly_fields_lits ),* ],
2754            list_filter: &[ #( #list_filter_lits ),* ],
2755            actions: &[ #( #actions_lits ),* ],
2756            fieldsets: &[ #( #fieldset_tokens ),* ],
2757            list_display_links: &[ #( #list_display_links_lits ),* ],
2758            search_help_text: #search_help_text,
2759            actions_on_top: #actions_on_top,
2760            actions_on_bottom: #actions_on_bottom,
2761            date_hierarchy: #date_hierarchy,
2762            prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2763            raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2764            autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2765            list_select_related: #list_select_related_tokens,
2766            formfield_overrides: &[ #( #formfield_tokens ),* ],
2767        })
2768    }
2769}
2770
2771fn inherent_impl_tokens(
2772    struct_name: &syn::Ident,
2773    fields: &CollectedFields,
2774    primary_key: Option<&(syn::Ident, String)>,
2775    column_consts: &TokenStream2,
2776    audited_fields: Option<&[&ColumnEntry]>,
2777    indexes: &[IndexAttr],
2778    manager_fns: &[syn::Ident],
2779) -> TokenStream2 {
2780    let root = rustango_root();
2781    // Audit-emit fragments threaded into write paths. Non-empty only
2782    // when the model carries `#[rustango(audit(...))]`. They reborrow
2783    // `_executor` (a `&mut PgConnection` for audited models — the
2784    // macro switches the signature below) so the data write and the
2785    // audit INSERT both run on the same caller-supplied connection.
2786    let executor_passes_to_data_write = if audited_fields.is_some() {
2787        quote!(&mut *_executor)
2788    } else {
2789        quote!(_executor)
2790    };
2791    let executor_param = if audited_fields.is_some() {
2792        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2793    } else {
2794        quote!(_executor: _E)
2795    };
2796    let executor_generics = if audited_fields.is_some() {
2797        quote!()
2798    } else {
2799        quote!(<'_c, _E>)
2800    };
2801    let executor_where = if audited_fields.is_some() {
2802        quote!()
2803    } else {
2804        quote! {
2805            where
2806                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2807        }
2808    };
2809    // For audited models the `_on` methods take `&mut PgConnection`, so
2810    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2811    // must acquire a connection first. Non-audited models keep the
2812    // direct delegation since `&PgPool` IS an Executor.
2813    let pool_to_save_on = if audited_fields.is_some() {
2814        quote! {
2815            let mut _conn = pool.acquire().await?;
2816            self.save_on(&mut *_conn).await
2817        }
2818    } else {
2819        quote!(self.save_on(pool).await)
2820    };
2821    let pool_to_insert_on = if audited_fields.is_some() {
2822        quote! {
2823            let mut _conn = pool.acquire().await?;
2824            self.insert_on(&mut *_conn).await
2825        }
2826    } else {
2827        quote!(self.insert_on(pool).await)
2828    };
2829    let pool_to_delete_on = if audited_fields.is_some() {
2830        quote! {
2831            let mut _conn = pool.acquire().await?;
2832            self.delete_on(&mut *_conn).await
2833        }
2834    } else {
2835        quote!(self.delete_on(pool).await)
2836    };
2837    let pool_to_bulk_insert_on = if audited_fields.is_some() {
2838        quote! {
2839            let mut _conn = pool.acquire().await?;
2840            Self::bulk_insert_on(rows, &mut *_conn).await
2841        }
2842    } else {
2843        quote!(Self::bulk_insert_on(rows, pool).await)
2844    };
2845    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2846    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2847    // directly, but `upsert_on` for audited models takes
2848    // `&mut PgConnection` (the audit emit needs a real connection).
2849    // Add the missing acquire shim to keep audited Auto-PK upsert
2850    // compiling.
2851    let pool_to_upsert_on = if audited_fields.is_some() {
2852        quote! {
2853            let mut _conn = pool.acquire().await?;
2854            self.upsert_on(&mut *_conn).await
2855        }
2856    } else {
2857        quote!(self.upsert_on(pool).await)
2858    };
2859
2860    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2861    // (audit-on-connection over &Pool needs a bi-dialect transaction
2862    // helper, deferred). Two body shapes:
2863    // - has_auto: build InsertQuery skipping Auto::Unset columns,
2864    //   request Auto cols in `returning`, dispatch via
2865    //   `insert_returning_pool`, then on the returned `PgRow` /
2866    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
2867    //   row OR drop the single i64 into the first Auto field on MySQL
2868    //   (multi-Auto models on MySQL error at runtime since
2869    //   `LAST_INSERT_ID()` only reports one)
2870    // - non-Auto: build InsertQuery with explicit columns/values and
2871    //   call `insert_pool` (no returning needed)
2872    // pool_insert_method body for the audited Auto-PK case is moved
2873    // to after audit_pair_tokens / audit_pk_to_string (they live
2874    // ~150 lines below). This block keeps the non-audited and
2875    // non-Auto branches in place — the audited Auto-PK arm is
2876    // computed below and merged via the dispatch helper variable.
2877    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2878        // Audited models with explicit (non-Auto) PKs go through
2879        // the non-Auto insert path below — the audit emit is one
2880        // round-trip after the INSERT inside the same tx via
2881        // audit::save_one_with_audit? No, INSERT semantics
2882        // differ. For non-Auto PK + audited, route through a
2883        // dedicated insert + audit emit on the same tx, but defer
2884        // the macro emission to the audit-bundle-aware block below
2885        // — this `quote!()` placeholder gets overwritten there.
2886        quote!()
2887    } else if audited_fields.is_some() && fields.has_auto {
2888        // Audited Auto-PK insert_pool — assembled after the audit
2889        // bundles. Placeholder; real emission below.
2890        quote!()
2891    } else if fields.has_auto {
2892        let pushes = &fields.insert_pushes;
2893        let returning_cols = &fields.returning_cols;
2894        // When every `Auto<T>` field is filled Rust-side
2895        // (`default_uuid_v7`, issue #823), there is no column to read
2896        // back from the database — `returning_cols` is empty. Route
2897        // through plain `insert_pool` instead of
2898        // `insert_returning_pool` to skip the redundant RETURNING /
2899        // LAST_INSERT_ID round-trip.
2900        if fields.returning_cols.is_empty() {
2901            quote! {
2902                /// Insert this row against either backend. Every
2903                /// `Auto<T>` PK on this model is filled Rust-side
2904                /// (e.g. `default_uuid_v7`) before binding, so no
2905                /// RETURNING round-trip is needed.
2906                ///
2907                /// # Errors
2908                /// As [`Self::insert`].
2909                pub async fn insert_pool(
2910                    &mut self,
2911                    pool: &#root::sql::Pool,
2912                ) -> ::core::result::Result<(), #root::sql::ExecError> {
2913                    let mut _columns: ::std::vec::Vec<&'static str> =
2914                        ::std::vec::Vec::new();
2915                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2916                        ::std::vec::Vec::new();
2917                    #( #pushes )*
2918                    let _query = #root::core::InsertQuery {
2919                        model: <Self as #root::core::Model>::SCHEMA,
2920                        columns: _columns,
2921                        values: _values,
2922                        returning: ::std::vec::Vec::new(),
2923                        on_conflict: ::core::option::Option::None,
2924                    };
2925                    #root::sql::insert_pool(pool, &_query).await
2926                }
2927
2928                /// Eloquent `Model::insertOrIgnore()` — same shape
2929                /// as the auto-PK branch above. Returns `Ok(true)`
2930                /// when inserted, `Ok(false)` when a conflict caused
2931                /// the INSERT to silently skip.
2932                ///
2933                /// # Errors
2934                /// As [`Self::insert`].
2935                pub async fn insert_or_ignore(
2936                    &mut self,
2937                    pool: &#root::sql::Pool,
2938                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
2939                    let mut _columns: ::std::vec::Vec<&'static str> =
2940                        ::std::vec::Vec::new();
2941                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2942                        ::std::vec::Vec::new();
2943                    #( #pushes )*
2944                    let _query = #root::core::InsertQuery {
2945                        model: <Self as #root::core::Model>::SCHEMA,
2946                        columns: _columns,
2947                        values: _values,
2948                        returning: ::std::vec::Vec::new(),
2949                        on_conflict: ::core::option::Option::Some(
2950                            #root::core::ConflictClause::DoNothing,
2951                        ),
2952                    };
2953                    let dialect = pool.dialect();
2954                    let stmt = dialect.compile_insert(&_query)?;
2955                    let rows = #root::sql::raw_execute_pool(
2956                        pool, &stmt.sql, stmt.params,
2957                    ).await?;
2958                    ::core::result::Result::Ok(rows > 0)
2959                }
2960            }
2961        } else {
2962            quote! {
2963                /// Insert this row against either backend, populating any
2964                /// `Auto<T>` PK from the auto-assigned value.
2965                ///
2966                /// # Errors
2967                /// As [`Self::insert`].
2968                pub async fn insert_pool(
2969                    &mut self,
2970                    pool: &#root::sql::Pool,
2971                ) -> ::core::result::Result<(), #root::sql::ExecError> {
2972                    let mut _columns: ::std::vec::Vec<&'static str> =
2973                        ::std::vec::Vec::new();
2974                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2975                        ::std::vec::Vec::new();
2976                    #( #pushes )*
2977                    let _query = #root::core::InsertQuery {
2978                        model: <Self as #root::core::Model>::SCHEMA,
2979                        columns: _columns,
2980                        values: _values,
2981                        returning: ::std::vec![ #( #returning_cols ),* ],
2982                        on_conflict: ::core::option::Option::None,
2983                    };
2984                    let _result = #root::sql::insert_returning_pool(
2985                        pool, &_query,
2986                    ).await?;
2987                    #root::sql::apply_auto_pk(_result, self)
2988                }
2989
2990                /// Eloquent `Model::insertOrIgnore()` — INSERT this
2991                /// row or silently skip on unique-constraint
2992                /// violation. Returns `Ok(true)` when a row was
2993                /// inserted, `Ok(false)` when a conflict caused the
2994                /// INSERT to silently skip.
2995                ///
2996                /// **Caveat on auto-PK models**: when the row is
2997                /// skipped (conflict), this instance's `Auto<T>`
2998                /// fields stay `Unset` — no PK is back-populated
2999                /// because the server didn't auto-assign one. For
3000                /// "insert then read back the PK or the existing
3001                /// row's PK", use the `upsert` family or
3002                /// `get_or_create`.
3003                ///
3004                /// # Errors
3005                /// As [`Self::insert`].
3006                pub async fn insert_or_ignore(
3007                    &mut self,
3008                    pool: &#root::sql::Pool,
3009                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3010                    let mut _columns: ::std::vec::Vec<&'static str> =
3011                        ::std::vec::Vec::new();
3012                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3013                        ::std::vec::Vec::new();
3014                    #( #pushes )*
3015                    let _query = #root::core::InsertQuery {
3016                        model: <Self as #root::core::Model>::SCHEMA,
3017                        columns: _columns,
3018                        values: _values,
3019                        returning: ::std::vec::Vec::new(),
3020                        on_conflict: ::core::option::Option::Some(
3021                            #root::core::ConflictClause::DoNothing,
3022                        ),
3023                    };
3024                    let dialect = pool.dialect();
3025                    let stmt = dialect.compile_insert(&_query)?;
3026                    let rows = #root::sql::raw_execute_pool(
3027                        pool, &stmt.sql, stmt.params,
3028                    ).await?;
3029                    ::core::result::Result::Ok(rows > 0)
3030                }
3031            }
3032        }
3033    } else {
3034        let insert_columns = &fields.insert_columns;
3035        let insert_values = &fields.insert_values;
3036        quote! {
3037            /// Insert this row into its table against either backend.
3038            /// Equivalent to [`Self::insert`] but takes
3039            /// [`#root::sql::Pool`].
3040            ///
3041            /// # Errors
3042            /// As [`Self::insert`].
3043            pub async fn insert_pool(
3044                &self,
3045                pool: &#root::sql::Pool,
3046            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3047                let _query = #root::core::InsertQuery {
3048                    model: <Self as #root::core::Model>::SCHEMA,
3049                    columns: ::std::vec![ #( #insert_columns ),* ],
3050                    values: ::std::vec![ #( #insert_values ),* ],
3051                    returning: ::std::vec::Vec::new(),
3052                    on_conflict: ::core::option::Option::None,
3053                };
3054                #root::sql::insert_pool(pool, &_query).await
3055            }
3056
3057            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3058            /// or silently skip on unique-constraint violation. Maps
3059            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3060            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3061            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3062            /// `INSERT IGNORE INTO …`.
3063            ///
3064            /// Returns `Ok(true)` when a row was inserted,
3065            /// `Ok(false)` when a conflict caused the INSERT to
3066            /// silently skip.
3067            ///
3068            /// Use for "create-if-absent" patterns where you don't
3069            /// need the row back. For "find-or-create with the row
3070            /// returned", use the queryset-level
3071            /// `crate::sql::get_or_create` free function.
3072            ///
3073            /// # Errors
3074            /// As [`Self::insert`], plus any dialect-specific
3075            /// translation error from the ConflictClause writer.
3076            pub async fn insert_or_ignore(
3077                &self,
3078                pool: &#root::sql::Pool,
3079            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3080                let _query = #root::core::InsertQuery {
3081                    model: <Self as #root::core::Model>::SCHEMA,
3082                    columns: ::std::vec![ #( #insert_columns ),* ],
3083                    values: ::std::vec![ #( #insert_values ),* ],
3084                    returning: ::std::vec::Vec::new(),
3085                    on_conflict: ::core::option::Option::Some(
3086                        #root::core::ConflictClause::DoNothing,
3087                    ),
3088                };
3089                let dialect = pool.dialect();
3090                let stmt = dialect.compile_insert(&_query)?;
3091                let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3092                ::core::result::Result::Ok(rows > 0)
3093            }
3094        }
3095    };
3096
3097    // pool_save_method moved to after audit_pair_tokens /
3098    // audit_pk_to_string (they live ~70 lines below) — needed for
3099    // the audited branch which builds an UpdateQuery + PendingEntry
3100    // and dispatches via audit::save_one_with_audit.
3101
3102    // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3103    // are computed (they live ~80 lines below).
3104
3105    // Build the (column, JSON value) pair list used by every
3106    // snapshot-style audit emission. Reused across delete_on,
3107    // soft_delete_on, restore_on, and (later) bulk paths. Empty
3108    // when the model isn't audited.
3109    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3110        .map(|tracked| {
3111            tracked
3112                .iter()
3113                .map(|c| {
3114                    let column_lit = c.column.as_str();
3115                    let ident = &c.ident;
3116                    quote! {
3117                        (
3118                            #column_lit,
3119                            #root::__serde_json::to_value(&self.#ident)
3120                                .unwrap_or(#root::__serde_json::Value::Null),
3121                        )
3122                    }
3123                })
3124                .collect()
3125        })
3126        .unwrap_or_default();
3127    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3128        if fields.pk_is_auto {
3129            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3130        } else {
3131            quote!(::std::format!("{}", &self.#pk_ident))
3132        }
3133    } else {
3134        quote!(::std::string::String::new())
3135    };
3136    let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3137        if audited_fields.is_some() {
3138            let pairs = audit_pair_tokens.iter();
3139            let pk_str = audit_pk_to_string.clone();
3140            quote! {
3141                let _audit_entry = #root::audit::PendingEntry {
3142                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3143                    entity_pk: #pk_str,
3144                    operation: #op_path,
3145                    source: #root::audit::current_source(),
3146                    changes: #root::audit::snapshot_changes(&[
3147                        #( #pairs ),*
3148                    ]),
3149                };
3150                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3151            }
3152        } else {
3153            quote!()
3154        }
3155    };
3156    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3157    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3158    let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3159    let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3160
3161    // `save_pool(&Pool)` — emitted for every model with a PK.
3162    // Audited Auto-PK models are deferred (the Auto::Unset →
3163    // insert_pool path needs the audited-insert flow from a future
3164    // batch). Three body shapes:
3165    // - non-audited, plain PK: build UpdateQuery + dispatch through
3166    //   sql::update_pool
3167    // - non-audited, Auto-PK: same, but Auto::Unset routes to
3168    //   self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3169    // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3170    //   through audit::save_one_with_audit (per-backend tx wraps
3171    //   UPDATE + audit emit atomically). Snapshot-style audit (post-
3172    //   write field values) — diff-style audit (with pre-UPDATE
3173    //   SELECT for `before` values) needs per-tracked-column codegen
3174    //   that doesn't fit the runtime-helper pattern; legacy &PgPool
3175    //   `save` keeps the diff for now.
3176    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3177        let pk_column_lit = pk_col.as_str();
3178        let assignments = &fields.update_assignments;
3179        if audited_fields.is_some() {
3180            if fields.pk_is_auto {
3181                // Auto-PK + audited: defer. The Auto::Unset insert
3182                // path needs a transactional INSERT + LAST_INSERT_ID
3183                // + audit emit flow — that's a follow-up batch.
3184                quote!()
3185            } else {
3186                let pairs = audit_pair_tokens.iter();
3187                let pairs2 = audit_pair_tokens.iter();
3188                let pk_str = audit_pk_to_string.clone();
3189                let pk_str2 = audit_pk_to_string.clone();
3190                quote! {
3191                    /// Save (UPDATE) this row against either backend
3192                    /// with audit emission inside the same transaction.
3193                    /// Bi-dialect counterpart of [`Self::save`] for
3194                    /// audited models with non-`Auto<T>` PKs.
3195                    ///
3196                    /// Captures **post-write** field state (snapshot
3197                    /// audit). The legacy &PgPool [`Self::save`]
3198                    /// captures BEFORE+AFTER for true diff audit;
3199                    /// porting that to the &Pool path needs runtime
3200                    /// per-tracked-column decoding and is deferred.
3201                    ///
3202                    /// # Errors
3203                    /// As [`Self::save`].
3204                    pub async fn save_pool(
3205                        &mut self,
3206                        pool: &#root::sql::Pool,
3207                    ) -> ::core::result::Result<(), #root::sql::ExecError> {
3208                        let _query = #root::core::UpdateQuery {
3209                            model: <Self as #root::core::Model>::SCHEMA,
3210                            set: ::std::vec![ #( #assignments ),* ],
3211                            where_clause: #root::core::WhereExpr::Predicate(
3212                                #root::core::Filter {
3213                                    column: #pk_column_lit,
3214                                    op: #root::core::Op::Eq,
3215                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3216                                        ::core::clone::Clone::clone(&self.#pk_ident)
3217                                    ),
3218                                }
3219                            ),
3220                        };
3221                        let _audit_entry = #root::audit::PendingEntry {
3222                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3223                            entity_pk: #pk_str,
3224                            operation: #root::audit::AuditOp::Update,
3225                            source: #root::audit::current_source(),
3226                            changes: #root::audit::snapshot_changes(&[
3227                                #( #pairs ),*
3228                            ]),
3229                        };
3230                        let _ = #root::audit::save_one_with_audit(
3231                            pool, &_query, &_audit_entry,
3232                        ).await?;
3233                        ::core::result::Result::Ok(())
3234                    }
3235
3236                    /// `save_pool` narrowed to a Rust-field allowlist — issue #66
3237                    /// (Django `Model.save(update_fields=[...])`).
3238                    /// Audit emission shrinks to the same column set so
3239                    /// the audit log reflects exactly what was written.
3240                    ///
3241                    /// # Errors
3242                    /// As [`Self::save_pool`], plus
3243                    /// [`#root::core::QueryError::UnknownField`] wrapped
3244                    /// in `ExecError::Query` for unknown field names.
3245                    pub async fn save_partial(
3246                        &mut self,
3247                        fields: &[&str],
3248                        pool: &#root::sql::Pool,
3249                    ) -> ::core::result::Result<(), #root::sql::ExecError> {
3250                        if fields.is_empty() {
3251                            #root::__tracing::warn!(
3252                                target: "rustango::save_partial",
3253                                model = <Self as #root::core::Model>::SCHEMA.name,
3254                                "save_partial called with empty field list — no-op"
3255                            );
3256                            return ::core::result::Result::Ok(());
3257                        }
3258                        let _schema = <Self as #root::core::Model>::SCHEMA;
3259                        let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3260                            ::std::collections::HashSet::with_capacity(fields.len());
3261                        for f in fields {
3262                            match _schema.field(f) {
3263                                ::core::option::Option::Some(fs) => {
3264                                    _wanted_cols.insert(fs.column);
3265                                }
3266                                ::core::option::Option::None => {
3267                                    return ::core::result::Result::Err(
3268                                        #root::sql::ExecError::Query(
3269                                            #root::core::QueryError::UnknownField {
3270                                                model: _schema.name,
3271                                                field: (*f).to_owned(),
3272                                            }
3273                                        )
3274                                    );
3275                                }
3276                            }
3277                        }
3278                        let _full: ::std::vec::Vec<#root::core::Assignment> =
3279                            ::std::vec![ #( #assignments ),* ];
3280                        let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3281                            .into_iter()
3282                            .filter(|a| _wanted_cols.contains(a.column))
3283                            .collect();
3284                        if _filtered.is_empty() {
3285                            #root::__tracing::warn!(
3286                                target: "rustango::save_partial",
3287                                model = _schema.name,
3288                                "save_partial: every named field maps to a non-assignable column — no-op"
3289                            );
3290                            return ::core::result::Result::Ok(());
3291                        }
3292                        let _query = #root::core::UpdateQuery {
3293                            model: _schema,
3294                            set: _filtered,
3295                            where_clause: #root::core::WhereExpr::Predicate(
3296                                #root::core::Filter {
3297                                    column: #pk_column_lit,
3298                                    op: #root::core::Op::Eq,
3299                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3300                                        ::core::clone::Clone::clone(&self.#pk_ident)
3301                                    ),
3302                                }
3303                            ),
3304                        };
3305                        // Narrow the audit snapshot to the same column set.
3306                        let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3307                            ::std::vec![ #( #pairs2 ),* ];
3308                        let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3309                            _all_pairs
3310                                .into_iter()
3311                                .filter(|(col, _)| _wanted_cols.contains(col))
3312                                .collect();
3313                        let _audit_entry = #root::audit::PendingEntry {
3314                            entity_table: _schema.table,
3315                            entity_pk: #pk_str2,
3316                            operation: #root::audit::AuditOp::Update,
3317                            source: #root::audit::current_source(),
3318                            changes: #root::audit::snapshot_changes(&_narrowed),
3319                        };
3320                        let _ = #root::audit::save_one_with_audit(
3321                            pool, &_query, &_audit_entry,
3322                        ).await?;
3323                        ::core::result::Result::Ok(())
3324                    }
3325
3326                    /// Typed-column counterpart of [`Self::save_partial`] —
3327                    /// issue #67. `fields` is a tuple of [`Column`]
3328                    /// constants whose `Model` matches `Self`; typos and
3329                    /// model mismatches surface at *compile time*
3330                    /// (`Author::name` inside a `Post::save_partial_typed`
3331                    /// call is a type error, no runtime check).
3332                    ///
3333                    /// ```ignore
3334                    /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3335                    /// ```
3336                    ///
3337                    /// Lowers to [`Self::save_partial`] under the hood;
3338                    /// audit narrowing + every other semantic is identical.
3339                    ///
3340                    /// [`Column`]: #root::core::Column
3341                    ///
3342                    /// # Errors
3343                    /// As [`Self::save_partial`].
3344                    pub async fn save_partial_typed<
3345                        L: #root::core::TypedFieldList<Self>,
3346                    >(
3347                        &mut self,
3348                        fields: L,
3349                        pool: &#root::sql::Pool,
3350                    ) -> ::core::result::Result<(), #root::sql::ExecError> {
3351                        let _names = fields.rust_field_names();
3352                        let _refs: ::std::vec::Vec<&str> =
3353                            _names.iter().copied().collect();
3354                        self.save_partial(&_refs, pool).await
3355                    }
3356                }
3357            }
3358        } else {
3359            let dispatch_unset = if fields.pk_is_auto {
3360                quote! {
3361                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3362                        return self.insert_pool(pool).await;
3363                    }
3364                }
3365            } else {
3366                quote!()
3367            };
3368            quote! {
3369                /// Save this row to its table against either backend.
3370                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3371                /// `UPDATE` keyed on the PK.
3372                ///
3373                /// # Errors
3374                /// As [`Self::save`].
3375                pub async fn save_pool(
3376                    &mut self,
3377                    pool: &#root::sql::Pool,
3378                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3379                    #dispatch_unset
3380                    let _query = #root::core::UpdateQuery {
3381                        model: <Self as #root::core::Model>::SCHEMA,
3382                        set: ::std::vec![ #( #assignments ),* ],
3383                        where_clause: #root::core::WhereExpr::Predicate(
3384                            #root::core::Filter {
3385                                column: #pk_column_lit,
3386                                op: #root::core::Op::Eq,
3387                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3388                                    ::core::clone::Clone::clone(&self.#pk_ident)
3389                                ),
3390                            }
3391                        ),
3392                    };
3393                    let _ = #root::sql::update_pool(pool, &_query).await?;
3394                    ::core::result::Result::Ok(())
3395                }
3396
3397                /// Save (UPDATE) only the listed Rust-side fields,
3398                /// leaving every other column untouched. Issue #66 —
3399                /// Django's `Model.save(update_fields=[...])` shape.
3400                ///
3401                /// `fields` are Rust-side struct field names; the macro
3402                /// resolves each to its SQL column. Unknown field
3403                /// names return [`#root::core::QueryError::UnknownField`]
3404                /// wrapped in `ExecError::Query`. An empty list is a
3405                /// no-op (returns `Ok(())` and logs a `tracing::warn!`),
3406                /// matching Django's "nothing to do" semantic.
3407                ///
3408                /// Use this when:
3409                /// * you only mutated a couple of fields on a wide row
3410                ///   (avoid re-writing every column on every save), or
3411                /// * two writers diverged after their initial read and
3412                ///   you want to preserve the other writer's changes to
3413                ///   columns you didn't touch.
3414                ///
3415                /// Auto-PK models with an unset PK return
3416                /// [`#root::core::QueryError::UnknownField`] with
3417                /// field name `<pk>` — `save_partial` is an
3418                /// UPDATE-only path. Call [`Self::insert_pool`]
3419                /// (or [`Self::save_pool`] which dispatches based on
3420                /// PK state) for the INSERT case.
3421                ///
3422                /// # Errors
3423                /// As [`Self::save_pool`], plus `UnknownField` for
3424                /// unknown / empty / Auto-Unset cases.
3425                pub async fn save_partial(
3426                    &mut self,
3427                    fields: &[&str],
3428                    pool: &#root::sql::Pool,
3429                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3430                    if fields.is_empty() {
3431                        #root::__tracing::warn!(
3432                            target: "rustango::save_partial",
3433                            model = <Self as #root::core::Model>::SCHEMA.name,
3434                            "save_partial called with empty field list — no-op"
3435                        );
3436                        return ::core::result::Result::Ok(());
3437                    }
3438                    let _schema = <Self as #root::core::Model>::SCHEMA;
3439                    // Validate field names against the schema.
3440                    let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3441                        ::std::collections::HashSet::with_capacity(fields.len());
3442                    for f in fields {
3443                        match _schema.field(f) {
3444                            ::core::option::Option::Some(fs) => {
3445                                _wanted_cols.insert(fs.column);
3446                            }
3447                            ::core::option::Option::None => {
3448                                return ::core::result::Result::Err(
3449                                    #root::sql::ExecError::Query(
3450                                        #root::core::QueryError::UnknownField {
3451                                            model: _schema.name,
3452                                            field: (*f).to_owned(),
3453                                        }
3454                                    )
3455                                );
3456                            }
3457                        }
3458                    }
3459                    // Build the full assignment vec, then keep only the
3460                    // assignments whose column is in `_wanted_cols`.
3461                    let _full: ::std::vec::Vec<#root::core::Assignment> =
3462                        ::std::vec![ #( #assignments ),* ];
3463                    let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3464                        .into_iter()
3465                        .filter(|a| _wanted_cols.contains(a.column))
3466                        .collect();
3467                    if _filtered.is_empty() {
3468                        // All field names valid, but they all map to
3469                        // non-assignable slots (PK column, computed/
3470                        // virtual fields, relations without an
3471                        // assignment). Same no-op semantic as Django.
3472                        #root::__tracing::warn!(
3473                            target: "rustango::save_partial",
3474                            model = _schema.name,
3475                            "save_partial: every named field maps to a non-assignable column — no-op"
3476                        );
3477                        return ::core::result::Result::Ok(());
3478                    }
3479                    let _query = #root::core::UpdateQuery {
3480                        model: _schema,
3481                        set: _filtered,
3482                        where_clause: #root::core::WhereExpr::Predicate(
3483                            #root::core::Filter {
3484                                column: #pk_column_lit,
3485                                op: #root::core::Op::Eq,
3486                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3487                                    ::core::clone::Clone::clone(&self.#pk_ident)
3488                                ),
3489                            }
3490                        ),
3491                    };
3492                    let _ = #root::sql::update_pool(pool, &_query).await?;
3493                    ::core::result::Result::Ok(())
3494                }
3495
3496                /// Typed-column counterpart of [`Self::save_partial`] —
3497                /// issue #67. `fields` is a tuple of [`Column`]
3498                /// constants whose `Model` matches `Self`; typos and
3499                /// model mismatches surface at *compile time*
3500                /// (`Author::name` inside a `Post::save_partial_typed`
3501                /// call is a type error, no runtime check).
3502                ///
3503                /// ```ignore
3504                /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3505                /// ```
3506                ///
3507                /// Lowers to [`Self::save_partial`] under the hood — the
3508                /// tuple is reduced to a `&[&str]` slice of Rust-side
3509                /// field names and forwarded.
3510                ///
3511                /// [`Column`]: #root::core::Column
3512                ///
3513                /// # Errors
3514                /// As [`Self::save_partial`].
3515                pub async fn save_partial_typed<
3516                    L: #root::core::TypedFieldList<Self>,
3517                >(
3518                    &mut self,
3519                    fields: L,
3520                    pool: &#root::sql::Pool,
3521                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3522                    let _names = fields.rust_field_names();
3523                    let _refs: ::std::vec::Vec<&str> =
3524                        _names.iter().copied().collect();
3525                    self.save_partial(&_refs, pool).await
3526                }
3527            }
3528        }
3529    } else {
3530        quote!()
3531    };
3532
3533    // Audited `insert_pool` (overrides the placeholder set higher up
3534    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3535    // audited models get insert_pool routing through
3536    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3537    // + auto-PK readback + audit emit). Snapshot-style audit (the
3538    // PendingEntry's `changes` carries post-write field values).
3539    let pool_insert_method = if audited_fields.is_some() {
3540        if let Some(_) = primary_key {
3541            let pushes = if fields.has_auto {
3542                fields.insert_pushes.clone()
3543            } else {
3544                // For non-Auto-PK models, the macro normally builds
3545                // {columns, values} from fields.insert_columns +
3546                // fields.insert_values rather than insert_pushes.
3547                // Map those into the pushes shape.
3548                fields
3549                    .insert_columns
3550                    .iter()
3551                    .zip(&fields.insert_values)
3552                    .map(|(col, val)| {
3553                        quote! {
3554                            _columns.push(#col);
3555                            _values.push(#val);
3556                        }
3557                    })
3558                    .collect()
3559            };
3560            let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3561                fields.returning_cols.clone()
3562            } else {
3563                // Non-Auto-PK: still need RETURNING something for the
3564                // audit helper's contract (it errors on empty
3565                // returning). Return the PK column so the audit row
3566                // can carry the assigned PK back. Some non-Auto PKs
3567                // are server-side-default (e.g. UUIDv4 default), so
3568                // RETURNING is genuinely useful.
3569                primary_key
3570                    .map(|(_, col)| {
3571                        let lit = col.as_str();
3572                        vec![quote!(#lit)]
3573                    })
3574                    .unwrap_or_default()
3575            };
3576            let pairs = audit_pair_tokens.iter();
3577            let pk_str = audit_pk_to_string.clone();
3578            quote! {
3579                /// Insert this row against either backend with audit
3580                /// emission inside the same transaction. Bi-dialect
3581                /// counterpart of [`Self::insert`] for audited models.
3582                ///
3583                /// Snapshot-style audit (post-write field values).
3584                ///
3585                /// # Errors
3586                /// As [`Self::insert`].
3587                pub async fn insert_pool(
3588                    &mut self,
3589                    pool: &#root::sql::Pool,
3590                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3591                    let mut _columns: ::std::vec::Vec<&'static str> =
3592                        ::std::vec::Vec::new();
3593                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3594                        ::std::vec::Vec::new();
3595                    #( #pushes )*
3596                    let _query = #root::core::InsertQuery {
3597                        model: <Self as #root::core::Model>::SCHEMA,
3598                        columns: _columns,
3599                        values: _values,
3600                        returning: ::std::vec![ #( #returning_cols ),* ],
3601                        on_conflict: ::core::option::Option::None,
3602                    };
3603                    let _audit_entry = #root::audit::PendingEntry {
3604                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
3605                        entity_pk: #pk_str,
3606                        operation: #root::audit::AuditOp::Create,
3607                        source: #root::audit::current_source(),
3608                        changes: #root::audit::snapshot_changes(&[
3609                            #( #pairs ),*
3610                        ]),
3611                    };
3612                    let _result = #root::audit::insert_one_with_audit(
3613                        pool, &_query, &_audit_entry,
3614                    ).await?;
3615                    #root::sql::apply_auto_pk(_result, self)
3616                }
3617            }
3618        } else {
3619            quote!()
3620        }
3621    } else {
3622        // Keep the non-audited pool_insert_method we built earlier.
3623        pool_insert_method
3624    };
3625
3626    // Update audited save_pool: now that insert_pool is wired for
3627    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3628    // insert_pool. Non-audited save_pool already does this.
3629    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3630    // Replaces the snapshot-only emission with a per-backend transaction
3631    // body that:
3632    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3633    //     column), capturing BEFORE values
3634    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3635    //  3. builds AFTER pairs from &self
3636    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3637    //     AuditOp::Update + diff_changes(...) on the same tx connection
3638    //  5. commits
3639    //
3640    // Per-backend arms inline the SQL string + placeholder shape, then
3641    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3642    // is polymorphic over Row type — the same tokens work against
3643    // PgRow and MySqlRow as long as the field's Rust type implements
3644    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3645    // primitives + chrono/uuid/serde_json::Value all do).
3646    let pool_save_method = if let Some(tracked) = audited_fields {
3647        if let Some((pk_ident, pk_col)) = primary_key {
3648            let pk_column_lit = pk_col.as_str();
3649            // Two iterators — quote!'s `#(#var)*` consumes the
3650            // iterator, and we need to splice the same after-pairs
3651            // sequence into both per-backend arms.
3652            let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3653            let pk_str = audit_pk_to_string.clone();
3654            // Per-tracked-column BEFORE-pair token list. Each entry
3655            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3656            // The Row alias resolves to PgRow / MySqlRow per call site,
3657            // so the same template generates both the PG and MySQL bodies.
3658            let mk_before_pairs =
3659                |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3660                    tracked
3661                        .iter()
3662                        .map(|c| {
3663                            let column_lit = c.column.as_str();
3664                            let value_ty = &c.value_ty;
3665                            quote! {
3666                                (
3667                                    #column_lit,
3668                                    match #getter::<#value_ty>(
3669                                        _audit_before_row, #column_lit,
3670                                    ) {
3671                                        ::core::result::Result::Ok(v) => {
3672                                            #root::__serde_json::to_value(&v)
3673                                                .unwrap_or(#root::__serde_json::Value::Null)
3674                                        }
3675                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3676                                    },
3677                                )
3678                            }
3679                        })
3680                        .collect()
3681                };
3682            let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3683                mk_before_pairs(quote!(#root::sql::try_get_returning));
3684            let before_pairs_my: Vec<proc_macro2::TokenStream> =
3685                mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3686            let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3687                mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3688            let pg_select_cols: String = tracked
3689                .iter()
3690                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3691                .collect::<Vec<_>>()
3692                .join(", ");
3693            let my_select_cols: String = tracked
3694                .iter()
3695                .map(|c| format!("`{}`", c.column.replace('`', "``")))
3696                .collect::<Vec<_>>()
3697                .join(", ");
3698            // SQLite uses double-quote identifier quoting (same as
3699            // Postgres in default config), so the column-list shape
3700            // matches PG.
3701            let sqlite_select_cols: String = pg_select_cols.clone();
3702            let pk_value_for_bind = if fields.pk_is_auto {
3703                quote!(self.#pk_ident.get().copied().unwrap_or_default())
3704            } else {
3705                quote!(::core::clone::Clone::clone(&self.#pk_ident))
3706            };
3707            let assignments = &fields.update_assignments;
3708            let unset_dispatch = if fields.has_auto {
3709                quote! {
3710                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3711                        return self.insert_pool(pool).await;
3712                    }
3713                }
3714            } else {
3715                quote!()
3716            };
3717            quote! {
3718                /// Save this row against either backend with audit
3719                /// emission (diff-style: BEFORE+AFTER) inside the
3720                /// same transaction. Auto::Unset PK routes to
3721                /// insert_pool. Bi-dialect counterpart of
3722                /// [`Self::save`] for audited models.
3723                ///
3724                /// The audit row's `changes` JSON contains one
3725                /// `{ "field": { "before": …, "after": … } }` entry
3726                /// per tracked column whose value actually changed
3727                /// — same shape as the existing &PgPool save() emits.
3728                ///
3729                /// # Errors
3730                /// As [`Self::save`].
3731                pub async fn save_pool(
3732                    &mut self,
3733                    pool: &#root::sql::Pool,
3734                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3735                    #unset_dispatch
3736                    let _query = #root::core::UpdateQuery {
3737                        model: <Self as #root::core::Model>::SCHEMA,
3738                        set: ::std::vec![ #( #assignments ),* ],
3739                        where_clause: #root::core::WhereExpr::Predicate(
3740                            #root::core::Filter {
3741                                column: #pk_column_lit,
3742                                op: #root::core::Op::Eq,
3743                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3744                                    ::core::clone::Clone::clone(&self.#pk_ident)
3745                                ),
3746                            }
3747                        ),
3748                    };
3749                    let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3750                        ::std::vec![ #( #after_pairs_pg ),* ];
3751                    #root::audit::save_one_with_diff(
3752                        pool,
3753                        &_query,
3754                        #pk_column_lit,
3755                        ::core::convert::Into::<#root::core::SqlValue>::into(
3756                            #pk_value_for_bind,
3757                        ),
3758                        <Self as #root::core::Model>::SCHEMA.table,
3759                        #pk_str,
3760                        _after_pairs,
3761                        #pg_select_cols,
3762                        #my_select_cols,
3763                        #sqlite_select_cols,
3764                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3765                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3766                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3767                    ).await
3768                }
3769            }
3770        } else {
3771            quote!()
3772        }
3773    } else {
3774        pool_save_method
3775    };
3776
3777    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3778    // body shapes:
3779    // - non-audited: simple dispatch through `sql::delete_pool`
3780    // - audited: routes through `audit::delete_one_with_audit`,
3781    //   which opens a per-backend transaction wrapping DELETE +
3782    //   audit emit so the data write and audit row commit atomically.
3783    let pool_delete_method = {
3784        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3785        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3786        if let Some(pk_ident) = pk_ident_for_pool {
3787            if audited_fields.is_some() {
3788                let pairs = audit_pair_tokens.iter();
3789                let pk_str = audit_pk_to_string.clone();
3790                quote! {
3791                    /// Delete this row against either backend with audit
3792                    /// emission inside the same transaction. Bi-dialect
3793                    /// counterpart of [`Self::delete`] for audited models.
3794                    ///
3795                    /// # Errors
3796                    /// As [`Self::delete`].
3797                    pub async fn delete_pool(
3798                        &self,
3799                        pool: &#root::sql::Pool,
3800                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3801                        let _query = #root::core::DeleteQuery {
3802                            model: <Self as #root::core::Model>::SCHEMA,
3803                            where_clause: #root::core::WhereExpr::Predicate(
3804                                #root::core::Filter {
3805                                    column: #pk_column_lit,
3806                                    op: #root::core::Op::Eq,
3807                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3808                                        ::core::clone::Clone::clone(&self.#pk_ident)
3809                                    ),
3810                                }
3811                            ),
3812                        };
3813                        let _audit_entry = #root::audit::PendingEntry {
3814                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3815                            entity_pk: #pk_str,
3816                            operation: #root::audit::AuditOp::Delete,
3817                            source: #root::audit::current_source(),
3818                            changes: #root::audit::snapshot_changes(&[
3819                                #( #pairs ),*
3820                            ]),
3821                        };
3822                        #root::audit::delete_one_with_audit(
3823                            pool, &_query, &_audit_entry,
3824                        ).await
3825                    }
3826                }
3827            } else {
3828                quote! {
3829                    /// Delete the row identified by this instance's primary key
3830                    /// against either backend. Equivalent to [`Self::delete`] but
3831                    /// takes [`#root::sql::Pool`] and dispatches per backend.
3832                    ///
3833                    /// # Errors
3834                    /// As [`Self::delete`].
3835                    pub async fn delete_pool(
3836                        &self,
3837                        pool: &#root::sql::Pool,
3838                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3839                        let _query = #root::core::DeleteQuery {
3840                            model: <Self as #root::core::Model>::SCHEMA,
3841                            where_clause: #root::core::WhereExpr::Predicate(
3842                                #root::core::Filter {
3843                                    column: #pk_column_lit,
3844                                    op: #root::core::Op::Eq,
3845                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3846                                        ::core::clone::Clone::clone(&self.#pk_ident)
3847                                    ),
3848                                }
3849                            ),
3850                        };
3851                        #root::sql::delete_pool(pool, &_query).await
3852                    }
3853                }
3854            }
3855        } else {
3856            quote!()
3857        }
3858    };
3859
3860    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3861    // matching this instance's PK and overwrite the in-memory state
3862    // with the freshly-fetched columns. Django's `refresh_from_db`.
3863    // Issue #825. Only emitted when the model declares a PK; non-PK
3864    // models can't address a specific row.
3865    //
3866    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3867    // every field from `self`; resets the PK to `Auto::Unset` when
3868    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3869    // a fresh autoincrement value. Non-Auto PKs preserve the source
3870    // PK — the caller must overwrite before insert. Pure-Rust, no
3871    // I/O, no dialect surface.
3872    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
3873        let other_field_clones: Vec<TokenStream2> = fields
3874            .column_entries
3875            .iter()
3876            .filter(|c| &c.ident != pk_ident)
3877            .map(|c| {
3878                let ident = &c.ident;
3879                quote! {
3880                    #ident: ::core::clone::Clone::clone(&self.#ident)
3881                }
3882            })
3883            .collect();
3884        let pk_clone_token = if fields.pk_is_auto {
3885            quote! { #pk_ident: #root::sql::Auto::Unset }
3886        } else {
3887            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
3888        };
3889        let replicate_doc = if fields.pk_is_auto {
3890            quote! {
3891                /// Eloquent-style `replicate()` — return a clone of this
3892                /// row with the primary key reset to [`Auto::Unset`] so
3893                /// the copy is ready for `insert_pool` / `save_pool` to
3894                /// allocate a fresh autoincrement value. Every other
3895                /// field is `Clone`d verbatim. Issue #825.
3896                ///
3897                /// `auto_now_add` / `auto_now` timestamp fields are
3898                /// **not** reset (Eloquent's `replicate` doesn't reset
3899                /// them either) — pass them through the normal insert
3900                /// path if you want fresh values, or assign them
3901                /// explicitly after the call.
3902            }
3903        } else {
3904            quote! {
3905                /// Eloquent-style `replicate()` — clone this row
3906                /// verbatim. Because the primary key is **not** an
3907                /// `Auto<T>`, the clone keeps the source PK; the
3908                /// caller must overwrite `copy.<pk>` before inserting
3909                /// to avoid a unique-key violation. Issue #825.
3910            }
3911        };
3912        // 2026-06-07 — field-name / shortcut collision guard.
3913        //
3914        // The macro emits both `pub const <field>: <field>_col = ...`
3915        // (per-field typed-column const, used by the typed-builder
3916        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
3917        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
3918        // model has a field named e.g. `count`, both items would land
3919        // in the same inherent impl with the same name, and the
3920        // compiler rejects the derive with "duplicate definitions".
3921        //
3922        // Drop the conflicting shortcut for that model. Callers can
3923        // still reach the same behavior via
3924        // `QuerySet::<Model>::default().count_pool(&pool)`.
3925        let column_names: ::std::collections::HashSet<String> = fields
3926            .column_entries
3927            .iter()
3928            .map(|c| c.ident.to_string())
3929            .collect();
3930        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
3931            if column_names.contains(name) {
3932                quote! {}
3933            } else {
3934                tokens
3935            }
3936        };
3937        let count_method = emit_if_no_field_collision(
3938            "count",
3939            quote! {
3940                /// Count rows of this model — `SELECT COUNT(*) FROM
3941                /// <table>`. Eloquent `Model::count()` parity.
3942                ///
3943                /// Skipped on models that already declare a field named
3944                /// `count`. Drop into `QuerySet::<Self>::default().count_pool(&pool)`
3945                /// in that case.
3946                ///
3947                /// # Errors
3948                /// As [`CounterPool::count_pool`].
3949                ///
3950                /// [`CounterPool::count_pool`]: rustango::sql::CounterPool::count_pool
3951                pub async fn count(
3952                    pool: &#root::sql::Pool,
3953                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
3954                    use #root::sql::CounterPool as _;
3955                    #root::query::QuerySet::<Self>::default()
3956                        .count_pool(pool)
3957                        .await
3958                }
3959            },
3960        );
3961        let value_method = emit_if_no_field_collision(
3962            "value",
3963            quote! {
3964                /// Pluck a single scalar from the first row.
3965                /// Eloquent `Model::query()->value($col)` parity.
3966                ///
3967                /// Skipped on models that already declare a field named
3968                /// `value`. Drop into
3969                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
3970                ///
3971                /// # Errors
3972                /// As `ValuesFlatQuerySet::first`.
3973                pub async fn value<U>(
3974                    col: &str,
3975                    pool: &#root::sql::Pool,
3976                ) -> ::core::result::Result<
3977                    ::core::option::Option<U>,
3978                    #root::sql::ExecError,
3979                >
3980                where
3981                    U: #root::sql::MaybePgScalar
3982                        + #root::sql::MaybeMyScalar
3983                        + #root::sql::MaybeSqliteScalar
3984                        + ::core::marker::Send
3985                        + ::core::marker::Unpin,
3986                {
3987                    let _col_static: &'static str = Self::__resolve_col(col)?;
3988                    #root::query::QuerySet::<Self>::default()
3989                        .values_list_flat(_col_static)
3990                        .first::<U>(pool)
3991                        .await
3992                }
3993            },
3994        );
3995        let sum_method = emit_if_no_field_collision(
3996            "sum",
3997            quote! {
3998                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
3999                /// Skipped on models that already declare a field named
4000                /// `sum`.
4001                ///
4002                /// # Errors
4003                /// As [`#root::sql::fetch_aggregate_pool`].
4004                pub async fn sum<U>(
4005                    col: &str,
4006                    pool: &#root::sql::Pool,
4007                ) -> ::core::result::Result<
4008                    ::core::option::Option<U>,
4009                    #root::sql::ExecError,
4010                >
4011                where
4012                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4013                        + #root::sql::MaybeMyFromRow
4014                        + #root::sql::MaybeSqliteFromRow
4015                        + ::core::marker::Send
4016                        + ::core::marker::Unpin,
4017                {
4018                    Self::__aggregate_one_pool::<U>(
4019                        col,
4020                        |c| #root::core::AggregateExpr::Sum(c),
4021                        pool,
4022                    )
4023                    .await
4024                }
4025            },
4026        );
4027        let avg_method = emit_if_no_field_collision(
4028            "avg",
4029            quote! {
4030                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4031                /// models that already declare a field named `avg`.
4032                ///
4033                /// # Errors
4034                /// As [`#root::sql::fetch_aggregate_pool`].
4035                pub async fn avg<U>(
4036                    col: &str,
4037                    pool: &#root::sql::Pool,
4038                ) -> ::core::result::Result<
4039                    ::core::option::Option<U>,
4040                    #root::sql::ExecError,
4041                >
4042                where
4043                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4044                        + #root::sql::MaybeMyFromRow
4045                        + #root::sql::MaybeSqliteFromRow
4046                        + ::core::marker::Send
4047                        + ::core::marker::Unpin,
4048                {
4049                    Self::__aggregate_one_pool::<U>(
4050                        col,
4051                        |c| #root::core::AggregateExpr::Avg(c),
4052                        pool,
4053                    )
4054                    .await
4055                }
4056            },
4057        );
4058        let min_method = emit_if_no_field_collision(
4059            "min",
4060            quote! {
4061                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4062                /// models that already declare a field named `min`.
4063                ///
4064                /// # Errors
4065                /// As [`#root::sql::fetch_aggregate_pool`].
4066                pub async fn min<U>(
4067                    col: &str,
4068                    pool: &#root::sql::Pool,
4069                ) -> ::core::result::Result<
4070                    ::core::option::Option<U>,
4071                    #root::sql::ExecError,
4072                >
4073                where
4074                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4075                        + #root::sql::MaybeMyFromRow
4076                        + #root::sql::MaybeSqliteFromRow
4077                        + ::core::marker::Send
4078                        + ::core::marker::Unpin,
4079                {
4080                    Self::__aggregate_one_pool::<U>(
4081                        col,
4082                        |c| #root::core::AggregateExpr::Min(c),
4083                        pool,
4084                    )
4085                    .await
4086                }
4087            },
4088        );
4089        let max_method = emit_if_no_field_collision(
4090            "max",
4091            quote! {
4092                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4093                /// models that already declare a field named `max`.
4094                ///
4095                /// # Errors
4096                /// As [`#root::sql::fetch_aggregate_pool`].
4097                pub async fn max<U>(
4098                    col: &str,
4099                    pool: &#root::sql::Pool,
4100                ) -> ::core::result::Result<
4101                    ::core::option::Option<U>,
4102                    #root::sql::ExecError,
4103                >
4104                where
4105                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4106                        + #root::sql::MaybeMyFromRow
4107                        + #root::sql::MaybeSqliteFromRow
4108                        + ::core::marker::Send
4109                        + ::core::marker::Unpin,
4110                {
4111                    Self::__aggregate_one_pool::<U>(
4112                        col,
4113                        |c| #root::core::AggregateExpr::Max(c),
4114                        pool,
4115                    )
4116                    .await
4117                }
4118            },
4119        );
4120        let first_method = emit_if_no_field_collision(
4121            "first",
4122            quote! {
4123                /// First row of this model. Eloquent `Model::first()`.
4124                /// Skipped on models that already declare a field named
4125                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4126                ///
4127                /// # Errors
4128                /// As `QuerySet::first`.
4129                pub async fn first(
4130                    pool: &#root::sql::Pool,
4131                ) -> ::core::result::Result<
4132                    ::core::option::Option<Self>,
4133                    #root::sql::ExecError,
4134                > {
4135                    #root::query::QuerySet::<Self>::default()
4136                        .first(pool)
4137                        .await
4138                }
4139            },
4140        );
4141        let last_method = emit_if_no_field_collision(
4142            "last",
4143            quote! {
4144                /// Last row of this model by primary-key DESC.
4145                /// Eloquent `Model::query()->latest('id')->first()`
4146                /// parity — fetches the highest-PK row without
4147                /// requiring the caller to spell the PK column.
4148                /// Returns `None` on an empty table.
4149                ///
4150                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4151                /// Skipped on models that already declare a field
4152                /// named `last`.
4153                ///
4154                /// # Errors
4155                /// As `QuerySet::last`.
4156                pub async fn last(
4157                    pool: &#root::sql::Pool,
4158                ) -> ::core::result::Result<
4159                    ::core::option::Option<Self>,
4160                    #root::sql::ExecError,
4161                > {
4162                    #root::query::QuerySet::<Self>::default()
4163                        .last(pool)
4164                        .await
4165                }
4166            },
4167        );
4168        quote! {
4169            /// Re-SELECT this row by its primary key and overwrite
4170            /// every in-memory field with the freshly-fetched value.
4171            /// Django's [`Model.refresh_from_db`]. Issue #825.
4172            ///
4173            /// Use this when the row may have been modified by another
4174            /// process / connection / job since you read it — e.g. after
4175            /// a queued task callback, or to re-sync stale UI state
4176            /// before re-saving.
4177            ///
4178            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4179            /// when the primary key no longer matches any row (e.g.
4180            /// the row was deleted concurrently).
4181            ///
4182            /// # Errors
4183            /// As [`FetcherPool::fetch_pool`]; also `RowNotFound` when
4184            /// the PK no longer exists.
4185            ///
4186            /// [`Model.refresh_from_db`]: https://docs.djangoproject.com/en/5.1/ref/models/instances/#django.db.models.Model.refresh_from_db
4187            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4188            pub async fn refresh_from_db(
4189                &mut self,
4190                pool: &#root::sql::Pool,
4191            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4192                use #root::sql::FetcherPool as _;
4193                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4194                    ::core::clone::Clone::clone(&self.#pk_ident),
4195                );
4196                let mut _rows: ::std::vec::Vec<Self> =
4197                    #root::query::QuerySet::<Self>::default()
4198                        .filter(::core::stringify!(#pk_ident), _pk_val)
4199                        .limit(1)
4200                        .fetch_pool(pool)
4201                        .await?;
4202                match _rows.into_iter().next() {
4203                    ::core::option::Option::Some(_fresh) => {
4204                        *self = _fresh;
4205                        ::core::result::Result::Ok(())
4206                    }
4207                    ::core::option::Option::None => ::core::result::Result::Err(
4208                        #root::sql::ExecError::Driver(
4209                            #root::sql::sqlx::Error::RowNotFound,
4210                        ),
4211                    ),
4212                }
4213            }
4214
4215            /// Atomically increment the integer column `col` by
4216            /// `by` for this row. Equivalent to
4217            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4218            /// Eloquent `Model::increment($col, $by)` / Django
4219            /// `Model.objects.filter(pk=…).update(col=F('col')+$by)`
4220            /// parity.
4221            ///
4222            /// **Doesn't mutate `self`** — the in-memory copy is now
4223            /// stale; call [`Self::refresh_from_db_pool`] /
4224            /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4225            /// affected count (0 when the PK doesn't match any row,
4226            /// 1 on success).
4227            ///
4228            /// `col` is the Rust field name as a string; unknown
4229            /// fields surface as `UnknownField` at runtime. Negative
4230            /// `by` values atomically decrement (see also
4231            /// [`Self::decrement_pool`]).
4232            ///
4233            /// # Errors
4234            /// As [`UpdaterPool::execute_pool`].
4235            ///
4236            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4237            pub async fn increment(
4238                &self,
4239                col: &str,
4240                by: i64,
4241                pool: &#root::sql::Pool,
4242            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4243                Self::__increment_one(self, col, by, pool).await
4244            }
4245
4246            /// Sibling of [`Self::increment`] — atomically
4247            /// decrement this row's `col` by `by`. Eloquent
4248            /// `$model->decrement($col, $by)` parity. Equivalent to
4249            /// `self.increment(col, -by, &pool)`; the separate name
4250            /// keeps call sites readable.
4251            ///
4252            /// # Errors
4253            /// As [`Self::increment`].
4254            pub async fn decrement(
4255                &self,
4256                col: &str,
4257                by: i64,
4258                pool: &#root::sql::Pool,
4259            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4260                Self::__increment_one(self, col, -by, pool).await
4261            }
4262
4263            /// Bulk-increment: add `by` to `col` on every row of the
4264            /// table. Eloquent `Model::query()->increment($col, $by)`
4265            /// parity. Use for counters, score adjustments, view
4266            /// rollups.
4267            ///
4268            /// # Errors
4269            /// As [`UpdaterPool::execute_pool`].
4270            ///
4271            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4272            pub async fn increment_each(
4273                col: &str,
4274                by: i64,
4275                pool: &#root::sql::Pool,
4276            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4277                Self::__increment_all(col, by, pool).await
4278            }
4279
4280            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4281            ///
4282            /// # Errors
4283            /// As [`Self::increment_each`].
4284            pub async fn decrement_each(
4285                col: &str,
4286                by: i64,
4287                pool: &#root::sql::Pool,
4288            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4289                Self::__increment_all(col, -by, pool).await
4290            }
4291
4292            /// Internal: forward to
4293            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4294            /// One-line wrapper kept as a per-Model method so the
4295            /// macro's emitted `increment` / `decrement` instance
4296            /// calls don't have to thread `Self` through manually.
4297            #[doc(hidden)]
4298            pub async fn __increment_one(
4299                this: &Self,
4300                col: &str,
4301                by: i64,
4302                pool: &#root::sql::Pool,
4303            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4304                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4305                    ::core::clone::Clone::clone(&this.#pk_ident),
4306                );
4307                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4308                    ::core::stringify!(#pk_ident),
4309                    _pk_val,
4310                    col,
4311                    by,
4312                    pool,
4313                )
4314                .await
4315            }
4316
4317            /// Internal: forward to
4318            /// [`#root::sql::model_shortcuts::increment_all_pool`].
4319            #[doc(hidden)]
4320            pub async fn __increment_all(
4321                col: &str,
4322                by: i64,
4323                pool: &#root::sql::Pool,
4324            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4325                #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4326            }
4327
4328            /// Internal: forward to
4329            /// [`#root::sql::model_shortcuts::resolve_col`].
4330            #[doc(hidden)]
4331            pub fn __resolve_col(
4332                col: &str,
4333            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4334                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4335            }
4336
4337            /// Internal: forward to
4338            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4339            #[doc(hidden)]
4340            #[must_use]
4341            pub fn __add_signed_expr(
4342                col_static: &'static str,
4343                signed_by: i64,
4344            ) -> #root::core::Expr {
4345                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4346            }
4347
4348            /// Re-SELECT this row by its primary key and return a
4349            /// **new** instance with the freshly-fetched fields.
4350            /// Eloquent `Model::fresh()` parity — non-mutating
4351            /// counterpart of [`Self::refresh_from_db_pool`].
4352            ///
4353            /// Returns `Ok(None)` when the row was deleted
4354            /// concurrently — vs [`Self::refresh_from_db_pool`]
4355            /// which surfaces that as `RowNotFound` because
4356            /// in-place mutation has nothing to write to.
4357            ///
4358            /// Useful when you want to compare the in-memory
4359            /// instance against the persisted state (audit-style
4360            /// diffs, conflict detection) without mutating the
4361            /// reference you already hold.
4362            ///
4363            /// # Errors
4364            /// As [`FetcherPool::fetch_pool`].
4365            ///
4366            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4367            pub async fn fresh(
4368                &self,
4369                pool: &#root::sql::Pool,
4370            ) -> ::core::result::Result<
4371                ::core::option::Option<Self>,
4372                #root::sql::ExecError,
4373            > {
4374                use #root::sql::FetcherPool as _;
4375                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4376                    ::core::clone::Clone::clone(&self.#pk_ident),
4377                );
4378                let _rows: ::std::vec::Vec<Self> =
4379                    #root::query::QuerySet::<Self>::default()
4380                        .filter(::core::stringify!(#pk_ident), _pk_val)
4381                        .limit(1)
4382                        .fetch_pool(pool)
4383                        .await?;
4384                ::core::result::Result::Ok(_rows.into_iter().next())
4385            }
4386
4387            #replicate_doc
4388            #[must_use]
4389            pub fn replicate(&self) -> Self {
4390                Self {
4391                    #pk_clone_token,
4392                    #( #other_field_clones, )*
4393                }
4394            }
4395
4396            #first_method
4397
4398            #last_method
4399
4400            /// Throwing counterpart of [`Self::first_pool`] —
4401            /// errors with `RowNotFound` when the table is empty.
4402            /// Eloquent `Model::firstOrFail()` parity.
4403            ///
4404            /// # Errors
4405            /// As [`Self::first_pool`]; additionally
4406            /// [`sqlx::Error::RowNotFound`] on empty tables.
4407            ///
4408            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4409            pub async fn first_or_fail(
4410                pool: &#root::sql::Pool,
4411            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4412                // Route through the queryset rather than `Self::first`,
4413                // which is suppressed on models with a field named
4414                // `first` (field/shortcut collision guard).
4415                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4416                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4417                    ::core::option::Option::None => ::core::result::Result::Err(
4418                        #root::sql::ExecError::Driver(
4419                            #root::sql::sqlx::Error::RowNotFound,
4420                        ),
4421                    ),
4422                }
4423            }
4424
4425            /// Single-column projection — `SELECT <col> FROM
4426            /// <table>`. Returns `Vec<U>` where each element is the
4427            /// decoded value of the column. Eloquent
4428            /// `Model::pluck($column)` / Django
4429            /// `Model.objects.values_list('col', flat=True)` parity.
4430            ///
4431            /// Thin wrapper over `QuerySet::<Self>::default()
4432            /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4433            /// be decodable from the column's SQL type on every
4434            /// dialect the binary targets (common picks: `i64` /
4435            /// `i32` / `String` / `bool` / `f64`).
4436            ///
4437            /// # Errors
4438            /// As `ValuesFlatQuerySet::fetch`.
4439            pub async fn pluck<U>(
4440                col: &'static str,
4441                pool: &#root::sql::Pool,
4442            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4443            where
4444                U: #root::sql::MaybePgScalar
4445                    + #root::sql::MaybeMyScalar
4446                    + #root::sql::MaybeSqliteScalar
4447                    + ::core::marker::Send
4448                    + ::core::marker::Unpin,
4449            {
4450                #root::query::QuerySet::<Self>::default()
4451                    .values_list_flat(col)
4452                    .fetch::<U>(pool)
4453                    .await
4454            }
4455
4456            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4457            /// stream every row of this model in batches of `n`,
4458            /// invoking the callback once per batch. Stable PK-ASC
4459            /// ordering so the LIMIT/OFFSET pagination is
4460            /// deterministic across drivers.
4461            ///
4462            /// The callback is async — it can do further DB work
4463            /// (writes, related-row lookups, queue dispatch) per
4464            /// batch. Return `Err(...)` from the callback to abort
4465            /// the iteration early; the error bubbles up.
4466            ///
4467            /// **When to use**: bulk processing flows that can't
4468            /// fit the whole table in memory — sending newsletters,
4469            /// running data migrations, computing summary
4470            /// statistics. For small / known-bounded tables, plain
4471            /// `Self::all(&pool)` is simpler.
4472            ///
4473            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4474            /// large tables. For multi-million-row scans prefer
4475            /// keyset-by-PK (the standard "WHERE id > last_seen"
4476            /// shape) over `chunk(...)`.
4477            ///
4478            /// Skipped on models without a primary key — chunking
4479            /// needs a stable order to avoid skipping / repeating
4480            /// rows across batches.
4481            pub async fn chunk<F, Fut>(
4482                n: i64,
4483                pool: &#root::sql::Pool,
4484                mut cb: F,
4485            ) -> ::core::result::Result<(), #root::sql::ExecError>
4486            where
4487                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4488                Fut: ::core::future::Future<
4489                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4490                >,
4491            {
4492                use #root::sql::FetcherPool as _;
4493                let pk_col = match Self::primary_key_column() {
4494                    ::core::option::Option::Some(c) => c,
4495                    ::core::option::Option::None => {
4496                        return ::core::result::Result::Ok(());
4497                    }
4498                };
4499                let mut offset: i64 = 0;
4500                loop {
4501                    let rows: ::std::vec::Vec<Self> =
4502                        #root::query::QuerySet::<Self>::default()
4503                            .order_by(&[(pk_col, false)])
4504                            .limit(n)
4505                            .offset(offset)
4506                            .fetch_pool(pool)
4507                            .await?;
4508                    if rows.is_empty() {
4509                        return ::core::result::Result::Ok(());
4510                    }
4511                    let len = rows.len() as i64;
4512                    cb(rows).await?;
4513                    if len < n {
4514                        return ::core::result::Result::Ok(());
4515                    }
4516                    offset += n;
4517                }
4518            }
4519
4520            /// Eloquent `Model::chunkById($n, fn (...))` — same
4521            /// per-batch callback shape as [`Self::chunk`], but uses
4522            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4523            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4524            /// — the right choice for multi-million-row sweeps.
4525            ///
4526            /// Requires the primary key to be a signed integer type
4527            /// (`i64` / `i32`); the keyset comparison rides on
4528            /// `__rustango_pk_value()` lowering through
4529            /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4530            /// non-integer PKs (UUID, String) — those should use
4531            /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4532            /// keyset loop.
4533            ///
4534            /// Callback errors abort iteration; the error bubbles up
4535            /// unchanged. Empty table → callback invoked zero times.
4536            pub async fn chunk_by_id<F, Fut>(
4537                n: i64,
4538                pool: &#root::sql::Pool,
4539                mut cb: F,
4540            ) -> ::core::result::Result<(), #root::sql::ExecError>
4541            where
4542                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4543                Fut: ::core::future::Future<
4544                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4545                >,
4546            {
4547                use #root::sql::FetcherPool as _;
4548                let pk_col = match Self::primary_key_column() {
4549                    ::core::option::Option::Some(c) => c,
4550                    ::core::option::Option::None => {
4551                        return ::core::result::Result::Ok(());
4552                    }
4553                };
4554                // Track the largest PK seen so the next batch picks
4555                // up from there. `i64::MIN` as the sentinel — the
4556                // very first iteration's `> MIN` matches every row,
4557                // so the loop entry is uniform with subsequent
4558                // iterations.
4559                let mut last_seen: i64 = i64::MIN;
4560                loop {
4561                    let key = ::std::format!("{}__gt", pk_col);
4562                    let rows: ::std::vec::Vec<Self> =
4563                        #root::query::QuerySet::<Self>::default()
4564                            .filter(key.as_str(), last_seen)
4565                            .order_by(&[(pk_col, false)])
4566                            .limit(n)
4567                            .fetch_pool(pool)
4568                            .await?;
4569                    if rows.is_empty() {
4570                        return ::core::result::Result::Ok(());
4571                    }
4572                    let len = rows.len() as i64;
4573                    // Capture the last row's PK BEFORE moving rows
4574                    // into the callback.
4575                    let max_pk = match rows
4576                        .last()
4577                        .map(|r| r.__rustango_pk_value())
4578                    {
4579                        ::core::option::Option::Some(
4580                            #root::core::SqlValue::I64(v),
4581                        ) => v,
4582                        ::core::option::Option::Some(
4583                            #root::core::SqlValue::I32(v),
4584                        ) => i64::from(v),
4585                        _ => return ::core::result::Result::Ok(()),
4586                    };
4587                    cb(rows).await?;
4588                    if len < n {
4589                        return ::core::result::Result::Ok(());
4590                    }
4591                    last_seen = max_pk;
4592                }
4593            }
4594
4595            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4596            /// per-row callback companion to [`Self::chunk`].
4597            /// Streams every row in keyset-paginated batches of
4598            /// `batch` size, calling `cb` once per row.
4599            ///
4600            /// Inherits the keyset-paginated scan of
4601            /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4602            /// non-integer PKs are silently a no-op).
4603            ///
4604            /// ```ignore
4605            /// Post::each(500, &pool, |p| async move {
4606            ///     reindex(p).await?;
4607            ///     Ok(())
4608            /// }).await?;
4609            /// ```
4610            ///
4611            /// Return `Err(...)` from the callback to abort
4612            /// iteration; the error bubbles up unchanged.
4613            pub async fn each<F, Fut>(
4614                batch: i64,
4615                pool: &#root::sql::Pool,
4616                mut cb: F,
4617            ) -> ::core::result::Result<(), #root::sql::ExecError>
4618            where
4619                F: ::core::ops::FnMut(Self) -> Fut,
4620                Fut: ::core::future::Future<
4621                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4622                >,
4623            {
4624                use #root::sql::FetcherPool as _;
4625                let pk_col = match Self::primary_key_column() {
4626                    ::core::option::Option::Some(c) => c,
4627                    ::core::option::Option::None => {
4628                        return ::core::result::Result::Ok(());
4629                    }
4630                };
4631                let mut last_seen: i64 = i64::MIN;
4632                loop {
4633                    let key = ::std::format!("{}__gt", pk_col);
4634                    let rows: ::std::vec::Vec<Self> =
4635                        #root::query::QuerySet::<Self>::default()
4636                            .filter(key.as_str(), last_seen)
4637                            .order_by(&[(pk_col, false)])
4638                            .limit(batch)
4639                            .fetch_pool(pool)
4640                            .await?;
4641                    if rows.is_empty() {
4642                        return ::core::result::Result::Ok(());
4643                    }
4644                    let len = rows.len() as i64;
4645                    let max_pk = match rows
4646                        .last()
4647                        .map(|r| r.__rustango_pk_value())
4648                    {
4649                        ::core::option::Option::Some(
4650                            #root::core::SqlValue::I64(v),
4651                        ) => v,
4652                        ::core::option::Option::Some(
4653                            #root::core::SqlValue::I32(v),
4654                        ) => i64::from(v),
4655                        _ => return ::core::result::Result::Ok(()),
4656                    };
4657                    for row in rows {
4658                        cb(row).await?;
4659                    }
4660                    if len < batch {
4661                        return ::core::result::Result::Ok(());
4662                    }
4663                    last_seen = max_pk;
4664                }
4665            }
4666
4667            /// Delete every row of this model — `TRUNCATE TABLE
4668            /// <table> RESTART IDENTITY CASCADE` on Postgres,
4669            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4670            /// support `TRUNCATE` inside foreign-key constraints
4671            /// or — for SQLite — at all). Eloquent `Model::truncate()`
4672            /// / Django `Model.objects.all().delete()` parity.
4673            ///
4674            /// **Use only in tests / fixture-reset flows.** Production
4675            /// writes through this would silently bypass the
4676            /// `pre_delete` / `post_delete` signals (no per-row hooks
4677            /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4678            /// row's audit-log entry.
4679            ///
4680            /// # Errors
4681            /// As [`raw_execute_pool`].
4682            ///
4683            /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4684            pub async fn truncate(
4685                pool: &#root::sql::Pool,
4686            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4687                let _table = <Self as #root::core::Model>::SCHEMA.table;
4688                let _dialect = pool.dialect();
4689                let _quoted = _dialect.quote_ident(_table);
4690                let _sql = if _dialect.name() == "postgres" {
4691                    ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4692                } else {
4693                    ::std::format!("DELETE FROM {}", _quoted)
4694                };
4695                #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4696            }
4697
4698            /// Bulk-delete every row whose primary key is in
4699            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4700            /// Returns the affected row count.
4701            ///
4702            /// Eloquent `Model::destroy([1, 2, 3])` / Django
4703            /// `Model.objects.filter(pk__in=[...]).delete()` parity.
4704            /// Empty `pks` is a no-op (returns 0).
4705            ///
4706            /// Accepts any iterable whose elements are
4707            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4708            /// `[i64; N]`, etc.
4709            ///
4710            /// # Errors
4711            /// As `delete_pool`.
4712            pub async fn destroy<V>(
4713                pks: impl ::core::iter::IntoIterator<Item = V>,
4714                pool: &#root::sql::Pool,
4715            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4716            where
4717                V: ::core::convert::Into<#root::core::SqlValue>,
4718            {
4719                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4720                    pks.into_iter().map(::core::convert::Into::into).collect();
4721                if _values.is_empty() {
4722                    return ::core::result::Result::Ok(0);
4723                }
4724                let _query = #root::core::DeleteQuery {
4725                    model: <Self as #root::core::Model>::SCHEMA,
4726                    where_clause: #root::core::WhereExpr::Predicate(
4727                        #root::core::Filter {
4728                            column: <Self as #root::core::Model>::SCHEMA
4729                                .primary_key()
4730                                .ok_or_else(|| {
4731                                    #root::sql::ExecError::Sql(
4732                                        #root::sql::SqlError::MissingPrimaryKey,
4733                                    )
4734                                })?
4735                                .column,
4736                            op: #root::core::Op::In,
4737                            value: #root::core::SqlValue::List(_values),
4738                        },
4739                    ),
4740                };
4741                #root::sql::delete_pool(pool, &_query).await
4742            }
4743
4744            /// Fetch every row where `<col> = <val>`. Eloquent
4745            /// `Model::where($col, $val)->get()` / Django
4746            /// `Model.objects.filter(col=val).all()` parity.
4747            ///
4748            /// Thin wrapper over `QuerySet::<Self>::default()
4749            /// .filter(col, val).fetch_pool(pool)`. For one row,
4750            /// use [`Self::first_where_pool`]; for a chain that
4751            /// needs further `.filter()` / `.order_by()` /
4752            /// `.limit()`, drop down to `Self::query().filter(...)`
4753            /// directly.
4754            ///
4755            /// `val` accepts any value `Into<SqlValue>` so plain
4756            /// strings, ints, UUIDs, etc. all work.
4757            ///
4758            /// # Errors
4759            /// As [`FetcherPool::fetch_pool`].
4760            ///
4761            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4762            pub async fn where_(
4763                col: &str,
4764                val: impl ::core::convert::Into<#root::core::SqlValue>,
4765                pool: &#root::sql::Pool,
4766            ) -> ::core::result::Result<
4767                ::std::vec::Vec<Self>,
4768                #root::sql::ExecError,
4769            > {
4770                use #root::sql::FetcherPool as _;
4771                #root::query::QuerySet::<Self>::default()
4772                    .filter(col, val)
4773                    .fetch_pool(pool)
4774                    .await
4775            }
4776
4777            /// Fetch every row where `<col> IN (vals)`. Eloquent
4778            /// `Model::whereIn($col, $vals)->get()` parity. Empty
4779            /// `vals` returns no rows (matches SQL's empty-IN
4780            /// semantics).
4781            ///
4782            /// `vals` accepts any iterable whose items are
4783            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4784            /// etc.
4785            ///
4786            /// # Errors
4787            /// As [`FetcherPool::fetch_pool`].
4788            ///
4789            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4790            pub async fn where_in<V>(
4791                col: &str,
4792                vals: impl ::core::iter::IntoIterator<Item = V>,
4793                pool: &#root::sql::Pool,
4794            ) -> ::core::result::Result<
4795                ::std::vec::Vec<Self>,
4796                #root::sql::ExecError,
4797            >
4798            where
4799                V: ::core::convert::Into<#root::core::SqlValue>,
4800            {
4801                use #root::sql::FetcherPool as _;
4802                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4803                    vals.into_iter().map(::core::convert::Into::into).collect();
4804                if _values.is_empty() {
4805                    return ::core::result::Result::Ok(::std::vec::Vec::new());
4806                }
4807                let _key = ::std::format!("{}__in", col);
4808                #root::query::QuerySet::<Self>::default()
4809                    .filter(&_key, #root::core::SqlValue::List(_values))
4810                    .fetch_pool(pool)
4811                    .await
4812            }
4813
4814            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4815            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4816            /// `vals` returns every row (matches SQL's empty-NOT-IN
4817            /// semantics — vacuously true for every row).
4818            ///
4819            /// # Errors
4820            /// As [`FetcherPool::fetch_pool`].
4821            ///
4822            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4823            pub async fn where_not_in<V>(
4824                col: &str,
4825                vals: impl ::core::iter::IntoIterator<Item = V>,
4826                pool: &#root::sql::Pool,
4827            ) -> ::core::result::Result<
4828                ::std::vec::Vec<Self>,
4829                #root::sql::ExecError,
4830            >
4831            where
4832                V: ::core::convert::Into<#root::core::SqlValue>,
4833            {
4834                use #root::sql::FetcherPool as _;
4835                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4836                    vals.into_iter().map(::core::convert::Into::into).collect();
4837                if _values.is_empty() {
4838                    return #root::query::QuerySet::<Self>::default()
4839                        .fetch_pool(pool)
4840                        .await;
4841                }
4842                let _key = ::std::format!("{}__not_in", col);
4843                #root::query::QuerySet::<Self>::default()
4844                    .filter(&_key, #root::core::SqlValue::List(_values))
4845                    .fetch_pool(pool)
4846                    .await
4847            }
4848
4849            /// Fetch every row where `<col> IS NULL`. Eloquent
4850            /// `Model::whereNull($col)->get()` parity.
4851            ///
4852            /// # Errors
4853            /// As [`FetcherPool::fetch_pool`].
4854            ///
4855            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4856            pub async fn where_null(
4857                col: &str,
4858                pool: &#root::sql::Pool,
4859            ) -> ::core::result::Result<
4860                ::std::vec::Vec<Self>,
4861                #root::sql::ExecError,
4862            > {
4863                use #root::sql::FetcherPool as _;
4864                let _key = ::std::format!("{}__isnull", col);
4865                #root::query::QuerySet::<Self>::default()
4866                    .filter(&_key, #root::core::SqlValue::Bool(true))
4867                    .fetch_pool(pool)
4868                    .await
4869            }
4870
4871            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4872            /// `Model::whereNotNull($col)->get()` parity.
4873            ///
4874            /// # Errors
4875            /// As [`FetcherPool::fetch_pool`].
4876            ///
4877            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4878            pub async fn where_not_null(
4879                col: &str,
4880                pool: &#root::sql::Pool,
4881            ) -> ::core::result::Result<
4882                ::std::vec::Vec<Self>,
4883                #root::sql::ExecError,
4884            > {
4885                use #root::sql::FetcherPool as _;
4886                let _key = ::std::format!("{}__isnull", col);
4887                #root::query::QuerySet::<Self>::default()
4888                    .filter(&_key, #root::core::SqlValue::Bool(false))
4889                    .fetch_pool(pool)
4890                    .await
4891            }
4892
4893            /// Fetch up to `n` rows in random order. Eloquent
4894            /// `Model::inRandomOrder()->limit($n)->get()` /
4895            /// `Model::query()->inRandomOrder()->get()->take($n)`
4896            /// parity. **Performance caveat**: random ordering
4897            /// forces a full table scan + per-row random key sort;
4898            /// the optimizer cannot use an index. Prefer a
4899            /// `pk >= rand_offset LIMIT N` walk for huge tables.
4900            ///
4901            /// # Errors
4902            /// As [`FetcherPool::fetch_pool`].
4903            ///
4904            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4905            pub async fn random_n(
4906                n: i64,
4907                pool: &#root::sql::Pool,
4908            ) -> ::core::result::Result<
4909                ::std::vec::Vec<Self>,
4910                #root::sql::ExecError,
4911            > {
4912                use #root::sql::FetcherPool as _;
4913                #root::query::QuerySet::<Self>::default()
4914                    .order_random()
4915                    .limit(n)
4916                    .fetch_pool(pool)
4917                    .await
4918            }
4919
4920            /// Fetch one row in random order. Eloquent
4921            /// `Model::inRandomOrder()->first()` parity. Same
4922            /// performance caveat as [`Self::random_n_pool`].
4923            ///
4924            /// # Errors
4925            /// As [`FetcherPool::fetch_pool`].
4926            ///
4927            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4928            pub async fn random(
4929                pool: &#root::sql::Pool,
4930            ) -> ::core::result::Result<
4931                ::core::option::Option<Self>,
4932                #root::sql::ExecError,
4933            > {
4934                ::core::result::Result::Ok(
4935                    Self::random_n(1, pool).await?.into_iter().next(),
4936                )
4937            }
4938
4939            /// Fetch every row ordered ASC by `field`. Eloquent
4940            /// `Model::oldest($field)->get()` parity — the multi-row
4941            /// counterpart of [`Self::earliest_pool`].
4942            ///
4943            /// # Errors
4944            /// As [`FetcherPool::fetch_pool`].
4945            ///
4946            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4947            pub async fn oldest(
4948                field: &str,
4949                pool: &#root::sql::Pool,
4950            ) -> ::core::result::Result<
4951                ::std::vec::Vec<Self>,
4952                #root::sql::ExecError,
4953            > {
4954                use #root::sql::FetcherPool as _;
4955                #root::query::QuerySet::<Self>::default()
4956                    .order_by(&[(field, false)])
4957                    .fetch_pool(pool)
4958                    .await
4959            }
4960
4961            /// Fetch every row ordered DESC by `field`. Eloquent
4962            /// `Model::latest($field)->get()` parity — the multi-row
4963            /// counterpart of [`Self::latest_pool`].
4964            ///
4965            /// # Errors
4966            /// As [`FetcherPool::fetch_pool`].
4967            ///
4968            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4969            pub async fn newest(
4970                field: &str,
4971                pool: &#root::sql::Pool,
4972            ) -> ::core::result::Result<
4973                ::std::vec::Vec<Self>,
4974                #root::sql::ExecError,
4975            > {
4976                use #root::sql::FetcherPool as _;
4977                #root::query::QuerySet::<Self>::default()
4978                    .order_by(&[(field, true)])
4979                    .fetch_pool(pool)
4980                    .await
4981            }
4982
4983            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
4984            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
4985            /// Routes through the existing `__year` lookup suffix
4986            /// (issue #829).
4987            ///
4988            /// # Errors
4989            /// As [`FetcherPool::fetch_pool`].
4990            ///
4991            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
4992            pub async fn where_year(
4993                col: &str,
4994                year: i64,
4995                pool: &#root::sql::Pool,
4996            ) -> ::core::result::Result<
4997                ::std::vec::Vec<Self>,
4998                #root::sql::ExecError,
4999            > {
5000                use #root::sql::FetcherPool as _;
5001                let _key = ::std::format!("{}__year", col);
5002                #root::query::QuerySet::<Self>::default()
5003                    .filter(&_key, #root::core::SqlValue::I64(year))
5004                    .fetch_pool(pool)
5005                    .await
5006            }
5007
5008            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5009            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5010            /// `month` is 1–12.
5011            ///
5012            /// # Errors
5013            /// As [`FetcherPool::fetch_pool`].
5014            ///
5015            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5016            pub async fn where_month(
5017                col: &str,
5018                month: i64,
5019                pool: &#root::sql::Pool,
5020            ) -> ::core::result::Result<
5021                ::std::vec::Vec<Self>,
5022                #root::sql::ExecError,
5023            > {
5024                use #root::sql::FetcherPool as _;
5025                let _key = ::std::format!("{}__month", col);
5026                #root::query::QuerySet::<Self>::default()
5027                    .filter(&_key, #root::core::SqlValue::I64(month))
5028                    .fetch_pool(pool)
5029                    .await
5030            }
5031
5032            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5033            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5034            /// `day` is 1–31.
5035            ///
5036            /// # Errors
5037            /// As [`FetcherPool::fetch_pool`].
5038            ///
5039            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5040            pub async fn where_day(
5041                col: &str,
5042                day: i64,
5043                pool: &#root::sql::Pool,
5044            ) -> ::core::result::Result<
5045                ::std::vec::Vec<Self>,
5046                #root::sql::ExecError,
5047            > {
5048                use #root::sql::FetcherPool as _;
5049                let _key = ::std::format!("{}__day", col);
5050                #root::query::QuerySet::<Self>::default()
5051                    .filter(&_key, #root::core::SqlValue::I64(day))
5052                    .fetch_pool(pool)
5053                    .await
5054            }
5055
5056            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5057            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5058            /// `hour` is 0–23.
5059            ///
5060            /// # Errors
5061            /// As [`FetcherPool::fetch_pool`].
5062            ///
5063            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5064            pub async fn where_hour(
5065                col: &str,
5066                hour: i64,
5067                pool: &#root::sql::Pool,
5068            ) -> ::core::result::Result<
5069                ::std::vec::Vec<Self>,
5070                #root::sql::ExecError,
5071            > {
5072                use #root::sql::FetcherPool as _;
5073                let _key = ::std::format!("{}__hour", col);
5074                #root::query::QuerySet::<Self>::default()
5075                    .filter(&_key, #root::core::SqlValue::I64(hour))
5076                    .fetch_pool(pool)
5077                    .await
5078            }
5079
5080            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5081            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5082            /// `minute` is 0–59.
5083            ///
5084            /// # Errors
5085            /// As [`FetcherPool::fetch_pool`].
5086            ///
5087            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5088            pub async fn where_minute(
5089                col: &str,
5090                minute: i64,
5091                pool: &#root::sql::Pool,
5092            ) -> ::core::result::Result<
5093                ::std::vec::Vec<Self>,
5094                #root::sql::ExecError,
5095            > {
5096                use #root::sql::FetcherPool as _;
5097                let _key = ::std::format!("{}__minute", col);
5098                #root::query::QuerySet::<Self>::default()
5099                    .filter(&_key, #root::core::SqlValue::I64(minute))
5100                    .fetch_pool(pool)
5101                    .await
5102            }
5103
5104            /// Fetch every row where `<col> LIKE <pattern>` —
5105            /// caller-supplied pattern (must include `%` / `_`
5106            /// wildcards manually). Eloquent `Model::whereLike`
5107            /// parity.
5108            ///
5109            /// # Errors
5110            /// As [`FetcherPool::fetch_pool`].
5111            ///
5112            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5113            pub async fn where_like(
5114                col: &str,
5115                pattern: impl ::core::convert::Into<::std::string::String>,
5116                pool: &#root::sql::Pool,
5117            ) -> ::core::result::Result<
5118                ::std::vec::Vec<Self>,
5119                #root::sql::ExecError,
5120            > {
5121                use #root::sql::FetcherPool as _;
5122                let _key = ::std::format!("{}__like", col);
5123                #root::query::QuerySet::<Self>::default()
5124                    .filter(
5125                        &_key,
5126                        #root::core::SqlValue::String(pattern.into()),
5127                    )
5128                    .fetch_pool(pool)
5129                    .await
5130            }
5131
5132            /// Fetch every row where `<col> ILIKE <pattern>` —
5133            /// case-insensitive LIKE (PG native, MySQL/SQLite
5134            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5135            ///
5136            /// # Errors
5137            /// As [`FetcherPool::fetch_pool`].
5138            ///
5139            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5140            pub async fn where_ilike(
5141                col: &str,
5142                pattern: impl ::core::convert::Into<::std::string::String>,
5143                pool: &#root::sql::Pool,
5144            ) -> ::core::result::Result<
5145                ::std::vec::Vec<Self>,
5146                #root::sql::ExecError,
5147            > {
5148                use #root::sql::FetcherPool as _;
5149                let _key = ::std::format!("{}__ilike", col);
5150                #root::query::QuerySet::<Self>::default()
5151                    .filter(
5152                        &_key,
5153                        #root::core::SqlValue::String(pattern.into()),
5154                    )
5155                    .fetch_pool(pool)
5156                    .await
5157            }
5158
5159            /// Fetch every row where `<col>` starts with `prefix`
5160            /// (auto-appends `%`). Django `__startswith` / Eloquent
5161            /// `whereLike("col", "$prefix%")` parity.
5162            ///
5163            /// # Errors
5164            /// As [`FetcherPool::fetch_pool`].
5165            ///
5166            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5167            pub async fn where_starts_with(
5168                col: &str,
5169                prefix: impl ::core::convert::Into<::std::string::String>,
5170                pool: &#root::sql::Pool,
5171            ) -> ::core::result::Result<
5172                ::std::vec::Vec<Self>,
5173                #root::sql::ExecError,
5174            > {
5175                use #root::sql::FetcherPool as _;
5176                let _key = ::std::format!("{}__startswith", col);
5177                #root::query::QuerySet::<Self>::default()
5178                    .filter(
5179                        &_key,
5180                        #root::core::SqlValue::String(prefix.into()),
5181                    )
5182                    .fetch_pool(pool)
5183                    .await
5184            }
5185
5186            /// Fetch every row where `<col>` ends with `suffix`
5187            /// (auto-prepends `%`). Django `__endswith` / Eloquent
5188            /// `whereLike("col", "%$suffix")` parity.
5189            ///
5190            /// # Errors
5191            /// As [`FetcherPool::fetch_pool`].
5192            ///
5193            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5194            pub async fn where_ends_with(
5195                col: &str,
5196                suffix: impl ::core::convert::Into<::std::string::String>,
5197                pool: &#root::sql::Pool,
5198            ) -> ::core::result::Result<
5199                ::std::vec::Vec<Self>,
5200                #root::sql::ExecError,
5201            > {
5202                use #root::sql::FetcherPool as _;
5203                let _key = ::std::format!("{}__endswith", col);
5204                #root::query::QuerySet::<Self>::default()
5205                    .filter(
5206                        &_key,
5207                        #root::core::SqlValue::String(suffix.into()),
5208                    )
5209                    .fetch_pool(pool)
5210                    .await
5211            }
5212
5213            /// Fetch every row where `<col>` contains `substr`
5214            /// (auto-wraps with `%`). Django `__contains` /
5215            /// Eloquent `whereLike("col", "%$substr%")` parity.
5216            ///
5217            /// # Errors
5218            /// As [`FetcherPool::fetch_pool`].
5219            ///
5220            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5221            pub async fn where_contains(
5222                col: &str,
5223                substr: impl ::core::convert::Into<::std::string::String>,
5224                pool: &#root::sql::Pool,
5225            ) -> ::core::result::Result<
5226                ::std::vec::Vec<Self>,
5227                #root::sql::ExecError,
5228            > {
5229                use #root::sql::FetcherPool as _;
5230                let _key = ::std::format!("{}__contains", col);
5231                #root::query::QuerySet::<Self>::default()
5232                    .filter(
5233                        &_key,
5234                        #root::core::SqlValue::String(substr.into()),
5235                    )
5236                    .fetch_pool(pool)
5237                    .await
5238            }
5239
5240            /// Fetch every row where `<col> > val`. Eloquent
5241            /// `Model::where($col, ">", $val)->get()` parity.
5242            ///
5243            /// # Errors
5244            /// As [`FetcherPool::fetch_pool`].
5245            ///
5246            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5247            pub async fn where_gt(
5248                col: &str,
5249                val: impl ::core::convert::Into<#root::core::SqlValue>,
5250                pool: &#root::sql::Pool,
5251            ) -> ::core::result::Result<
5252                ::std::vec::Vec<Self>,
5253                #root::sql::ExecError,
5254            > {
5255                use #root::sql::FetcherPool as _;
5256                let _key = ::std::format!("{}__gt", col);
5257                #root::query::QuerySet::<Self>::default()
5258                    .filter(&_key, val)
5259                    .fetch_pool(pool)
5260                    .await
5261            }
5262
5263            /// Fetch every row where `<col> >= val`. Eloquent
5264            /// `Model::where($col, ">=", $val)->get()` parity.
5265            ///
5266            /// # Errors
5267            /// As [`FetcherPool::fetch_pool`].
5268            ///
5269            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5270            pub async fn where_gte(
5271                col: &str,
5272                val: impl ::core::convert::Into<#root::core::SqlValue>,
5273                pool: &#root::sql::Pool,
5274            ) -> ::core::result::Result<
5275                ::std::vec::Vec<Self>,
5276                #root::sql::ExecError,
5277            > {
5278                use #root::sql::FetcherPool as _;
5279                let _key = ::std::format!("{}__gte", col);
5280                #root::query::QuerySet::<Self>::default()
5281                    .filter(&_key, val)
5282                    .fetch_pool(pool)
5283                    .await
5284            }
5285
5286            /// Fetch every row where `<col> < val`. Eloquent
5287            /// `Model::where($col, "<", $val)->get()` parity.
5288            ///
5289            /// # Errors
5290            /// As [`FetcherPool::fetch_pool`].
5291            ///
5292            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5293            pub async fn where_lt(
5294                col: &str,
5295                val: impl ::core::convert::Into<#root::core::SqlValue>,
5296                pool: &#root::sql::Pool,
5297            ) -> ::core::result::Result<
5298                ::std::vec::Vec<Self>,
5299                #root::sql::ExecError,
5300            > {
5301                use #root::sql::FetcherPool as _;
5302                let _key = ::std::format!("{}__lt", col);
5303                #root::query::QuerySet::<Self>::default()
5304                    .filter(&_key, val)
5305                    .fetch_pool(pool)
5306                    .await
5307            }
5308
5309            /// Fetch every row where `<col> <= val`. Eloquent
5310            /// `Model::where($col, "<=", $val)->get()` parity.
5311            ///
5312            /// # Errors
5313            /// As [`FetcherPool::fetch_pool`].
5314            ///
5315            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5316            pub async fn where_lte(
5317                col: &str,
5318                val: impl ::core::convert::Into<#root::core::SqlValue>,
5319                pool: &#root::sql::Pool,
5320            ) -> ::core::result::Result<
5321                ::std::vec::Vec<Self>,
5322                #root::sql::ExecError,
5323            > {
5324                use #root::sql::FetcherPool as _;
5325                let _key = ::std::format!("{}__lte", col);
5326                #root::query::QuerySet::<Self>::default()
5327                    .filter(&_key, val)
5328                    .fetch_pool(pool)
5329                    .await
5330            }
5331
5332            /// Fetch every row where `<col> <> val`. Eloquent
5333            /// `Model::where($col, "!=", $val)->get()` parity.
5334            ///
5335            /// # Errors
5336            /// As [`FetcherPool::fetch_pool`].
5337            ///
5338            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5339            pub async fn where_ne(
5340                col: &str,
5341                val: impl ::core::convert::Into<#root::core::SqlValue>,
5342                pool: &#root::sql::Pool,
5343            ) -> ::core::result::Result<
5344                ::std::vec::Vec<Self>,
5345                #root::sql::ExecError,
5346            > {
5347                use #root::sql::FetcherPool as _;
5348                let _key = ::std::format!("{}__ne", col);
5349                #root::query::QuerySet::<Self>::default()
5350                    .filter(&_key, val)
5351                    .fetch_pool(pool)
5352                    .await
5353            }
5354
5355            /// Fetch every row matching `<col> = val` for ANY of the
5356            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5357            /// parity. Empty `cols` returns no rows.
5358            ///
5359            /// Resolves each `&str` column to its SCHEMA-registered
5360            /// `&'static str` once and builds a single OR-composed
5361            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5362            ///
5363            /// # Errors
5364            /// As [`FetcherPool::fetch_pool`]; `QueryError::UnknownField`
5365            /// when any column is not declared on the model.
5366            ///
5367            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5368            pub async fn where_any(
5369                cols: &[&str],
5370                val: impl ::core::convert::Into<#root::core::SqlValue>,
5371                pool: &#root::sql::Pool,
5372            ) -> ::core::result::Result<
5373                ::std::vec::Vec<Self>,
5374                #root::sql::ExecError,
5375            > {
5376                Self::__where_multi(cols, val, false, pool).await
5377            }
5378
5379            /// Fetch every row matching `<col> = val` for ALL listed
5380            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5381            /// Empty `cols` returns every row (vacuous AND).
5382            ///
5383            /// # Errors
5384            /// As [`Self::where_any`].
5385            pub async fn where_all(
5386                cols: &[&str],
5387                val: impl ::core::convert::Into<#root::core::SqlValue>,
5388                pool: &#root::sql::Pool,
5389            ) -> ::core::result::Result<
5390                ::std::vec::Vec<Self>,
5391                #root::sql::ExecError,
5392            > {
5393                Self::__where_multi(cols, val, true, pool).await
5394            }
5395
5396            /// Internal: build a Q expression composing `cols` via
5397            /// AND (`all`) or OR (`!all`), then fetch. Backs
5398            /// `where_any` / `where_all`.
5399            #[doc(hidden)]
5400            pub async fn __where_multi(
5401                cols: &[&str],
5402                val: impl ::core::convert::Into<#root::core::SqlValue>,
5403                all: bool,
5404                pool: &#root::sql::Pool,
5405            ) -> ::core::result::Result<
5406                ::std::vec::Vec<Self>,
5407                #root::sql::ExecError,
5408            > {
5409                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5410                    .await
5411            }
5412
5413            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5414            /// parity / Django `Model.objects.all()[:n]`. PK-ordered
5415            /// is NOT guaranteed without an explicit `order_by` —
5416            /// drop into `Self::query()` for that.
5417            ///
5418            /// # Errors
5419            /// As [`FetcherPool::fetch_pool`].
5420            ///
5421            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5422            pub async fn take(
5423                n: i64,
5424                pool: &#root::sql::Pool,
5425            ) -> ::core::result::Result<
5426                ::std::vec::Vec<Self>,
5427                #root::sql::ExecError,
5428            > {
5429                use #root::sql::FetcherPool as _;
5430                #root::query::QuerySet::<Self>::default()
5431                    .limit(n)
5432                    .fetch_pool(pool)
5433                    .await
5434            }
5435
5436            /// Fetch the page-th window of `per_page` rows
5437            /// (1-indexed). Eloquent
5438            /// `Model::query()->forPage($page, $perPage)->get()`
5439            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5440            /// LIMIT per_page`; for large offsets this is O(N) —
5441            /// prefer keyset pagination via PK on hot paths.
5442            ///
5443            /// # Errors
5444            /// As [`FetcherPool::fetch_pool`].
5445            ///
5446            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5447            pub async fn for_page(
5448                page: i64,
5449                per_page: i64,
5450                pool: &#root::sql::Pool,
5451            ) -> ::core::result::Result<
5452                ::std::vec::Vec<Self>,
5453                #root::sql::ExecError,
5454            > {
5455                use #root::sql::FetcherPool as _;
5456                let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5457                #root::query::QuerySet::<Self>::default()
5458                    .limit(per_page)
5459                    .offset(_offset)
5460                    .fetch_pool(pool)
5461                    .await
5462            }
5463
5464            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5465            /// one page of rows AND the total row count in one
5466            /// call. Returns `(rows, total)`. Two queries: a
5467            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5468            /// for the total.
5469            ///
5470            /// Useful for paginated UIs that need both the visible
5471            /// rows AND a "Page X of Y" / total-count widget. For
5472            /// large tables prefer keyset pagination + cached count;
5473            /// every call to `paginate` re-counts the full table.
5474            ///
5475            /// Same 1-indexed `page` convention as [`Self::for_page`].
5476            ///
5477            /// # Errors
5478            /// As [`Self::for_page`] and [`Self::count`].
5479            pub async fn paginate(
5480                page: i64,
5481                per_page: i64,
5482                pool: &#root::sql::Pool,
5483            ) -> ::core::result::Result<
5484                (::std::vec::Vec<Self>, i64),
5485                #root::sql::ExecError,
5486            > {
5487                // Route through the queryset rather than `Self::count`:
5488                // the `count` inherent method is suppressed on models
5489                // that declare a field named `count` (field/shortcut
5490                // collision guard), and `paginate` must still compile
5491                // for those models.
5492                let total = {
5493                    use #root::sql::CounterPool as _;
5494                    #root::query::QuerySet::<Self>::default()
5495                        .count_pool(pool)
5496                        .await?
5497                };
5498                let rows = Self::for_page(page, per_page, pool).await?;
5499                ::core::result::Result::Ok((rows, total))
5500            }
5501
5502            /// Bulk-update — set `set_col = set_val` on every row
5503            /// matching `where_col = where_val`. Returns affected row
5504            /// count. Eloquent
5505            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5506            /// parity.
5507            ///
5508            /// For multi-column updates drop into the queryset
5509            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5510            ///
5511            /// # Errors
5512            /// As [`UpdaterPool::execute_pool`].
5513            ///
5514            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5515            pub async fn update_where(
5516                where_col: &str,
5517                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5518                set_col: &str,
5519                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5520                pool: &#root::sql::Pool,
5521            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5522                use #root::sql::UpdaterPool as _;
5523                #root::query::QuerySet::<Self>::default()
5524                    .filter(where_col, where_val)
5525                    .update()
5526                    .set(set_col, set_val)
5527                    .execute_pool(pool)
5528                    .await
5529            }
5530
5531            /// Bulk-delete — remove every row matching
5532            /// `where_col = where_val`. Returns affected row count.
5533            /// Eloquent
5534            /// `Model::where($where_col, $where_val)->delete()` parity.
5535            ///
5536            /// For more complex filters drop into the queryset
5537            /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5538            ///
5539            /// # Errors
5540            /// As [`#root::sql::delete_pool`].
5541            pub async fn delete_where(
5542                where_col: &str,
5543                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5544                pool: &#root::sql::Pool,
5545            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5546                let _query = #root::core::DeleteQuery {
5547                    model: <Self as #root::core::Model>::SCHEMA,
5548                    where_clause: #root::core::WhereExpr::Predicate(
5549                        #root::core::Filter {
5550                            column: <Self as #root::core::Model>::SCHEMA
5551                                .field(where_col)
5552                                .ok_or_else(|| {
5553                                    #root::sql::ExecError::Query(
5554                                        #root::core::QueryError::UnknownField {
5555                                            model: <Self as #root::core::Model>::SCHEMA.name,
5556                                            field: ::std::string::ToString::to_string(where_col),
5557                                        },
5558                                    )
5559                                })?
5560                                .column,
5561                            op: #root::core::Op::Eq,
5562                            value: ::core::convert::Into::into(where_val),
5563                        },
5564                    ),
5565                };
5566                #root::sql::delete_pool(pool, &_query).await
5567            }
5568
5569            /// Bulk-update — set `set_col = set_val` on EVERY row of
5570            /// this model's table. **No WHERE clause** — use with
5571            /// care. Eloquent `Model::query()->update([$col => $val])`
5572            /// parity.
5573            ///
5574            /// Use for backfills, one-shot reset flows, etc. The
5575            /// returned count is rows affected.
5576            ///
5577            /// # Errors
5578            /// As [`UpdaterPool::execute_pool`].
5579            ///
5580            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5581            pub async fn update_all(
5582                set_col: &str,
5583                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5584                pool: &#root::sql::Pool,
5585            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5586                use #root::sql::UpdaterPool as _;
5587                #root::query::QuerySet::<Self>::default()
5588                    .update()
5589                    .set(set_col, set_val)
5590                    .execute_pool(pool)
5591                    .await
5592            }
5593
5594            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5595            /// Eloquent `Model::whereNotLike` parity. Pattern is
5596            /// passed verbatim — caller controls `%` / `_`.
5597            ///
5598            /// # Errors
5599            /// As [`FetcherPool::fetch_pool`].
5600            ///
5601            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5602            pub async fn where_not_like(
5603                col: &str,
5604                pattern: impl ::core::convert::Into<::std::string::String>,
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!("{}__not_like", col);
5612                #root::query::QuerySet::<Self>::default()
5613                    .filter(
5614                        &_key,
5615                        #root::core::SqlValue::String(pattern.into()),
5616                    )
5617                    .fetch_pool(pool)
5618                    .await
5619            }
5620
5621            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5622            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5623            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5624            ///
5625            /// # Errors
5626            /// As [`FetcherPool::fetch_pool`].
5627            ///
5628            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5629            pub async fn where_not_ilike(
5630                col: &str,
5631                pattern: impl ::core::convert::Into<::std::string::String>,
5632                pool: &#root::sql::Pool,
5633            ) -> ::core::result::Result<
5634                ::std::vec::Vec<Self>,
5635                #root::sql::ExecError,
5636            > {
5637                use #root::sql::FetcherPool as _;
5638                let _key = ::std::format!("{}__not_ilike", col);
5639                #root::query::QuerySet::<Self>::default()
5640                    .filter(
5641                        &_key,
5642                        #root::core::SqlValue::String(pattern.into()),
5643                    )
5644                    .fetch_pool(pool)
5645                    .await
5646            }
5647
5648            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5649            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5650            /// parity.
5651            ///
5652            /// # Errors
5653            /// As [`FetcherPool::fetch_pool`].
5654            ///
5655            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5656            pub async fn where_not_between(
5657                col: &str,
5658                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5659                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5660                pool: &#root::sql::Pool,
5661            ) -> ::core::result::Result<
5662                ::std::vec::Vec<Self>,
5663                #root::sql::ExecError,
5664            > {
5665                use #root::sql::FetcherPool as _;
5666                let _key = ::std::format!("{}__not_between", col);
5667                let _vals = #root::core::SqlValue::List(::std::vec![
5668                    ::core::convert::Into::into(lo),
5669                    ::core::convert::Into::into(hi),
5670                ]);
5671                #root::query::QuerySet::<Self>::default()
5672                    .filter(&_key, _vals)
5673                    .fetch_pool(pool)
5674                    .await
5675            }
5676
5677            /// Returns the SQL table name for this model. Eloquent
5678            /// `$model->getTable()` parity.
5679            #[must_use]
5680            pub fn table_name() -> &'static str {
5681                <Self as #root::core::Model>::SCHEMA.table
5682            }
5683
5684            /// Returns the SQL column name of this model's primary
5685            /// key, or `None` when the model has no
5686            /// `#[rustango(primary_key)]`. Eloquent
5687            /// `$model->getKeyName()` parity.
5688            #[must_use]
5689            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5690                <Self as #root::core::Model>::SCHEMA
5691                    .primary_key()
5692                    .map(|f| f.column)
5693            }
5694
5695            /// Fetch every row where `<col> BETWEEN lo AND hi`
5696            /// (inclusive on both ends — same as SQL). Eloquent
5697            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5698            ///
5699            /// # Errors
5700            /// As [`FetcherPool::fetch_pool`].
5701            ///
5702            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5703            pub async fn where_between(
5704                col: &str,
5705                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5706                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5707                pool: &#root::sql::Pool,
5708            ) -> ::core::result::Result<
5709                ::std::vec::Vec<Self>,
5710                #root::sql::ExecError,
5711            > {
5712                use #root::sql::FetcherPool as _;
5713                let _key = ::std::format!("{}__between", col);
5714                let _vals = #root::core::SqlValue::List(::std::vec![
5715                    ::core::convert::Into::into(lo),
5716                    ::core::convert::Into::into(hi),
5717                ]);
5718                #root::query::QuerySet::<Self>::default()
5719                    .filter(&_key, _vals)
5720                    .fetch_pool(pool)
5721                    .await
5722            }
5723
5724            /// Fetch the first row where `<col> = <val>`. Returns
5725            /// `Ok(None)` when no row matches. Eloquent
5726            /// `Model::firstWhere($col, $val)` / Django
5727            /// `Model.objects.filter(col=val).first()` parity.
5728            ///
5729            /// Thin wrapper over `QuerySet::<Self>::default()
5730            /// .filter(col, val).first(pool)`. Use this when you
5731            /// want one row identified by a non-PK column (e.g.
5732            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5733            ///
5734            /// `val` accepts any value `Into<SqlValue>` so plain
5735            /// strings, ints, UUIDs, etc. all work.
5736            ///
5737            /// # Errors
5738            /// As `QuerySet::first`.
5739            pub async fn first_where(
5740                col: &str,
5741                val: impl ::core::convert::Into<#root::core::SqlValue>,
5742                pool: &#root::sql::Pool,
5743            ) -> ::core::result::Result<
5744                ::core::option::Option<Self>,
5745                #root::sql::ExecError,
5746            > {
5747                #root::query::QuerySet::<Self>::default()
5748                    .filter(col, val)
5749                    .first(pool)
5750                    .await
5751            }
5752
5753            #value_method
5754
5755            /// Fetch the row with the largest `field` value —
5756            /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5757            /// `Ok(None)` for an empty table. Eloquent
5758            /// `Model::latest($field)->first()` / Django
5759            /// `Model.objects.latest(field)` (non-throwing) parity.
5760            /// Thin wrapper over `QuerySet::<Self>::default()
5761            /// .latest(field, pool)`.
5762            ///
5763            /// **Field name** is the Rust field ident as a string
5764            /// (not the SQL column). Unknown fields surface as
5765            /// `ExecError::Query(QueryError::UnknownField)` at
5766            /// compile time.
5767            ///
5768            /// # Errors
5769            /// As `QuerySet::latest`.
5770            pub async fn latest(
5771                field: &str,
5772                pool: &#root::sql::Pool,
5773            ) -> ::core::result::Result<
5774                ::core::option::Option<Self>,
5775                #root::sql::ExecError,
5776            > {
5777                #root::query::QuerySet::<Self>::default()
5778                    .latest(field, pool)
5779                    .await
5780            }
5781
5782            /// Sibling of [`Self::latest_pool`] — fetches the row
5783            /// with the smallest `field` value (`ORDER BY <field>
5784            /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5785            /// ->first()` / Django `Model.objects.earliest(field)`
5786            /// parity.
5787            ///
5788            /// # Errors
5789            /// As [`Self::latest_pool`].
5790            pub async fn earliest(
5791                field: &str,
5792                pool: &#root::sql::Pool,
5793            ) -> ::core::result::Result<
5794                ::core::option::Option<Self>,
5795                #root::sql::ExecError,
5796            > {
5797                #root::query::QuerySet::<Self>::default()
5798                    .earliest(field, pool)
5799                    .await
5800            }
5801
5802            #count_method
5803
5804            /// `true` when the table contains at least one row.
5805            /// Eloquent `Model::query()->exists()` / Django
5806            /// `Model.objects.exists()` parity. Thin wrapper over
5807            /// `QuerySet::<Self>::default().exists_pool(pool)`.
5808            ///
5809            /// # Errors
5810            /// As [`ExistsPool::exists_pool`].
5811            ///
5812            /// [`ExistsPool::exists_pool`]: rustango::sql::ExistsPool::exists_pool
5813            pub async fn exists(
5814                pool: &#root::sql::Pool,
5815            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5816                use #root::sql::ExistsPool as _;
5817                #root::query::QuerySet::<Self>::default()
5818                    .exists_pool(pool)
5819                    .await
5820            }
5821
5822            /// Inverse of [`Self::exists_pool`] — returns `true` when
5823            /// the table has zero rows. Eloquent
5824            /// `Model::doesntExist()` parity.
5825            ///
5826            /// # Errors
5827            /// As [`Self::exists_pool`].
5828            pub async fn doesnt_exist(
5829                pool: &#root::sql::Pool,
5830            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5831                Self::exists(pool).await.map(|e| !e)
5832            }
5833
5834            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5835            /// `true` when a row with primary key `pk` exists in
5836            /// the table. Sugar over
5837            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5838            ///
5839            /// Differs from [`Self::exists`] (which checks "any row
5840            /// in the table") by checking a specific PK existence.
5841            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5842            /// because the row is never materialized — the SQL is
5843            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5844            ///
5845            /// # Errors
5846            /// As [`#root::sql::ExistsPool::contains_pk`].
5847            pub async fn contains_pk(
5848                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5849                pool: &#root::sql::Pool,
5850            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5851                use #root::sql::ExistsPool as _;
5852                #root::query::QuerySet::<Self>::default()
5853                    .contains_pk(pool, pk)
5854                    .await
5855            }
5856
5857            #sum_method
5858            #avg_method
5859            #min_method
5860            #max_method
5861
5862            /// Internal: forward to
5863            /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5864            /// Backs `sum` / `avg` / `min` / `max`.
5865            #[doc(hidden)]
5866            pub async fn __aggregate_one_pool<U>(
5867                col: &str,
5868                build: fn(&'static str) -> #root::core::AggregateExpr,
5869                pool: &#root::sql::Pool,
5870            ) -> ::core::result::Result<
5871                ::core::option::Option<U>,
5872                #root::sql::ExecError,
5873            >
5874            where
5875                (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5876                    + #root::sql::MaybeMyFromRow
5877                    + #root::sql::MaybeSqliteFromRow
5878                    + ::core::marker::Send
5879                    + ::core::marker::Unpin,
5880            {
5881                #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
5882                    .await
5883            }
5884
5885            /// Fetch every row of this model from `pool`. Eloquent
5886            /// `Model::all()` parity — a thin wrapper over
5887            /// `QuerySet::<Self>::default().fetch_pool(pool)`.
5888            ///
5889            /// **Use with care on large tables** — there's no
5890            /// pagination or limit; the entire table is materialized
5891            /// into memory. For anything beyond fixture / lookup
5892            /// tables, page through `QuerySet::<Self>::default()
5893            /// .order_by(...).limit(N).offset(M).fetch_pool(pool)`
5894            /// or stream via `.iterator(chunk_size)`.
5895            ///
5896            /// # Errors
5897            /// As [`FetcherPool::fetch_pool`].
5898            ///
5899            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5900            pub async fn all(
5901                pool: &#root::sql::Pool,
5902            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5903            {
5904                use #root::sql::FetcherPool as _;
5905                #root::query::QuerySet::<Self>::default()
5906                    .fetch_pool(pool)
5907                    .await
5908            }
5909
5910            /// Look up every row whose primary key is in `pks`.
5911            /// Returns the matching rows in **inventory** order — NOT
5912            /// the order of `pks`. Empty `pks` returns an empty
5913            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5914            /// with a list) / Django `Model.objects.filter(pk__in=[...])`
5915            /// parity.
5916            ///
5917            /// Thin wrapper over `QuerySet::<Self>::default()
5918            /// .filter("<pk>__in", SqlValue::List([...])).fetch_pool(pool)`.
5919            /// Caller-supplied PKs that don't match a row are
5920            /// silently skipped (the returned vec is shorter than
5921            /// the input list). For an order-preserving / "fail
5922            /// when any missing" variant, build the queryset
5923            /// explicitly with `in_bulk` instead.
5924            ///
5925            /// Accepts any iterable whose elements are
5926            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5927            /// `Vec<Uuid>`, etc.
5928            ///
5929            /// # Errors
5930            /// As [`FetcherPool::fetch_pool`].
5931            ///
5932            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5933            pub async fn find_many<V>(
5934                pks: impl ::core::iter::IntoIterator<Item = V>,
5935                pool: &#root::sql::Pool,
5936            ) -> ::core::result::Result<
5937                ::std::vec::Vec<Self>,
5938                #root::sql::ExecError,
5939            >
5940            where
5941                V: ::core::convert::Into<#root::core::SqlValue>,
5942            {
5943                use #root::sql::FetcherPool as _;
5944                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5945                    pks.into_iter().map(::core::convert::Into::into).collect();
5946                if _values.is_empty() {
5947                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5948                }
5949                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
5950                #root::query::QuerySet::<Self>::default()
5951                    .filter(&_key, #root::core::SqlValue::List(_values))
5952                    .fetch_pool(pool)
5953                    .await
5954            }
5955
5956            /// Look up the row whose primary key equals `pk`. Returns
5957            /// `Ok(None)` when no row matches; this is the
5958            /// non-throwing counterpart of Django's `.get(pk=…)`
5959            /// (which raises `DoesNotExist`). Eloquent `Model::find`
5960            /// shape — accepts any value `Into<SqlValue>`.
5961            ///
5962            /// One-liner shortcut for the common
5963            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
5964            /// .limit(1).fetch_pool(pool).await?.into_iter().next()`
5965            /// dance.
5966            ///
5967            /// # Errors
5968            /// As [`FetcherPool::fetch_pool`].
5969            ///
5970            /// [`FetcherPool::fetch_pool`]: rustango::sql::FetcherPool::fetch_pool
5971            pub async fn find(
5972                pk: impl ::core::convert::Into<#root::core::SqlValue>,
5973                pool: &#root::sql::Pool,
5974            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
5975            {
5976                use #root::sql::FetcherPool as _;
5977                let _pk_val: #root::core::SqlValue = pk.into();
5978                let mut _rows: ::std::vec::Vec<Self> =
5979                    #root::query::QuerySet::<Self>::default()
5980                        .filter(::core::stringify!(#pk_ident), _pk_val)
5981                        .limit(1)
5982                        .fetch_pool(pool)
5983                        .await?;
5984                ::core::result::Result::Ok(_rows.into_iter().next())
5985            }
5986
5987            /// Look up the row whose primary key equals `pk`. Errors
5988            /// when no row matches — the throwing counterpart of
5989            /// [`Self::find_pool`]. Eloquent `Model::findOrFail` /
5990            /// Django `Model.objects.get(pk=…)` (which raises
5991            /// `DoesNotExist`) parity.
5992            ///
5993            /// Translates the miss into
5994            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
5995            /// so callers can `?`-bubble straight through the typical
5996            /// `ExecError` error chain.
5997            ///
5998            /// # Errors
5999            /// As [`Self::find_pool`]; additionally
6000            /// [`sqlx::Error::RowNotFound`] when no row matches.
6001            ///
6002            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6003            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6004            pub async fn find_or_fail(
6005                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6006                pool: &#root::sql::Pool,
6007            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6008                match Self::find(pk, pool).await? {
6009                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6010                    ::core::option::Option::None => ::core::result::Result::Err(
6011                        #root::sql::ExecError::Driver(
6012                            #root::sql::sqlx::Error::RowNotFound,
6013                        ),
6014                    ),
6015                }
6016            }
6017
6018            /// Look up multiple rows by primary key, **failing** if
6019            /// any requested PK is missing. Eloquent
6020            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6021            /// inventory order (NOT request order — same as
6022            /// [`Self::find_many`]).
6023            ///
6024            /// Empty `pks` returns an empty `Vec` (no rows requested
6025            /// → nothing to fail on).
6026            ///
6027            /// Differs from [`Self::find_many`] in that this method
6028            /// requires every PK to resolve. If even one is missing,
6029            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6030            /// in `ExecError::Driver`). Rows are deduped at the SQL
6031            /// layer, so passing the same PK twice in `pks` counts as
6032            /// one expected row.
6033            ///
6034            /// # Errors
6035            /// As [`Self::find_many`], plus `RowNotFound` when the
6036            /// returned row count is less than the count of distinct
6037            /// requested PKs.
6038            pub async fn find_many_or_fail<V>(
6039                pks: impl ::core::iter::IntoIterator<Item = V>,
6040                pool: &#root::sql::Pool,
6041            ) -> ::core::result::Result<
6042                ::std::vec::Vec<Self>,
6043                #root::sql::ExecError,
6044            >
6045            where
6046                V: ::core::convert::Into<#root::core::SqlValue>,
6047            {
6048                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6049                    pks.into_iter().map(::core::convert::Into::into).collect();
6050                if _values.is_empty() {
6051                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6052                }
6053                // Dedup the requested PK list before counting so
6054                // duplicate-PK requests don't false-fail (the SQL
6055                // `IN (...)` clause naturally dedups too).
6056                let mut _seen: ::std::collections::HashSet<
6057                    ::std::string::String,
6058                > = ::std::collections::HashSet::new();
6059                for v in &_values {
6060                    _seen.insert(v.to_display_string());
6061                }
6062                let _expected = _seen.len();
6063                let _rows = Self::find_many(_values, pool).await?;
6064                if _rows.len() < _expected {
6065                    return ::core::result::Result::Err(
6066                        #root::sql::ExecError::Driver(
6067                            #root::sql::sqlx::Error::RowNotFound,
6068                        ),
6069                    );
6070                }
6071                ::core::result::Result::Ok(_rows)
6072            }
6073
6074            /// Find by primary key or run `fallback` to produce a
6075            /// default row to return. Eloquent
6076            /// `Model::findOr($pk, fn() => …)` parity.
6077            ///
6078            /// Unlike [`Self::find_or_fail_pool`] (which raises on
6079            /// miss), this is the "give me something sensible"
6080            /// branch: typical use is "fetch the user's row, else
6081            /// fall back to an anonymous/guest stub".
6082            ///
6083            /// `fallback` runs only when no row matches — the DB
6084            /// round-trip happens unconditionally.
6085            ///
6086            /// # Errors
6087            /// As [`Self::find_pool`].
6088            pub async fn find_or<F>(
6089                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6090                pool: &#root::sql::Pool,
6091                fallback: F,
6092            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6093            where
6094                F: ::core::ops::FnOnce() -> Self,
6095            {
6096                ::core::result::Result::Ok(
6097                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6098                )
6099            }
6100
6101            /// Same as [`Self::find_or`] but also returns a `bool`
6102            /// indicating whether the row was found in the DB
6103            /// (`true`) or freshly built from `fallback` (`false`).
6104            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6105            /// in PHP the returned model also exposes `->exists`;
6106            /// here that's surfaced as the second tuple element.
6107            ///
6108            /// Useful for edit-or-create form handlers where the
6109            /// caller needs to know whether to PATCH or POST when
6110            /// the user submits the form.
6111            ///
6112            /// # Errors
6113            /// As [`Self::find`].
6114            pub async fn find_or_new<F>(
6115                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6116                pool: &#root::sql::Pool,
6117                fallback: F,
6118            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6119            where
6120                F: ::core::ops::FnOnce() -> Self,
6121            {
6122                match Self::find(pk, pool).await? {
6123                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6124                    ::core::option::Option::None => {
6125                        ::core::result::Result::Ok((fallback(), false))
6126                    }
6127                }
6128            }
6129
6130            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6131            /// [`Self::find_or_new`] but **persists** the new row
6132            /// when the PK isn't found. Returns `(row, exists: bool)`
6133            /// — `exists=true` when the PK was found,
6134            /// `false` when a fresh row was inserted.
6135            ///
6136            /// ```ignore
6137            /// let (post, found) = Post::find_or_insert(
6138            ///     pk,
6139            ///     &pool,
6140            ///     || Post { id: Auto::default(), title: "new".into() },
6141            /// ).await?;
6142            /// // `found` true → returned existing row;
6143            /// // `found` false → fallback was inserted, `post.id` populated.
6144            /// ```
6145            ///
6146            /// **Caveat**: two concurrent calls that both miss the
6147            /// find can both INSERT, violating uniqueness. Wrap in a
6148            /// transaction or rely on a UNIQUE constraint + handle
6149            /// the conflict error if you need race-free semantics.
6150            ///
6151            /// # Errors
6152            /// As [`Self::find`] and [`Self::save_pool`].
6153            pub async fn find_or_insert<F>(
6154                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6155                pool: &#root::sql::Pool,
6156                fallback: F,
6157            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6158            where
6159                F: ::core::ops::FnOnce() -> Self,
6160            {
6161                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6162                    return ::core::result::Result::Ok((_row, true));
6163                }
6164                let mut _new = fallback();
6165                _new.save_pool(pool).await?;
6166                ::core::result::Result::Ok((_new, false))
6167            }
6168
6169            /// Fetch the first row of the table, or run `fallback`
6170            /// when the table is empty. Eloquent
6171            /// `Model::firstOr(fn() => …)` parity.
6172            ///
6173            /// # Errors
6174            /// As [`Self::first_pool`].
6175            pub async fn first_or<F>(
6176                pool: &#root::sql::Pool,
6177                fallback: F,
6178            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6179            where
6180                F: ::core::ops::FnOnce() -> Self,
6181            {
6182                // Route through the queryset rather than `Self::first`,
6183                // which is suppressed on models with a field named
6184                // `first` (field/shortcut collision guard).
6185                ::core::result::Result::Ok(
6186                    #root::query::QuerySet::<Self>::default()
6187                        .first(pool)
6188                        .await?
6189                        .unwrap_or_else(fallback),
6190                )
6191            }
6192
6193            /// Fetch exactly one row matching `<col> = <val>`. Errors
6194            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6195            /// or more than one matches
6196            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6197            /// Eloquent `Model::sole($col, $val)` parity.
6198            ///
6199            /// # Errors
6200            /// As [`Self::where_pool`] plus the explicit
6201            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6202            pub async fn sole(
6203                col: &str,
6204                val: impl ::core::convert::Into<#root::core::SqlValue>,
6205                pool: &#root::sql::Pool,
6206            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6207                let mut _rows = Self::where_(col, val, pool).await?;
6208                match _rows.len() {
6209                    0 => ::core::result::Result::Err(
6210                        #root::sql::ExecError::Driver(
6211                            #root::sql::sqlx::Error::RowNotFound,
6212                        ),
6213                    ),
6214                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6215                    n => ::core::result::Result::Err(
6216                        #root::sql::ExecError::MultipleRowsReturned {
6217                            op: "sole",
6218                            table: <Self as #root::core::Model>::SCHEMA.name,
6219                            count: n,
6220                        },
6221                    ),
6222                }
6223            }
6224        }
6225    } else {
6226        quote!()
6227    };
6228
6229    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6230    // the non-audited `_pool` methods but execute against an open
6231    // `PoolTx` so the writes participate in the caller's transaction.
6232    // Auditing inside TX is deferred; these always use the plain
6233    // executor primitives regardless of whether the model is audited.
6234    let tx_insert_method = if fields.has_auto {
6235        let pushes = &fields.insert_pushes;
6236        let returning_cols = &fields.returning_cols;
6237        quote! {
6238            /// Insert this row inside an open transaction, populating
6239            /// any `Auto<T>` PK from the auto-assigned value. Works
6240            /// against any backend that `tx` wraps.
6241            ///
6242            /// # Errors
6243            /// As [`Self::insert_pool`].
6244            pub async fn insert_tx(
6245                &mut self,
6246                tx: &mut #root::sql::PoolTx<'_>,
6247            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6248                let mut _columns: ::std::vec::Vec<&'static str> =
6249                    ::std::vec::Vec::new();
6250                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6251                    ::std::vec::Vec::new();
6252                #( #pushes )*
6253                let _query = #root::core::InsertQuery {
6254                    model: <Self as #root::core::Model>::SCHEMA,
6255                    columns: _columns,
6256                    values: _values,
6257                    returning: ::std::vec![ #( #returning_cols ),* ],
6258                    on_conflict: ::core::option::Option::None,
6259                };
6260                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6261                #root::sql::apply_auto_pk(_result, self)
6262            }
6263        }
6264    } else {
6265        let insert_columns = &fields.insert_columns;
6266        let insert_values = &fields.insert_values;
6267        quote! {
6268            /// Insert this row inside an open transaction.
6269            ///
6270            /// # Errors
6271            /// As [`Self::insert_pool`].
6272            pub async fn insert_tx(
6273                &self,
6274                tx: &mut #root::sql::PoolTx<'_>,
6275            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6276                let _query = #root::core::InsertQuery {
6277                    model: <Self as #root::core::Model>::SCHEMA,
6278                    columns: ::std::vec![ #( #insert_columns ),* ],
6279                    values: ::std::vec![ #( #insert_values ),* ],
6280                    returning: ::std::vec::Vec::new(),
6281                    on_conflict: ::core::option::Option::None,
6282                };
6283                #root::sql::insert_tx(tx, &_query).await
6284            }
6285        }
6286    };
6287
6288    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6289        let pk_column_lit = pk_col.as_str();
6290        let assignments = &fields.update_assignments;
6291        let dispatch_unset = if fields.pk_is_auto {
6292            quote! {
6293                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6294                    return self.insert_tx(tx).await;
6295                }
6296            }
6297        } else {
6298            quote!()
6299        };
6300        quote! {
6301            /// Save this row inside an open transaction. `INSERT` when
6302            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6303            /// PK. Works against any backend that `tx` wraps.
6304            ///
6305            /// # Errors
6306            /// As [`Self::save_pool`].
6307            pub async fn save_tx(
6308                &mut self,
6309                tx: &mut #root::sql::PoolTx<'_>,
6310            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6311                #dispatch_unset
6312                let _query = #root::core::UpdateQuery {
6313                    model: <Self as #root::core::Model>::SCHEMA,
6314                    set: ::std::vec![ #( #assignments ),* ],
6315                    where_clause: #root::core::WhereExpr::Predicate(
6316                        #root::core::Filter {
6317                            column: #pk_column_lit,
6318                            op: #root::core::Op::Eq,
6319                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
6320                                ::core::clone::Clone::clone(&self.#pk_ident)
6321                            ),
6322                        }
6323                    ),
6324                };
6325                let _ = #root::sql::update_tx(tx, &_query).await?;
6326                ::core::result::Result::Ok(())
6327            }
6328        }
6329    } else {
6330        quote!()
6331    };
6332
6333    let tx_delete_method = {
6334        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6335        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6336        if let Some(pk_ident) = pk_ident_for_tx {
6337            quote! {
6338                /// Delete the row identified by this instance's PK
6339                /// inside an open transaction. Works against any backend
6340                /// that `tx` wraps.
6341                ///
6342                /// # Errors
6343                /// As [`Self::delete_pool`].
6344                pub async fn delete_tx(
6345                    &self,
6346                    tx: &mut #root::sql::PoolTx<'_>,
6347                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6348                    let _query = #root::core::DeleteQuery {
6349                        model: <Self as #root::core::Model>::SCHEMA,
6350                        where_clause: #root::core::WhereExpr::Predicate(
6351                            #root::core::Filter {
6352                                column: #pk_column_lit,
6353                                op: #root::core::Op::Eq,
6354                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6355                                    ::core::clone::Clone::clone(&self.#pk_ident)
6356                                ),
6357                            }
6358                        ),
6359                    };
6360                    #root::sql::delete_tx(tx, &_query).await
6361                }
6362            }
6363        } else {
6364            quote!()
6365        }
6366    };
6367
6368    // Update emission captures both BEFORE and AFTER state — runs an
6369    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6370    // each tracked field's prior value, then after the UPDATE diffs
6371    // against the in-memory `&self`. `diff_changes` drops unchanged
6372    // columns so the JSON only contains the actual delta.
6373    //
6374    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6375    // and binds `_audit_before_pairs`; `audit_update_post` runs
6376    // after the UPDATE and emits the PendingEntry.
6377    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6378        audited_fields
6379    {
6380        if tracked.is_empty() {
6381            (quote!(), quote!())
6382        } else {
6383            let select_cols: String = tracked
6384                .iter()
6385                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6386                .collect::<Vec<_>>()
6387                .join(", ");
6388            let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6389            let select_cols_lit = select_cols;
6390            let pk_column_lit_for_select = pk_column_for_select;
6391            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6392                if fields.pk_is_auto {
6393                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6394                } else {
6395                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6396                }
6397            } else {
6398                quote!(0_i64)
6399            };
6400            let before_pairs = tracked.iter().map(|c| {
6401                let column_lit = c.column.as_str();
6402                let value_ty = &c.value_ty;
6403                quote! {
6404                    (
6405                        #column_lit,
6406                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6407                            &_audit_before_row, #column_lit,
6408                        ) {
6409                            ::core::result::Result::Ok(v) => {
6410                                #root::__serde_json::to_value(&v)
6411                                    .unwrap_or(#root::__serde_json::Value::Null)
6412                            }
6413                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6414                        },
6415                    )
6416                }
6417            });
6418            let after_pairs = tracked.iter().map(|c| {
6419                let column_lit = c.column.as_str();
6420                let ident = &c.ident;
6421                quote! {
6422                    (
6423                        #column_lit,
6424                        #root::__serde_json::to_value(&self.#ident)
6425                            .unwrap_or(#root::__serde_json::Value::Null),
6426                    )
6427                }
6428            });
6429            let pk_str = audit_pk_to_string.clone();
6430            let pre = quote! {
6431                let _audit_select_sql = ::std::format!(
6432                    r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6433                    #select_cols_lit,
6434                    <Self as #root::core::Model>::SCHEMA.table,
6435                    #pk_column_lit_for_select,
6436                );
6437                let _audit_before_pairs:
6438                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6439                    match #root::sql::sqlx::query(&_audit_select_sql)
6440                        .bind(#pk_value_for_bind)
6441                        .fetch_optional(&mut *_executor)
6442                        .await
6443                    {
6444                        ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6445                            ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6446                        }
6447                        _ => ::core::option::Option::None,
6448                    };
6449            };
6450            let post = quote! {
6451                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6452                    let _audit_after:
6453                        ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6454                        ::std::vec![ #( #after_pairs ),* ];
6455                    let _audit_entry = #root::audit::PendingEntry {
6456                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
6457                        entity_pk: #pk_str,
6458                        operation: #root::audit::AuditOp::Update,
6459                        source: #root::audit::current_source(),
6460                        changes: #root::audit::diff_changes(
6461                            &_audit_before,
6462                            &_audit_after,
6463                        ),
6464                    };
6465                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6466                }
6467            };
6468            (pre, post)
6469        }
6470    } else {
6471        (quote!(), quote!())
6472    };
6473
6474    // Bulk-insert audit: capture every row's tracked fields after the
6475    // RETURNING populates each PK, then push one batched INSERT INTO
6476    // audit_log via `emit_many`. One round-trip regardless of N rows.
6477    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6478        let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6479            if fields.pk_is_auto {
6480                quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6481            } else {
6482                quote!(::std::format!("{}", &_row.#pk_ident))
6483            }
6484        } else {
6485            quote!(::std::string::String::new())
6486        };
6487        let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6488            let column_lit = c.column.as_str();
6489            let ident = &c.ident;
6490            quote! {
6491                (
6492                    #column_lit,
6493                    #root::__serde_json::to_value(&_row.#ident)
6494                        .unwrap_or(#root::__serde_json::Value::Null),
6495                )
6496            }
6497        });
6498        quote! {
6499            let _audit_source = #root::audit::current_source();
6500            let mut _audit_entries:
6501                ::std::vec::Vec<#root::audit::PendingEntry> =
6502                    ::std::vec::Vec::with_capacity(rows.len());
6503            for _row in rows.iter() {
6504                _audit_entries.push(#root::audit::PendingEntry {
6505                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
6506                    entity_pk: #row_pk_str,
6507                    operation: #root::audit::AuditOp::Create,
6508                    source: _audit_source.clone(),
6509                    changes: #root::audit::snapshot_changes(&[
6510                        #( #row_pairs ),*
6511                    ]),
6512                });
6513            }
6514            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6515        }
6516    } else {
6517        quote!()
6518    };
6519
6520    let save_method = if fields.pk_is_auto {
6521        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6522        let pk_column_lit = pk_column.as_str();
6523        let assignments = &fields.update_assignments;
6524        let upsert_cols = &fields.upsert_update_columns;
6525        let upsert_pushes = &fields.insert_pushes;
6526        let upsert_returning = &fields.returning_cols;
6527        let upsert_auto_assigns = &fields.auto_assigns;
6528        // Conflict target: prefer the first declared `unique_together`
6529        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6530        // `BIGSERIAL` and never collide on insert, so a PK-only target
6531        // would silently turn `upsert()` into "always-insert" for
6532        // surrogate-PK models with composite UNIQUE constraints — see
6533        // `RolePermission` / `UserRole` / `UserPermission` in the
6534        // tenancy permission engine. When no `unique_together` is
6535        // declared we keep the PK target (the original behaviour).
6536        let upsert_target_columns: Vec<String> = indexes
6537            .iter()
6538            .find(|i| i.unique && !i.columns.is_empty())
6539            .map(|i| i.columns.clone())
6540            .unwrap_or_else(|| vec![pk_column.clone()]);
6541        let upsert_target_lits = upsert_target_columns
6542            .iter()
6543            .map(String::as_str)
6544            .collect::<Vec<_>>();
6545        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6546            quote!(#root::core::ConflictClause::DoNothing)
6547        } else {
6548            quote!(#root::core::ConflictClause::DoUpdate {
6549                target: ::std::vec![ #( #upsert_target_lits ),* ],
6550                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6551            })
6552        };
6553        Some(quote! {
6554            /// Insert this row if its `Auto<T>` primary key is
6555            /// `Unset`, otherwise update the existing row matching the
6556            /// PK. Mirrors Django's `save()` — caller doesn't need to
6557            /// pick `insert` vs the bulk-update path manually.
6558            ///
6559            /// On the insert branch, populates the PK from `RETURNING`
6560            /// (same behavior as `insert`). On the update branch,
6561            /// writes every non-PK column back; if no row matches the
6562            /// PK, returns `Ok(())` silently.
6563            ///
6564            /// Only generated when the primary key is declared as
6565            /// `Auto<T>`. Models with a manually-managed PK must use
6566            /// `insert` or the QuerySet update builder.
6567            ///
6568            /// # Errors
6569            /// Returns [`#root::sql::ExecError`] for SQL-writing
6570            /// or driver failures.
6571            #[cfg(feature = "postgres")]
6572            pub async fn save(
6573                &mut self,
6574                pool: &#root::sql::sqlx::PgPool,
6575            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6576                #pool_to_save_on
6577            }
6578
6579            /// Like [`Self::save`] but accepts any sqlx executor —
6580            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6581            /// escape hatch for tenant-scoped writes: schema-mode
6582            /// tenants share the registry pool but rely on a per-
6583            /// checkout `SET search_path`, so passing `&PgPool` would
6584            /// silently hit the wrong schema. Acquire a connection
6585            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6586            ///
6587            /// # Errors
6588            /// As [`Self::save`].
6589            #[cfg(feature = "postgres")]
6590            pub async fn save_on #executor_generics (
6591                &mut self,
6592                #executor_param,
6593            ) -> ::core::result::Result<(), #root::sql::ExecError>
6594            #executor_where
6595            {
6596                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6597                    return self.insert_on(#executor_passes_to_data_write).await;
6598                }
6599                #audit_update_pre
6600                let _query = #root::core::UpdateQuery {
6601                    model: <Self as #root::core::Model>::SCHEMA,
6602                    set: ::std::vec![ #( #assignments ),* ],
6603                    where_clause: #root::core::WhereExpr::Predicate(
6604                        #root::core::Filter {
6605                            column: #pk_column_lit,
6606                            op: #root::core::Op::Eq,
6607                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
6608                                ::core::clone::Clone::clone(&self.#pk_ident)
6609                            ),
6610                        }
6611                    ),
6612                };
6613                let _ = #root::sql::__macro_internals::update_on(
6614                    #executor_passes_to_data_write,
6615                    &_query,
6616                ).await?;
6617                #audit_update_post
6618                ::core::result::Result::Ok(())
6619            }
6620
6621            /// Per-call override for the audit source. Runs
6622            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6623            /// scope so the resulting audit entry records `source`
6624            /// instead of the task-local default. Useful for seed
6625            /// scripts and one-off CLI tools that don't sit inside an
6626            /// admin handler. The override applies only to this call;
6627            /// no global state changes.
6628            ///
6629            /// # Errors
6630            /// As [`Self::save_on`].
6631            #[cfg(feature = "postgres")]
6632            pub async fn save_on_with #executor_generics (
6633                &mut self,
6634                #executor_param,
6635                source: #root::audit::AuditSource,
6636            ) -> ::core::result::Result<(), #root::sql::ExecError>
6637            #executor_where
6638            {
6639                #root::audit::with_source(source, self.save_on(_executor)).await
6640            }
6641
6642            /// Insert this row or update it in-place if the primary key already
6643            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6644            ///
6645            /// With `Auto::Unset` PK the server assigns a new key and no conflict
6646            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6647            /// inserted if absent or all non-PK columns are overwritten if present.
6648            ///
6649            /// # Errors
6650            /// As [`Self::insert_on`].
6651            #[cfg(feature = "postgres")]
6652            pub async fn upsert(
6653                &mut self,
6654                pool: &#root::sql::sqlx::PgPool,
6655            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6656                #pool_to_upsert_on
6657            }
6658
6659            /// Like [`Self::upsert`] but accepts any sqlx executor.
6660            /// See [`Self::save_on`] for tenancy-scoped rationale.
6661            ///
6662            /// # Errors
6663            /// As [`Self::upsert`].
6664            #[cfg(feature = "postgres")]
6665            pub async fn upsert_on #executor_generics (
6666                &mut self,
6667                #executor_param,
6668            ) -> ::core::result::Result<(), #root::sql::ExecError>
6669            #executor_where
6670            {
6671                let mut _columns: ::std::vec::Vec<&'static str> =
6672                    ::std::vec::Vec::new();
6673                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6674                    ::std::vec::Vec::new();
6675                #( #upsert_pushes )*
6676                let query = #root::core::InsertQuery {
6677                    model: <Self as #root::core::Model>::SCHEMA,
6678                    columns: _columns,
6679                    values: _values,
6680                    returning: ::std::vec![ #( #upsert_returning ),* ],
6681                    on_conflict: ::core::option::Option::Some(#conflict_clause),
6682                };
6683                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6684                    #executor_passes_to_data_write,
6685                    &query,
6686                ).await?;
6687                let _returning_row = &_returning_row_v;
6688                #( #upsert_auto_assigns )*
6689                ::core::result::Result::Ok(())
6690            }
6691        })
6692    } else {
6693        None
6694    };
6695
6696    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6697        let pk_column_lit = pk_column.as_str();
6698        // Optional `soft_delete_on` / `restore_on` companions when the
6699        // model has a `#[rustango(soft_delete)]` column. They land
6700        // alongside the regular `delete_on` so callers have both
6701        // options — a hard delete (audit-tracked as a real DELETE) and
6702        // a logical delete (audit-tracked as an UPDATE setting the
6703        // deleted_at column to NOW()).
6704        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6705            let col_lit = col;
6706            let sd_field_ident = fields
6707                .soft_delete_field_ident
6708                .clone()
6709                .expect("soft_delete_column without ident");
6710            quote! {
6711                /// Soft-delete this row by setting its
6712                /// `#[rustango(soft_delete)]` column to `NOW()`.
6713                /// Mirrors Django's `SoftDeleteModel.delete()` shape:
6714                /// the row stays in the table; query helpers can
6715                /// filter it out by checking the column for `IS NOT
6716                /// NULL`.
6717                ///
6718                /// # Errors
6719                /// As [`Self::delete`].
6720                pub async fn soft_delete_on #executor_generics (
6721                    &self,
6722                    #executor_param,
6723                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6724                #executor_where
6725                {
6726                    let _query = #root::core::UpdateQuery {
6727                        model: <Self as #root::core::Model>::SCHEMA,
6728                        set: ::std::vec![
6729                            #root::core::Assignment {
6730                                column: #col_lit,
6731                                value: ::core::convert::Into::<#root::core::Expr>::into(
6732                                    ::core::convert::Into::<#root::core::SqlValue>::into(
6733                                        #root::__chrono::Utc::now()
6734                                    )
6735                                ),
6736                            },
6737                        ],
6738                        where_clause: #root::core::WhereExpr::Predicate(
6739                            #root::core::Filter {
6740                                column: #pk_column_lit,
6741                                op: #root::core::Op::Eq,
6742                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6743                                    ::core::clone::Clone::clone(&self.#pk_ident)
6744                                ),
6745                            }
6746                        ),
6747                    };
6748                    let _affected = #root::sql::__macro_internals::update_on(
6749                        #executor_passes_to_data_write,
6750                        &_query,
6751                    ).await?;
6752                    #audit_softdelete_emit
6753                    ::core::result::Result::Ok(_affected)
6754                }
6755
6756                /// Inverse of [`Self::soft_delete_on`] — clears the
6757                /// soft-delete column back to NULL so the row is
6758                /// considered live again.
6759                ///
6760                /// # Errors
6761                /// As [`Self::delete`].
6762                pub async fn restore_on #executor_generics (
6763                    &self,
6764                    #executor_param,
6765                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6766                #executor_where
6767                {
6768                    let _query = #root::core::UpdateQuery {
6769                        model: <Self as #root::core::Model>::SCHEMA,
6770                        set: ::std::vec![
6771                            #root::core::Assignment {
6772                                column: #col_lit,
6773                                value: ::core::convert::Into::<#root::core::Expr>::into(
6774                                    #root::core::SqlValue::Null
6775                                ),
6776                            },
6777                        ],
6778                        where_clause: #root::core::WhereExpr::Predicate(
6779                            #root::core::Filter {
6780                                column: #pk_column_lit,
6781                                op: #root::core::Op::Eq,
6782                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6783                                    ::core::clone::Clone::clone(&self.#pk_ident)
6784                                ),
6785                            }
6786                        ),
6787                    };
6788                    let _affected = #root::sql::__macro_internals::update_on(
6789                        #executor_passes_to_data_write,
6790                        &_query,
6791                    ).await?;
6792                    #audit_restore_emit
6793                    ::core::result::Result::Ok(_affected)
6794                }
6795
6796                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6797                /// — takes [`#root::sql::Pool`] and dispatches per
6798                /// backend. Eloquent `Model::delete()` semantics on
6799                /// soft-delete-enabled models (closes #821 partial).
6800                ///
6801                /// Sets the `#[rustango(soft_delete)]` column to
6802                /// `NOW()` on every backend. Query helpers
6803                /// (`QuerySet::active()` / `only_trashed()`,
6804                /// `soft_delete::active_filter` /
6805                /// `compose_with_active`) filter trashed rows out by
6806                /// reading `IS NULL` on the same column.
6807                ///
6808                /// # Errors
6809                /// As [`#root::sql::update_pool`].
6810                pub async fn soft_delete(
6811                    &self,
6812                    pool: &#root::sql::Pool,
6813                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6814                    let _query = #root::core::UpdateQuery {
6815                        model: <Self as #root::core::Model>::SCHEMA,
6816                        set: ::std::vec![
6817                            #root::core::Assignment {
6818                                column: #col_lit,
6819                                value: ::core::convert::Into::<#root::core::Expr>::into(
6820                                    ::core::convert::Into::<#root::core::SqlValue>::into(
6821                                        #root::__chrono::Utc::now()
6822                                    )
6823                                ),
6824                            },
6825                        ],
6826                        where_clause: #root::core::WhereExpr::Predicate(
6827                            #root::core::Filter {
6828                                column: #pk_column_lit,
6829                                op: #root::core::Op::Eq,
6830                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6831                                    ::core::clone::Clone::clone(&self.#pk_ident)
6832                                ),
6833                            }
6834                        ),
6835                    };
6836                    #root::sql::update_pool(pool, &_query).await
6837                }
6838
6839                /// Tri-dialect counterpart of [`Self::restore_on`].
6840                /// Clears the `#[rustango(soft_delete)]` column back
6841                /// to `NULL`, marking the row live again. Eloquent
6842                /// `Model::restore()` parity.
6843                ///
6844                /// # Errors
6845                /// As [`#root::sql::update_pool`].
6846                pub async fn restore(
6847                    &self,
6848                    pool: &#root::sql::Pool,
6849                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6850                    let _query = #root::core::UpdateQuery {
6851                        model: <Self as #root::core::Model>::SCHEMA,
6852                        set: ::std::vec![
6853                            #root::core::Assignment {
6854                                column: #col_lit,
6855                                value: ::core::convert::Into::<#root::core::Expr>::into(
6856                                    #root::core::SqlValue::Null
6857                                ),
6858                            },
6859                        ],
6860                        where_clause: #root::core::WhereExpr::Predicate(
6861                            #root::core::Filter {
6862                                column: #pk_column_lit,
6863                                op: #root::core::Op::Eq,
6864                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6865                                    ::core::clone::Clone::clone(&self.#pk_ident)
6866                                ),
6867                            }
6868                        ),
6869                    };
6870                    #root::sql::update_pool(pool, &_query).await
6871                }
6872
6873                /// Hard-delete this row, ignoring the soft-delete
6874                /// column. Eloquent `Model::forceDelete()` parity —
6875                /// the escape hatch when you need to actually purge
6876                /// data (GDPR, fixture cleanup, etc.).
6877                ///
6878                /// Equivalent to [`Self::delete_pool`] (the framework's
6879                /// non-soft delete) — exposed under the Eloquent name
6880                /// for muscle-memory + so soft-delete-enabled models
6881                /// have all three operations (soft / restore / force)
6882                /// in one place.
6883                ///
6884                /// # Errors
6885                /// As [`Self::delete_pool`].
6886                pub async fn force_delete(
6887                    &self,
6888                    pool: &#root::sql::Pool,
6889                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6890                    Self::delete_pool(self, pool).await
6891                }
6892
6893                /// Returns `true` when this row is soft-deleted (its
6894                /// `#[rustango(soft_delete)]` column is currently
6895                /// set — Eloquent `$model->trashed()` parity).
6896                ///
6897                /// Pure in-memory predicate over `&self`; does not
6898                /// hit the database. Useful in admin/template code
6899                /// like `{% if post.trashed() %}…{% endif %}` and in
6900                /// guard clauses on restore/force-delete flows.
6901                pub fn trashed(&self) -> bool {
6902                    ::core::option::Option::is_some(&self.#sd_field_ident)
6903                }
6904
6905                /// Fetch every row whose `#[rustango(soft_delete)]`
6906                /// column is `NULL` (a.k.a. the live, non-trashed
6907                /// rows). Eloquent's default `Model::all()` behavior on
6908                /// a soft-delete model (Eloquent auto-scopes trashed
6909                /// rows out; rustango doesn't have auto-scoping yet —
6910                /// see issue #820 — so this is the explicit shortcut).
6911                ///
6912                /// One-liner over `QuerySet::<Self>::default()
6913                /// .active().fetch_pool(pool)`. Closes #821 partial.
6914                ///
6915                /// # Errors
6916                /// As [`#root::sql::FetcherPool::fetch_pool`].
6917                pub async fn active(
6918                    pool: &#root::sql::Pool,
6919                ) -> ::core::result::Result<
6920                    ::std::vec::Vec<Self>,
6921                    #root::sql::ExecError,
6922                > {
6923                    use #root::sql::FetcherPool as _;
6924                    #root::query::QuerySet::<Self>::default()
6925                        .active()
6926                        .fetch_pool(pool)
6927                        .await
6928                }
6929
6930                /// Fetch ONLY soft-deleted rows. Eloquent
6931                /// `Model::onlyTrashed()->get()` parity — drives the
6932                /// admin "Trash" page, restore flows, GDPR purge
6933                /// scans, etc.
6934                ///
6935                /// One-liner over `QuerySet::<Self>::default()
6936                /// .only_trashed().fetch_pool(pool)`. Closes #821
6937                /// partial.
6938                ///
6939                /// # Errors
6940                /// As [`#root::sql::FetcherPool::fetch_pool`].
6941                pub async fn only_trashed(
6942                    pool: &#root::sql::Pool,
6943                ) -> ::core::result::Result<
6944                    ::std::vec::Vec<Self>,
6945                    #root::sql::ExecError,
6946                > {
6947                    use #root::sql::FetcherPool as _;
6948                    #root::query::QuerySet::<Self>::default()
6949                        .only_trashed()
6950                        .fetch_pool(pool)
6951                        .await
6952                }
6953
6954                /// Fetch every row, both live and soft-deleted.
6955                /// Eloquent `Model::withTrashed()->get()` parity.
6956                ///
6957                /// Today every queryset already includes trashed rows
6958                /// (rustango has no global-scope tracking yet — issue
6959                /// #820), so this is functionally equivalent to
6960                /// [`Self::all_pool`]. Exposed as a named shortcut so
6961                /// soft-delete-aware code reads `Model::with_trashed_pool`
6962                /// rather than `Model::all_pool` — keeps intent visible
6963                /// in callers and stays correct when auto-scoping lands.
6964                ///
6965                /// Closes #821 partial.
6966                ///
6967                /// # Errors
6968                /// As [`#root::sql::FetcherPool::fetch_pool`].
6969                pub async fn with_trashed(
6970                    pool: &#root::sql::Pool,
6971                ) -> ::core::result::Result<
6972                    ::std::vec::Vec<Self>,
6973                    #root::sql::ExecError,
6974                > {
6975                    use #root::sql::FetcherPool as _;
6976                    #root::query::QuerySet::<Self>::default()
6977                        .with_trashed()
6978                        .fetch_pool(pool)
6979                        .await
6980                }
6981
6982            }
6983        } else {
6984            quote!()
6985        };
6986        quote! {
6987            /// Delete the row identified by this instance's primary key.
6988            ///
6989            /// Returns the number of rows affected (0 or 1).
6990            ///
6991            /// # Errors
6992            /// Returns [`#root::sql::ExecError`] for SQL-writing or
6993            /// driver failures.
6994            #[cfg(feature = "postgres")]
6995            pub async fn delete(
6996                &self,
6997                pool: &#root::sql::sqlx::PgPool,
6998            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6999                #pool_to_delete_on
7000            }
7001
7002            /// Like [`Self::delete`] but accepts any sqlx executor —
7003            /// for tenant-scoped deletes against an explicitly-acquired
7004            /// connection. See [`Self::save_on`] for the rationale.
7005            ///
7006            /// # Errors
7007            /// As [`Self::delete`].
7008            #[cfg(feature = "postgres")]
7009            pub async fn delete_on #executor_generics (
7010                &self,
7011                #executor_param,
7012            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7013            #executor_where
7014            {
7015                let query = #root::core::DeleteQuery {
7016                    model: <Self as #root::core::Model>::SCHEMA,
7017                    where_clause: #root::core::WhereExpr::Predicate(
7018                        #root::core::Filter {
7019                            column: #pk_column_lit,
7020                            op: #root::core::Op::Eq,
7021                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
7022                                ::core::clone::Clone::clone(&self.#pk_ident)
7023                            ),
7024                        }
7025                    ),
7026                };
7027                let _affected = #root::sql::__macro_internals::delete_on(
7028                    #executor_passes_to_data_write,
7029                    &query,
7030                ).await?;
7031                #audit_delete_emit
7032                ::core::result::Result::Ok(_affected)
7033            }
7034
7035            /// Per-call audit-source override for [`Self::delete_on`].
7036            /// See [`Self::save_on_with`] for shape rationale.
7037            ///
7038            /// # Errors
7039            /// As [`Self::delete_on`].
7040            #[cfg(feature = "postgres")]
7041            pub async fn delete_on_with #executor_generics (
7042                &self,
7043                #executor_param,
7044                source: #root::audit::AuditSource,
7045            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7046            #executor_where
7047            {
7048                #root::audit::with_source(source, self.delete_on(_executor)).await
7049            }
7050            #pool_delete_method
7051            #pool_insert_method
7052            #pool_save_method
7053            #refresh_replicate_methods
7054            #tx_delete_method
7055            #tx_insert_method
7056            #tx_save_method
7057            #soft_delete_methods
7058
7059            /// Returns `true` when `other` represents the same DB
7060            /// row as `self` — i.e. their primary keys compare
7061            /// equal. Eloquent `$model->is($other)` parity.
7062            ///
7063            /// Because both arguments are typed `&Self`, the
7064            /// model/table check is automatic — `Post::is` cannot
7065            /// be invoked against a `Comment` at compile time. Only
7066            /// the PK has to be compared at runtime.
7067            pub fn is(&self, other: &Self) -> bool {
7068                self.#pk_ident == other.#pk_ident
7069            }
7070
7071            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7072            /// parity.
7073            pub fn is_not(&self, other: &Self) -> bool {
7074                self.#pk_ident != other.#pk_ident
7075            }
7076
7077            /// Returns this row's primary-key value as an
7078            /// [`#root::core::SqlValue`]. Eloquent
7079            /// `$model->getKey()` parity.
7080            ///
7081            /// Useful when you need to thread the PK through a
7082            /// generic `Into<SqlValue>`-bound API without knowing the
7083            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7084            #[must_use]
7085            pub fn get_key(&self) -> #root::core::SqlValue {
7086                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7087            }
7088
7089        }
7090    });
7091
7092    let insert_method = if fields.has_auto {
7093        let pushes = &fields.insert_pushes;
7094        let returning_cols = &fields.returning_cols;
7095        let auto_assigns = &fields.auto_assigns;
7096        quote! {
7097            /// Insert this row into its table. Skips columns whose
7098            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7099            /// sequence fills them in, then reads each `Auto` column
7100            /// back via `RETURNING` and stores it on `self`.
7101            ///
7102            /// # Errors
7103            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7104            /// driver failures.
7105            #[cfg(feature = "postgres")]
7106            pub async fn insert(
7107                &mut self,
7108                pool: &#root::sql::sqlx::PgPool,
7109            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7110                #pool_to_insert_on
7111            }
7112
7113            /// Like [`Self::insert`] but accepts any sqlx executor.
7114            /// See [`Self::save_on`] for tenancy-scoped rationale.
7115            ///
7116            /// # Errors
7117            /// As [`Self::insert`].
7118            #[cfg(feature = "postgres")]
7119            pub async fn insert_on #executor_generics (
7120                &mut self,
7121                #executor_param,
7122            ) -> ::core::result::Result<(), #root::sql::ExecError>
7123            #executor_where
7124            {
7125                let mut _columns: ::std::vec::Vec<&'static str> =
7126                    ::std::vec::Vec::new();
7127                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7128                    ::std::vec::Vec::new();
7129                #( #pushes )*
7130                let query = #root::core::InsertQuery {
7131                    model: <Self as #root::core::Model>::SCHEMA,
7132                    columns: _columns,
7133                    values: _values,
7134                    returning: ::std::vec![ #( #returning_cols ),* ],
7135                    on_conflict: ::core::option::Option::None,
7136                };
7137                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7138                    #executor_passes_to_data_write,
7139                    &query,
7140                ).await?;
7141                let _returning_row = &_returning_row_v;
7142                #( #auto_assigns )*
7143                #audit_insert_emit
7144                ::core::result::Result::Ok(())
7145            }
7146
7147            /// Per-call audit-source override for [`Self::insert_on`].
7148            /// See [`Self::save_on_with`] for shape rationale.
7149            ///
7150            /// # Errors
7151            /// As [`Self::insert_on`].
7152            #[cfg(feature = "postgres")]
7153            pub async fn insert_on_with #executor_generics (
7154                &mut self,
7155                #executor_param,
7156                source: #root::audit::AuditSource,
7157            ) -> ::core::result::Result<(), #root::sql::ExecError>
7158            #executor_where
7159            {
7160                #root::audit::with_source(source, self.insert_on(_executor)).await
7161            }
7162        }
7163    } else {
7164        let insert_columns = &fields.insert_columns;
7165        let insert_values = &fields.insert_values;
7166        quote! {
7167            /// Insert this row into its table.
7168            ///
7169            /// # Errors
7170            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7171            /// driver failures.
7172            #[cfg(feature = "postgres")]
7173            pub async fn insert(
7174                &self,
7175                pool: &#root::sql::sqlx::PgPool,
7176            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7177                self.insert_on(pool).await
7178            }
7179
7180            /// Like [`Self::insert`] but accepts any sqlx executor.
7181            /// See [`Self::save_on`] for tenancy-scoped rationale.
7182            ///
7183            /// # Errors
7184            /// As [`Self::insert`].
7185            #[cfg(feature = "postgres")]
7186            pub async fn insert_on<'_c, _E>(
7187                &self,
7188                _executor: _E,
7189            ) -> ::core::result::Result<(), #root::sql::ExecError>
7190            where
7191                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7192            {
7193                let query = #root::core::InsertQuery {
7194                    model: <Self as #root::core::Model>::SCHEMA,
7195                    columns: ::std::vec![ #( #insert_columns ),* ],
7196                    values: ::std::vec![ #( #insert_values ),* ],
7197                    returning: ::std::vec::Vec::new(),
7198                    on_conflict: ::core::option::Option::None,
7199                };
7200                #root::sql::__macro_internals::insert_on(_executor, &query).await
7201            }
7202        }
7203    };
7204
7205    let bulk_insert_method = if fields.has_auto {
7206        let cols_no_auto = &fields.bulk_columns_no_auto;
7207        let cols_all = &fields.bulk_columns_all;
7208        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7209        let pushes_all = &fields.bulk_pushes_all;
7210        let returning_cols = &fields.returning_cols;
7211        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7212        let uniformity = &fields.bulk_auto_uniformity;
7213        let first_auto_ident = fields
7214            .first_auto_ident
7215            .as_ref()
7216            .expect("has_auto implies first_auto_ident is Some");
7217        quote! {
7218            /// Bulk-insert `rows` in a single round-trip. Every row's
7219            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7220            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7221            /// (caller-supplied values). Mixed Set/Unset is rejected
7222            /// — call `insert` per row for that case.
7223            ///
7224            /// Empty slice is a no-op. Each row's `Auto` fields are
7225            /// populated from the `RETURNING` clause in input order
7226            /// before this returns.
7227            ///
7228            /// # Errors
7229            /// Returns [`#root::sql::ExecError`] for validation,
7230            /// SQL-writing, mixed-Auto rejection, or driver failures.
7231            #[cfg(feature = "postgres")]
7232            pub async fn bulk_insert(
7233                rows: &mut [Self],
7234                pool: &#root::sql::sqlx::PgPool,
7235            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7236                #pool_to_bulk_insert_on
7237            }
7238
7239            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7240            /// See [`Self::save_on`] for tenancy-scoped rationale.
7241            ///
7242            /// # Errors
7243            /// As [`Self::bulk_insert`].
7244            #[cfg(feature = "postgres")]
7245            pub async fn bulk_insert_on #executor_generics (
7246                rows: &mut [Self],
7247                #executor_param,
7248            ) -> ::core::result::Result<(), #root::sql::ExecError>
7249            #executor_where
7250            {
7251                if rows.is_empty() {
7252                    return ::core::result::Result::Ok(());
7253                }
7254                let _first_unset = matches!(
7255                    rows[0].#first_auto_ident,
7256                    #root::sql::Auto::Unset
7257                );
7258                #( #uniformity )*
7259
7260                let mut _all_rows: ::std::vec::Vec<
7261                    ::std::vec::Vec<#root::core::SqlValue>,
7262                > = ::std::vec::Vec::with_capacity(rows.len());
7263                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7264                    for _row in rows.iter() {
7265                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7266                            ::std::vec::Vec::new();
7267                        #( #pushes_no_auto )*
7268                        _all_rows.push(_row_vals);
7269                    }
7270                    ::std::vec![ #( #cols_no_auto ),* ]
7271                } else {
7272                    for _row in rows.iter() {
7273                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7274                            ::std::vec::Vec::new();
7275                        #( #pushes_all )*
7276                        _all_rows.push(_row_vals);
7277                    }
7278                    ::std::vec![ #( #cols_all ),* ]
7279                };
7280
7281                let _query = #root::core::BulkInsertQuery {
7282                    model: <Self as #root::core::Model>::SCHEMA,
7283                    columns: _columns,
7284                    rows: _all_rows,
7285                    returning: ::std::vec![ #( #returning_cols ),* ],
7286                    on_conflict: ::core::option::Option::None,
7287                };
7288                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7289                    #executor_passes_to_data_write,
7290                    &_query,
7291                ).await?;
7292                if _returned.len() != rows.len() {
7293                    return ::core::result::Result::Err(
7294                        #root::sql::ExecError::Sql(
7295                            #root::sql::SqlError::BulkInsertReturningMismatch {
7296                                expected: rows.len(),
7297                                actual: _returned.len(),
7298                            }
7299                        )
7300                    );
7301                }
7302                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7303                    #auto_assigns_for_row
7304                }
7305                #audit_bulk_insert_emit
7306                ::core::result::Result::Ok(())
7307            }
7308        }
7309    } else {
7310        let cols_all = &fields.bulk_columns_all;
7311        let pushes_all = &fields.bulk_pushes_all;
7312        quote! {
7313            /// Bulk-insert `rows` in a single round-trip. Every row's
7314            /// fields are written verbatim — there are no `Auto<T>`
7315            /// fields on this model.
7316            ///
7317            /// Empty slice is a no-op.
7318            ///
7319            /// # Errors
7320            /// Returns [`#root::sql::ExecError`] for validation,
7321            /// SQL-writing, or driver failures.
7322            #[cfg(feature = "postgres")]
7323            pub async fn bulk_insert(
7324                rows: &[Self],
7325                pool: &#root::sql::sqlx::PgPool,
7326            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7327                Self::bulk_insert_on(rows, pool).await
7328            }
7329
7330            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7331            /// See [`Self::save_on`] for tenancy-scoped rationale.
7332            ///
7333            /// # Errors
7334            /// As [`Self::bulk_insert`].
7335            #[cfg(feature = "postgres")]
7336            pub async fn bulk_insert_on<'_c, _E>(
7337                rows: &[Self],
7338                _executor: _E,
7339            ) -> ::core::result::Result<(), #root::sql::ExecError>
7340            where
7341                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7342            {
7343                if rows.is_empty() {
7344                    return ::core::result::Result::Ok(());
7345                }
7346                let mut _all_rows: ::std::vec::Vec<
7347                    ::std::vec::Vec<#root::core::SqlValue>,
7348                > = ::std::vec::Vec::with_capacity(rows.len());
7349                for _row in rows.iter() {
7350                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7351                        ::std::vec::Vec::new();
7352                    #( #pushes_all )*
7353                    _all_rows.push(_row_vals);
7354                }
7355                let _query = #root::core::BulkInsertQuery {
7356                    model: <Self as #root::core::Model>::SCHEMA,
7357                    columns: ::std::vec![ #( #cols_all ),* ],
7358                    rows: _all_rows,
7359                    returning: ::std::vec::Vec::new(),
7360                    on_conflict: ::core::option::Option::None,
7361                };
7362                let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7363                ::core::result::Result::Ok(())
7364            }
7365        }
7366    };
7367
7368    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7369    // (no postgres-feature gate); routes through the existing
7370    // `bulk_insert_pool` + per-dialect conflict writer.
7371    //
7372    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7373    // sequence picks the PK for fresh inserts; the UPDATE branch never
7374    // touches the Auto column.
7375    let bulk_upsert_pool_method = {
7376        // Pick the "no Auto" columns when the model has Auto fields,
7377        // else every column.
7378        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7379            (
7380                fields.bulk_columns_no_auto.clone(),
7381                fields.bulk_pushes_no_auto.clone(),
7382            )
7383        } else {
7384            (
7385                fields.bulk_columns_all.clone(),
7386                fields.bulk_pushes_all.clone(),
7387            )
7388        };
7389        quote! {
7390            /// Tri-dialect `bulk_create(update_conflicts=True)` — Django's
7391            /// canonical "import a batch idempotently" shape. Issue #267
7392            /// / T1.5.
7393            ///
7394            /// Per-row values are extracted and lowered into a
7395            /// [`#root::core::BulkInsertQuery`] with
7396            /// `on_conflict = DoUpdate { target, update_columns }`. The
7397            /// writer dispatches per-dialect:
7398            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7399            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7400            ///
7401            /// `target` names the column(s) whose unique constraint
7402            /// defines the conflict (typically a `unique` or
7403            /// `unique_together` natural-key column, NOT the `Auto<T>`
7404            /// PK). `update_cols` names the columns to overwrite on
7405            /// conflict — every other column is left untouched on the
7406            /// existing row.
7407            ///
7408            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7409            /// picks the PK on insert; the update path never touches
7410            /// the Auto column). Auto-set rows trigger a hard error.
7411            /// Empty slice is a no-op.
7412            ///
7413            /// # Errors
7414            /// Returns [`#root::sql::ExecError`] for validation,
7415            /// SQL-writing, or driver failures.
7416            pub async fn bulk_upsert_pool(
7417                rows: &[Self],
7418                target: &[&'static str],
7419                update_cols: &[&'static str],
7420                pool: &#root::sql::Pool,
7421            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7422                if rows.is_empty() {
7423                    return ::core::result::Result::Ok(());
7424                }
7425                let mut _all_rows: ::std::vec::Vec<
7426                    ::std::vec::Vec<#root::core::SqlValue>,
7427                > = ::std::vec::Vec::with_capacity(rows.len());
7428                for _row in rows.iter() {
7429                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7430                        ::std::vec::Vec::new();
7431                    #( #upsert_pushes )*
7432                    _all_rows.push(_row_vals);
7433                }
7434                let _query = #root::core::BulkInsertQuery {
7435                    model: <Self as #root::core::Model>::SCHEMA,
7436                    columns: ::std::vec![ #( #upsert_cols ),* ],
7437                    rows: _all_rows,
7438                    returning: ::std::vec::Vec::new(),
7439                    on_conflict: ::core::option::Option::Some(
7440                        #root::core::ConflictClause::DoUpdate {
7441                            target: target.to_vec(),
7442                            update_columns: update_cols.to_vec(),
7443                        }
7444                    ),
7445                };
7446                #root::sql::bulk_insert_pool(pool, &_query).await
7447            }
7448
7449            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7450            /// skip rows that would violate a unique constraint. Issue
7451            /// #267 / T1.5. Same per-dialect dispatch as
7452            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7453            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7454            /// <pivot> = <pivot>` (MySQL no-op write).
7455            ///
7456            /// # Errors
7457            /// As [`Self::bulk_upsert_pool`].
7458            pub async fn bulk_insert_or_ignore_pool(
7459                rows: &[Self],
7460                pool: &#root::sql::Pool,
7461            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7462                if rows.is_empty() {
7463                    return ::core::result::Result::Ok(());
7464                }
7465                let mut _all_rows: ::std::vec::Vec<
7466                    ::std::vec::Vec<#root::core::SqlValue>,
7467                > = ::std::vec::Vec::with_capacity(rows.len());
7468                for _row in rows.iter() {
7469                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7470                        ::std::vec::Vec::new();
7471                    #( #upsert_pushes )*
7472                    _all_rows.push(_row_vals);
7473                }
7474                let _query = #root::core::BulkInsertQuery {
7475                    model: <Self as #root::core::Model>::SCHEMA,
7476                    columns: ::std::vec![ #( #upsert_cols ),* ],
7477                    rows: _all_rows,
7478                    returning: ::std::vec::Vec::new(),
7479                    on_conflict: ::core::option::Option::Some(
7480                        #root::core::ConflictClause::DoNothing
7481                    ),
7482                };
7483                #root::sql::bulk_insert_pool(pool, &_query).await
7484            }
7485        }
7486    };
7487
7488    // Ergonomic `Model::bulk_update(objs, fields)` — Django's
7489    // `QuerySet.bulk_update`. The SQL/IR/executor stack
7490    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7491    // `write_bulk_update_*` writers) already existed; what was missing
7492    // was the per-model constructor that maps `&[Self]` + a runtime
7493    // column list into rows of `[pk, col_vals…]` so callers don't
7494    // hand-build the IR. Emitted only when the model has a primary key
7495    // (the PK is the join key and can't itself be updated).
7496    let bulk_update_method = match &fields.primary_key {
7497        None => quote! {},
7498        Some((pk_ident, pk_col)) => {
7499            // One pair of match arms per non-PK column: a validation arm
7500            // resolving the runtime name to its `&'static str` column,
7501            // and a value arm pushing that field off the row.
7502            let mut col_arms: Vec<TokenStream2> = Vec::new();
7503            let mut val_arms: Vec<TokenStream2> = Vec::new();
7504            for entry in &fields.column_entries {
7505                if &entry.column == pk_col {
7506                    continue;
7507                }
7508                let col = &entry.column;
7509                let ident = &entry.ident;
7510                col_arms.push(quote! { #col => #col, });
7511                val_arms.push(quote! {
7512                    #col => _row_vals.push(
7513                        ::core::convert::Into::<#root::core::SqlValue>::into(
7514                            ::core::clone::Clone::clone(&_o.#ident)
7515                        )
7516                    ),
7517                });
7518            }
7519            quote! {
7520                /// Django's `QuerySet.bulk_update(objs, fields)` — write
7521                /// per-row-different values for the named `fields` across
7522                /// every object in `objs` in a single statement, matched
7523                /// by primary key.
7524                ///
7525                /// `fields` names the **columns** to update. The primary
7526                /// key identifies each row and cannot itself be updated
7527                /// (pass it and you get
7528                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7529                /// Empty `objs` or `fields` is a no-op returning `0`.
7530                /// Objects whose PK matches no row are simply not updated.
7531                /// Returns the number of rows affected.
7532                ///
7533                /// Tri-dialect: lowers to one
7534                /// [`#root::core::BulkUpdateQuery`] and dispatches
7535                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7536                /// a CTE + correlated subquery on SQLite, an inner
7537                /// `JOIN (VALUES …)` on MySQL.
7538                ///
7539                /// # Errors
7540                /// [`#root::core::QueryError::UnknownField`] for a name
7541                /// that isn't a column on this model,
7542                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7543                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7544                /// SQL-writing / driver failures.
7545                pub async fn bulk_update(
7546                    objs: &[Self],
7547                    fields: &[&str],
7548                    pool: &#root::sql::Pool,
7549                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7550                    if objs.is_empty() || fields.is_empty() {
7551                        return ::core::result::Result::Ok(0);
7552                    }
7553                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7554                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7555                        ::std::vec::Vec::with_capacity(fields.len());
7556                    for &_f in fields {
7557                        let _col: &'static str = match _f {
7558                            #pk_col => {
7559                                return ::core::result::Result::Err(
7560                                    ::core::convert::Into::into(
7561                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7562                                            model: _model_name,
7563                                            field: ::std::string::ToString::to_string(_f),
7564                                        }
7565                                    )
7566                                );
7567                            }
7568                            #( #col_arms )*
7569                            _ => {
7570                                return ::core::result::Result::Err(
7571                                    ::core::convert::Into::into(
7572                                        #root::core::QueryError::UnknownField {
7573                                            model: _model_name,
7574                                            field: ::std::string::ToString::to_string(_f),
7575                                        }
7576                                    )
7577                                );
7578                            }
7579                        };
7580                        _update_columns.push(_col);
7581                    }
7582                    let mut _rows: ::std::vec::Vec<
7583                        ::std::vec::Vec<#root::core::SqlValue>,
7584                    > = ::std::vec::Vec::with_capacity(objs.len());
7585                    for _o in objs.iter() {
7586                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7587                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7588                        // PK first — the writers expect `[pk, …update cols]`.
7589                        _row_vals.push(
7590                            ::core::convert::Into::<#root::core::SqlValue>::into(
7591                                ::core::clone::Clone::clone(&_o.#pk_ident)
7592                            )
7593                        );
7594                        for &_f in fields {
7595                            match _f {
7596                                #( #val_arms )*
7597                                // Unreachable: every name was validated
7598                                // against the same arm set above.
7599                                _ => {}
7600                            }
7601                        }
7602                        _rows.push(_row_vals);
7603                    }
7604                    let _query = #root::core::BulkUpdateQuery {
7605                        model: <Self as #root::core::Model>::SCHEMA,
7606                        update_columns: _update_columns,
7607                        rows: _rows,
7608                    };
7609                    #root::sql::bulk_update_pool(pool, &_query).await
7610                }
7611            }
7612        }
7613    };
7614
7615    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7616        quote! {
7617            /// Hidden runtime accessor for the primary-key value as a
7618            /// [`SqlValue`]. Used by reverse-relation helpers
7619            /// (`<parent>::<child>_set`) emitted from sibling models'
7620            /// FK fields. Not part of the public API.
7621            #[doc(hidden)]
7622            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7623                ::core::convert::Into::<#root::core::SqlValue>::into(
7624                    ::core::clone::Clone::clone(&self.#pk_ident)
7625                )
7626            }
7627        }
7628    });
7629
7630    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7631        quote! {
7632            impl #root::sql::HasPkValue for #struct_name {
7633                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7634                    ::core::convert::Into::<#root::core::SqlValue>::into(
7635                        ::core::clone::Clone::clone(&self.#pk_ident)
7636                    )
7637                }
7638            }
7639        }
7640    });
7641
7642    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7643
7644    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7645    // dispatch to the right per-backend body without the macro emitting
7646    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7647    // so audited models with non-Auto PKs (which still go through
7648    // `insert_one_with_audit` → `apply_auto_pk`) link.
7649    let assign_auto_pk_pool_impl = {
7650        let auto_assigns = &fields.auto_assigns;
7651        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7652        // so the body is structurally identical to `auto_assigns` —
7653        // only the helper name swaps from `try_get_returning` to
7654        // `try_get_returning_sqlite` so the closure typechecks against
7655        // a `SqliteRow` instead of a `PgRow`.
7656        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7657            .auto_field_idents
7658            .iter()
7659            .map(|(ident, column)| {
7660                quote! {
7661                    self.#ident = #root::sql::try_get_returning_sqlite(
7662                        _returning_row, #column
7663                    )?;
7664                }
7665            })
7666            .collect();
7667        let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7668            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7669            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7670            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7671            // those models can't use AUTO_INCREMENT). The trait is only
7672            // touched on the MySQL arm at runtime, so PG-only consumers
7673            // never see the bound failure.
7674            //
7675            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7676            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7677            // runtime with "multi-column RETURNING". MySQL has no
7678            // multi-column RETURNING semantic and a follow-up SELECT
7679            // would need cross-trait plumbing. Pragmatic shape: succeed
7680            // with the FIRST Auto field populated from LAST_INSERT_ID();
7681            // any other Auto fields stay `Auto::Unset`. Callers that
7682            // need the DB-defaulted timestamp / UUID can re-fetch the
7683            // row by PK after `save_pool`. Fixes the cookbook chapter
7684            // 12 dialect divergence.
7685            let value_ty = fields
7686                .first_auto_value_ty
7687                .as_ref()
7688                .expect("first_auto_value_ty set whenever first_auto_ident is");
7689            quote! {
7690                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7691                    ::rustango_from_mysql_auto_id(_id)?;
7692                self.#first = #root::sql::Auto::Set(_converted);
7693                ::core::result::Result::Ok(())
7694            }
7695        } else {
7696            quote! {
7697                let _ = _id;
7698                ::core::result::Result::Ok(())
7699            }
7700        };
7701        quote! {
7702            impl #root::sql::AssignAutoPkPool for #struct_name {
7703                fn __rustango_assign_from_pg_row(
7704                    &mut self,
7705                    _returning_row: &#root::sql::PgReturningRow,
7706                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7707                    #( #auto_assigns )*
7708                    ::core::result::Result::Ok(())
7709                }
7710                fn __rustango_assign_from_mysql_id(
7711                    &mut self,
7712                    _id: i64,
7713                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7714                    #mysql_body
7715                }
7716                fn __rustango_assign_from_sqlite_row(
7717                    &mut self,
7718                    _returning_row: &#root::sql::SqliteReturningRow,
7719                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7720                    #( #auto_assigns_sqlite )*
7721                    ::core::result::Result::Ok(())
7722                }
7723            }
7724        }
7725    };
7726
7727    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7728    let aliased_row_helper = quote! {
7729        /// Decode a row's aliased target columns (produced by
7730        /// `select_related`'s LEFT JOIN) into a fresh instance of
7731        /// this model. Reads each column via
7732        /// `format!("{prefix}__{col}")`, matching the alias the
7733        /// SELECT writer emitted. Slice 9.0d.
7734        #[doc(hidden)]
7735        #[cfg(feature = "postgres")]
7736        pub fn __rustango_from_aliased_row(
7737            row: &#root::sql::sqlx::postgres::PgRow,
7738            prefix: &str,
7739        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7740            ::core::result::Result::Ok(Self {
7741                #( #from_aliased_row_inits ),*
7742            })
7743        }
7744    };
7745    // v0.23.0-batch8 — MySQL counterpart, gated through the
7746    // cfg-aware macro_rules so PG-only builds expand to nothing.
7747    let aliased_row_helper_my = quote! {
7748        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7749            #( #from_aliased_row_inits ),*
7750        });
7751    };
7752
7753    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7754    // pattern + cfg gate on the `sqlite` feature.
7755    let aliased_row_helper_sqlite = quote! {
7756        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7757            #( #from_aliased_row_inits ),*
7758        });
7759    };
7760
7761    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7762    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7763    let load_related_impl_sqlite =
7764        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7765
7766    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7767    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7768    // default `Self::objects()`. Each accessor returns a fresh
7769    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7770    // methods the user defined.
7771    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7772        .iter()
7773        .map(|fn_ident| {
7774            let model_name_str = struct_name.to_string();
7775            let fn_name_str = fn_ident.to_string();
7776            let doc = format!(
7777                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7778                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7779                 equivalent to `Self::objects()`. Chains with any \
7780                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7781                 extension methods."
7782            );
7783            quote! {
7784                #[doc = #doc]
7785                #[must_use]
7786                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7787                    #root::query::QuerySet::new()
7788                }
7789            }
7790        })
7791        .collect();
7792
7793    quote! {
7794        impl #struct_name {
7795            /// Start a new `QuerySet` over this model. Django shape.
7796            #[must_use]
7797            pub fn objects() -> #root::query::QuerySet<#struct_name> {
7798                #root::query::QuerySet::new()
7799            }
7800
7801            /// Eloquent-shape alias of [`Self::objects`]. Returns
7802            /// a fresh `QuerySet<Self>` ready for `.filter()` /
7803            /// `.where_()` / etc. Matches Laravel muscle-memory:
7804            ///
7805            /// ```ignore
7806            /// // Eloquent:    Post::query()->where('published', true)
7807            /// // Django:      Post.objects.filter(published=True)
7808            /// // rustango:    Post::query().filter("published", true)
7809            /// //         or:  Post::objects().filter("published", true)
7810            /// ```
7811            ///
7812            /// Both names point at the same underlying constructor;
7813            /// neither is preferred.
7814            #[must_use]
7815            pub fn query() -> #root::query::QuerySet<#struct_name> {
7816                #root::query::QuerySet::new()
7817            }
7818
7819            #( #extra_manager_fns )*
7820
7821            #insert_method
7822
7823            #bulk_insert_method
7824
7825            #bulk_upsert_pool_method
7826
7827            #bulk_update_method
7828
7829            #save_method
7830
7831            #pk_methods
7832
7833            #pk_value_helper
7834
7835            #aliased_row_helper
7836
7837            #column_consts
7838        }
7839
7840        #aliased_row_helper_my
7841
7842        #aliased_row_helper_sqlite
7843
7844        #load_related_impl
7845
7846        #load_related_impl_my
7847
7848        #load_related_impl_sqlite
7849
7850        #has_pk_value_impl
7851
7852        #fk_pk_access_impl
7853
7854        #assign_auto_pk_pool_impl
7855    }
7856}
7857
7858/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7859/// `auto_assigns` but reading from `_returning_row` and writing to
7860/// `_row_mut` instead of `self`.
7861fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7862    let root = rustango_root();
7863    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7864        let col_lit = column.as_str();
7865        quote! {
7866            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7867                _returning_row,
7868                #col_lit,
7869            )?;
7870        }
7871    });
7872    quote! { #( #lines )* }
7873}
7874
7875/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7876fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7877    let lines = entries.iter().map(|e| {
7878        let ident = &e.ident;
7879        let col_ty = column_type_ident(ident);
7880        quote! {
7881            #[allow(non_upper_case_globals)]
7882            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7883        }
7884    });
7885    quote! { #(#lines)* }
7886}
7887
7888/// Emit a hidden per-model module carrying one zero-sized type per field,
7889/// each with a `Column` impl pointing back at the model.
7890fn column_module_tokens(
7891    module_ident: &syn::Ident,
7892    struct_name: &syn::Ident,
7893    entries: &[ColumnEntry],
7894) -> TokenStream2 {
7895    let root = rustango_root();
7896    let items = entries.iter().map(|e| {
7897        let col_ty = column_type_ident(&e.ident);
7898        let value_ty = &e.value_ty;
7899        let name = &e.name;
7900        let column = &e.column;
7901        let field_type_tokens = &e.field_type_tokens;
7902        quote! {
7903            #[derive(::core::clone::Clone, ::core::marker::Copy)]
7904            pub struct #col_ty;
7905
7906            impl #root::core::Column for #col_ty {
7907                type Model = super::#struct_name;
7908                type Value = #value_ty;
7909                const NAME: &'static str = #name;
7910                const COLUMN: &'static str = #column;
7911                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7912            }
7913        }
7914    });
7915    quote! {
7916        #[doc(hidden)]
7917        #[allow(non_camel_case_types, non_snake_case)]
7918        pub mod #module_ident {
7919            // Re-import the parent scope so field types referencing
7920            // sibling models (e.g. `ForeignKey<Author>`) resolve
7921            // inside this submodule. Without this we'd hit
7922            // `proc_macro_derive_resolution_fallback` warnings.
7923            #[allow(unused_imports)]
7924            use super::*;
7925            #(#items)*
7926        }
7927    }
7928}
7929
7930fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
7931    syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
7932}
7933
7934fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
7935    syn::Ident::new(
7936        &format!("__rustango_cols_{struct_name}"),
7937        struct_name.span(),
7938    )
7939}
7940
7941fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
7942    let root = rustango_root();
7943    // The Postgres impl is always emitted — every rustango build pulls in
7944    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
7945    // routed through `#root::__impl_my_from_row!`, a cfg-gated
7946    // macro_rules whose body collapses to nothing when rustango is built
7947    // without the `mysql` feature. No user-facing feature shim required.
7948    //
7949    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
7950    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
7951    // back into a comma-separated list inside square brackets. Since each
7952    // entry is already in `field: expr` shape, the existing tokens slot in.
7953    quote! {
7954        #[cfg(feature = "postgres")]
7955        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
7956            for #struct_name
7957        {
7958            fn from_row(
7959                row: &'r #root::sql::sqlx::postgres::PgRow,
7960            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7961                ::core::result::Result::Ok(Self {
7962                    #( #from_row_inits ),*
7963                })
7964            }
7965        }
7966
7967        #root::__impl_my_from_row!(#struct_name, |row| {
7968            #( #from_row_inits ),*
7969        });
7970
7971        #root::__impl_sqlite_from_row!(#struct_name, |row| {
7972            #( #from_row_inits ),*
7973        });
7974    }
7975}
7976
7977struct ContainerAttrs {
7978    table: Option<String>,
7979    display: Option<(String, proc_macro2::Span)>,
7980    /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
7981    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
7982    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
7983    /// at runtime — this attribute is the override for cases where
7984    /// the inference is wrong (e.g. a model that conceptually belongs
7985    /// to one app but is physically in another module).
7986    app: Option<String>,
7987    /// Django ModelAdmin-shape per-model knobs from
7988    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
7989    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
7990    /// admin code falls back to `AdminConfig::DEFAULT`.
7991    admin: Option<AdminAttrs>,
7992    /// Per-model audit configuration from `#[rustango(audit(...))]`.
7993    /// `None` when the model isn't audited — write paths emit no
7994    /// audit entries. When present, single-row writes capture
7995    /// before/after for the listed fields and bulk writes batch
7996    /// snapshots into one INSERT into `rustango_audit_log`.
7997    audit: Option<AuditAttrs>,
7998    /// `true` when `#[rustango(permissions)]` is present. Signals that
7999    /// `auto_create_permissions` should seed the four CRUD codenames for
8000    /// this model.
8001    permissions: bool,
8002    /// Many-to-many relations declared via
8003    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8004    ///                 src = "post_id", dst = "tag_id"))]`.
8005    m2m: Vec<M2MAttr>,
8006    /// Polymorphic M2M relations declared via
8007    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8008    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8009    ///   related_column = "tag_id"))]` (issue #818).
8010    generic_m2m: Vec<GenericM2MAttr>,
8011    /// Composite indexes declared via
8012    /// `#[rustango(index("col1, col2"))]` or
8013    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8014    /// Single-column indexes from `#[rustango(index)]` on fields are
8015    /// accumulated here during field collection.
8016    indexes: Vec<IndexAttr>,
8017    /// Table-level CHECK constraints declared via
8018    /// `#[rustango(check(name = "…", expr = "…"))]`.
8019    checks: Vec<CheckAttr>,
8020    /// Table-level PG `EXCLUDE` constraints declared via
8021    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8022    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8023    /// migration writer renders them on Postgres and skips with a
8024    /// warning on MySQL/SQLite. Issue #319.
8025    excludes: Vec<ExcludeAttr>,
8026    /// Composite (multi-column) FKs declared via
8027    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8028    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8029    composite_fks: Vec<CompositeFkAttr>,
8030    /// Generic ("any model") FKs declared via
8031    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8032    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8033    generic_fks: Vec<GenericFkAttr>,
8034    /// Where this model lives in a tenancy deployment, declared via
8035    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8036    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8037    /// to partition diff output between registry-scoped and
8038    /// tenant-scoped migration files.
8039    scope: Option<String>,
8040    /// Custom-Manager extension-trait name from
8041    /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8042    /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8043    /// adjacent to the model so users can write
8044    /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8045    /// and discover the convention from the model definition.
8046    manager_ext: Option<syn::Ident>,
8047    /// Extra QuerySet accessor names from
8048    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8049    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8050    /// the default `Self::objects()`. Multiple attributes allowed.
8051    manager_fns: Vec<syn::Ident>,
8052    /// Default ordering declared via `#[rustango(default_order =
8053    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8054    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8055    /// `-` prefix means descending; the `+` prefix or no prefix means
8056    /// ascending.
8057    default_order: Vec<(String, bool, proc_macro2::Span)>,
8058    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8059    /// Routes the emitted schema's `is_view = true` so the migration
8060    /// snapshot skips this model (its underlying SQL view is operator-
8061    /// managed, not rustango-managed).
8062    is_view: bool,
8063    /// Django-shape `Meta.managed` from `#[rustango(managed = false)]`.
8064    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8065    /// the migration snapshot skips this model so `makemigrations` /
8066    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8067    /// TABLE` against it (operator-managed schema).
8068    managed: bool,
8069    /// Django-shape `Meta.base_manager_name` from
8070    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8071    /// `ModelSchema::base_manager_name`. Declarative-only today.
8072    base_manager_name: Option<String>,
8073    /// Django-shape `Meta.order_with_respect_to = "parent_fk"` from
8074    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8075    /// field this model's instances are ordered relative to.
8076    /// Declarative-only today; threaded onto
8077    /// `ModelSchema::order_with_respect_to`.
8078    order_with_respect_to: Option<String>,
8079    /// Django-shape `Meta.proxy = True` from `#[rustango(proxy)]` /
8080    /// `#[rustango(proxy = true)]`. Marks the model as a proxy that
8081    /// shares its DB table with another struct. Threaded into
8082    /// `ModelSchema::proxy` so future codegen can skip table-owning
8083    /// behavior for proxies.
8084    proxy: bool,
8085    /// Django-shape `Meta.required_db_features` from
8086    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8087    /// Each comma-separated capability token surfaces on
8088    /// `ModelSchema::required_db_features` so `manage check --deploy`
8089    /// can warn when the active dialect lacks one.
8090    required_db_features: Vec<String>,
8091    /// Django-shape `Meta.required_db_vendor` from
8092    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8093    /// Normalized to the dialect name `manage check --deploy`
8094    /// compares against `Settings.database.backend`. Aliases
8095    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8096    /// stored under the canonical name.
8097    required_db_vendor: Option<String>,
8098    /// Django-shape `Meta.default_related_name` from
8099    /// `#[rustango(default_related_name = "...")]`. Threaded into
8100    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8101    /// name to use when an FK / M2M field doesn't override it.
8102    /// Today rustango doesn't auto-emit reverse managers; the
8103    /// metadata is the foundation for that work.
8104    default_related_name: Option<String>,
8105    /// Django-shape `Meta.db_table_comment` (4.2+) from
8106    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8107    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8108    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8109    /// inline `COMMENT='...'`, SQLite: no-op).
8110    db_table_comment: Option<String>,
8111    /// Django-shape `Meta.get_latest_by` from
8112    /// `#[rustango(get_latest_by = "created_at")]` /
8113    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8114    /// `(column, descending)` where `descending = true` when the
8115    /// attribute value starts with `-`. Threaded into
8116    /// `ModelSchema::get_latest_by`.
8117    get_latest_by: Option<(String, bool)>,
8118    /// Django-shape `Meta.permissions = [(codename, name), ...]`
8119    /// from `#[rustango(extra_permissions = "approve:Can approve,
8120    /// archive:Can archive")]`. Comma-separated `codename:label`
8121    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8122    extra_permissions: Vec<(String, String)>,
8123    /// Django-shape `Meta.default_permissions` — which CRUD codenames
8124    /// (`"add"` / `"change"` / `"delete"` / `"view"`) the framework
8125    /// auto-creates. Empty `Vec` (default) means **all four** — matches
8126    /// Django's behavior when the operator omits the option. Set via
8127    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8128    /// Validated at parse time; unknown actions fail with a span-pointing
8129    /// error.
8130    default_permissions: Vec<String>,
8131    /// `#[rustango(verbose_name = "blog post")]` — Django-shape
8132    /// human-readable singular label for the model. Threaded into
8133    /// `ModelSchema::verbose_name` so admin section headers /
8134    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8135    /// over the Rust struct identifier.
8136    verbose_name: Option<String>,
8137    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8138    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8139    /// When unset, `display_label_plural()` auto-suffixes `s`.
8140    verbose_name_plural: Option<String>,
8141    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8142    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8143    /// a name (used by `QuerySet::without_global_scope`) with a
8144    /// `fn() -> WhereExpr` path that the macro emits into
8145    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8146    global_scopes: Vec<GlobalScopeAttr>,
8147    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8148    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8149    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8150    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8151    /// entry emits an inherent `<name>_through(&self)` accessor that
8152    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8153    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8154    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8155    through_relations: Vec<ThroughAttr>,
8156    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8157    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8158    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8159    /// Each entry emits two associated functions on the parent —
8160    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8161    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8162    /// over a correlated subquery against the child table. Users
8163    /// drop the result into `QuerySet::where_raw(...)`.
8164    reverse_has_relations: Vec<ReverseHasAttr>,
8165    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8166    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8167    /// entry so the relation-existence family resolves polymorphic
8168    /// children by name.
8169    generic_has_relations: Vec<GenericHasAttr>,
8170}
8171
8172/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8173/// declaration. Each entry becomes one `core::GlobalScope` in the
8174/// emitted schema literal; `apply` resolves at macro-expand time
8175/// against the consumer's scope so the function must be in scope at
8176/// the use site. Issue #820.
8177struct GlobalScopeAttr {
8178    name: String,
8179    apply: syn::Path,
8180}
8181
8182/// Parsed `#[rustango(through(...))]` declaration. Issue
8183/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8184/// `hasManyThrough` / `hasOneThrough` parity.
8185///
8186/// `Country hasManyThrough Post via User` declares as:
8187///
8188/// ```ignore
8189/// #[rustango(through(
8190///     name                   = "posts",
8191///     far                    = "Post",
8192///     far_fk_column          = "author_id",
8193///     intermediate           = "User",
8194///     intermediate_fk_column = "country_id",
8195/// ))]
8196/// struct Country { ... }
8197/// ```
8198///
8199/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8200/// which returns a queryset filtered via a correlated subquery —
8201/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8202/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8203/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8204/// because the subquery lives inside a `WhereExpr::InSubquery` node
8205/// and the outer queryset's pending list stays empty.
8206///
8207/// All four required arguments use **SQL column / table names**
8208/// (not Rust field names) to sidestep the multi-hop-filter substrate
8209/// gap. Once that substrate lands, a higher-level Rust-field-name
8210/// shorthand can be added without breaking this surface.
8211struct ThroughAttr {
8212    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8213    name: String,
8214    /// Far model type identifier. `far = "Post"` → returns
8215    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8216    /// the derive expands.
8217    far: syn::Ident,
8218    /// SQL column on the far model's table that references the
8219    /// intermediate model's primary key. For `Post`'s
8220    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8221    /// default `<field>_id` convention) or whatever the user
8222    /// declared via `#[rustango(db_column = "...")]`.
8223    far_fk_column: String,
8224    /// Intermediate model type identifier. Needed to look up its
8225    /// `SCHEMA` so the subquery's `FROM` clause points at the
8226    /// intermediate table. `intermediate = "User"`.
8227    intermediate: syn::Ident,
8228    /// SQL column on the intermediate's table that references the
8229    /// source (this) model's primary key. For `User`'s
8230    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8231    intermediate_fk_column: String,
8232    /// SQL primary-key column on the intermediate's table — the
8233    /// column the subquery projects. Optional; defaults to `"id"`
8234    /// (rustango's default PK column name). Override when the
8235    /// intermediate declares a custom PK column.
8236    intermediate_pk_column: String,
8237}
8238
8239/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8240/// child_fk_column = "..."))]` declaration. Issue
8241/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8242/// `whereHas` / `whereDoesntHave` parity.
8243///
8244/// `Post hasMany Comment` declares as:
8245///
8246/// ```ignore
8247/// #[rustango(reverse_has(
8248///     name             = "comments",
8249///     child            = "Comment",
8250///     child_fk_column  = "post_id",
8251/// ))]
8252/// struct Post { ... }
8253/// ```
8254///
8255/// The macro emits two associated functions on `Post`:
8256///
8257/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8258///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8259///   <outer>.<self_pk_column>)`.
8260/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8261///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8262///
8263/// User code:
8264///
8265/// ```ignore
8266/// // Posts with at least one comment:
8267/// Post::objects().where_raw(Post::comments_exists_expr()).fetch_pool(&pool)
8268/// // Posts with no comments:
8269/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch_pool(&pool)
8270/// ```
8271///
8272/// As with #817, all column / table identifiers are **SQL names**
8273/// (not Rust field names) so the substrate is independent of the
8274/// outstanding multi-hop filter resolver gap. The emitted
8275/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8276/// SQL-emit time via the writer's scope stack.
8277struct ReverseHasAttr {
8278    /// Accessor name. `name = "comments"` → emits
8279    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8280    name: String,
8281    /// Child model type identifier. `child = "Comment"` — needed to
8282    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8283    /// points at the child table.
8284    child: syn::Ident,
8285    /// SQL column on the child's table that references this model's
8286    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8287    /// column is `"post_id"`.
8288    child_fk_column: String,
8289    /// SQL primary-key column on **this** model's table — the column
8290    /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8291    /// (rustango's default PK column name).
8292    self_pk_column: String,
8293}
8294
8295/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8296/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8297/// relation-existence family (issue #830). The child is a polymorphic,
8298/// content-type-discriminated model; emits a `Model::
8299/// generic_reverse_relations()` entry the queryset resolves by name.
8300struct GenericHasAttr {
8301    /// Accessor name, e.g. `name = "tags"`.
8302    name: String,
8303    /// Child model type identifier (`child = "Tag"`) — looked up for its
8304    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8305    child: syn::Ident,
8306    /// Column on the child table holding the parent's content-type id.
8307    /// Optional; defaults to `"content_type_id"`.
8308    ct_column: String,
8309    /// Column on the child table holding the parent's PK value.
8310    /// Optional; defaults to `"object_pk"`.
8311    pk_column: String,
8312    /// SQL primary-key column on **this** (parent) model's table.
8313    /// Optional; defaults to `"id"`.
8314    self_pk_column: String,
8315}
8316
8317/// Parsed form of one index declaration (field-level or container-level).
8318struct IndexAttr {
8319    /// Index name; auto-derived when `None` at parse time.
8320    name: Option<String>,
8321    /// Column names in the index.
8322    columns: Vec<String>,
8323    /// `true` for `CREATE UNIQUE INDEX`.
8324    unique: bool,
8325    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8326    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8327    /// `"btree"` when the attribute is absent — the DDL writer omits
8328    /// the `USING` clause and the backend uses its own default
8329    /// (btree on every supported dialect).
8330    method: String,
8331    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8332    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8333    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8334    where_clause: Option<String>,
8335    /// Django `Index(fields=..., include=[...])` covering-index
8336    /// columns (PG 11+ `INCLUDE (...)` clause). Empty `Vec` (the
8337    /// default) means "no covering columns".
8338    include: Vec<String>,
8339}
8340
8341/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8342struct CheckAttr {
8343    name: String,
8344    expr: String,
8345}
8346
8347/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8348/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8349/// PG-only — surfaced on every backend in the macro emit; the migration
8350/// writer skips the op on MySQL/SQLite. Issue #319.
8351struct ExcludeAttr {
8352    /// Constraint name (free-form Rust identifier).
8353    name: String,
8354    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8355    using: String,
8356    /// Comma-separated `(column, operator)` pairs, in declaration
8357    /// order. Parsed from `"col WITH op, col WITH op"`.
8358    elements: Vec<(String, String)>,
8359    /// Optional `WHERE` predicate (raw SQL).
8360    where_clause: Option<String>,
8361}
8362
8363/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8364/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8365/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8366/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8367/// the model, not the field.
8368struct CompositeFkAttr {
8369    /// Logical relation name (free-form Rust identifier).
8370    name: String,
8371    /// SQL table name of the target.
8372    to: String,
8373    /// Source-side column names, in declaration order.
8374    from: Vec<String>,
8375    /// Target-side column names, same length / order as `from`.
8376    on: Vec<String>,
8377}
8378
8379/// Parsed form of one `#[rustango(generic_fk(name = "target",
8380/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8381/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8382/// generic ("any model") FKs live on the model, not the field.
8383struct GenericFkAttr {
8384    /// Logical relation name (free-form Rust identifier).
8385    name: String,
8386    /// Source-side column carrying the `content_type_id` value.
8387    ct_column: String,
8388    /// Source-side column carrying the target row's primary key.
8389    pk_column: String,
8390}
8391
8392/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8393struct M2MAttr {
8394    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8395    name: String,
8396    /// Target table (e.g. `"app_tags"`).
8397    to: String,
8398    /// Junction table (e.g. `"post_tags"`).
8399    through: String,
8400    /// Source FK column in the junction table (e.g. `"post_id"`).
8401    src: String,
8402    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8403    dst: String,
8404    /// Whether the migration writer should auto-create the junction
8405    /// table. Default `true`. Set `auto_create = false` (#324) when
8406    /// the operator declares the through table as its own
8407    /// `#[derive(Model)]` struct with extra columns.
8408    auto_create: bool,
8409}
8410
8411/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8412/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8413/// junction carries a ContentType discriminator so unrelated models
8414/// share one pivot + related set.
8415struct GenericM2MAttr {
8416    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8417    name: String,
8418    /// Polymorphic junction table (e.g. `"taggables"`).
8419    through: String,
8420    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8421    pk_column: String,
8422    /// Junction column holding the owning model's `content_type_id`
8423    /// discriminator (e.g. `"taggable_type"`).
8424    ct_column: String,
8425    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8426    related_column: String,
8427}
8428
8429/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8430/// "user"))]`. `track` is a comma-separated list of field names whose
8431/// before/after values land in the JSONB `changes` column. `source`
8432/// is informational only — it pins a default source when the model
8433/// is written outside any `audit::with_source(...)` scope (rare).
8434#[derive(Default)]
8435struct AuditAttrs {
8436    /// Field names to capture in the `changes` JSONB. Validated
8437    /// against declared scalar fields at compile time. Empty means
8438    /// "track every scalar field" — Django's audit-everything default.
8439    track: Option<(Vec<String>, proc_macro2::Span)>,
8440}
8441
8442/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8443/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8444/// comma-separated strings; we validate each ident against the model's
8445/// declared fields at compile time.
8446#[derive(Default)]
8447struct AdminAttrs {
8448    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8449    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8450    list_per_page: Option<usize>,
8451    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8452    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8453    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8454    /// Bulk action names. No field-validation against model fields —
8455    /// these are action handlers, not column references.
8456    actions: Option<(Vec<String>, proc_macro2::Span)>,
8457    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8458    /// sections, comma-separated fields per section, optional
8459    /// `Title:` prefix. Empty title omits the `<legend>`.
8460    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8461    /// `admin(list_display_links = "title")` — Django-shape. Names
8462    /// from `list_display` whose cells should link to detail/edit.
8463    /// Issue #350.
8464    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8465    /// `admin(search_help_text = "...")` — Django-shape. Short
8466    /// caption rendered beside the admin list view's search box.
8467    /// Issue #353.
8468    search_help_text: Option<String>,
8469    /// `admin(actions_on_top = false)` — Django-shape. Hides the
8470    /// action-bar above the table. Default `true`. Issue #354.
8471    actions_on_top: Option<bool>,
8472    /// `admin(actions_on_bottom = true)` — Django-shape. Renders an
8473    /// additional action-bar below the table. Default `false`.
8474    /// Issue #354.
8475    actions_on_bottom: Option<bool>,
8476    /// `admin(date_hierarchy = "created_at")` — Django-shape. Name of
8477    /// a date / datetime field whose values render as a clickable
8478    /// year / month / day drill-down strip above the list table.
8479    /// Empty / unset disables the strip. Issue #355.
8480    date_hierarchy: Option<String>,
8481    /// `admin(prepopulated_fields = "slug:title")` — Django-shape.
8482    /// Each entry is `target:source[+source2]`; multiple entries are
8483    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8484    /// The admin change-form emits JS that slugifies the source values
8485    /// into the target field on every keystroke. Issue #356.
8486    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8487    /// `admin(raw_id_fields = "parent, owner")` — Django-shape. Names
8488    /// of FK fields whose change-form widget renders a lookup link
8489    /// next to the input. Issue #357.
8490    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8491    /// `admin(autocomplete_fields = "author_id")` — Django-shape.
8492    /// Names of FK fields whose change-form widget renders an
8493    /// Ajax-driven typeahead populated from a `__autocomplete`
8494    /// endpoint on the target model. Issue #358.
8495    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8496    /// `admin(list_select_related = "all" | "none" | "author, …")`
8497    /// — Django-shape. Tunes the admin list view's FK auto-JOIN
8498    /// policy. Default `"all"` matches rustango's join-everything
8499    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8500    list_select_related: Option<String>,
8501    /// `admin(formfield_overrides = "field:widget, field2:widget2")` —
8502    /// Django-shape. Each entry is `field_name:widget_name`; multiple
8503    /// entries comma-separated. Empty / unset → no overrides. The
8504    /// list of widget names supported is documented on
8505    /// `AdminConfig::formfield_overrides`. Issue #359.
8506    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8507}
8508
8509fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8510    let mut out = ContainerAttrs {
8511        table: None,
8512        display: None,
8513        app: None,
8514        admin: None,
8515        audit: None,
8516        // Default `permissions = true` so every `#[derive(Model)]`
8517        // gets the four CRUD codenames seeded by `auto_create_permissions`
8518        // and is visible to non-superusers in the tenant admin without
8519        // manual per-model annotation. Models that intentionally don't
8520        // want permission rows (registry-internal types, framework
8521        // tables operators shouldn't manage directly) opt out via
8522        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8523        // out-of-the-box admin invisibility regression (#62).
8524        permissions: true,
8525        m2m: Vec::new(),
8526        generic_m2m: Vec::new(),
8527        indexes: Vec::new(),
8528        checks: Vec::new(),
8529        excludes: Vec::new(),
8530        composite_fks: Vec::new(),
8531        generic_fks: Vec::new(),
8532        scope: None,
8533        manager_ext: None,
8534        manager_fns: Vec::new(),
8535        default_order: Vec::new(),
8536        is_view: false,
8537        managed: true,
8538        verbose_name: None,
8539        verbose_name_plural: None,
8540        base_manager_name: None,
8541        order_with_respect_to: None,
8542        proxy: false,
8543        required_db_features: Vec::new(),
8544        required_db_vendor: None,
8545        default_related_name: None,
8546        db_table_comment: None,
8547        get_latest_by: None,
8548        extra_permissions: Vec::new(),
8549        default_permissions: Vec::new(),
8550        global_scopes: Vec::new(),
8551        through_relations: Vec::new(),
8552        reverse_has_relations: Vec::new(),
8553        generic_has_relations: Vec::new(),
8554    };
8555    for attr in &input.attrs {
8556        if !attr.path().is_ident("rustango") {
8557            continue;
8558        }
8559        attr.parse_nested_meta(|meta| {
8560            if meta.path.is_ident("table") {
8561                let s: LitStr = meta.value()?.parse()?;
8562                let name = s.value();
8563                // v0.27.3 (#65) — macro-time guard against table names
8564                // that compile but break SQL downstream. Hyphens are
8565                // the common footgun: PostgreSQL accepts a quoted
8566                // `"intermediate-region"` in CREATE TABLE, but the
8567                // FK / index name derivation in `migrate::ddl`
8568                // emits `intermediate-region_field_fkey` unquoted,
8569                // which then fails the SQL parser. Same shape rule
8570                // as Postgres regular identifiers so the safe path
8571                // is the only path.
8572                validate_table_name(&name, s.span())?;
8573                out.table = Some(name);
8574                return Ok(());
8575            }
8576            if meta.path.is_ident("display") {
8577                let s: LitStr = meta.value()?.parse()?;
8578                out.display = Some((s.value(), s.span()));
8579                return Ok(());
8580            }
8581            if meta.path.is_ident("app") {
8582                let s: LitStr = meta.value()?.parse()?;
8583                out.app = Some(s.value());
8584                return Ok(());
8585            }
8586            if meta.path.is_ident("scope") {
8587                let s: LitStr = meta.value()?.parse()?;
8588                let val = s.value();
8589                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8590                    return Err(meta.error(format!(
8591                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8592                    )));
8593                }
8594                out.scope = Some(val);
8595                return Ok(());
8596            }
8597            if meta.path.is_ident("admin") {
8598                let mut admin = AdminAttrs::default();
8599                meta.parse_nested_meta(|inner| {
8600                    if inner.path.is_ident("list_display") {
8601                        let s: LitStr = inner.value()?.parse()?;
8602                        admin.list_display =
8603                            Some((split_field_list(&s.value()), s.span()));
8604                        return Ok(());
8605                    }
8606                    if inner.path.is_ident("search_fields") {
8607                        let s: LitStr = inner.value()?.parse()?;
8608                        admin.search_fields =
8609                            Some((split_field_list(&s.value()), s.span()));
8610                        return Ok(());
8611                    }
8612                    if inner.path.is_ident("readonly_fields") {
8613                        let s: LitStr = inner.value()?.parse()?;
8614                        admin.readonly_fields =
8615                            Some((split_field_list(&s.value()), s.span()));
8616                        return Ok(());
8617                    }
8618                    if inner.path.is_ident("list_per_page") {
8619                        let lit: syn::LitInt = inner.value()?.parse()?;
8620                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8621                        return Ok(());
8622                    }
8623                    if inner.path.is_ident("ordering") {
8624                        let s: LitStr = inner.value()?.parse()?;
8625                        admin.ordering = Some((
8626                            parse_ordering_list(&s.value()),
8627                            s.span(),
8628                        ));
8629                        return Ok(());
8630                    }
8631                    if inner.path.is_ident("list_filter") {
8632                        let s: LitStr = inner.value()?.parse()?;
8633                        admin.list_filter =
8634                            Some((split_field_list(&s.value()), s.span()));
8635                        return Ok(());
8636                    }
8637                    if inner.path.is_ident("actions") {
8638                        let s: LitStr = inner.value()?.parse()?;
8639                        admin.actions =
8640                            Some((split_field_list(&s.value()), s.span()));
8641                        return Ok(());
8642                    }
8643                    if inner.path.is_ident("fieldsets") {
8644                        let s: LitStr = inner.value()?.parse()?;
8645                        admin.fieldsets =
8646                            Some((parse_fieldset_list(&s.value()), s.span()));
8647                        return Ok(());
8648                    }
8649                    if inner.path.is_ident("list_display_links") {
8650                        let s: LitStr = inner.value()?.parse()?;
8651                        admin.list_display_links =
8652                            Some((split_field_list(&s.value()), s.span()));
8653                        return Ok(());
8654                    }
8655                    if inner.path.is_ident("search_help_text") {
8656                        let s: LitStr = inner.value()?.parse()?;
8657                        admin.search_help_text = Some(s.value());
8658                        return Ok(());
8659                    }
8660                    if inner.path.is_ident("actions_on_top") {
8661                        let lit: syn::LitBool = inner.value()?.parse()?;
8662                        admin.actions_on_top = Some(lit.value);
8663                        return Ok(());
8664                    }
8665                    if inner.path.is_ident("actions_on_bottom") {
8666                        let lit: syn::LitBool = inner.value()?.parse()?;
8667                        admin.actions_on_bottom = Some(lit.value);
8668                        return Ok(());
8669                    }
8670                    if inner.path.is_ident("date_hierarchy") {
8671                        let s: LitStr = inner.value()?.parse()?;
8672                        admin.date_hierarchy = Some(s.value());
8673                        return Ok(());
8674                    }
8675                    if inner.path.is_ident("prepopulated_fields") {
8676                        let s: LitStr = inner.value()?.parse()?;
8677                        admin.prepopulated_fields =
8678                            Some((parse_prepopulated_list(&s.value()), s.span()));
8679                        return Ok(());
8680                    }
8681                    if inner.path.is_ident("raw_id_fields") {
8682                        let s: LitStr = inner.value()?.parse()?;
8683                        admin.raw_id_fields =
8684                            Some((split_field_list(&s.value()), s.span()));
8685                        return Ok(());
8686                    }
8687                    if inner.path.is_ident("autocomplete_fields") {
8688                        let s: LitStr = inner.value()?.parse()?;
8689                        admin.autocomplete_fields =
8690                            Some((split_field_list(&s.value()), s.span()));
8691                        return Ok(());
8692                    }
8693                    if inner.path.is_ident("list_select_related") {
8694                        let s: LitStr = inner.value()?.parse()?;
8695                        admin.list_select_related = Some(s.value());
8696                        return Ok(());
8697                    }
8698                    if inner.path.is_ident("formfield_overrides") {
8699                        let s: LitStr = inner.value()?.parse()?;
8700                        admin.formfield_overrides =
8701                            Some((parse_formfield_overrides(&s.value()), s.span()));
8702                        return Ok(());
8703                    }
8704                    Err(inner.error(
8705                        "unknown admin attribute (supported: \
8706                         `list_display`, `list_display_links`, \
8707                         `search_fields`, `search_help_text`, \
8708                         `readonly_fields`, \
8709                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8710                         `actions_on_top`, `actions_on_bottom`, \
8711                         `date_hierarchy`, \
8712                         `prepopulated_fields`, \
8713                         `raw_id_fields`, \
8714                         `autocomplete_fields`, \
8715                         `list_select_related`, \
8716                         `formfield_overrides`, \
8717                         `fieldsets`)",
8718                    ))
8719                })?;
8720                out.admin = Some(admin);
8721                return Ok(());
8722            }
8723            if meta.path.is_ident("manager") {
8724                // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8725                // Stretch `from_queryset = "..."` (Django Manager.from_queryset
8726                // shape) is left as a follow-up — the issue's primary
8727                // acceptance is the `ext = ...` trait emission.
8728                meta.parse_nested_meta(|inner| {
8729                    if inner.path.is_ident("ext") {
8730                        let s: LitStr = inner.value()?.parse()?;
8731                        let name = s.value();
8732                        if name.is_empty() {
8733                            return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8734                        }
8735                        out.manager_ext =
8736                            Some(syn::Ident::new(&name, s.span()));
8737                        return Ok(());
8738                    }
8739                    Err(inner.error(
8740                        "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8741                    ))
8742                })?;
8743                return Ok(());
8744            }
8745            if meta.path.is_ident("manager_fn") {
8746                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8747                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8748                // next to the default `Self::objects()`. Multiple
8749                // attributes accumulate.
8750                let s: LitStr = meta.value()?.parse()?;
8751                let name = s.value();
8752                if name.is_empty() {
8753                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8754                }
8755                if name == "objects" {
8756                    return Err(meta.error(
8757                        "`manager_fn = \"objects\"` collides with the default \
8758                         accessor — pick a different name",
8759                    ));
8760                }
8761                let ident = syn::Ident::new(&name, s.span());
8762                if out.manager_fns.iter().any(|prev| *prev == ident) {
8763                    return Err(meta.error(format!(
8764                        "duplicate `manager_fn = \"{name}\"`"
8765                    )));
8766                }
8767                out.manager_fns.push(ident);
8768                return Ok(());
8769            }
8770            if meta.path.is_ident("default_order") {
8771                // `#[rustango(default_order = "-created_at, status")]`
8772                // — issue #291 / T2.5. Comma-separated list; `-prefix`
8773                // means descending, `+prefix` or bare name means ascending.
8774                // Per-query opt-in via `QuerySet::with_default_order()`.
8775                let s: LitStr = meta.value()?.parse()?;
8776                let raw = s.value();
8777                let span = s.span();
8778                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8779                    Vec::new();
8780                for entry in raw.split(',') {
8781                    let trimmed = entry.trim();
8782                    if trimmed.is_empty() {
8783                        return Err(syn::Error::new(
8784                            span,
8785                            "`default_order = \"...\"` has an empty entry — \
8786                             check for a stray comma",
8787                        ));
8788                    }
8789                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8790                        (true, rest.trim().to_owned())
8791                    } else if let Some(rest) = trimmed.strip_prefix('+') {
8792                        (false, rest.trim().to_owned())
8793                    } else {
8794                        (false, trimmed.to_owned())
8795                    };
8796                    if name.is_empty() {
8797                        return Err(syn::Error::new(
8798                            span,
8799                            "`default_order` entry has no column name after the prefix",
8800                        ));
8801                    }
8802                    if parsed.iter().any(|(n, _, _)| *n == name) {
8803                        return Err(syn::Error::new(
8804                            span,
8805                            format!("duplicate column `{name}` in `default_order`"),
8806                        ));
8807                    }
8808                    parsed.push((name, desc, span));
8809                }
8810                if parsed.is_empty() {
8811                    return Err(syn::Error::new(
8812                        span,
8813                        "`default_order = \"...\"` cannot be empty",
8814                    ));
8815                }
8816                out.default_order = parsed;
8817                return Ok(());
8818            }
8819            if meta.path.is_ident("global_scope") {
8820                // `#[rustango(global_scope(name = "active", apply =
8821                //  path::to::fn))]` — issue #820. The apply function
8822                // path resolves at macro-expand time in the consumer's
8823                // scope; the macro re-emits it verbatim into the
8824                // `ModelSchema::global_scopes` slice literal.
8825                let span = meta.path.span();
8826                let mut scope_name: Option<String> = None;
8827                let mut apply_path: Option<syn::Path> = None;
8828                meta.parse_nested_meta(|inner| {
8829                    if inner.path.is_ident("name") {
8830                        let s: LitStr = inner.value()?.parse()?;
8831                        let raw = s.value();
8832                        if raw.trim().is_empty() {
8833                            return Err(syn::Error::new(
8834                                s.span(),
8835                                "`global_scope(name = \"...\")` must not be empty",
8836                            ));
8837                        }
8838                        scope_name = Some(raw);
8839                        return Ok(());
8840                    }
8841                    if inner.path.is_ident("apply") {
8842                        let p: syn::Path = inner.value()?.parse()?;
8843                        apply_path = Some(p);
8844                        return Ok(());
8845                    }
8846                    Err(inner.error(
8847                        "unknown `global_scope` attribute (supported: \
8848                         `name`, `apply`)",
8849                    ))
8850                })?;
8851                let Some(name) = scope_name else {
8852                    return Err(syn::Error::new(
8853                        span,
8854                        "`global_scope` requires `name = \"...\"`",
8855                    ));
8856                };
8857                let Some(apply) = apply_path else {
8858                    return Err(syn::Error::new(
8859                        span,
8860                        "`global_scope` requires `apply = fn_path`",
8861                    ));
8862                };
8863                if out.global_scopes.iter().any(|s| s.name == name) {
8864                    return Err(syn::Error::new(
8865                        span,
8866                        format!(
8867                            "duplicate global scope name `{name}` — \
8868                             pick a unique identifier so \
8869                             `QuerySet::without_global_scope(\"{name}\")` \
8870                             is unambiguous"
8871                        ),
8872                    ));
8873                }
8874                out.global_scopes.push(GlobalScopeAttr { name, apply });
8875                return Ok(());
8876            }
8877            if meta.path.is_ident("through") {
8878                // `#[rustango(through(name = "posts", far = "Post",
8879                //  far_fk_column = "author_id", intermediate = "User",
8880                //  intermediate_fk_column = "country_id"
8881                //  [, intermediate_pk_column = "id"]))]` — issue #817.
8882                let span = meta.path.span();
8883                let mut accessor_name: Option<String> = None;
8884                let mut far_ident: Option<syn::Ident> = None;
8885                let mut far_fk_column: Option<String> = None;
8886                let mut intermediate_ident: Option<syn::Ident> = None;
8887                let mut intermediate_fk_column: Option<String> = None;
8888                let mut intermediate_pk_column: Option<String> = None;
8889                fn parse_nonempty_string(
8890                    inner: &syn::meta::ParseNestedMeta<'_>,
8891                    field: &str,
8892                ) -> syn::Result<String> {
8893                    let s: LitStr = inner.value()?.parse()?;
8894                    let raw = s.value();
8895                    let trimmed = raw.trim();
8896                    if trimmed.is_empty() {
8897                        return Err(syn::Error::new(
8898                            s.span(),
8899                            format!("`through({field} = \"...\")` must not be empty"),
8900                        ));
8901                    }
8902                    Ok(trimmed.to_owned())
8903                }
8904                meta.parse_nested_meta(|inner| {
8905                    if inner.path.is_ident("name") {
8906                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
8907                        return Ok(());
8908                    }
8909                    if inner.path.is_ident("far") {
8910                        // Far model name accepted as a string literal
8911                        // so the attribute fits inside the existing
8912                        // `parse_nested_meta` shape; parsed back into a
8913                        // `syn::Ident` so the emitted accessor can
8914                        // reference the type directly.
8915                        let s: LitStr = inner.value()?.parse()?;
8916                        let raw = s.value();
8917                        let trimmed = raw.trim();
8918                        if trimmed.is_empty() {
8919                            return Err(syn::Error::new(
8920                                s.span(),
8921                                "`through(far = \"...\")` must not be empty",
8922                            ));
8923                        }
8924                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
8925                        return Ok(());
8926                    }
8927                    if inner.path.is_ident("far_fk_column") {
8928                        far_fk_column =
8929                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
8930                        return Ok(());
8931                    }
8932                    if inner.path.is_ident("intermediate") {
8933                        let s: LitStr = inner.value()?.parse()?;
8934                        let raw = s.value();
8935                        let trimmed = raw.trim();
8936                        if trimmed.is_empty() {
8937                            return Err(syn::Error::new(
8938                                s.span(),
8939                                "`through(intermediate = \"...\")` must not be empty",
8940                            ));
8941                        }
8942                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
8943                        return Ok(());
8944                    }
8945                    if inner.path.is_ident("intermediate_fk_column") {
8946                        intermediate_fk_column =
8947                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
8948                        return Ok(());
8949                    }
8950                    if inner.path.is_ident("intermediate_pk_column") {
8951                        intermediate_pk_column =
8952                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
8953                        return Ok(());
8954                    }
8955                    Err(inner.error(
8956                        "unknown `through` attribute (supported: \
8957                         `name`, `far`, `far_fk_column`, \
8958                         `intermediate`, `intermediate_fk_column`, \
8959                         `intermediate_pk_column`)",
8960                    ))
8961                })?;
8962                let Some(name) = accessor_name else {
8963                    return Err(syn::Error::new(
8964                        span,
8965                        "`through` requires `name = \"...\"`",
8966                    ));
8967                };
8968                let Some(far) = far_ident else {
8969                    return Err(syn::Error::new(
8970                        span,
8971                        "`through` requires `far = \"FarModelType\"`",
8972                    ));
8973                };
8974                let Some(far_fk_column) = far_fk_column else {
8975                    return Err(syn::Error::new(
8976                        span,
8977                        "`through` requires `far_fk_column = \"<column>\"`",
8978                    ));
8979                };
8980                let Some(intermediate) = intermediate_ident else {
8981                    return Err(syn::Error::new(
8982                        span,
8983                        "`through` requires `intermediate = \"IntermediateModelType\"`",
8984                    ));
8985                };
8986                let Some(intermediate_fk_column) = intermediate_fk_column else {
8987                    return Err(syn::Error::new(
8988                        span,
8989                        "`through` requires `intermediate_fk_column = \"<column>\"`",
8990                    ));
8991                };
8992                let intermediate_pk_column =
8993                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
8994                if out.through_relations.iter().any(|t| t.name == name) {
8995                    return Err(syn::Error::new(
8996                        span,
8997                        format!(
8998                            "duplicate `through(name = \"{name}\")` — \
8999                             pick a unique accessor name"
9000                        ),
9001                    ));
9002                }
9003                out.through_relations.push(ThroughAttr {
9004                    name,
9005                    far,
9006                    far_fk_column,
9007                    intermediate,
9008                    intermediate_fk_column,
9009                    intermediate_pk_column,
9010                });
9011                return Ok(());
9012            }
9013            if meta.path.is_ident("reverse_has") {
9014                // `#[rustango(reverse_has(name = "comments",
9015                //  child = "Comment", child_fk_column = "post_id"
9016                //  [, self_pk_column = "id"]))]` — issue #830.
9017                let span = meta.path.span();
9018                let mut accessor_name: Option<String> = None;
9019                let mut child_ident: Option<syn::Ident> = None;
9020                let mut child_fk_column: Option<String> = None;
9021                let mut self_pk_column: Option<String> = None;
9022                meta.parse_nested_meta(|inner| {
9023                    if inner.path.is_ident("name") {
9024                        let s: LitStr = inner.value()?.parse()?;
9025                        let raw = s.value();
9026                        if raw.trim().is_empty() {
9027                            return Err(syn::Error::new(
9028                                s.span(),
9029                                "`reverse_has(name = \"...\")` must not be empty",
9030                            ));
9031                        }
9032                        accessor_name = Some(raw);
9033                        return Ok(());
9034                    }
9035                    if inner.path.is_ident("child") {
9036                        let s: LitStr = inner.value()?.parse()?;
9037                        let raw = s.value();
9038                        let trimmed = raw.trim();
9039                        if trimmed.is_empty() {
9040                            return Err(syn::Error::new(
9041                                s.span(),
9042                                "`reverse_has(child = \"...\")` must not be empty",
9043                            ));
9044                        }
9045                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9046                        return Ok(());
9047                    }
9048                    if inner.path.is_ident("child_fk_column") {
9049                        let s: LitStr = inner.value()?.parse()?;
9050                        let raw = s.value();
9051                        let trimmed = raw.trim();
9052                        if trimmed.is_empty() {
9053                            return Err(syn::Error::new(
9054                                s.span(),
9055                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9056                            ));
9057                        }
9058                        child_fk_column = Some(trimmed.to_owned());
9059                        return Ok(());
9060                    }
9061                    if inner.path.is_ident("self_pk_column") {
9062                        let s: LitStr = inner.value()?.parse()?;
9063                        let raw = s.value();
9064                        let trimmed = raw.trim();
9065                        if trimmed.is_empty() {
9066                            return Err(syn::Error::new(
9067                                s.span(),
9068                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9069                            ));
9070                        }
9071                        self_pk_column = Some(trimmed.to_owned());
9072                        return Ok(());
9073                    }
9074                    Err(inner.error(
9075                        "unknown `reverse_has` attribute (supported: \
9076                         `name`, `child`, `child_fk_column`, \
9077                         `self_pk_column`)",
9078                    ))
9079                })?;
9080                let Some(name) = accessor_name else {
9081                    return Err(syn::Error::new(
9082                        span,
9083                        "`reverse_has` requires `name = \"...\"`",
9084                    ));
9085                };
9086                let Some(child) = child_ident else {
9087                    return Err(syn::Error::new(
9088                        span,
9089                        "`reverse_has` requires `child = \"ChildModelType\"`",
9090                    ));
9091                };
9092                let Some(child_fk_column) = child_fk_column else {
9093                    return Err(syn::Error::new(
9094                        span,
9095                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9096                    ));
9097                };
9098                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9099                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9100                    return Err(syn::Error::new(
9101                        span,
9102                        format!(
9103                            "duplicate `reverse_has(name = \"{name}\")` — \
9104                             pick a unique accessor name"
9105                        ),
9106                    ));
9107                }
9108                out.reverse_has_relations.push(ReverseHasAttr {
9109                    name,
9110                    child,
9111                    child_fk_column,
9112                    self_pk_column,
9113                });
9114                return Ok(());
9115            }
9116            if meta.path.is_ident("generic_has") {
9117                // `#[rustango(generic_has(name = "tags",
9118                //  child = "Tag", ct_column = "content_type_id",
9119                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9120                //  — issue #830, the reverse generic-FK (GFK) arm of the
9121                //  relation-existence family.
9122                let span = meta.path.span();
9123                let mut accessor_name: Option<String> = None;
9124                let mut child_ident: Option<syn::Ident> = None;
9125                let mut ct_column: Option<String> = None;
9126                let mut pk_column: Option<String> = None;
9127                let mut self_pk_column: Option<String> = None;
9128                meta.parse_nested_meta(|inner| {
9129                    if inner.path.is_ident("name") {
9130                        let s: LitStr = inner.value()?.parse()?;
9131                        let raw = s.value();
9132                        if raw.trim().is_empty() {
9133                            return Err(syn::Error::new(
9134                                s.span(),
9135                                "`generic_has(name = \"...\")` must not be empty",
9136                            ));
9137                        }
9138                        accessor_name = Some(raw);
9139                        return Ok(());
9140                    }
9141                    if inner.path.is_ident("child") {
9142                        let s: LitStr = inner.value()?.parse()?;
9143                        let trimmed = s.value().trim().to_owned();
9144                        if trimmed.is_empty() {
9145                            return Err(syn::Error::new(
9146                                s.span(),
9147                                "`generic_has(child = \"...\")` must not be empty",
9148                            ));
9149                        }
9150                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9151                        return Ok(());
9152                    }
9153                    if inner.path.is_ident("ct_column") {
9154                        let s: LitStr = inner.value()?.parse()?;
9155                        let trimmed = s.value().trim().to_owned();
9156                        if trimmed.is_empty() {
9157                            return Err(syn::Error::new(
9158                                s.span(),
9159                                "`generic_has(ct_column = \"...\")` must not be empty",
9160                            ));
9161                        }
9162                        ct_column = Some(trimmed);
9163                        return Ok(());
9164                    }
9165                    if inner.path.is_ident("pk_column") {
9166                        let s: LitStr = inner.value()?.parse()?;
9167                        let trimmed = s.value().trim().to_owned();
9168                        if trimmed.is_empty() {
9169                            return Err(syn::Error::new(
9170                                s.span(),
9171                                "`generic_has(pk_column = \"...\")` must not be empty",
9172                            ));
9173                        }
9174                        pk_column = Some(trimmed);
9175                        return Ok(());
9176                    }
9177                    if inner.path.is_ident("self_pk_column") {
9178                        let s: LitStr = inner.value()?.parse()?;
9179                        let trimmed = s.value().trim().to_owned();
9180                        if trimmed.is_empty() {
9181                            return Err(syn::Error::new(
9182                                s.span(),
9183                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9184                            ));
9185                        }
9186                        self_pk_column = Some(trimmed);
9187                        return Ok(());
9188                    }
9189                    Err(inner.error(
9190                        "unknown `generic_has` attribute (supported: \
9191                         `name`, `child`, `ct_column`, `pk_column`, \
9192                         `self_pk_column`)",
9193                    ))
9194                })?;
9195                let Some(name) = accessor_name else {
9196                    return Err(syn::Error::new(
9197                        span,
9198                        "`generic_has` requires `name = \"...\"`",
9199                    ));
9200                };
9201                let Some(child) = child_ident else {
9202                    return Err(syn::Error::new(
9203                        span,
9204                        "`generic_has` requires `child = \"ChildModelType\"`",
9205                    ));
9206                };
9207                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9208                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9209                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9210                if out.generic_has_relations.iter().any(|r| r.name == name) {
9211                    return Err(syn::Error::new(
9212                        span,
9213                        format!(
9214                            "duplicate `generic_has(name = \"{name}\")` — \
9215                             pick a unique accessor name"
9216                        ),
9217                    ));
9218                }
9219                out.generic_has_relations.push(GenericHasAttr {
9220                    name,
9221                    child,
9222                    ct_column,
9223                    pk_column,
9224                    self_pk_column,
9225                });
9226                return Ok(());
9227            }
9228            if meta.path.is_ident("audit") {
9229                let mut audit = AuditAttrs::default();
9230                meta.parse_nested_meta(|inner| {
9231                    if inner.path.is_ident("track") {
9232                        let s: LitStr = inner.value()?.parse()?;
9233                        audit.track =
9234                            Some((split_field_list(&s.value()), s.span()));
9235                        return Ok(());
9236                    }
9237                    Err(inner.error(
9238                        "unknown audit attribute (supported: `track`)",
9239                    ))
9240                })?;
9241                out.audit = Some(audit);
9242                return Ok(());
9243            }
9244            if meta.path.is_ident("permissions") {
9245                // Two forms accepted:
9246                //   #[rustango(permissions)]          — flag form, true
9247                //   #[rustango(permissions = false)]  — explicit opt-out
9248                //   #[rustango(permissions = true)]   — explicit opt-in
9249                if let Ok(v) = meta.value() {
9250                    let lit: syn::LitBool = v.parse()?;
9251                    out.permissions = lit.value;
9252                } else {
9253                    out.permissions = true;
9254                }
9255                return Ok(());
9256            }
9257            if meta.path.is_ident("view") {
9258                // Issue #293 / T2.10. Two forms accepted, matching
9259                // the `permissions` flag pattern:
9260                //   #[rustango(view)]          — flag form, true
9261                //   #[rustango(view = false)]  — explicit opt-out
9262                //   #[rustango(view = true)]   — explicit opt-in
9263                if let Ok(v) = meta.value() {
9264                    let lit: syn::LitBool = v.parse()?;
9265                    out.is_view = lit.value;
9266                } else {
9267                    out.is_view = true;
9268                }
9269                return Ok(());
9270            }
9271            if meta.path.is_ident("managed") {
9272                // Django-shape Meta.managed. Issue #321.
9273                //   #[rustango(managed = false)]  — operator-managed table
9274                //   #[rustango(managed = true)]   — rustango-managed (the default)
9275                // Bare-flag form is intentionally not accepted: writing
9276                // `#[rustango(managed)]` reads as "yes please manage it"
9277                // which is already the default. The opt-out is the only
9278                // useful state, so it must be explicit.
9279                let v = meta.value()?;
9280                let lit: syn::LitBool = v.parse()?;
9281                out.managed = lit.value;
9282                return Ok(());
9283            }
9284            if meta.path.is_ident("verbose_name") {
9285                let s: LitStr = meta.value()?.parse()?;
9286                out.verbose_name = Some(s.value());
9287                return Ok(());
9288            }
9289            if meta.path.is_ident("verbose_name_plural") {
9290                let s: LitStr = meta.value()?.parse()?;
9291                out.verbose_name_plural = Some(s.value());
9292                return Ok(());
9293            }
9294            if meta.path.is_ident("db_table_comment") {
9295                // Django-shape `Meta.db_table_comment` (4.2+) — free-form
9296                // table-level comment attached to the DB catalog.
9297                let s: LitStr = meta.value()?.parse()?;
9298                out.db_table_comment = Some(s.value());
9299                return Ok(());
9300            }
9301            if meta.path.is_ident("proxy") {
9302                // Django-shape `Meta.proxy = True` — declarative flag
9303                // marking the struct as a proxy of another model that
9304                // shares its DB table. Stored on `ModelSchema::proxy`
9305                // so future codegen can skip `CreateTable` emission
9306                // for proxies (parent owns the table) and route
9307                // per-instance method resolution to the proxy class.
9308                //
9309                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9310                let value = if meta.input.peek(syn::Token![=]) {
9311                    meta.value()?.parse::<syn::LitBool>()?.value
9312                } else {
9313                    true
9314                };
9315                out.proxy = value;
9316                return Ok(());
9317            }
9318            if meta.path.is_ident("order_with_respect_to") {
9319                // Django-shape `Meta.order_with_respect_to = "parent_fk"` —
9320                // the model's instances are intrinsically ordered
9321                // relative to their parent FK. Django auto-generates
9322                // a `_order` integer column + admin reordering UI.
9323                //
9324                // rustango stores the FK field name on
9325                // `ModelSchema::order_with_respect_to`. Declarative-only
9326                // today: the migration writer + admin surfaces still
9327                // treat every model identically. Future codegen will
9328                // key off the metadata to auto-emit the `_order`
9329                // column and reorder helpers.
9330                //
9331                // Validated as a Rust-shape identifier so the macro
9332                // can reject typos at derive time.
9333                let s: LitStr = meta.value()?.parse()?;
9334                let raw = s.value();
9335                if raw.is_empty() {
9336                    return Err(syn::Error::new(
9337                        s.span(),
9338                        "`order_with_respect_to` must be a non-empty FK field name",
9339                    ));
9340                }
9341                let valid = raw
9342                    .chars()
9343                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9344                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9345                if !valid {
9346                    return Err(syn::Error::new(
9347                        s.span(),
9348                        format!(
9349                            "`order_with_respect_to` must be a valid Rust \
9350                             identifier (letters / digits / underscores, \
9351                             not starting with a digit); got `{raw}`"
9352                        ),
9353                    ));
9354                }
9355                out.order_with_respect_to = Some(raw);
9356                return Ok(());
9357            }
9358            if meta.path.is_ident("required_db_features") {
9359                // Django-shape `Meta.required_db_features` — capability
9360                // tokens the model needs (e.g. `"json_extract"`,
9361                // `"window_functions"`, `"row_security"`). Comma-separated.
9362                // `manage check --deploy` walks every model and warns
9363                // when the active backend doesn't advertise the
9364                // capability.
9365                //
9366                // rustango ships a small registry of capability tokens
9367                // each dialect supports — see `Dialect::supports`.
9368                // Unknown tokens still parse (they end up on the
9369                // schema and show up in the warning) so projects can
9370                // declare aspirational capabilities and the check
9371                // verb will keep nagging until the dialect implements
9372                // them.
9373                let s: LitStr = meta.value()?.parse()?;
9374                let raw = s.value();
9375                let features: Vec<String> = raw
9376                    .split(',')
9377                    .map(str::trim)
9378                    .filter(|s| !s.is_empty())
9379                    .map(str::to_owned)
9380                    .collect();
9381                if features.is_empty() {
9382                    return Err(syn::Error::new(
9383                        s.span(),
9384                        "`required_db_features` must list at least one \
9385                         comma-separated capability token",
9386                    ));
9387                }
9388                out.required_db_features = features;
9389                return Ok(());
9390            }
9391            if meta.path.is_ident("required_db_vendor") {
9392                // Django-shape `Meta.required_db_vendor` — the model
9393                // is only meant to run against the named DB backend.
9394                // `manage check --deploy` flags a mismatch so
9395                // ops catches "I forgot to switch DATABASE_URL" at
9396                // deploy time rather than runtime.
9397                //
9398                // Django spells it as a free-form string; rustango
9399                // restricts to the three backends it ships dialects
9400                // for so the check verb can compare reliably.
9401                let s: LitStr = meta.value()?.parse()?;
9402                let raw = s.value().to_ascii_lowercase();
9403                match raw.as_str() {
9404                    "postgresql" | "postgres" | "pg" => {
9405                        out.required_db_vendor = Some("postgres".to_owned());
9406                    }
9407                    "mysql" | "mariadb" => {
9408                        out.required_db_vendor = Some("mysql".to_owned());
9409                    }
9410                    "sqlite" | "sqlite3" => {
9411                        out.required_db_vendor = Some("sqlite".to_owned());
9412                    }
9413                    _ => {
9414                        return Err(syn::Error::new(
9415                            s.span(),
9416                            format!(
9417                                "unknown required_db_vendor `{raw}` — \
9418                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9419                                 `mysql` (alias: `mariadb`), or `sqlite` \
9420                                 (alias: `sqlite3`)"
9421                            ),
9422                        ));
9423                    }
9424                }
9425                return Ok(());
9426            }
9427            if meta.path.is_ident("base_manager_name") {
9428                // Django-shape `Meta.base_manager_name` — name of the
9429                // Manager subclass that `<instance>.<relation>_set`
9430                // uses when resolving reverse-relation managers.
9431                // Distinct from `default_manager_name` (what
9432                // `Model.objects` returns at the class level).
9433                // Stored on `ModelSchema::base_manager_name`.
9434                //
9435                // Validated as a Rust identifier so it stays safe to
9436                // re-emit as code in future reverse-manager codegen.
9437                let s: LitStr = meta.value()?.parse()?;
9438                let raw = s.value();
9439                if raw.is_empty() {
9440                    return Err(syn::Error::new(
9441                        s.span(),
9442                        "`base_manager_name` must be a non-empty string",
9443                    ));
9444                }
9445                let valid = raw
9446                    .chars()
9447                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9448                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9449                if !valid {
9450                    return Err(syn::Error::new(
9451                        s.span(),
9452                        format!(
9453                            "`base_manager_name` must be a valid Rust \
9454                             identifier (letters / digits / underscores, \
9455                             not starting with a digit); got `{raw}`"
9456                        ),
9457                    ));
9458                }
9459                out.base_manager_name = Some(raw);
9460                return Ok(());
9461            }
9462            if meta.path.is_ident("default_related_name") {
9463                // Django-shape `Meta.default_related_name` — the name
9464                // reverse-relation accessors use when callers don't
9465                // override `related_name=...` on the FK / M2M field.
9466                // Stored on `ModelSchema::default_related_name` so
9467                // future reverse-manager codegen / DRF schema emit /
9468                // admin templates can pick the right accessor name
9469                // (today rustango doesn't auto-emit reverse managers;
9470                // the metadata is the foundation for that work).
9471                //
9472                // Django requires snake_case + no `+` suffix; we
9473                // enforce non-empty + ASCII identifier-shape so the
9474                // string is safe to use as a Rust ident later.
9475                let s: LitStr = meta.value()?.parse()?;
9476                let raw = s.value();
9477                if raw.is_empty() {
9478                    return Err(syn::Error::new(
9479                        s.span(),
9480                        "`default_related_name` must be a non-empty string",
9481                    ));
9482                }
9483                let valid = raw
9484                    .chars()
9485                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9486                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9487                if !valid {
9488                    return Err(syn::Error::new(
9489                        s.span(),
9490                        format!(
9491                            "`default_related_name` must be snake_case ASCII \
9492                             (lowercase letters / digits / underscores, not \
9493                             starting with a digit); got `{raw}`"
9494                        ),
9495                    ));
9496                }
9497                out.default_related_name = Some(raw);
9498                return Ok(());
9499            }
9500            if meta.path.is_ident("extra_permissions") {
9501                // Django-shape `Meta.permissions = [(codename, name), ...]`.
9502                // Comma-separated `codename:label` pairs.
9503                let s: LitStr = meta.value()?.parse()?;
9504                let raw = s.value();
9505                let mut pairs = Vec::new();
9506                for entry in raw.split(',') {
9507                    let entry = entry.trim();
9508                    if entry.is_empty() {
9509                        continue;
9510                    }
9511                    let (codename, label) = match entry.split_once(':') {
9512                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9513                        None => (entry.to_owned(), entry.to_owned()),
9514                    };
9515                    if codename.is_empty() {
9516                        return Err(meta.error(
9517                            "`extra_permissions` entries must be `codename:label` pairs",
9518                        ));
9519                    }
9520                    pairs.push((codename, label));
9521                }
9522                if pairs.is_empty() {
9523                    return Err(meta
9524                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9525                }
9526                out.extra_permissions = pairs;
9527                return Ok(());
9528            }
9529            if meta.path.is_ident("default_permissions") {
9530                // Django-shape `Meta.default_permissions = ('view',
9531                // 'change')`. Comma-separated subset of the CRUD
9532                // action set. Empty means all four (the framework
9533                // default — matches Django when the option is
9534                // omitted).
9535                let s: LitStr = meta.value()?.parse()?;
9536                let raw = s.value();
9537                let mut actions: Vec<String> = Vec::new();
9538                for entry in raw.split(',') {
9539                    let action = entry.trim().to_ascii_lowercase();
9540                    if action.is_empty() {
9541                        continue;
9542                    }
9543                    match action.as_str() {
9544                        "add" | "change" | "delete" | "view" => {}
9545                        other => {
9546                            return Err(syn::Error::new(
9547                                s.span(),
9548                                format!(
9549                                    "unknown default_permissions action `{other}` — \
9550                                     expected one of `add`, `change`, `delete`, `view`"
9551                                ),
9552                            ));
9553                        }
9554                    }
9555                    if !actions.contains(&action) {
9556                        actions.push(action);
9557                    }
9558                }
9559                if actions.is_empty() {
9560                    return Err(syn::Error::new(
9561                        s.span(),
9562                        "`default_permissions = \"…\"` must list at least one action; \
9563                         use `permissions = false` on the container if you want NO \
9564                         permissions seeded for this model.",
9565                    ));
9566                }
9567                out.default_permissions = actions;
9568                return Ok(());
9569            }
9570            if meta.path.is_ident("get_latest_by") {
9571                // Django-shape `Meta.get_latest_by`. The `-` prefix
9572                // selects descending order (Django muscle memory).
9573                let s: LitStr = meta.value()?.parse()?;
9574                let raw = s.value();
9575                let trimmed = raw.trim();
9576                if trimmed.is_empty() {
9577                    return Err(meta.error("`get_latest_by` must name a column"));
9578                }
9579                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9580                    (stripped.to_owned(), true)
9581                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9582                    (stripped.to_owned(), false)
9583                } else {
9584                    (trimmed.to_owned(), false)
9585                };
9586                if col.is_empty() {
9587                    return Err(meta.error("`get_latest_by` must name a column"));
9588                }
9589                out.get_latest_by = Some((col, desc));
9590                return Ok(());
9591            }
9592            if meta.path.is_ident("unique_together") {
9593                // Django-shape composite UNIQUE index. Two syntaxes:
9594                //
9595                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9596                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9597                //
9598                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9599                // (col1, col2)`, where <name> defaults to
9600                // `<table>_<col1>_<col2>_uq` when not supplied.
9601                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9602                out.indexes.push(IndexAttr {
9603                    name,
9604                    columns,
9605                    unique: true,
9606                    method: "btree".to_owned(),
9607                    where_clause: None,
9608                include: Vec::new(),
9609                });
9610                return Ok(());
9611            }
9612            if meta.path.is_ident("index_together") {
9613                // Django-shape composite (non-unique) index. Two syntaxes
9614                // mirroring `unique_together`.
9615                //
9616                //   #[rustango(index_together = "created_at, status")]
9617                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9618                let (columns, name) = parse_together_attr(&meta, "index_together")?;
9619                out.indexes.push(IndexAttr {
9620                    name,
9621                    columns,
9622                    unique: false,
9623                    method: "btree".to_owned(),
9624                    where_clause: None,
9625                include: Vec::new(),
9626                });
9627                return Ok(());
9628            }
9629            if meta.path.is_ident("unique_when") {
9630                // Django 4.0+ `UniqueConstraint(condition=Q(...))` —
9631                // partial unique index. Issue #265 / T1.3.
9632                //
9633                //   #[rustango(unique_when(
9634                //       columns   = "email",
9635                //       condition = "deleted_at IS NULL",
9636                //       name      = "unique_active_email"
9637                //   ))]
9638                //
9639                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9640                // on PG / SQLite (both ship partial indexes natively).
9641                // MySQL falls back to a plain UNIQUE index — the
9642                // condition is lost; document the limitation in the
9643                // generated migration.
9644                let mut columns: Option<Vec<String>> = None;
9645                let mut condition: Option<String> = None;
9646                let mut name: Option<String> = None;
9647                let mut include: Vec<String> = Vec::new();
9648                meta.parse_nested_meta(|inner| {
9649                    if inner.path.is_ident("columns") {
9650                        let s: LitStr = inner.value()?.parse()?;
9651                        columns = Some(split_field_list(&s.value()));
9652                        return Ok(());
9653                    }
9654                    if inner.path.is_ident("condition") {
9655                        let s: LitStr = inner.value()?.parse()?;
9656                        condition = Some(s.value());
9657                        return Ok(());
9658                    }
9659                    if inner.path.is_ident("name") {
9660                        let s: LitStr = inner.value()?.parse()?;
9661                        name = Some(s.value());
9662                        return Ok(());
9663                    }
9664                    if inner.path.is_ident("include") {
9665                        // Django `UniqueConstraint(include=[...])` — PG
9666                        // 11+ covering-index columns. Non-key columns
9667                        // travel with the index leaf for index-only
9668                        // scans. Dropped on MySQL/SQLite by the writer.
9669                        let s: LitStr = inner.value()?.parse()?;
9670                        include = split_field_list(&s.value());
9671                        return Ok(());
9672                    }
9673                    Err(inner.error(
9674                        "unknown unique_when attribute (supported: \
9675                         `columns = \"...\"`, `condition = \"...\"`, \
9676                         `name = \"...\"`, `include = \"...\"`)",
9677                    ))
9678                })?;
9679                let columns = columns.ok_or_else(|| {
9680                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9681                })?;
9682                let condition = condition.ok_or_else(|| {
9683                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9684                })?;
9685                if columns.is_empty() {
9686                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9687                }
9688                out.indexes.push(IndexAttr {
9689                    name,
9690                    columns,
9691                    unique: true,
9692                    method: "btree".to_owned(),
9693                    where_clause: Some(condition),
9694                    include,
9695                });
9696                return Ok(());
9697            }
9698            if meta.path.is_ident("index_when") {
9699                // Django `Index(fields=..., condition=Q(...))` parity —
9700                // non-unique partial index. Sibling of `unique_when`
9701                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9702                //
9703                //   #[rustango(index_when(
9704                //       columns   = "status, created_at",
9705                //       condition = "deleted_at IS NULL",
9706                //       name      = "active_status_created_idx"
9707                //   ))]
9708                //
9709                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9710                // on PG / SQLite (both ship partial indexes natively).
9711                // MySQL has no native partial-index support — the writer
9712                // emits a plain CREATE INDEX and the condition is lost;
9713                // operators wanting that selectivity on MySQL should
9714                // declare a covering index plus an application-level
9715                // filter.
9716                let mut columns: Option<Vec<String>> = None;
9717                let mut condition: Option<String> = None;
9718                let mut name: Option<String> = None;
9719                let mut method: String = "btree".to_owned();
9720                let mut include: Vec<String> = Vec::new();
9721                meta.parse_nested_meta(|inner| {
9722                    if inner.path.is_ident("columns") {
9723                        let s: LitStr = inner.value()?.parse()?;
9724                        columns = Some(split_field_list(&s.value()));
9725                        return Ok(());
9726                    }
9727                    if inner.path.is_ident("condition") {
9728                        let s: LitStr = inner.value()?.parse()?;
9729                        condition = Some(s.value());
9730                        return Ok(());
9731                    }
9732                    if inner.path.is_ident("name") {
9733                        let s: LitStr = inner.value()?.parse()?;
9734                        name = Some(s.value());
9735                        return Ok(());
9736                    }
9737                    if inner.path.is_ident("method") {
9738                        let s: LitStr = inner.value()?.parse()?;
9739                        method = s.value();
9740                        return Ok(());
9741                    }
9742                    if inner.path.is_ident("include") {
9743                        // Django `Index(include=[...])` — PG 11+
9744                        // covering-index columns; non-key columns
9745                        // travel with the index leaf. Dropped on
9746                        // MySQL/SQLite.
9747                        let s: LitStr = inner.value()?.parse()?;
9748                        include = split_field_list(&s.value());
9749                        return Ok(());
9750                    }
9751                    Err(inner.error(
9752                        "unknown index_when attribute (supported: \
9753                         `columns = \"...\"`, `condition = \"...\"`, \
9754                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9755                         `include = \"...\"`)",
9756                    ))
9757                })?;
9758                let columns = columns
9759                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9760                let condition = condition.ok_or_else(|| {
9761                    meta.error("`index_when(...)` requires `condition = \"...\"`")
9762                })?;
9763                if columns.is_empty() {
9764                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
9765                }
9766                out.indexes.push(IndexAttr {
9767                    name,
9768                    columns,
9769                    unique: false,
9770                    method,
9771                    where_clause: Some(condition),
9772                    include,
9773                });
9774                return Ok(());
9775            }
9776            if meta.path.is_ident("index") {
9777                // Container-level composite index — legacy entry that
9778                // was advertised with a trailing `, unique, name = ...`
9779                // flag block which doesn't actually compose under
9780                // `parse_nested_meta`. Prefer `unique_together` /
9781                // `index_together` (above) for new code. The bare
9782                // `index = "..."` form is kept for back-compat: it
9783                // emits a non-unique composite index.
9784                let cols_lit: LitStr = meta.value()?.parse()?;
9785                let columns = split_field_list(&cols_lit.value());
9786                out.indexes.push(IndexAttr {
9787                    name: None,
9788                    columns,
9789                    unique: false,
9790                    method: "btree".to_owned(),
9791                    where_clause: None,
9792                include: Vec::new(),
9793                });
9794                return Ok(());
9795            }
9796            if meta.path.is_ident("check") {
9797                // #[rustango(check(name = "…", expr = "…"))]
9798                let mut name: Option<String> = None;
9799                let mut expr: Option<String> = None;
9800                meta.parse_nested_meta(|inner| {
9801                    if inner.path.is_ident("name") {
9802                        let s: LitStr = inner.value()?.parse()?;
9803                        name = Some(s.value());
9804                        return Ok(());
9805                    }
9806                    if inner.path.is_ident("expr") {
9807                        let s: LitStr = inner.value()?.parse()?;
9808                        expr = Some(s.value());
9809                        return Ok(());
9810                    }
9811                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9812                })?;
9813                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9814                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9815                out.checks.push(CheckAttr { name, expr });
9816                return Ok(());
9817            }
9818            if meta.path.is_ident("exclude") {
9819                // #[rustango(exclude(name = "…", using = "gist",
9820                //                    elements = "col WITH op, col WITH op",
9821                //                    where = "…"))]
9822                let mut name: Option<String> = None;
9823                let mut using: Option<String> = None;
9824                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9825                let mut where_clause: Option<String> = None;
9826                meta.parse_nested_meta(|inner| {
9827                    if inner.path.is_ident("name") {
9828                        let s: LitStr = inner.value()?.parse()?;
9829                        name = Some(s.value());
9830                        return Ok(());
9831                    }
9832                    if inner.path.is_ident("using") {
9833                        let s: LitStr = inner.value()?.parse()?;
9834                        using = Some(s.value());
9835                        return Ok(());
9836                    }
9837                    if inner.path.is_ident("elements") {
9838                        let s: LitStr = inner.value()?.parse()?;
9839                        elements_raw = Some((s.value(), s.span()));
9840                        return Ok(());
9841                    }
9842                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9843                        let s: LitStr = inner.value()?.parse()?;
9844                        where_clause = Some(s.value());
9845                        return Ok(());
9846                    }
9847                    Err(inner.error(
9848                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9849                    ))
9850                })?;
9851                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9852                let using = using.unwrap_or_else(|| "gist".to_owned());
9853                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
9854                    meta.error(
9855                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
9856                    )
9857                })?;
9858                // Parse `col WITH op` pairs separated by commas.
9859                let mut elements: Vec<(String, String)> = Vec::new();
9860                for pair in elements_str.split(',') {
9861                    let pair = pair.trim();
9862                    if pair.is_empty() {
9863                        continue;
9864                    }
9865                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
9866                    let col = split.next().unwrap_or("").trim();
9867                    let rest = split.next().unwrap_or("").trim();
9868                    // `WITH op` — case-insensitive on `WITH`, then op.
9869                    let rest_lc = rest.to_ascii_lowercase();
9870                    let op = rest_lc
9871                        .strip_prefix("with")
9872                        .map(|r| r.trim_start())
9873                        .filter(|r| !r.is_empty())
9874                        .map(|_| {
9875                            // Pull the original-case op from `rest` after the
9876                            // `WITH ` token (5 chars).
9877                            rest[4..].trim_start().to_owned()
9878                        });
9879                    let Some(op) = op else {
9880                        return Err(syn::Error::new(
9881                            elements_span,
9882                            format!(
9883                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
9884                                 (e.g. `room_id WITH =` or `during WITH &&`)"
9885                            ),
9886                        ));
9887                    };
9888                    if col.is_empty() || op.is_empty() {
9889                        return Err(syn::Error::new(
9890                            elements_span,
9891                            format!(
9892                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
9893                                 (both sides non-empty)"
9894                            ),
9895                        ));
9896                    }
9897                    elements.push((col.to_owned(), op));
9898                }
9899                if elements.is_empty() {
9900                    return Err(syn::Error::new(
9901                        elements_span,
9902                        "exclude requires at least one `col WITH op` element",
9903                    ));
9904                }
9905                out.excludes.push(ExcludeAttr {
9906                    name,
9907                    using,
9908                    elements,
9909                    where_clause,
9910                });
9911                return Ok(());
9912            }
9913            if meta.path.is_ident("generic_fk") {
9914                let mut gfk = GenericFkAttr {
9915                    name: String::new(),
9916                    ct_column: String::new(),
9917                    pk_column: String::new(),
9918                };
9919                meta.parse_nested_meta(|inner| {
9920                    if inner.path.is_ident("name") {
9921                        let s: LitStr = inner.value()?.parse()?;
9922                        gfk.name = s.value();
9923                        return Ok(());
9924                    }
9925                    if inner.path.is_ident("ct_column") {
9926                        let s: LitStr = inner.value()?.parse()?;
9927                        gfk.ct_column = s.value();
9928                        return Ok(());
9929                    }
9930                    if inner.path.is_ident("pk_column") {
9931                        let s: LitStr = inner.value()?.parse()?;
9932                        gfk.pk_column = s.value();
9933                        return Ok(());
9934                    }
9935                    Err(inner.error(
9936                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
9937                    ))
9938                })?;
9939                if gfk.name.is_empty() {
9940                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
9941                }
9942                if gfk.ct_column.is_empty() {
9943                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
9944                }
9945                if gfk.pk_column.is_empty() {
9946                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
9947                }
9948                out.generic_fks.push(gfk);
9949                return Ok(());
9950            }
9951            if meta.path.is_ident("fk_composite") {
9952                let mut fk = CompositeFkAttr {
9953                    name: String::new(),
9954                    to: String::new(),
9955                    from: Vec::new(),
9956                    on: Vec::new(),
9957                };
9958                meta.parse_nested_meta(|inner| {
9959                    if inner.path.is_ident("name") {
9960                        let s: LitStr = inner.value()?.parse()?;
9961                        fk.name = s.value();
9962                        return Ok(());
9963                    }
9964                    if inner.path.is_ident("to") {
9965                        let s: LitStr = inner.value()?.parse()?;
9966                        fk.to = s.value();
9967                        return Ok(());
9968                    }
9969                    // `on = ("col1", "col2", ...)` — parse a parenthesised
9970                    // comma-list of string literals.
9971                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
9972                        let value = inner.value()?;
9973                        let content;
9974                        syn::parenthesized!(content in value);
9975                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
9976                            content.parse_terminated(
9977                                |p| p.parse::<syn::LitStr>(),
9978                                syn::Token![,],
9979                            )?;
9980                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
9981                        if inner.path.is_ident("on") {
9982                            fk.on = cols;
9983                        } else {
9984                            fk.from = cols;
9985                        }
9986                        return Ok(());
9987                    }
9988                    Err(inner.error(
9989                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
9990                    ))
9991                })?;
9992                if fk.name.is_empty() {
9993                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
9994                }
9995                if fk.to.is_empty() {
9996                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
9997                }
9998                if fk.from.is_empty() || fk.on.is_empty() {
9999                    return Err(meta.error(
10000                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10001                    ));
10002                }
10003                if fk.from.len() != fk.on.len() {
10004                    return Err(meta.error(format!(
10005                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10006                        fk.from.len(),
10007                        fk.on.len(),
10008                    )));
10009                }
10010                out.composite_fks.push(fk);
10011                return Ok(());
10012            }
10013            if meta.path.is_ident("m2m") {
10014                let mut m2m = M2MAttr {
10015                    name: String::new(),
10016                    to: String::new(),
10017                    through: String::new(),
10018                    src: String::new(),
10019                    dst: String::new(),
10020                    auto_create: true,
10021                };
10022                meta.parse_nested_meta(|inner| {
10023                    if inner.path.is_ident("name") {
10024                        let s: LitStr = inner.value()?.parse()?;
10025                        m2m.name = s.value();
10026                        return Ok(());
10027                    }
10028                    if inner.path.is_ident("to") {
10029                        let s: LitStr = inner.value()?.parse()?;
10030                        m2m.to = s.value();
10031                        return Ok(());
10032                    }
10033                    if inner.path.is_ident("through") {
10034                        let s: LitStr = inner.value()?.parse()?;
10035                        m2m.through = s.value();
10036                        return Ok(());
10037                    }
10038                    if inner.path.is_ident("src") {
10039                        let s: LitStr = inner.value()?.parse()?;
10040                        m2m.src = s.value();
10041                        return Ok(());
10042                    }
10043                    if inner.path.is_ident("dst") {
10044                        let s: LitStr = inner.value()?.parse()?;
10045                        m2m.dst = s.value();
10046                        return Ok(());
10047                    }
10048                    if inner.path.is_ident("auto_create") {
10049                        let lit: syn::LitBool = inner.value()?.parse()?;
10050                        m2m.auto_create = lit.value;
10051                        return Ok(());
10052                    }
10053                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10054                })?;
10055                if m2m.name.is_empty() {
10056                    return Err(meta.error("m2m requires `name = \"...\"`"));
10057                }
10058                if m2m.to.is_empty() {
10059                    return Err(meta.error("m2m requires `to = \"...\"`"));
10060                }
10061                if m2m.through.is_empty() {
10062                    return Err(meta.error("m2m requires `through = \"...\"`"));
10063                }
10064                if m2m.src.is_empty() {
10065                    return Err(meta.error("m2m requires `src = \"...\"`"));
10066                }
10067                if m2m.dst.is_empty() {
10068                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10069                }
10070                out.m2m.push(m2m);
10071                return Ok(());
10072            }
10073            if meta.path.is_ident("generic_m2m") {
10074                let mut gm = GenericM2MAttr {
10075                    name: String::new(),
10076                    through: String::new(),
10077                    pk_column: String::new(),
10078                    ct_column: String::new(),
10079                    related_column: String::new(),
10080                };
10081                meta.parse_nested_meta(|inner| {
10082                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10083                        let s: LitStr = inner.value()?.parse()?;
10084                        Ok(s.value())
10085                    };
10086                    if inner.path.is_ident("name") {
10087                        gm.name = field(&inner)?;
10088                        return Ok(());
10089                    }
10090                    if inner.path.is_ident("through") {
10091                        gm.through = field(&inner)?;
10092                        return Ok(());
10093                    }
10094                    if inner.path.is_ident("pk_column") {
10095                        gm.pk_column = field(&inner)?;
10096                        return Ok(());
10097                    }
10098                    if inner.path.is_ident("ct_column") {
10099                        gm.ct_column = field(&inner)?;
10100                        return Ok(());
10101                    }
10102                    if inner.path.is_ident("related_column") {
10103                        gm.related_column = field(&inner)?;
10104                        return Ok(());
10105                    }
10106                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10107                })?;
10108                for (val, label) in [
10109                    (&gm.name, "name"),
10110                    (&gm.through, "through"),
10111                    (&gm.pk_column, "pk_column"),
10112                    (&gm.ct_column, "ct_column"),
10113                    (&gm.related_column, "related_column"),
10114                ] {
10115                    if val.is_empty() {
10116                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10117                    }
10118                }
10119                out.generic_m2m.push(gm);
10120                return Ok(());
10121            }
10122            Err(meta.error("unknown rustango container attribute"))
10123        })?;
10124    }
10125    Ok(out)
10126}
10127
10128/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10129/// owned field names, trimming whitespace and skipping empty entries.
10130/// Field-name validation against the model is done by the caller.
10131fn split_field_list(raw: &str) -> Vec<String> {
10132    raw.split(',')
10133        .map(str::trim)
10134        .filter(|s| !s.is_empty())
10135        .map(str::to_owned)
10136        .collect()
10137}
10138
10139/// Shared parser for `unique_together` and `index_together` container
10140/// attrs. Accepts both shapes:
10141///
10142///   * `attr = "col1, col2"`              — auto-derived index name.
10143///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10144///
10145/// Returns `(columns, name)`.
10146fn parse_together_attr(
10147    meta: &syn::meta::ParseNestedMeta<'_>,
10148    attr: &str,
10149) -> syn::Result<(Vec<String>, Option<String>)> {
10150    // Disambiguate by whether the next token is `=` (key-value) or
10151    // `(` (parenthesized).
10152    if meta.input.peek(syn::Token![=]) {
10153        let cols_lit: LitStr = meta.value()?.parse()?;
10154        let columns = split_field_list(&cols_lit.value());
10155        check_together_columns(meta, attr, &columns)?;
10156        return Ok((columns, None));
10157    }
10158    let mut columns: Option<Vec<String>> = None;
10159    let mut name: Option<String> = None;
10160    meta.parse_nested_meta(|inner| {
10161        if inner.path.is_ident("columns") {
10162            let s: LitStr = inner.value()?.parse()?;
10163            columns = Some(split_field_list(&s.value()));
10164            return Ok(());
10165        }
10166        if inner.path.is_ident("name") {
10167            let s: LitStr = inner.value()?.parse()?;
10168            name = Some(s.value());
10169            return Ok(());
10170        }
10171        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10172    })?;
10173    let columns = columns.ok_or_else(|| {
10174        meta.error(format!(
10175            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10176        ))
10177    })?;
10178    check_together_columns(meta, attr, &columns)?;
10179    Ok((columns, name))
10180}
10181
10182fn check_together_columns(
10183    meta: &syn::meta::ParseNestedMeta<'_>,
10184    attr: &str,
10185    columns: &[String],
10186) -> syn::Result<()> {
10187    if columns.len() < 2 {
10188        let single = if attr == "unique_together" {
10189            "#[rustango(unique)] on the field"
10190        } else {
10191            "#[rustango(index)] on the field"
10192        };
10193        return Err(meta.error(format!(
10194            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10195        )));
10196    }
10197    Ok(())
10198}
10199
10200/// Parse the fieldsets DSL: pipe-separated sections, optional
10201/// `"Title:"` prefix on each, comma-separated field names after.
10202/// Examples:
10203/// * `"name, office"` → one untitled section with two fields
10204/// * `"Identity: name, office | Metadata: created_at"` → two titled
10205///   sections
10206///
10207/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10208fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10209    raw.split('|')
10210        .map(str::trim)
10211        .filter(|s| !s.is_empty())
10212        .map(|section| {
10213            // Split off an optional `Title:` prefix (first colon).
10214            let (title, rest) = match section.split_once(':') {
10215                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10216                _ => (String::new(), section),
10217            };
10218            let fields = split_field_list(rest);
10219            (title, fields)
10220        })
10221        .collect()
10222}
10223
10224/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10225/// comma-separated entry maps a target field to one or more source
10226/// fields joined with `+`. Whitespace around tokens is trimmed.
10227/// Entries missing `:` or with empty target/source lists are dropped.
10228fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10229    raw.split(',')
10230        .map(str::trim)
10231        .filter(|s| !s.is_empty())
10232        .filter_map(|entry| {
10233            let (target, sources_raw) = entry.split_once(':')?;
10234            let target = target.trim().to_owned();
10235            if target.is_empty() {
10236                return None;
10237            }
10238            let sources: Vec<String> = sources_raw
10239                .split('+')
10240                .map(|s| s.trim().to_owned())
10241                .filter(|s| !s.is_empty())
10242                .collect();
10243            if sources.is_empty() {
10244                return None;
10245            }
10246            Some((target, sources))
10247        })
10248        .collect()
10249}
10250
10251/// Parse Django-shape `formfield_overrides` — `"field:widget,field2:widget2"`
10252/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10253/// and empty halves drop silently — the macro layer only enforces shape,
10254/// not field-name vs. widget-name validity (those checks happen at
10255/// `AdminConfig` consumption time). Issue #359.
10256fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10257    raw.split(',')
10258        .map(str::trim)
10259        .filter(|s| !s.is_empty())
10260        .filter_map(|entry| {
10261            let (field, widget) = entry.split_once(':')?;
10262            let field = field.trim().to_owned();
10263            let widget = widget.trim().to_owned();
10264            if field.is_empty() || widget.is_empty() {
10265                return None;
10266            }
10267            Some((field, widget))
10268        })
10269        .collect()
10270}
10271
10272/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
10273/// Returns `(field_name, desc)` pairs in the same order as the input.
10274fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10275    raw.split(',')
10276        .map(str::trim)
10277        .filter(|s| !s.is_empty())
10278        .map(|spec| {
10279            spec.strip_prefix('-')
10280                .map_or((spec.to_owned(), false), |rest| {
10281                    (rest.trim().to_owned(), true)
10282                })
10283        })
10284        .collect()
10285}
10286
10287struct FieldAttrs {
10288    column: Option<String>,
10289    primary_key: bool,
10290    fk: Option<String>,
10291    o2o: Option<String>,
10292    on: Option<String>,
10293    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10294    /// "set_default" | "no_action")]` — Django-shape
10295    /// `ForeignKey(on_delete=…)`. Only meaningful when `fk` / `o2o` is
10296    /// also set; the macro errors at compile time if applied to a
10297    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10298    /// DDL writer renders `ON DELETE <action>` after the constraint
10299    /// clause when this is `Some`; `None` falls back to the database
10300    /// default (NO ACTION on every backend rustango supports).
10301    on_delete: Option<String>,
10302    /// `#[rustango(related_name = "...")]` — Django-shape per-FK
10303    /// reverse-accessor override. When set, the derive emits
10304    /// `Parent::<related_name>[_pool]` instead of the container-level
10305    /// `default_related_name` or the `<child_snake>_set[_pool]`
10306    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10307    /// silently ignored on non-FK fields. Follow-up to #816.
10308    related_name: Option<String>,
10309    max_length: Option<u32>,
10310    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10311    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10312    /// unconstrained `vector` column.
10313    vector_dims: Option<u32>,
10314    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10315    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10316    /// unconstrained `geometry(Point)` column (SRID 0).
10317    geometry_srid: Option<u32>,
10318    min: Option<i64>,
10319    max: Option<i64>,
10320    default: Option<String>,
10321    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10322    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10323    /// "gen_random_uuid()"`. The Rust field type must be
10324    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10325    /// INSERTs so the DB DEFAULT fires.
10326    auto_uuid: bool,
10327    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10328    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10329    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10330    /// the field is `Auto::Unset`, then bound as a normal parameter
10331    /// rather than relying on a per-dialect DB function. Issue #823
10332    /// (Eloquent `HasUuids`).
10333    ///
10334    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10335    /// Composes with every backend (PG / MySQL / SQLite) — no
10336    /// `gen_random_uuid()` requirement on the database.
10337    default_uuid_v7: bool,
10338    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10339    /// Server-set on insert, immutable from app code afterwards.
10340    /// Implies `auto + default = "now()"`. Field type must be
10341    /// `DateTime<Utc>`.
10342    auto_now_add: bool,
10343    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10344    /// every insert AND every update. Implies `auto + default =
10345    /// "now()"`; the macro additionally rewrites `update_on` /
10346    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10347    /// field value.
10348    auto_now: bool,
10349    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10350    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10351    /// `soft_delete_on(executor)` and `restore_on(executor)`
10352    /// methods on the model.
10353    soft_delete: bool,
10354    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10355    /// the column in the generated DDL.
10356    unique: bool,
10357    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10358    /// generates a `CREATE INDEX` for this column. `unique` here means
10359    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10360    index: bool,
10361    index_unique: bool,
10362    index_name: Option<String>,
10363    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10364    /// `"btree"`. Issue #34.
10365    index_method: String,
10366    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10367    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10368    /// the macro skips this column from every INSERT and UPDATE
10369    /// path, so the database always recomputes the value from
10370    /// `EXPR`. Backlog item #35.
10371    generated_as: Option<String>,
10372    /// `#[rustango(help_text = "…")]` — Django-shape help text
10373    /// rendered below the admin form's input. Threaded into
10374    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10375    /// layers can read it.
10376    help_text: Option<String>,
10377    /// `#[rustango(choices = "value:Label, value:Label")]` — Django-shape
10378    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10379    /// as a `&'static [(&'static str, &'static str)]` slice. When
10380    /// present, the admin form renders a `<select>` instead of `<input>`
10381    /// and the validator rejects values not in the list. Only meaningful
10382    /// for `FieldType::String`; the macro errors at compile time if
10383    /// applied to a non-string field.
10384    choices: Option<Vec<(String, String)>>,
10385    /// `#[rustango(db_comment = "…")]` — Django-shape DB-side column
10386    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10387    /// the comment in CREATE TABLE; Postgres emits a separate
10388    /// `COMMENT ON COLUMN` statement after the table is created;
10389    /// SQLite silently drops the value (no native column comments).
10390    db_comment: Option<String>,
10391    /// `#[rustango(verbose_name = "…")]` — Django-shape human-readable
10392    /// label for the field. Threaded into `FieldSchema::verbose_name`
10393    /// so admin column headers, form labels, and other display
10394    /// surfaces can prefer the friendly caption over the Rust
10395    /// identifier. `None` means renderers fall back to the field name.
10396    verbose_name: Option<String>,
10397    /// `#[rustango(editable = false)]` — Django-shape opt-out from
10398    /// auto-generated form rendering. Defaults to `true` so existing
10399    /// fields keep their current admin / form behavior; setting
10400    /// `false` removes the field from the admin change-form entirely
10401    /// (the value is still visible on detail / list views, just not
10402    /// editable).
10403    editable: bool,
10404    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` — Django-shape
10405    /// "form may submit empty even when DB is NOT NULL". Threaded into
10406    /// `FieldSchema::blank`. Defaults to `false`.
10407    blank: bool,
10408    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10409    /// Django-shape `CITextField`. Threaded into
10410    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10411    /// fields; the macro errors at derive time if applied elsewhere.
10412    case_insensitive: bool,
10413    /// `#[rustango(validators = "email,url")]` — Django-shape
10414    /// model-level validator chain. Comma-separated names that
10415    /// dispatch to the `validators::*` family in `validate_value`.
10416    /// Empty by default; fires on every typed INSERT/UPDATE.
10417    validators: Vec<String>,
10418}
10419
10420fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10421    let mut out = FieldAttrs {
10422        column: None,
10423        primary_key: false,
10424        fk: None,
10425        o2o: None,
10426        on: None,
10427        on_delete: None,
10428        related_name: None,
10429        max_length: None,
10430        vector_dims: None,
10431        geometry_srid: None,
10432        min: None,
10433        max: None,
10434        default: None,
10435        auto_uuid: false,
10436        default_uuid_v7: false,
10437        auto_now_add: false,
10438        auto_now: false,
10439        soft_delete: false,
10440        unique: false,
10441        index: false,
10442        index_unique: false,
10443        index_name: None,
10444        index_method: "btree".to_owned(),
10445        generated_as: None,
10446        help_text: None,
10447        choices: None,
10448        db_comment: None,
10449        verbose_name: None,
10450        editable: true,
10451        blank: false,
10452        case_insensitive: false,
10453        validators: Vec::new(),
10454    };
10455    for attr in &field.attrs {
10456        if !attr.path().is_ident("rustango") {
10457            continue;
10458        }
10459        attr.parse_nested_meta(|meta| {
10460            if meta.path.is_ident("column") {
10461                let s: LitStr = meta.value()?.parse()?;
10462                let name = s.value();
10463                validate_sql_identifier(&name, "column", s.span())?;
10464                out.column = Some(name);
10465                return Ok(());
10466            }
10467            if meta.path.is_ident("primary_key") {
10468                out.primary_key = true;
10469                return Ok(());
10470            }
10471            if meta.path.is_ident("fk") {
10472                let s: LitStr = meta.value()?.parse()?;
10473                out.fk = Some(s.value());
10474                return Ok(());
10475            }
10476            if meta.path.is_ident("o2o") {
10477                let s: LitStr = meta.value()?.parse()?;
10478                out.o2o = Some(s.value());
10479                return Ok(());
10480            }
10481            if meta.path.is_ident("on") {
10482                let s: LitStr = meta.value()?.parse()?;
10483                out.on = Some(s.value());
10484                return Ok(());
10485            }
10486            if meta.path.is_ident("on_delete") {
10487                let s: LitStr = meta.value()?.parse()?;
10488                let raw = s.value();
10489                let normalized = raw.trim().to_ascii_lowercase();
10490                // Validate at parse time so the user gets a clear span
10491                // rather than a downstream compile error in the emit.
10492                match normalized.as_str() {
10493                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10494                    _ => {
10495                        return Err(syn::Error::new(
10496                            s.span(),
10497                            format!(
10498                                "unknown on_delete action `{raw}`; expected one of \
10499                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10500                            ),
10501                        ));
10502                    }
10503                }
10504                out.on_delete = Some(normalized);
10505                return Ok(());
10506            }
10507            if meta.path.is_ident("related_name") {
10508                let s: LitStr = meta.value()?.parse()?;
10509                let raw = s.value();
10510                if raw.trim().is_empty() {
10511                    return Err(syn::Error::new(
10512                        s.span(),
10513                        "`related_name` must be a non-empty identifier",
10514                    ));
10515                }
10516                // Validate as a Rust-ident shape — the value becomes
10517                // a method name on the parent type. Same rule as
10518                // `default_related_name` so the two surfaces match.
10519                if !raw
10520                    .chars()
10521                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10522                    || raw.starts_with(char::is_numeric)
10523                {
10524                    return Err(syn::Error::new(
10525                        s.span(),
10526                        "`related_name` must be snake_case ASCII (lowercase letters, \
10527                         digits, underscores; no leading digit)",
10528                    ));
10529                }
10530                out.related_name = Some(raw);
10531                return Ok(());
10532            }
10533            if meta.path.is_ident("max_length") {
10534                let lit: syn::LitInt = meta.value()?.parse()?;
10535                out.max_length = Some(lit.base10_parse::<u32>()?);
10536                return Ok(());
10537            }
10538            // `#[rustango(vector(dims = N))]` — pgvector column
10539            // dimension (#824). Nested-meta form so it reads like the
10540            // other typed-column attrs.
10541            if meta.path.is_ident("vector") {
10542                meta.parse_nested_meta(|inner| {
10543                    if inner.path.is_ident("dims") {
10544                        let lit: syn::LitInt = inner.value()?.parse()?;
10545                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10546                        return Ok(());
10547                    }
10548                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10549                })?;
10550                return Ok(());
10551            }
10552            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10553            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10554            if meta.path.is_ident("geometry") {
10555                meta.parse_nested_meta(|inner| {
10556                    if inner.path.is_ident("srid") {
10557                        let lit: syn::LitInt = inner.value()?.parse()?;
10558                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10559                        return Ok(());
10560                    }
10561                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10562                })?;
10563                return Ok(());
10564            }
10565            if meta.path.is_ident("min") {
10566                out.min = Some(parse_signed_i64(&meta)?);
10567                return Ok(());
10568            }
10569            if meta.path.is_ident("max") {
10570                out.max = Some(parse_signed_i64(&meta)?);
10571                return Ok(());
10572            }
10573            if meta.path.is_ident("default") {
10574                let s: LitStr = meta.value()?.parse()?;
10575                out.default = Some(s.value());
10576                return Ok(());
10577            }
10578            if meta.path.is_ident("generated_as") {
10579                let s: LitStr = meta.value()?.parse()?;
10580                out.generated_as = Some(s.value());
10581                return Ok(());
10582            }
10583            if meta.path.is_ident("help_text") {
10584                let s: LitStr = meta.value()?.parse()?;
10585                out.help_text = Some(s.value());
10586                return Ok(());
10587            }
10588            if meta.path.is_ident("choices") {
10589                let s: LitStr = meta.value()?.parse()?;
10590                let raw = s.value();
10591                let mut pairs: Vec<(String, String)> = Vec::new();
10592                for chunk in raw.split(',') {
10593                    let chunk = chunk.trim();
10594                    if chunk.is_empty() {
10595                        continue;
10596                    }
10597                    let (value, label) = match chunk.split_once(':') {
10598                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10599                        None => (chunk.to_owned(), chunk.to_owned()),
10600                    };
10601                    if value.is_empty() {
10602                        return Err(syn::Error::new(
10603                            s.span(),
10604                            "`choices` entry has empty value before `:`",
10605                        ));
10606                    }
10607                    pairs.push((value, label));
10608                }
10609                if pairs.is_empty() {
10610                    return Err(syn::Error::new(
10611                        s.span(),
10612                        "`choices = \"…\"` must contain at least one value",
10613                    ));
10614                }
10615                out.choices = Some(pairs);
10616                return Ok(());
10617            }
10618            if meta.path.is_ident("db_comment") {
10619                let s: LitStr = meta.value()?.parse()?;
10620                out.db_comment = Some(s.value());
10621                return Ok(());
10622            }
10623            if meta.path.is_ident("verbose_name") {
10624                let s: LitStr = meta.value()?.parse()?;
10625                out.verbose_name = Some(s.value());
10626                return Ok(());
10627            }
10628            if meta.path.is_ident("editable") {
10629                // Two forms accepted:
10630                //   #[rustango(editable = false)] / true — explicit
10631                //   #[rustango(editable)] — flag form (= true, the
10632                //   default, so harmless; included for symmetry)
10633                if let Ok(v) = meta.value() {
10634                    let lit: syn::LitBool = v.parse()?;
10635                    out.editable = lit.value;
10636                } else {
10637                    out.editable = true;
10638                }
10639                return Ok(());
10640            }
10641            if meta.path.is_ident("blank") {
10642                // Two forms accepted:
10643                //   #[rustango(blank)] — flag form, true
10644                //   #[rustango(blank = false)] / true — explicit
10645                if let Ok(v) = meta.value() {
10646                    let lit: syn::LitBool = v.parse()?;
10647                    out.blank = lit.value;
10648                } else {
10649                    out.blank = true;
10650                }
10651                return Ok(());
10652            }
10653            if meta.path.is_ident("citext") {
10654                // Django-parity CITextField (#344). Two forms:
10655                //   #[rustango(citext)]          — flag form, true
10656                //   #[rustango(citext = true)]   — explicit
10657                //   #[rustango(citext = false)]  — explicit opt-out
10658                // String-only validation lives in the field-type
10659                // emitter (the FieldType discriminant is computed in
10660                // `detect_type`); the macro records the flag and the
10661                // DDL writer emits dialect-specific COLLATE / CITEXT.
10662                if let Ok(v) = meta.value() {
10663                    let lit: syn::LitBool = v.parse()?;
10664                    out.case_insensitive = lit.value;
10665                } else {
10666                    out.case_insensitive = true;
10667                }
10668                return Ok(());
10669            }
10670            if meta.path.is_ident("validators") {
10671                let s: LitStr = meta.value()?.parse()?;
10672                let raw = s.value();
10673                out.validators = raw
10674                    .split(',')
10675                    .map(str::trim)
10676                    .filter(|s| !s.is_empty())
10677                    .map(str::to_owned)
10678                    .collect();
10679                if out.validators.is_empty() {
10680                    return Err(syn::Error::new(
10681                        s.span(),
10682                        "`validators = \"…\"` must list at least one name",
10683                    ));
10684                }
10685                return Ok(());
10686            }
10687            if meta.path.is_ident("auto_uuid") {
10688                out.auto_uuid = true;
10689                // Implied: PK + auto + DEFAULT gen_random_uuid().
10690                // Each is also explicitly settable; the explicit
10691                // value wins if conflicting.
10692                out.primary_key = true;
10693                if out.default.is_none() {
10694                    out.default = Some("gen_random_uuid()".into());
10695                }
10696                return Ok(());
10697            }
10698            if meta.path.is_ident("default_uuid_v7") {
10699                // Backend-neutral counterpart of `auto_uuid` — issue #823.
10700                // No SQL DEFAULT (the macro fills the value Rust-side
10701                // before binding); just mark the field as PK + Auto
10702                // so the insert path is the `Auto::Unset → generate`
10703                // branch.
10704                out.default_uuid_v7 = true;
10705                out.primary_key = true;
10706                return Ok(());
10707            }
10708            if meta.path.is_ident("auto_now_add") {
10709                out.auto_now_add = true;
10710                if out.default.is_none() {
10711                    out.default = Some("now()".into());
10712                }
10713                return Ok(());
10714            }
10715            if meta.path.is_ident("auto_now") {
10716                out.auto_now = true;
10717                if out.default.is_none() {
10718                    out.default = Some("now()".into());
10719                }
10720                return Ok(());
10721            }
10722            if meta.path.is_ident("soft_delete") {
10723                out.soft_delete = true;
10724                return Ok(());
10725            }
10726            if meta.path.is_ident("unique") {
10727                out.unique = true;
10728                return Ok(());
10729            }
10730            if meta.path.is_ident("index") {
10731                out.index = true;
10732                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10733                if meta.input.peek(syn::token::Paren) {
10734                    meta.parse_nested_meta(|inner| {
10735                        if inner.path.is_ident("unique") {
10736                            out.index_unique = true;
10737                            return Ok(());
10738                        }
10739                        if inner.path.is_ident("name") {
10740                            let s: LitStr = inner.value()?.parse()?;
10741                            out.index_name = Some(s.value());
10742                            return Ok(());
10743                        }
10744                        if inner.path.is_ident("method") {
10745                            let s: LitStr = inner.value()?.parse()?;
10746                            let v = s.value();
10747                            match v.as_str() {
10748                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10749                                    out.index_method = v;
10750                                }
10751                                other => {
10752                                    return Err(inner.error(format!(
10753                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10754                                    )));
10755                                }
10756                            }
10757                            return Ok(());
10758                        }
10759                        Err(inner.error(
10760                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10761                        ))
10762                    })?;
10763                }
10764                return Ok(());
10765            }
10766            Err(meta.error("unknown rustango field attribute"))
10767        })?;
10768    }
10769    Ok(out)
10770}
10771
10772/// Parse a signed integer literal, accepting optional leading `-`.
10773fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10774    let expr: syn::Expr = meta.value()?.parse()?;
10775    match expr {
10776        syn::Expr::Lit(syn::ExprLit {
10777            lit: syn::Lit::Int(lit),
10778            ..
10779        }) => lit.base10_parse::<i64>(),
10780        syn::Expr::Unary(syn::ExprUnary {
10781            op: syn::UnOp::Neg(_),
10782            expr,
10783            ..
10784        }) => {
10785            if let syn::Expr::Lit(syn::ExprLit {
10786                lit: syn::Lit::Int(lit),
10787                ..
10788            }) = *expr
10789            {
10790                let v: i64 = lit.base10_parse()?;
10791                Ok(-v)
10792            } else {
10793                Err(syn::Error::new_spanned(expr, "expected integer literal"))
10794            }
10795        }
10796        other => Err(syn::Error::new_spanned(
10797            other,
10798            "expected integer literal (signed)",
10799        )),
10800    }
10801}
10802
10803struct FieldInfo<'a> {
10804    ident: &'a syn::Ident,
10805    column: String,
10806    primary_key: bool,
10807    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10808    /// skip this column when `Auto::Unset` and emit it under
10809    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10810    auto: bool,
10811    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10812    /// the `Column::Value` associated type for typed-column tokens.
10813    value_ty: &'a Type,
10814    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10815    field_type_tokens: TokenStream2,
10816    schema: TokenStream2,
10817    from_row_init: TokenStream2,
10818    /// Variant of [`Self::from_row_init`] that reads the column via
10819    /// `format!("{prefix}__{col}")` so a model can be decoded out of
10820    /// the aliased columns of a JOINed row. Drives slice 9.0d's
10821    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10822    /// helper that `select_related` calls when stitching loaded FKs.
10823    from_aliased_row_init: TokenStream2,
10824    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10825    /// relation helper emit (`Author::<child>_set`) needs to know `T`
10826    /// to point the generated method at the right child model.
10827    fk_inner: Option<Type>,
10828    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10829    /// `kind` (since ForeignKey detection sets `kind` to K's
10830    /// underlying type) but stored separately for clarity at the
10831    /// `FkRelation` construction site, which only sees the FK's
10832    /// surface fields.
10833    fk_pk_kind: DetectedKind,
10834    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10835    /// the bare `ForeignKey<T, K>`. Routes the load_related and
10836    /// fk_pk_access emitters to wrap assignments / accessors in
10837    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10838    /// FK column compiles end-to-end. The DDL writer reads this off
10839    /// the field schema (`nullable` flag); the macro just needs to
10840    /// keep the Rust-side codegen consistent.
10841    nullable: bool,
10842    /// `true` when this column was marked `#[rustango(auto_now)]` —
10843    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10844    /// column instead of the user-supplied value, so `updated_at`
10845    /// always reflects the latest write without the caller having
10846    /// to remember to set it.
10847    auto_now: bool,
10848    /// `true` when this column was marked `#[rustango(auto_now_add)]`
10849    /// — the column is server-set on INSERT (DB DEFAULT) and
10850    /// **immutable** afterwards. `update_on` / `save_on` skip the
10851    /// column entirely so a stale `created_at` value in memory never
10852    /// rewrites the persisted timestamp.
10853    auto_now_add: bool,
10854    /// `true` when this column was marked `#[rustango(soft_delete)]`.
10855    /// Triggers emission of `soft_delete_on(executor)` and
10856    /// `restore_on(executor)` on the model's inherent impl. There is
10857    /// at most one such column per model — emission asserts this.
10858    soft_delete: bool,
10859    /// `Some` when this column was marked
10860    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
10861    /// every INSERT and UPDATE path; the database recomputes the
10862    /// value from `EXPR`. Backlog item #35.
10863    generated_as: Option<String>,
10864    /// `true` when this column was marked
10865    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
10866    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
10867    /// with `Uuid::now_v7()` before binding, so the PK is generated
10868    /// Rust-side and the column is always present in the INSERT
10869    /// statement (no DB DEFAULT requirement). Issue #823.
10870    default_uuid_v7: bool,
10871    /// `Some` when this FK field carried `#[rustango(related_name =
10872    /// "...")]`. Threaded into [`FkRelation::related_name`] and
10873    /// consumed by [`reverse_helper_tokens`] to override the default
10874    /// `<child_snake>_set` accessor name. Follow-up to #816.
10875    related_name: Option<String>,
10876}
10877
10878/// Reject table names that won't survive SQL identifier
10879/// derivation downstream. Postgres' regular-identifier rule
10880/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
10881/// through the framework's unquoted FK / index / constraint name
10882/// emission without surprises. We also disallow leading-digit and
10883/// the empty string for clarity.
10884///
10885/// Reserved-word collisions (`select`, `from`, …) aren't flagged
10886/// here — those produce a runtime error from the SQL parser,
10887/// which is loud enough; statically enumerating reserved words
10888/// across the three supported dialects is more friction than help.
10889///
10890/// Backlog item #65.
10891fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
10892    validate_sql_identifier(name, "table", span)
10893}
10894
10895/// Reject SQL identifiers that compile but break downstream SQL
10896/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
10897/// `kind` is "table" / "column" — used for the error message so users
10898/// see which attribute caused the failure.
10899fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
10900    if name.is_empty() {
10901        return Err(syn::Error::new(
10902            span,
10903            format!("`{kind} = \"\"` is not a valid SQL identifier"),
10904        ));
10905    }
10906    let mut chars = name.chars();
10907    let first = chars.next().unwrap();
10908    if !(first.is_ascii_alphabetic() || first == '_') {
10909        return Err(syn::Error::new(
10910            span,
10911            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
10912        ));
10913    }
10914    for c in chars {
10915        if !(c.is_ascii_alphanumeric() || c == '_') {
10916            return Err(syn::Error::new(
10917                span,
10918                format!(
10919                    "{kind} name `{name}` contains invalid character {c:?} — \
10920                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
10921                     Hyphens in particular break FK / index name derivation \
10922                     downstream; use underscores instead (e.g. `{}`)",
10923                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
10924                ),
10925            ));
10926        }
10927    }
10928    Ok(())
10929}
10930
10931fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
10932    let root = rustango_root();
10933    let attrs = parse_field_attrs(field)?;
10934    let ident = field
10935        .ident
10936        .as_ref()
10937        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
10938    let name = ident.to_string();
10939    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
10940    let primary_key = attrs.primary_key;
10941    let DetectedType {
10942        kind,
10943        nullable,
10944        auto: detected_auto,
10945        fk_inner,
10946    } = detect_type(&field.ty)?;
10947    check_bound_compatibility(field, &attrs, kind)?;
10948    let auto = detected_auto;
10949    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
10950    // INSERT path: the user must wrap the field in `Auto<T>`, which
10951    // marks the column as DB-default-supplied. The mixin attrs then
10952    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
10953    // for `auto_now`, force the value on UPDATE too.
10954    if attrs.auto_uuid {
10955        if kind != DetectedKind::Uuid {
10956            return Err(syn::Error::new_spanned(
10957                field,
10958                "`#[rustango(auto_uuid)]` requires the field type to be \
10959                 `Auto<uuid::Uuid>`",
10960            ));
10961        }
10962        if !detected_auto {
10963            return Err(syn::Error::new_spanned(
10964                field,
10965                "`#[rustango(auto_uuid)]` requires the field type to be \
10966                 wrapped in `Auto<...>` so the macro skips the column on \
10967                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
10968            ));
10969        }
10970    }
10971    if attrs.default_uuid_v7 {
10972        if kind != DetectedKind::Uuid {
10973            return Err(syn::Error::new_spanned(
10974                field,
10975                "`#[rustango(default_uuid_v7)]` requires the field type to be \
10976                 `Auto<uuid::Uuid>`",
10977            ));
10978        }
10979        if !detected_auto {
10980            return Err(syn::Error::new_spanned(
10981                field,
10982                "`#[rustango(default_uuid_v7)]` requires the field type to be \
10983                 wrapped in `Auto<...>` so the macro can detect the \
10984                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
10985            ));
10986        }
10987        if attrs.auto_uuid {
10988            return Err(syn::Error::new_spanned(
10989                field,
10990                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
10991                 `#[rustango(auto_uuid)]` — the former generates the UUID \
10992                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
10993                 Pick one.",
10994            ));
10995        }
10996    }
10997    if attrs.auto_now_add || attrs.auto_now {
10998        if kind != DetectedKind::DateTime {
10999            return Err(syn::Error::new_spanned(
11000                field,
11001                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11002                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11003            ));
11004        }
11005        if !detected_auto {
11006            return Err(syn::Error::new_spanned(
11007                field,
11008                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11009                 the field type to be wrapped in `Auto<...>` so the macro skips \
11010                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11011            ));
11012        }
11013    }
11014    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11015        return Err(syn::Error::new_spanned(
11016            field,
11017            "`#[rustango(soft_delete)]` requires the field type to be \
11018             `Option<chrono::DateTime<chrono::Utc>>`",
11019        ));
11020    }
11021    let is_mixin_auto =
11022        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11023    if detected_auto && !primary_key && !is_mixin_auto {
11024        return Err(syn::Error::new_spanned(
11025            field,
11026            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11027             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11028             `auto_now`",
11029        ));
11030    }
11031    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11032        return Err(syn::Error::new_spanned(
11033            field,
11034            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11035             SERIAL / BIGSERIAL already supplies a default sequence.",
11036        ));
11037    }
11038    if fk_inner.is_some() && primary_key {
11039        return Err(syn::Error::new_spanned(
11040            field,
11041            "`ForeignKey<T>` is not allowed on a primary-key field — \
11042             a row's PK is its own identity, not a reference to a parent.",
11043        ));
11044    }
11045    if attrs.generated_as.is_some() {
11046        if primary_key {
11047            return Err(syn::Error::new_spanned(
11048                field,
11049                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11050                 primary-key field — a PK must be writable so the row \
11051                 has an identity at INSERT time.",
11052            ));
11053        }
11054        if attrs.default.is_some() {
11055            return Err(syn::Error::new_spanned(
11056                field,
11057                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11058                 `default = \"…\"` — Postgres rejects DEFAULT on \
11059                 generated columns. The expression IS the default.",
11060            ));
11061        }
11062        if detected_auto {
11063            return Err(syn::Error::new_spanned(
11064                field,
11065                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11066                 an `Auto<T>` field — generated columns are computed \
11067                 by the DB, not server-assigned via a sequence. Use a \
11068                 plain Rust type (e.g. `f64`).",
11069            ));
11070        }
11071        if fk_inner.is_some() {
11072            return Err(syn::Error::new_spanned(
11073                field,
11074                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11075                 ForeignKey field.",
11076            ));
11077        }
11078    }
11079    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11080    let column_lit = column.as_str();
11081    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11082    // dimension that the bare `Vector` Rust type can't carry, so emit
11083    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11084    // fallback of `Vector(0)`.
11085    let field_type_tokens = if kind == DetectedKind::Vector {
11086        let root = rustango_root();
11087        let dims = attrs.vector_dims.unwrap_or(0);
11088        quote!(#root::core::FieldType::Vector(#dims))
11089    } else if kind == DetectedKind::Geometry {
11090        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11091        // SRID that the bare `Point` Rust type can't carry, so emit
11092        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11093        let root = rustango_root();
11094        let srid = attrs.geometry_srid.unwrap_or(0);
11095        quote!(#root::core::FieldType::Geometry(#srid))
11096    } else {
11097        kind.variant_tokens()
11098    };
11099    let max_length = optional_u32(attrs.max_length);
11100    let min = optional_i64(attrs.min);
11101    let max = optional_i64(attrs.max);
11102    let default = optional_str(attrs.default.as_deref());
11103
11104    let unique = attrs.unique;
11105    let generated_as = optional_str(attrs.generated_as.as_deref());
11106    let help_text = optional_str(attrs.help_text.as_deref());
11107    let choices = optional_choices(attrs.choices.as_deref());
11108    let db_comment = optional_str(attrs.db_comment.as_deref());
11109    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11110    let editable = attrs.editable;
11111    let blank = attrs.blank;
11112    let case_insensitive = attrs.case_insensitive;
11113    let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11114    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11115        return Err(syn::Error::new_spanned(
11116            field,
11117            "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11118             or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11119             non-FK column.",
11120        ));
11121    }
11122    let fk_on_delete = match attrs.on_delete.as_deref() {
11123        None => quote!(::core::option::Option::None),
11124        Some(action) => {
11125            let variant = match action {
11126                "cascade" => quote!(Cascade),
11127                "restrict" => quote!(Restrict),
11128                "set_null" => quote!(SetNull),
11129                "set_default" => quote!(SetDefault),
11130                "no_action" => quote!(NoAction),
11131                // parse_field_attrs already validated this — guard against future drift.
11132                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11133            };
11134            quote!(::core::option::Option::Some(
11135                #root::core::OnDeleteAction::#variant
11136            ))
11137        }
11138    };
11139    let schema = quote! {
11140        #root::core::FieldSchema {
11141            name: #name,
11142            column: #column_lit,
11143            ty: #field_type_tokens,
11144            nullable: #nullable,
11145            primary_key: #primary_key,
11146            relation: #relation,
11147            max_length: #max_length,
11148            min: #min,
11149            max: #max,
11150            default: #default,
11151            auto: #auto,
11152            unique: #unique,
11153            generated_as: #generated_as,
11154            help_text: #help_text,
11155            choices: #choices,
11156            db_comment: #db_comment,
11157            verbose_name: #verbose_name,
11158            editable: #editable,
11159            blank: #blank,
11160            case_insensitive: #case_insensitive,
11161            fk_on_delete: #fk_on_delete,
11162            validators: &[ #(#validators_lits),* ],
11163        }
11164    };
11165
11166    let from_row_init = quote! {
11167        #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11168    };
11169    let from_aliased_row_init = quote! {
11170        #ident: #root::sql::sqlx::Row::try_get(
11171            row,
11172            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11173        )?
11174    };
11175
11176    Ok(FieldInfo {
11177        ident,
11178        column,
11179        primary_key,
11180        auto,
11181        value_ty: &field.ty,
11182        field_type_tokens,
11183        schema,
11184        from_row_init,
11185        from_aliased_row_init,
11186        fk_inner: fk_inner.cloned(),
11187        fk_pk_kind: kind,
11188        nullable,
11189        auto_now: attrs.auto_now,
11190        auto_now_add: attrs.auto_now_add,
11191        soft_delete: attrs.soft_delete,
11192        generated_as: attrs.generated_as.clone(),
11193        default_uuid_v7: attrs.default_uuid_v7,
11194        related_name: attrs.related_name.clone(),
11195    })
11196}
11197
11198fn check_bound_compatibility(
11199    field: &syn::Field,
11200    attrs: &FieldAttrs,
11201    kind: DetectedKind,
11202) -> syn::Result<()> {
11203    if attrs.max_length.is_some() && kind != DetectedKind::String {
11204        return Err(syn::Error::new_spanned(
11205            field,
11206            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11207        ));
11208    }
11209    if attrs.choices.is_some() && kind != DetectedKind::String {
11210        return Err(syn::Error::new_spanned(
11211            field,
11212            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11213             integer-valued enumerations should be modeled with a Rust enum and \
11214             custom (de)serializer for now",
11215        ));
11216    }
11217    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11218        return Err(syn::Error::new_spanned(
11219            field,
11220            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11221        ));
11222    }
11223    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11224        if min > max {
11225            return Err(syn::Error::new_spanned(
11226                field,
11227                format!("`min` ({min}) is greater than `max` ({max})"),
11228            ));
11229        }
11230    }
11231    Ok(())
11232}
11233
11234fn optional_u32(value: Option<u32>) -> TokenStream2 {
11235    if let Some(v) = value {
11236        quote!(::core::option::Option::Some(#v))
11237    } else {
11238        quote!(::core::option::Option::None)
11239    }
11240}
11241
11242fn optional_i64(value: Option<i64>) -> TokenStream2 {
11243    if let Some(v) = value {
11244        quote!(::core::option::Option::Some(#v))
11245    } else {
11246        quote!(::core::option::Option::None)
11247    }
11248}
11249
11250fn optional_str(value: Option<&str>) -> TokenStream2 {
11251    if let Some(v) = value {
11252        quote!(::core::option::Option::Some(#v))
11253    } else {
11254        quote!(::core::option::Option::None)
11255    }
11256}
11257
11258fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11259    let Some(pairs) = pairs else {
11260        return quote!(::core::option::Option::None);
11261    };
11262    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11263    quote!(::core::option::Option::Some(&[#(#entries),*]))
11264}
11265
11266fn relation_tokens(
11267    field: &syn::Field,
11268    attrs: &FieldAttrs,
11269    fk_inner: Option<&syn::Type>,
11270    table: &str,
11271) -> syn::Result<TokenStream2> {
11272    let root = rustango_root();
11273    if let Some(inner) = fk_inner {
11274        if attrs.fk.is_some() || attrs.o2o.is_some() {
11275            return Err(syn::Error::new_spanned(
11276                field,
11277                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11278                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11279            ));
11280        }
11281        let on = attrs.on.as_deref().unwrap_or("id");
11282        return Ok(quote! {
11283            ::core::option::Option::Some(#root::core::Relation::Fk {
11284                to: <#inner as #root::core::Model>::SCHEMA.table,
11285                on: #on,
11286            })
11287        });
11288    }
11289    match (&attrs.fk, &attrs.o2o) {
11290        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11291            field,
11292            "`fk` and `o2o` are mutually exclusive",
11293        )),
11294        (Some(to), None) => {
11295            let on = attrs.on.as_deref().unwrap_or("id");
11296            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11297            // the model's own table. Threaded as a literal string at
11298            // macro-expansion time to sidestep the const-eval cycle
11299            // that `Self::SCHEMA.table` would create when referenced
11300            // inside Self::SCHEMA's own initializer.
11301            let resolved = if to == "self" { table } else { to };
11302            Ok(quote! {
11303                ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11304            })
11305        }
11306        (None, Some(to)) => {
11307            let on = attrs.on.as_deref().unwrap_or("id");
11308            let resolved = if to == "self" { table } else { to };
11309            Ok(quote! {
11310                ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11311            })
11312        }
11313        (None, None) => {
11314            if attrs.on.is_some() {
11315                return Err(syn::Error::new_spanned(
11316                    field,
11317                    "`on` requires `fk` or `o2o`",
11318                ));
11319            }
11320            Ok(quote!(::core::option::Option::None))
11321        }
11322    }
11323}
11324
11325/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11326/// about kinds without depending on `rustango-core` (which would require a
11327/// proc-macro/normal split it doesn't have today).
11328#[derive(Clone, Copy, PartialEq, Eq)]
11329enum DetectedKind {
11330    I16,
11331    I32,
11332    I64,
11333    F32,
11334    F64,
11335    Bool,
11336    String,
11337    DateTime,
11338    Date,
11339    Time,
11340    Uuid,
11341    Json,
11342    Decimal,
11343    Binary,
11344    /// `Array<String>` → PG `text[]` (#341).
11345    ArrayText,
11346    /// `Array<i32>` → PG `integer[]` (#341).
11347    ArrayInt,
11348    /// `Array<i64>` → PG `bigint[]` (#341).
11349    ArrayBigInt,
11350    /// `Range<i32>` → PG `int4range` (#343).
11351    RangeInt,
11352    /// `Range<i64>` → PG `int8range` (#343).
11353    RangeBigInt,
11354    /// `Range<Decimal>` → PG `numrange` (#343).
11355    RangeNumeric,
11356    /// `Range<NaiveDate>` → PG `daterange` (#343).
11357    RangeDate,
11358    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11359    RangeDateTime,
11360    /// `HStore` → PG `hstore` (#342).
11361    HStore,
11362    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11363    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11364    /// in at the `FieldType` emission site (not carried on this enum).
11365    Vector,
11366    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11367    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11368    /// threaded in at the `FieldType` emission site.
11369    Geometry,
11370}
11371
11372impl DetectedKind {
11373    fn variant_tokens(self) -> TokenStream2 {
11374        let root = rustango_root();
11375        match self {
11376            Self::I16 => quote!(#root::core::FieldType::I16),
11377            Self::I32 => quote!(#root::core::FieldType::I32),
11378            Self::I64 => quote!(#root::core::FieldType::I64),
11379            Self::F32 => quote!(#root::core::FieldType::F32),
11380            Self::F64 => quote!(#root::core::FieldType::F64),
11381            Self::Bool => quote!(#root::core::FieldType::Bool),
11382            Self::String => quote!(#root::core::FieldType::String),
11383            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11384            Self::Date => quote!(#root::core::FieldType::Date),
11385            Self::Time => quote!(#root::core::FieldType::Time),
11386            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11387            Self::Json => quote!(#root::core::FieldType::Json),
11388            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11389            Self::Binary => quote!(#root::core::FieldType::Binary),
11390            Self::ArrayText => {
11391                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11392            }
11393            Self::ArrayInt => {
11394                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11395            }
11396            Self::ArrayBigInt => {
11397                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11398            }
11399            Self::RangeInt => {
11400                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11401            }
11402            Self::RangeBigInt => {
11403                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11404            }
11405            Self::RangeNumeric => {
11406                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11407            }
11408            Self::RangeDate => {
11409                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11410            }
11411            Self::RangeDateTime => {
11412                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11413            }
11414            Self::HStore => quote!(#root::core::FieldType::HStore),
11415            // Dimension comes from the `vector(dims = N)` attribute,
11416            // applied at the emission site; `0` here is just a fallback.
11417            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11418            // SRID comes from the `geometry(srid = N)` attribute, applied
11419            // at the emission site; `0` here is just a fallback.
11420            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11421        }
11422    }
11423
11424    fn is_integer(self) -> bool {
11425        matches!(self, Self::I16 | Self::I32 | Self::I64)
11426    }
11427
11428    /// `(SqlValue::<Variant>, default expr)` for emitting the
11429    /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11430    /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11431    /// fires only when the parent's `__rustango_pk_value` returns a
11432    /// different variant than expected, which is a compile-time bug —
11433    /// but we still need a value-typed fallback to keep the match
11434    /// total.
11435    fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11436        let root = rustango_root();
11437        match self {
11438            Self::I16 => (quote!(I16), quote!(0i16)),
11439            Self::I32 => (quote!(I32), quote!(0i32)),
11440            Self::I64 => (quote!(I64), quote!(0i64)),
11441            Self::F32 => (quote!(F32), quote!(0f32)),
11442            Self::F64 => (quote!(F64), quote!(0f64)),
11443            Self::Bool => (quote!(Bool), quote!(false)),
11444            Self::String => (quote!(String), quote!(::std::string::String::new())),
11445            Self::DateTime => (
11446                quote!(DateTime),
11447                quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11448            ),
11449            Self::Date => (
11450                quote!(Date),
11451                quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11452            ),
11453            Self::Time => (
11454                quote!(Time),
11455                quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11456            ),
11457            Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11458            Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11459            Self::Decimal => (
11460                quote!(Decimal),
11461                quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11462            ),
11463            Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11464            // Arrays (#341) can never be a foreign-key primary key, so
11465            // this arm is never reached at runtime — but the match must
11466            // stay total. A bare empty `SqlValue::Array` is the dummy.
11467            Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11468                (quote!(Array), quote!(::std::vec::Vec::new()))
11469            }
11470            // Ranges (#343) likewise can't be a FK PK — never reached.
11471            Self::RangeInt
11472            | Self::RangeBigInt
11473            | Self::RangeNumeric
11474            | Self::RangeDate
11475            | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11476            // HStore (#342) can't be a FK PK — never reached.
11477            Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11478            // Vector (#824) can't be a FK PK — never reached.
11479            Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11480            // Geometry (#443) can't be a FK PK — never reached (arm is
11481            // exhaustiveness-only; never interpolated into emitted code).
11482            Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11483        }
11484    }
11485}
11486
11487/// Result of walking a field's Rust type. `kind` is the underlying
11488/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11489/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11490/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11491/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11492#[derive(Clone, Copy)]
11493struct DetectedType<'a> {
11494    kind: DetectedKind,
11495    nullable: bool,
11496    auto: bool,
11497    fk_inner: Option<&'a syn::Type>,
11498}
11499
11500/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11501/// non-`Auto` types — the caller should already have routed Auto-only
11502/// codegen through this helper, so a `None` indicates a macro-internal
11503/// invariant break.
11504fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11505    let Type::Path(TypePath { path, qself: None }) = ty else {
11506        return None;
11507    };
11508    let last = path.segments.last()?;
11509    if last.ident != "Auto" {
11510        return None;
11511    }
11512    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11513        return None;
11514    };
11515    args.args.iter().find_map(|a| match a {
11516        syn::GenericArgument::Type(t) => Some(t),
11517        _ => None,
11518    })
11519}
11520
11521fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11522    let Type::Path(TypePath { path, qself: None }) = ty else {
11523        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11524    };
11525    let last = path
11526        .segments
11527        .last()
11528        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11529
11530    if last.ident == "Option" {
11531        let inner = generic_inner(ty, &last.arguments, "Option")?;
11532        let inner_det = detect_type(inner)?;
11533        if inner_det.nullable {
11534            return Err(syn::Error::new_spanned(
11535                ty,
11536                "nested Option is not supported",
11537            ));
11538        }
11539        if inner_det.auto {
11540            return Err(syn::Error::new_spanned(
11541                ty,
11542                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11543            ));
11544        }
11545        return Ok(DetectedType {
11546            nullable: true,
11547            ..inner_det
11548        });
11549    }
11550
11551    if last.ident == "Auto" {
11552        let inner = generic_inner(ty, &last.arguments, "Auto")?;
11553        let inner_det = detect_type(inner)?;
11554        if inner_det.auto {
11555            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11556        }
11557        if inner_det.nullable {
11558            return Err(syn::Error::new_spanned(
11559                ty,
11560                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11561            ));
11562        }
11563        if inner_det.fk_inner.is_some() {
11564            return Err(syn::Error::new_spanned(
11565                ty,
11566                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11567            ));
11568        }
11569        if !matches!(
11570            inner_det.kind,
11571            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11572        ) {
11573            return Err(syn::Error::new_spanned(
11574                ty,
11575                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11576                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11577                 (DEFAULT now())",
11578            ));
11579        }
11580        return Ok(DetectedType {
11581            auto: true,
11582            ..inner_det
11583        });
11584    }
11585
11586    if last.ident == "ForeignKey" {
11587        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11588        // Resolve the FK column's underlying SQL type from `K`. When the
11589        // user wrote `ForeignKey<T>` without a key parameter, the type
11590        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11591        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11592        // recurse into K so the column DDL emits the right SQL type
11593        // (VARCHAR for String, UUID for Uuid, …) and the load_related
11594        // emitter knows which `SqlValue` variant to match.
11595        let kind = match key_ty {
11596            Some(k) => detect_type(k)?.kind,
11597            None => DetectedKind::I64,
11598        };
11599        return Ok(DetectedType {
11600            kind,
11601            nullable: false,
11602            auto: false,
11603            fk_inner: Some(inner),
11604        });
11605    }
11606
11607    let kind = match last.ident.to_string().as_str() {
11608        "i16" => DetectedKind::I16,
11609        "i32" => DetectedKind::I32,
11610        "i64" => DetectedKind::I64,
11611        "f32" => DetectedKind::F32,
11612        "f64" => DetectedKind::F64,
11613        "bool" => DetectedKind::Bool,
11614        "String" => DetectedKind::String,
11615        "DateTime" => DetectedKind::DateTime,
11616        "NaiveDate" => DetectedKind::Date,
11617        "NaiveTime" => DetectedKind::Time,
11618        "Uuid" => DetectedKind::Uuid,
11619        "Value" => DetectedKind::Json,
11620        "Decimal" => DetectedKind::Decimal,
11621        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11622        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11623        // — that would emit Binary DDL and decode-fail at runtime.
11624        "Vec" => {
11625            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11626            if let Type::Path(TypePath { path, qself: None }) = inner {
11627                if let Some(seg) = path.segments.last() {
11628                    if seg.ident == "u8" && seg.arguments.is_empty() {
11629                        return Ok(DetectedType {
11630                            kind: DetectedKind::Binary,
11631                            nullable: false,
11632                            auto: false,
11633                            fk_inner: None,
11634                        });
11635                    }
11636                }
11637            }
11638            return Err(syn::Error::new_spanned(
11639                ty,
11640                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11641                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11642            ));
11643        }
11644        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11645        // `integer[]` / `bigint[]` (Django `ArrayField`, #341).
11646        "Array" => {
11647            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11648            let elem = match inner {
11649                Type::Path(TypePath { path, qself: None }) => {
11650                    path.segments.last().map(|s| s.ident.to_string())
11651                }
11652                _ => None,
11653            };
11654            let kind = match elem.as_deref() {
11655                Some("String") => DetectedKind::ArrayText,
11656                Some("i32") => DetectedKind::ArrayInt,
11657                Some("i64") => DetectedKind::ArrayBigInt,
11658                _ => {
11659                    return Err(syn::Error::new_spanned(
11660                        ty,
11661                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11662                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11663                    ));
11664                }
11665            };
11666            return Ok(DetectedType {
11667                kind,
11668                nullable: false,
11669                auto: false,
11670                fk_inner: None,
11671            });
11672        }
11673        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11674        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11675        // `int8range` / `numrange` / `daterange` / `tstzrange` (Django
11676        // `RangeField` family, #343).
11677        "Range" => {
11678            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11679            let elem = match inner {
11680                Type::Path(TypePath { path, qself: None }) => {
11681                    path.segments.last().map(|s| s.ident.to_string())
11682                }
11683                _ => None,
11684            };
11685            let kind = match elem.as_deref() {
11686                Some("i32") => DetectedKind::RangeInt,
11687                Some("i64") => DetectedKind::RangeBigInt,
11688                Some("Decimal") => DetectedKind::RangeNumeric,
11689                Some("NaiveDate") => DetectedKind::RangeDate,
11690                Some("DateTime") => DetectedKind::RangeDateTime,
11691                _ => {
11692                    return Err(syn::Error::new_spanned(
11693                        ty,
11694                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11695                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11696                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11697                         (→ tstzrange) are supported (#343)",
11698                    ));
11699                }
11700            };
11701            return Ok(DetectedType {
11702                kind,
11703                nullable: false,
11704                auto: false,
11705                fk_inner: None,
11706            });
11707        }
11708        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11709        // (the `CastValue` impl bridges logical↔stored); the field's own
11710        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11711        // schema just needs `FieldType::String`.
11712        "Cast" => {
11713            return Ok(DetectedType {
11714                kind: DetectedKind::String,
11715                nullable: false,
11716                auto: false,
11717                fk_inner: None,
11718            });
11719        }
11720        // `HStore` → PG `hstore` (Django `HStoreField`, #342). No generic
11721        // parameter — always a string→string map.
11722        "HStore" => {
11723            return Ok(DetectedType {
11724                kind: DetectedKind::HStore,
11725                nullable: false,
11726                auto: false,
11727                fk_inner: None,
11728            });
11729        }
11730        // `Vector` → pgvector `vector(N)` (#824). The dimension is
11731        // supplied by `#[rustango(vector(dims = N))]`, not the type.
11732        "Vector" => {
11733            return Ok(DetectedType {
11734                kind: DetectedKind::Vector,
11735                nullable: false,
11736                auto: false,
11737                fk_inner: None,
11738            });
11739        }
11740        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11741        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11742        "Point" => {
11743            return Ok(DetectedType {
11744                kind: DetectedKind::Geometry,
11745                nullable: false,
11746                auto: false,
11747                fk_inner: None,
11748            });
11749        }
11750        other => {
11751            return Err(syn::Error::new_spanned(
11752                ty,
11753                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)"),
11754            ));
11755        }
11756    };
11757    Ok(DetectedType {
11758        kind,
11759        nullable: false,
11760        auto: false,
11761        fk_inner: None,
11762    })
11763}
11764
11765fn generic_inner<'a>(
11766    ty: &'a Type,
11767    arguments: &'a PathArguments,
11768    wrapper: &str,
11769) -> syn::Result<&'a Type> {
11770    let PathArguments::AngleBracketed(args) = arguments else {
11771        return Err(syn::Error::new_spanned(
11772            ty,
11773            format!("{wrapper} requires a generic argument"),
11774        ));
11775    };
11776    args.args
11777        .iter()
11778        .find_map(|a| match a {
11779            GenericArgument::Type(t) => Some(t),
11780            _ => None,
11781        })
11782        .ok_or_else(|| {
11783            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11784        })
11785}
11786
11787/// Like [`generic_inner`] but pulls *two* type args — the first is
11788/// required, the second is optional. Used by the `ForeignKey<T, K>`
11789/// detection where K defaults to `i64` when omitted.
11790fn generic_pair<'a>(
11791    ty: &'a Type,
11792    arguments: &'a PathArguments,
11793    wrapper: &str,
11794) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11795    let PathArguments::AngleBracketed(args) = arguments else {
11796        return Err(syn::Error::new_spanned(
11797            ty,
11798            format!("{wrapper} requires a generic argument"),
11799        ));
11800    };
11801    let mut types = args.args.iter().filter_map(|a| match a {
11802        GenericArgument::Type(t) => Some(t),
11803        _ => None,
11804    });
11805    let first = types.next().ok_or_else(|| {
11806        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11807    })?;
11808    let second = types.next();
11809    Ok((first, second))
11810}
11811
11812fn to_snake_case(s: &str) -> String {
11813    let mut out = String::with_capacity(s.len() + 4);
11814    for (i, ch) in s.chars().enumerate() {
11815        if ch.is_ascii_uppercase() {
11816            if i > 0 {
11817                out.push('_');
11818            }
11819            out.push(ch.to_ascii_lowercase());
11820        } else {
11821            out.push(ch);
11822        }
11823    }
11824    out
11825}
11826
11827// ============================================================
11828//  #[derive(Form)]  —  slice 8.4B
11829// ============================================================
11830
11831/// Per-field `#[form(...)]` attributes recognised by the derive.
11832#[derive(Default)]
11833struct FormFieldAttrs {
11834    min: Option<i64>,
11835    max: Option<i64>,
11836    min_length: Option<u32>,
11837    max_length: Option<u32>,
11838    /// `#[form(clean = "fn_name")]` — Django-shape `clean_<field>` hook.
11839    /// The named static method on the form struct is called after the
11840    /// field's typed parse + length/range checks; it gets the parsed
11841    /// value by reference and returns `Result<<FieldType>, String>`.
11842    /// On Ok, the returned value replaces the parsed one; on Err, the
11843    /// message is attached to the field error list. Issue #372.
11844    clean: Option<syn::Ident>,
11845}
11846
11847/// Container-level `#[form(...)]` attributes. Currently only the
11848/// Django-shape cross-field `validate` hook (issue #373).
11849#[derive(Default)]
11850struct FormContainerAttrs {
11851    /// `#[form(validate = "fn_name")]` — Django-shape `clean()` hook.
11852    /// After every per-field parse succeeds, the named method on the
11853    /// form struct is called with `&self` and may return
11854    /// `Result<(), FormErrors>`. Errors merge into the field error
11855    /// list. Issue #373.
11856    validate: Option<syn::Ident>,
11857}
11858
11859/// Detected shape of a form field's Rust type.
11860#[derive(Clone, Copy)]
11861enum FormFieldKind {
11862    String,
11863    I16,
11864    I32,
11865    I64,
11866    F32,
11867    F64,
11868    Bool,
11869}
11870
11871impl FormFieldKind {
11872    fn parse_method(self) -> &'static str {
11873        match self {
11874            Self::I16 => "i16",
11875            Self::I32 => "i32",
11876            Self::I64 => "i64",
11877            Self::F32 => "f32",
11878            Self::F64 => "f64",
11879            // String + Bool don't go through `str::parse`; the codegen
11880            // handles them inline.
11881            Self::String | Self::Bool => "",
11882        }
11883    }
11884}
11885
11886fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
11887    let root = rustango_root();
11888    let struct_name = &input.ident;
11889
11890    let Data::Struct(data) = &input.data else {
11891        return Err(syn::Error::new_spanned(
11892            struct_name,
11893            "Form can only be derived on structs",
11894        ));
11895    };
11896    let Fields::Named(named) = &data.fields else {
11897        return Err(syn::Error::new_spanned(
11898            struct_name,
11899            "Form requires a struct with named fields",
11900        ));
11901    };
11902
11903    // #373 — container-level `#[form(validate = "fn")]` hook.
11904    let container = parse_form_container_attrs(input)?;
11905    let post_field_clean: Vec<TokenStream2> = Vec::new();
11906    let _ = post_field_clean;
11907
11908    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
11909    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
11910
11911    for field in &named.named {
11912        let ident = field
11913            .ident
11914            .as_ref()
11915            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11916        let attrs = parse_form_field_attrs(field)?;
11917        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
11918
11919        let name_lit = ident.to_string();
11920        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
11921        // #372 — append the per-field `clean_<field>` call right after
11922        // the parse block when the attribute is set. The clean fn
11923        // takes &T and returns Result<T, String>; on Err we attach
11924        // the message to the field error list without aborting
11925        // (matches Django's "collect all field errors" shape).
11926        let clean_block = if let Some(clean_fn) = &attrs.clean {
11927            quote! {
11928                if __errors.fields().get(#name_lit).is_none() {
11929                    match Self::#clean_fn(&#ident) {
11930                        ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
11931                        ::core::result::Result::Err(__msg) => {
11932                            __errors.add(#name_lit, __msg);
11933                        }
11934                    }
11935                }
11936            }
11937        } else {
11938            quote! {}
11939        };
11940        field_blocks.push(quote! {
11941            #parse_block
11942            #clean_block
11943        });
11944        field_idents.push(ident);
11945    }
11946
11947    // #373 — after every per-field parse + clean succeeds, call the
11948    // cross-field validator if declared. Errors merge into the
11949    // outgoing FormErrors via the existing `FormErrors::merge` helper
11950    // (same primitive the DRF serializer cross-field hook uses).
11951    let cross_field_call = if let Some(validate_fn) = &container.validate {
11952        quote! {
11953            if __errors.is_empty() {
11954                let __candidate = Self { #( #field_idents ),* };
11955                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
11956                    __errors.merge(__other);
11957                }
11958                if !__errors.is_empty() {
11959                    return ::core::result::Result::Err(__errors);
11960                }
11961                return ::core::result::Result::Ok(__candidate);
11962            }
11963        }
11964    } else {
11965        quote! {}
11966    };
11967
11968    Ok(quote! {
11969        impl #root::forms::Form for #struct_name {
11970            fn parse(
11971                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
11972            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
11973                let mut __errors = #root::forms::FormErrors::default();
11974                #( #field_blocks )*
11975                #cross_field_call
11976                if !__errors.is_empty() {
11977                    return ::core::result::Result::Err(__errors);
11978                }
11979                ::core::result::Result::Ok(Self {
11980                    #( #field_idents ),*
11981                })
11982            }
11983        }
11984    })
11985}
11986
11987fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
11988    let mut out = FormContainerAttrs::default();
11989    for attr in &input.attrs {
11990        if !attr.path().is_ident("form") {
11991            continue;
11992        }
11993        attr.parse_nested_meta(|meta| {
11994            if meta.path.is_ident("validate") {
11995                let s: LitStr = meta.value()?.parse()?;
11996                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
11997                return Ok(());
11998            }
11999            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12000        })?;
12001    }
12002    Ok(out)
12003}
12004
12005fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12006    let mut out = FormFieldAttrs::default();
12007    for attr in &field.attrs {
12008        if !attr.path().is_ident("form") {
12009            continue;
12010        }
12011        attr.parse_nested_meta(|meta| {
12012            if meta.path.is_ident("min") {
12013                let lit: syn::LitInt = meta.value()?.parse()?;
12014                out.min = Some(lit.base10_parse::<i64>()?);
12015                return Ok(());
12016            }
12017            if meta.path.is_ident("max") {
12018                let lit: syn::LitInt = meta.value()?.parse()?;
12019                out.max = Some(lit.base10_parse::<i64>()?);
12020                return Ok(());
12021            }
12022            if meta.path.is_ident("min_length") {
12023                let lit: syn::LitInt = meta.value()?.parse()?;
12024                out.min_length = Some(lit.base10_parse::<u32>()?);
12025                return Ok(());
12026            }
12027            if meta.path.is_ident("max_length") {
12028                let lit: syn::LitInt = meta.value()?.parse()?;
12029                out.max_length = Some(lit.base10_parse::<u32>()?);
12030                return Ok(());
12031            }
12032            if meta.path.is_ident("clean") {
12033                let s: LitStr = meta.value()?.parse()?;
12034                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12035                return Ok(());
12036            }
12037            Err(meta.error(
12038                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12039            ))
12040        })?;
12041    }
12042    Ok(out)
12043}
12044
12045fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12046    let Type::Path(TypePath { path, qself: None }) = ty else {
12047        return Err(syn::Error::new(
12048            span,
12049            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12050        ));
12051    };
12052    let last = path
12053        .segments
12054        .last()
12055        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12056
12057    if last.ident == "Option" {
12058        let inner = generic_inner(ty, &last.arguments, "Option")?;
12059        let (kind, nested) = detect_form_field(inner, span)?;
12060        if nested {
12061            return Err(syn::Error::new(
12062                span,
12063                "nested Option in Form fields is not supported",
12064            ));
12065        }
12066        return Ok((kind, true));
12067    }
12068
12069    let kind = match last.ident.to_string().as_str() {
12070        "String" => FormFieldKind::String,
12071        "i16" => FormFieldKind::I16,
12072        "i32" => FormFieldKind::I32,
12073        "i64" => FormFieldKind::I64,
12074        "f32" => FormFieldKind::F32,
12075        "f64" => FormFieldKind::F64,
12076        "bool" => FormFieldKind::Bool,
12077        other => {
12078            return Err(syn::Error::new(
12079                span,
12080                format!(
12081                    "Form field type `{other}` is not supported in v0.8 — use String / \
12082                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12083                ),
12084            ));
12085        }
12086    };
12087    Ok((kind, false))
12088}
12089
12090#[allow(clippy::too_many_lines)]
12091fn render_form_field_parse(
12092    ident: &syn::Ident,
12093    name_lit: &str,
12094    kind: FormFieldKind,
12095    nullable: bool,
12096    attrs: &FormFieldAttrs,
12097) -> TokenStream2 {
12098    // Pull the raw &str from the payload. Uses variable name `data` to
12099    // match the new `Form::parse(data: &HashMap<…>)` signature.
12100    let lookup = quote! {
12101        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12102    };
12103
12104    let parsed_value = match kind {
12105        FormFieldKind::Bool => quote! {
12106            let __v: bool = match __raw {
12107                ::core::option::Option::None => false,
12108                ::core::option::Option::Some(__s) => !matches!(
12109                    __s.to_ascii_lowercase().as_str(),
12110                    "" | "false" | "0" | "off" | "no"
12111                ),
12112            };
12113        },
12114        FormFieldKind::String => {
12115            if nullable {
12116                quote! {
12117                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12118                        ::core::option::Option::None => ::core::option::Option::None,
12119                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12120                            ::core::option::Option::None
12121                        }
12122                        ::core::option::Option::Some(__s) => {
12123                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12124                        }
12125                    };
12126                }
12127            } else {
12128                quote! {
12129                    let __v: ::std::string::String = match __raw {
12130                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12131                            ::core::clone::Clone::clone(__s)
12132                        }
12133                        _ => {
12134                            __errors.add(#name_lit, "This field is required.");
12135                            ::std::string::String::new()
12136                        }
12137                    };
12138                }
12139            }
12140        }
12141        FormFieldKind::I16
12142        | FormFieldKind::I32
12143        | FormFieldKind::I64
12144        | FormFieldKind::F32
12145        | FormFieldKind::F64 => {
12146            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12147            let ty_lit = kind.parse_method();
12148            let default_val = match kind {
12149                FormFieldKind::I16 => quote! { 0i16 },
12150                FormFieldKind::I32 => quote! { 0i32 },
12151                FormFieldKind::I64 => quote! { 0i64 },
12152                FormFieldKind::F32 => quote! { 0f32 },
12153                FormFieldKind::F64 => quote! { 0f64 },
12154                _ => quote! { Default::default() },
12155            };
12156            if nullable {
12157                quote! {
12158                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12159                        ::core::option::Option::None => ::core::option::Option::None,
12160                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12161                            ::core::option::Option::None
12162                        }
12163                        ::core::option::Option::Some(__s) => {
12164                            match __s.parse::<#parse_ty>() {
12165                                ::core::result::Result::Ok(__n) => {
12166                                    ::core::option::Option::Some(__n)
12167                                }
12168                                ::core::result::Result::Err(__e) => {
12169                                    __errors.add(
12170                                        #name_lit,
12171                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12172                                    );
12173                                    ::core::option::Option::None
12174                                }
12175                            }
12176                        }
12177                    };
12178                }
12179            } else {
12180                quote! {
12181                    let __v: #parse_ty = match __raw {
12182                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12183                            match __s.parse::<#parse_ty>() {
12184                                ::core::result::Result::Ok(__n) => __n,
12185                                ::core::result::Result::Err(__e) => {
12186                                    __errors.add(
12187                                        #name_lit,
12188                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12189                                    );
12190                                    #default_val
12191                                }
12192                            }
12193                        }
12194                        _ => {
12195                            __errors.add(#name_lit, "This field is required.");
12196                            #default_val
12197                        }
12198                    };
12199                }
12200            }
12201        }
12202    };
12203
12204    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12205
12206    quote! {
12207        // `mut` so the per-field `clean` hook (#372) can rewrite the
12208        // parsed value in-place when it returns Ok with a normalized
12209        // form (e.g. trim / lowercase).
12210        let mut #ident = {
12211            #lookup
12212            #parsed_value
12213            #validators
12214            __v
12215        };
12216    }
12217}
12218
12219fn render_form_validators(
12220    name_lit: &str,
12221    kind: FormFieldKind,
12222    nullable: bool,
12223    attrs: &FormFieldAttrs,
12224) -> TokenStream2 {
12225    let mut checks: Vec<TokenStream2> = Vec::new();
12226
12227    let val_ref = if nullable {
12228        quote! { __v.as_ref() }
12229    } else {
12230        quote! { ::core::option::Option::Some(&__v) }
12231    };
12232
12233    let is_string = matches!(kind, FormFieldKind::String);
12234    let is_numeric = matches!(
12235        kind,
12236        FormFieldKind::I16
12237            | FormFieldKind::I32
12238            | FormFieldKind::I64
12239            | FormFieldKind::F32
12240            | FormFieldKind::F64
12241    );
12242
12243    if is_string {
12244        if let Some(min_len) = attrs.min_length {
12245            let min_len_usize = min_len as usize;
12246            checks.push(quote! {
12247                if let ::core::option::Option::Some(__s) = #val_ref {
12248                    if __s.len() < #min_len_usize {
12249                        __errors.add(
12250                            #name_lit,
12251                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12252                        );
12253                    }
12254                }
12255            });
12256        }
12257        if let Some(max_len) = attrs.max_length {
12258            let max_len_usize = max_len as usize;
12259            checks.push(quote! {
12260                if let ::core::option::Option::Some(__s) = #val_ref {
12261                    if __s.len() > #max_len_usize {
12262                        __errors.add(
12263                            #name_lit,
12264                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12265                        );
12266                    }
12267                }
12268            });
12269        }
12270    }
12271
12272    if is_numeric {
12273        if let Some(min) = attrs.min {
12274            checks.push(quote! {
12275                if let ::core::option::Option::Some(__n) = #val_ref {
12276                    if (*__n as f64) < (#min as f64) {
12277                        __errors.add(
12278                            #name_lit,
12279                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12280                        );
12281                    }
12282                }
12283            });
12284        }
12285        if let Some(max) = attrs.max {
12286            checks.push(quote! {
12287                if let ::core::option::Option::Some(__n) = #val_ref {
12288                    if (*__n as f64) > (#max as f64) {
12289                        __errors.add(
12290                            #name_lit,
12291                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12292                        );
12293                    }
12294                }
12295            });
12296        }
12297    }
12298
12299    quote! { #( #checks )* }
12300}
12301
12302// ============================================================
12303//  #[derive(ViewSet)]
12304// ============================================================
12305
12306struct ViewSetAttrs {
12307    model: syn::Path,
12308    fields: Option<Vec<String>>,
12309    filter_fields: Vec<String>,
12310    search_fields: Vec<String>,
12311    /// (field_name, desc)
12312    ordering: Vec<(String, bool)>,
12313    page_size: Option<usize>,
12314    read_only: bool,
12315    perms: ViewSetPermsAttrs,
12316}
12317
12318#[derive(Default)]
12319struct ViewSetPermsAttrs {
12320    list: Vec<String>,
12321    retrieve: Vec<String>,
12322    create: Vec<String>,
12323    update: Vec<String>,
12324    destroy: Vec<String>,
12325}
12326
12327fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12328    let root = rustango_root();
12329    let struct_name = &input.ident;
12330
12331    // Must be a unit struct or an empty named struct.
12332    match &input.data {
12333        Data::Struct(s) => match &s.fields {
12334            Fields::Unit | Fields::Named(_) => {}
12335            Fields::Unnamed(_) => {
12336                return Err(syn::Error::new_spanned(
12337                    struct_name,
12338                    "ViewSet can only be derived on a unit struct or an empty named struct",
12339                ));
12340            }
12341        },
12342        _ => {
12343            return Err(syn::Error::new_spanned(
12344                struct_name,
12345                "ViewSet can only be derived on a struct",
12346            ));
12347        }
12348    }
12349
12350    let attrs = parse_viewset_attrs(input)?;
12351    let model_path = &attrs.model;
12352
12353    // `.fields(&[...])` call — None means skip (use all scalar fields).
12354    let fields_call = if let Some(ref fields) = attrs.fields {
12355        let lits = fields.iter().map(|f| f.as_str());
12356        quote!(.fields(&[ #(#lits),* ]))
12357    } else {
12358        quote!()
12359    };
12360
12361    let filter_fields_call = if attrs.filter_fields.is_empty() {
12362        quote!()
12363    } else {
12364        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12365        quote!(.filter_fields(&[ #(#lits),* ]))
12366    };
12367
12368    let search_fields_call = if attrs.search_fields.is_empty() {
12369        quote!()
12370    } else {
12371        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12372        quote!(.search_fields(&[ #(#lits),* ]))
12373    };
12374
12375    let ordering_call = if attrs.ordering.is_empty() {
12376        quote!()
12377    } else {
12378        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12379            let f = f.as_str();
12380            quote!((#f, #desc))
12381        });
12382        quote!(.ordering(&[ #(#pairs),* ]))
12383    };
12384
12385    let page_size_call = if let Some(n) = attrs.page_size {
12386        quote!(.page_size(#n))
12387    } else {
12388        quote!()
12389    };
12390
12391    let read_only_call = if attrs.read_only {
12392        quote!(.read_only())
12393    } else {
12394        quote!()
12395    };
12396
12397    let perms = &attrs.perms;
12398    let perms_call = if perms.list.is_empty()
12399        && perms.retrieve.is_empty()
12400        && perms.create.is_empty()
12401        && perms.update.is_empty()
12402        && perms.destroy.is_empty()
12403    {
12404        quote!()
12405    } else {
12406        let list_lits = perms.list.iter().map(|s| s.as_str());
12407        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12408        let create_lits = perms.create.iter().map(|s| s.as_str());
12409        let update_lits = perms.update.iter().map(|s| s.as_str());
12410        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12411        quote! {
12412            .permissions(#root::viewset::ViewSetPerms {
12413                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
12414                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12415                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
12416                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
12417                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
12418            })
12419        }
12420    };
12421
12422    Ok(quote! {
12423        impl #struct_name {
12424            /// Build an `axum::Router` with the six standard REST endpoints
12425            /// for this ViewSet, mounted at `prefix`.
12426            pub fn router(prefix: &str, pool: #root::sql::sqlx::PgPool) -> #root::__axum::Router {
12427                #root::viewset::ViewSet::for_model(
12428                    <#model_path as #root::core::Model>::SCHEMA
12429                )
12430                    #fields_call
12431                    #filter_fields_call
12432                    #search_fields_call
12433                    #ordering_call
12434                    #page_size_call
12435                    #perms_call
12436                    #read_only_call
12437                    .router(prefix, pool)
12438            }
12439        }
12440    })
12441}
12442
12443fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12444    let mut model: Option<syn::Path> = None;
12445    let mut fields: Option<Vec<String>> = None;
12446    let mut filter_fields: Vec<String> = Vec::new();
12447    let mut search_fields: Vec<String> = Vec::new();
12448    let mut ordering: Vec<(String, bool)> = Vec::new();
12449    let mut page_size: Option<usize> = None;
12450    let mut read_only = false;
12451    let mut perms = ViewSetPermsAttrs::default();
12452
12453    for attr in &input.attrs {
12454        if !attr.path().is_ident("viewset") {
12455            continue;
12456        }
12457        attr.parse_nested_meta(|meta| {
12458            if meta.path.is_ident("model") {
12459                let path: syn::Path = meta.value()?.parse()?;
12460                model = Some(path);
12461                return Ok(());
12462            }
12463            if meta.path.is_ident("fields") {
12464                let s: LitStr = meta.value()?.parse()?;
12465                fields = Some(split_field_list(&s.value()));
12466                return Ok(());
12467            }
12468            if meta.path.is_ident("filter_fields") {
12469                let s: LitStr = meta.value()?.parse()?;
12470                filter_fields = split_field_list(&s.value());
12471                return Ok(());
12472            }
12473            if meta.path.is_ident("search_fields") {
12474                let s: LitStr = meta.value()?.parse()?;
12475                search_fields = split_field_list(&s.value());
12476                return Ok(());
12477            }
12478            if meta.path.is_ident("ordering") {
12479                let s: LitStr = meta.value()?.parse()?;
12480                ordering = parse_ordering_list(&s.value());
12481                return Ok(());
12482            }
12483            if meta.path.is_ident("page_size") {
12484                let lit: syn::LitInt = meta.value()?.parse()?;
12485                page_size = Some(lit.base10_parse::<usize>()?);
12486                return Ok(());
12487            }
12488            if meta.path.is_ident("read_only") {
12489                read_only = true;
12490                return Ok(());
12491            }
12492            if meta.path.is_ident("permissions") {
12493                meta.parse_nested_meta(|inner| {
12494                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12495                        let s: LitStr = inner.value()?.parse()?;
12496                        Ok(split_field_list(&s.value()))
12497                    };
12498                    if inner.path.is_ident("list") {
12499                        perms.list = parse_codenames(&inner)?;
12500                    } else if inner.path.is_ident("retrieve") {
12501                        perms.retrieve = parse_codenames(&inner)?;
12502                    } else if inner.path.is_ident("create") {
12503                        perms.create = parse_codenames(&inner)?;
12504                    } else if inner.path.is_ident("update") {
12505                        perms.update = parse_codenames(&inner)?;
12506                    } else if inner.path.is_ident("destroy") {
12507                        perms.destroy = parse_codenames(&inner)?;
12508                    } else {
12509                        return Err(inner.error(
12510                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12511                        ));
12512                    }
12513                    Ok(())
12514                })?;
12515                return Ok(());
12516            }
12517            Err(meta.error(
12518                "unknown viewset attribute (supported: model, fields, filter_fields, \
12519                 search_fields, ordering, page_size, read_only, permissions(...))",
12520            ))
12521        })?;
12522    }
12523
12524    let model = model.ok_or_else(|| {
12525        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12526    })?;
12527
12528    Ok(ViewSetAttrs {
12529        model,
12530        fields,
12531        filter_fields,
12532        search_fields,
12533        ordering,
12534        page_size,
12535        read_only,
12536        perms,
12537    })
12538}
12539
12540// ============================================================ #[derive(Serializer)]
12541
12542struct SerializerContainerAttrs {
12543    model: syn::Path,
12544    /// `#[serializer(validate = "fn_name")]` on the struct — DRF-shape
12545    /// cross-field validation hook (#436). The named inherent method
12546    /// must take `&self` and return
12547    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12548    /// `validate()` runs every per-field validator first; then calls
12549    /// the cross-field method if declared; aggregates all errors into
12550    /// one `FormErrors`.
12551    cross_validate: Option<syn::Ident>,
12552}
12553
12554#[derive(Default)]
12555struct SerializerFieldAttrs {
12556    read_only: bool,
12557    write_only: bool,
12558    source: Option<String>,
12559    skip: bool,
12560    /// `#[serializer(method = "fn_name")]` — DRF SerializerMethodField
12561    /// analog. The macro emits `from_model` initializer that calls
12562    /// `Self::fn_name(&model)` and stores the return value.
12563    method: Option<String>,
12564    /// `#[serializer(validate = "fn_name")]` — per-field validator
12565    /// callable run by `Self::validate(&self)`. Must return
12566    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12567    /// the field name.
12568    validate: Option<String>,
12569    /// `#[serializer(nested)]` on a field whose type is another
12570    /// `Serializer` — the macro emits `from_model` initializer that
12571    /// reads the parent via `model.<source>.value()` then calls the
12572    /// child serializer's `from_model(parent)`. When the FK is
12573    /// unloaded the field falls back to `Default::default()` (does
12574    /// NOT panic) so a missing prefetch in prod degrades gracefully.
12575    /// Source field on the model defaults to the field name; override
12576    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12577    /// panic-on-unloaded behavior for tests.
12578    nested: bool,
12579    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12580    /// strict behavior: panic when the FK isn't loaded. Useful in
12581    /// test code where forgetting select_related must trip a hard
12582    /// failure rather than render a blank nested object.
12583    nested_strict: bool,
12584    /// `#[serializer(many = TagSerializer)]` — declare the field as
12585    /// a list of nested serializers. Field type must be `Vec<S>`
12586    /// where `S` is the inner serializer. The macro initializes the
12587    /// field to `Vec::new()` in `from_model` and emits a typed
12588    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12589    /// maps each model row through `S::from_model`. Auto-load isn't
12590    /// possible (the M2M / one-to-many accessor is async); callers
12591    /// fetch the children + call the setter post-from_model.
12592    many: Option<syn::Type>,
12593    /// `#[serializer(slug = "name")]` — DRF `SlugRelatedField` analog.
12594    /// Source field on the model must be a `ForeignKey<T>`; the
12595    /// macro emits `from_model` glue that walks
12596    /// `model.<source>.value()?.<slug>` and clones it. Field type on
12597    /// the serializer is typically `String` (whatever type the slug
12598    /// column has). When the FK is unloaded the field falls back to
12599    /// `Default::default()`, same graceful-degrade contract as
12600    /// `nested`. Source defaults to the field name; override with
12601    /// `source = "..."`. v0.44.
12602    slug: Option<String>,
12603}
12604
12605fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12606    let mut model: Option<syn::Path> = None;
12607    let mut cross_validate: Option<syn::Ident> = None;
12608    for attr in &input.attrs {
12609        if !attr.path().is_ident("serializer") {
12610            continue;
12611        }
12612        attr.parse_nested_meta(|meta| {
12613            if meta.path.is_ident("model") {
12614                let _eq: syn::Token![=] = meta.input.parse()?;
12615                model = Some(meta.input.parse()?);
12616                return Ok(());
12617            }
12618            if meta.path.is_ident("validate") {
12619                // #436 — container-level `validate = "fn_name"` for the
12620                // DRF cross-field-validation shape. Field-level
12621                // `#[serializer(validate = "...")]` on a field is
12622                // parsed separately in `parse_serializer_field_attrs`.
12623                let s: LitStr = meta.value()?.parse()?;
12624                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12625                return Ok(());
12626            }
12627            Err(meta.error(
12628                "unknown serializer container attribute \
12629                 (supported: `model`, `validate`)",
12630            ))
12631        })?;
12632    }
12633    let model = model.ok_or_else(|| {
12634        syn::Error::new_spanned(
12635            &input.ident,
12636            "`#[serializer(model = SomeModel)]` is required",
12637        )
12638    })?;
12639    Ok(SerializerContainerAttrs {
12640        model,
12641        cross_validate,
12642    })
12643}
12644
12645fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12646    let mut out = SerializerFieldAttrs::default();
12647    for attr in &field.attrs {
12648        if !attr.path().is_ident("serializer") {
12649            continue;
12650        }
12651        attr.parse_nested_meta(|meta| {
12652            if meta.path.is_ident("read_only") {
12653                out.read_only = true;
12654                return Ok(());
12655            }
12656            if meta.path.is_ident("write_only") {
12657                out.write_only = true;
12658                return Ok(());
12659            }
12660            if meta.path.is_ident("skip") {
12661                out.skip = true;
12662                return Ok(());
12663            }
12664            if meta.path.is_ident("source") {
12665                let s: LitStr = meta.value()?.parse()?;
12666                out.source = Some(s.value());
12667                return Ok(());
12668            }
12669            if meta.path.is_ident("method") {
12670                let s: LitStr = meta.value()?.parse()?;
12671                out.method = Some(s.value());
12672                return Ok(());
12673            }
12674            if meta.path.is_ident("validate") {
12675                let s: LitStr = meta.value()?.parse()?;
12676                out.validate = Some(s.value());
12677                return Ok(());
12678            }
12679            if meta.path.is_ident("many") {
12680                let _eq: syn::Token![=] = meta.input.parse()?;
12681                out.many = Some(meta.input.parse()?);
12682                return Ok(());
12683            }
12684            if meta.path.is_ident("nested") {
12685                out.nested = true;
12686                // Optional strict flag inside parentheses:
12687                //   #[serializer(nested(strict))]
12688                if meta.input.peek(syn::token::Paren) {
12689                    meta.parse_nested_meta(|inner| {
12690                        if inner.path.is_ident("strict") {
12691                            out.nested_strict = true;
12692                            return Ok(());
12693                        }
12694                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12695                    })?;
12696                }
12697                return Ok(());
12698            }
12699            if meta.path.is_ident("slug") {
12700                let s: LitStr = meta.value()?.parse()?;
12701                out.slug = Some(s.value());
12702                return Ok(());
12703            }
12704            Err(meta.error(
12705                "unknown serializer field attribute (supported: \
12706                 `read_only`, `write_only`, `source`, `skip`, `method`, \
12707                 `validate`, `nested`, `many`, `slug`)",
12708            ))
12709        })?;
12710    }
12711    // Validate: read_only + write_only is nonsensical
12712    if out.read_only && out.write_only {
12713        return Err(syn::Error::new_spanned(
12714            field,
12715            "a field cannot be both `read_only` and `write_only`",
12716        ));
12717    }
12718    if out.method.is_some() && out.source.is_some() {
12719        return Err(syn::Error::new_spanned(
12720            field,
12721            "`method` and `source` are mutually exclusive — `method` computes \
12722             the value from a method, `source` reads it from a different model field",
12723        ));
12724    }
12725    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12726        return Err(syn::Error::new_spanned(
12727            field,
12728            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12729             — pick one strategy for populating the field",
12730        ));
12731    }
12732    Ok(out)
12733}
12734
12735fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12736    let root = rustango_root();
12737    let struct_name = &input.ident;
12738    let struct_name_lit = struct_name.to_string();
12739
12740    let Data::Struct(data) = &input.data else {
12741        return Err(syn::Error::new_spanned(
12742            struct_name,
12743            "Serializer can only be derived on structs",
12744        ));
12745    };
12746    let Fields::Named(named) = &data.fields else {
12747        return Err(syn::Error::new_spanned(
12748            struct_name,
12749            "Serializer requires a struct with named fields",
12750        ));
12751    };
12752
12753    let container = parse_serializer_container_attrs(input)?;
12754    let model_path = &container.model;
12755
12756    // Classify each field. `ty` is only consumed by the
12757    // `#[cfg(feature = "openapi")]` block below, but we always
12758    // capture it to keep the field-info build a single pass.
12759    #[allow(dead_code)]
12760    struct FieldInfo {
12761        ident: syn::Ident,
12762        ty: syn::Type,
12763        attrs: SerializerFieldAttrs,
12764    }
12765    let mut fields_info: Vec<FieldInfo> = Vec::new();
12766    for field in &named.named {
12767        let ident = field.ident.clone().expect("named field has ident");
12768        let attrs = parse_serializer_field_attrs(field)?;
12769        fields_info.push(FieldInfo {
12770            ident,
12771            ty: field.ty.clone(),
12772            attrs,
12773        });
12774    }
12775
12776    // Generate from_model body: struct literal with each field assigned.
12777    let from_model_fields = fields_info.iter().map(|fi| {
12778        let ident = &fi.ident;
12779        let ty = &fi.ty;
12780        if let Some(_inner) = &fi.attrs.many {
12781            // Many — collection field. Initialize empty; caller
12782            // populates via the macro-emitted set_<field> helper
12783            // after fetching the M2M children.
12784            quote! { #ident: ::std::vec::Vec::new() }
12785        } else if let Some(method) = &fi.attrs.method {
12786            // SerializerMethodField: call Self::<method>(&model) to
12787            // compute the value. Method signature must be
12788            // `fn <method>(model: &T) -> <field type>`.
12789            let method_ident = syn::Ident::new(method, ident.span());
12790            quote! { #ident: Self::#method_ident(model) }
12791        } else if let Some(slug_field) = &fi.attrs.slug {
12792            // v0.44 — SlugRelatedField. Source defaults to the field
12793            // name on this struct; override via `source = "..."`. The
12794            // source field on the model is expected to be a
12795            // `ForeignKey<T>`; the slug field on the parent is named
12796            // by the attribute value. When the FK is unloaded the
12797            // field falls back to `Default::default()` — same
12798            // graceful-degrade contract as `nested`.
12799            let src_name = fi
12800                .attrs
12801                .source
12802                .as_deref()
12803                .unwrap_or(&fi.ident.to_string())
12804                .to_owned();
12805            let src_ident = syn::Ident::new(&src_name, ident.span());
12806            let slug_ident = syn::Ident::new(slug_field, ident.span());
12807            quote! {
12808                #ident: match model.#src_ident.value() {
12809                    ::core::option::Option::Some(__loaded) =>
12810                        ::core::clone::Clone::clone(&__loaded.#slug_ident),
12811                    ::core::option::Option::None =>
12812                        ::core::default::Default::default(),
12813                }
12814            }
12815        } else if fi.attrs.nested {
12816            // Nested serializer. Source defaults to the field name on
12817            // this struct; override via `source = "..."`. The source
12818            // field on the model is expected to be a `ForeignKey<T>`
12819            // whose `.value()` returns `Option<&T>` after lazy-load.
12820            //
12821            // Behavior matrix (tweakable per-field):
12822            //   * FK loaded   → nested object materializes via
12823            //                   ChildSerializer::from_model(parent).
12824            //   * FK unloaded → fall back to ChildSerializer::default()
12825            //                   (so prod doesn't crash on a missing
12826            //                   prefetch — just renders a blank nested
12827            //                   object). Add `#[serializer(nested,
12828            //                   strict)]` to keep the v0.18.1
12829            //                   panic-on-unloaded behavior for tests
12830            //                   that want hard guardrails.
12831            let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
12832            let src_ident = syn::Ident::new(&src_name, ident.span());
12833            if fi.attrs.nested_strict {
12834                let panic_msg = format!(
12835                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
12836                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
12837                );
12838                quote! {
12839                    #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
12840                        model.#src_ident.value().expect(#panic_msg),
12841                    )
12842                }
12843            } else {
12844                quote! {
12845                    #ident: match model.#src_ident.value() {
12846                        ::core::option::Option::Some(__loaded) =>
12847                            <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
12848                        ::core::option::Option::None =>
12849                            ::core::default::Default::default(),
12850                    }
12851                }
12852            }
12853        } else if fi.attrs.write_only || fi.attrs.skip {
12854            // Not read from model — use default
12855            quote! { #ident: ::core::default::Default::default() }
12856        } else if let Some(src) = &fi.attrs.source {
12857            let src_ident = syn::Ident::new(src, ident.span());
12858            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
12859        } else {
12860            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
12861        }
12862    });
12863
12864    // Per-field validators (DRF-shape `validators=[...]`). Emit a
12865    // `validate(&self)` method that runs each user-defined validator
12866    // and aggregates errors into `FormErrors`.
12867    let validator_calls: Vec<_> = fields_info
12868        .iter()
12869        .filter_map(|fi| {
12870            let ident = &fi.ident;
12871            let name_lit = ident.to_string();
12872            let method = fi.attrs.validate.as_ref()?;
12873            let method_ident = syn::Ident::new(method, ident.span());
12874            Some(quote! {
12875                if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
12876                    __errors.add(#name_lit.to_owned(), __e);
12877                }
12878            })
12879        })
12880        .collect();
12881    // #436 — DRF cross-field `validate(self)` shape. If the
12882    // container declared `#[serializer(validate = "fn_name")]`,
12883    // the macro-generated `validate(&self)` runs every per-field
12884    // validator first, then calls the user's cross-field method,
12885    // merging its `FormErrors` into the per-field errors. Either
12886    // alone is enough to emit the wrapper.
12887    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
12888        quote! {
12889            // Merge cross-field errors into the per-field bucket so
12890            // a single .validate() call surfaces both layers.
12891            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
12892                __errors.merge(__cross);
12893            }
12894        }
12895    });
12896    let validate_method = if validator_calls.is_empty() && container.cross_validate.is_none() {
12897        quote! {}
12898    } else {
12899        quote! {
12900            impl #struct_name {
12901                /// Run every `#[serializer(validate = "...")]` per-field
12902                /// validator and, when declared, the container-level
12903                /// cross-field validator. Aggregates errors into
12904                /// `FormErrors` keyed by the field name (plus any
12905                /// non-field keys the cross-field method adds).
12906                /// Returns `Ok(())` when all pass.
12907                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
12908                    let mut __errors = #root::forms::FormErrors::default();
12909                    #( #validator_calls )*
12910                    #cross_validate_call
12911                    if __errors.is_empty() {
12912                        ::core::result::Result::Ok(())
12913                    } else {
12914                        ::core::result::Result::Err(__errors)
12915                    }
12916                }
12917            }
12918        }
12919    };
12920
12921    // For every `#[serializer(many = S)]` field, emit a
12922    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
12923    // helper that maps the parents through `S::from_model`.
12924    let many_setters: Vec<_> = fields_info
12925        .iter()
12926        .filter_map(|fi| {
12927            let many_ty = fi.attrs.many.as_ref()?;
12928            let ident = &fi.ident;
12929            let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
12930            Some(quote! {
12931                /// Populate this `many` field by mapping each parent model
12932                /// through the inner serializer's `from_model`. Call after
12933                /// fetching the M2M / one-to-many children since
12934                /// `from_model` itself can't await an SQL query.
12935                pub fn #setter(
12936                    &mut self,
12937                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
12938                ) -> &mut Self {
12939                    self.#ident = models.iter()
12940                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
12941                        .collect();
12942                    self
12943                }
12944            })
12945        })
12946        .collect();
12947    let many_setters_impl = if many_setters.is_empty() {
12948        quote! {}
12949    } else {
12950        quote! {
12951            impl #struct_name {
12952                #( #many_setters )*
12953            }
12954        }
12955    };
12956
12957    // Generate custom Serialize: skip write_only fields
12958    let output_fields: Vec<_> = fields_info
12959        .iter()
12960        .filter(|fi| !fi.attrs.write_only)
12961        .collect();
12962    let output_field_count = output_fields.len();
12963    let serialize_fields = output_fields.iter().map(|fi| {
12964        let ident = &fi.ident;
12965        let name_lit = ident.to_string();
12966        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
12967    });
12968
12969    // writable_fields: normal + write_only.
12970    // Exclude:
12971    //   - `read_only` — server-computed.
12972    //   - `skip` — caller sets manually post-from_model.
12973    //   - `method` — computed from a Self::fn(&model) call; accepting
12974    //     it on write is meaningless.
12975    //   - `nested` / `many` — populated from related-model data, not
12976    //     from a field on the wire body.
12977    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
12978    // `many` in `writable_fields()`, which made the ViewSet write
12979    // path accept those fields from the JSON body and try to bind
12980    // them to the SQL UPDATE — a silent no-op at best, a type
12981    // mismatch at worst.
12982    let writable_lits: Vec<_> = fields_info
12983        .iter()
12984        .filter(|fi| {
12985            !fi.attrs.read_only
12986                && !fi.attrs.skip
12987                && fi.attrs.method.is_none()
12988                && !fi.attrs.nested
12989                && fi.attrs.many.is_none()
12990                && fi.attrs.slug.is_none()
12991        })
12992        .map(|fi| fi.ident.to_string())
12993        .collect();
12994
12995    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
12996    // Only includes fields shown in JSON output (skips write_only). For each
12997    // `Option<T>` field, omit from `required` and add `.nullable()`.
12998    let openapi_impl = {
12999        #[cfg(feature = "openapi")]
13000        {
13001            let property_calls = output_fields.iter().map(|fi| {
13002                let ident = &fi.ident;
13003                let name_lit = ident.to_string();
13004                let ty = &fi.ty;
13005                let nullable_call = if is_option(ty) {
13006                    quote! { .nullable() }
13007                } else {
13008                    quote! {}
13009                };
13010                quote! {
13011                    .property(
13012                        #name_lit,
13013                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13014                            #nullable_call,
13015                    )
13016                }
13017            });
13018            let required_lits: Vec<_> = output_fields
13019                .iter()
13020                .filter(|fi| !is_option(&fi.ty))
13021                .map(|fi| fi.ident.to_string())
13022                .collect();
13023            quote! {
13024                impl #root::openapi::OpenApiSchema for #struct_name {
13025                    fn openapi_schema() -> #root::openapi::Schema {
13026                        #root::openapi::Schema::object()
13027                            #( #property_calls )*
13028                            .required([ #( #required_lits ),* ])
13029                    }
13030                }
13031            }
13032        }
13033        #[cfg(not(feature = "openapi"))]
13034        {
13035            quote! {}
13036        }
13037    };
13038
13039    Ok(quote! {
13040        impl #root::serializer::ModelSerializer for #struct_name {
13041            type Model = #model_path;
13042
13043            fn from_model(model: &Self::Model) -> Self {
13044                Self {
13045                    #( #from_model_fields ),*
13046                }
13047            }
13048
13049            fn writable_fields() -> &'static [&'static str] {
13050                &[ #( #writable_lits ),* ]
13051            }
13052        }
13053
13054        impl #root::__serde::Serialize for #struct_name {
13055            fn serialize<S>(&self, serializer: S)
13056                -> ::core::result::Result<S::Ok, S::Error>
13057            where
13058                S: #root::__serde::Serializer,
13059            {
13060                use #root::__serde::ser::SerializeStruct;
13061                let mut __state = serializer.serialize_struct(
13062                    #struct_name_lit,
13063                    #output_field_count,
13064                )?;
13065                #( #serialize_fields )*
13066                __state.end()
13067            }
13068        }
13069
13070        #openapi_impl
13071
13072        #validate_method
13073
13074        #many_setters_impl
13075    })
13076}
13077
13078/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13079/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13080/// when the feature is off.
13081#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13082fn is_option(ty: &syn::Type) -> bool {
13083    if let syn::Type::Path(p) = ty {
13084        if let Some(last) = p.path.segments.last() {
13085            return last.ident == "Option";
13086        }
13087    }
13088    false
13089}