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/// Derive a `Model` impl. See crate docs for the supported attributes.
18#[proc_macro_derive(Model, attributes(rustango))]
19pub fn derive_model(input: TokenStream) -> TokenStream {
20    let input = parse_macro_input!(input as DeriveInput);
21    expand(&input)
22        .unwrap_or_else(syn::Error::into_compile_error)
23        .into()
24}
25
26/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
27/// marker struct, wiring the full CRUD ViewSet in one annotation.
28///
29/// ```ignore
30/// #[derive(ViewSet)]
31/// #[viewset(
32///     model        = Post,
33///     fields       = "id, title, body, author_id",
34///     filter_fields = "author_id",
35///     search_fields = "title, body",
36///     ordering     = "-published_at",
37///     page_size    = 20,
38/// )]
39/// pub struct PostViewSet;
40///
41/// // Mount into your app:
42/// let app = Router::new()
43///     .merge(PostViewSet::router("/api/posts", pool.clone()));
44/// ```
45///
46/// Attributes:
47/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
48///   `SCHEMA` constant drives the endpoints.
49/// * `fields = "a, b, c"` — scalar fields included in list/retrieve JSON
50///   and accepted on create/update (default: all scalar fields).
51/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
52/// * `search_fields = "a, b"` — fields searched by `?search=...`.
53/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
54/// * `page_size = N` — default page size (default: 20, max: 1000).
55/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
56/// * `permissions(list = "...", retrieve = "...", create = "...",
57///   update = "...", destroy = "...")` — codenames required per action.
58#[proc_macro_derive(ViewSet, attributes(viewset))]
59pub fn derive_viewset(input: TokenStream) -> TokenStream {
60    let input = parse_macro_input!(input as DeriveInput);
61    expand_viewset(&input)
62        .unwrap_or_else(syn::Error::into_compile_error)
63        .into()
64}
65
66/// Derive `rustango::forms::FormStruct` (slice 8.4B). Generates a
67/// `parse(&HashMap<String, String>) -> Result<Self, FormError>` impl
68/// that walks every named field and:
69///
70/// * Parses the string value into the field's Rust type (`String`,
71///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
72///   nullable case).
73/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
74///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
75///   validators in declaration order, returning `FormError::Parse`
76///   on the first failure.
77///
78/// Example:
79///
80/// ```ignore
81/// #[derive(Form)]
82/// pub struct CreateItemForm {
83///     #[form(min_length = 1, max_length = 64)]
84///     pub name: String,
85///     #[form(min = 0, max = 150)]
86///     pub age: i32,
87///     pub active: bool,
88///     pub email: Option<String>,
89/// }
90///
91/// let parsed = CreateItemForm::parse(&form_map)?;
92/// ```
93#[proc_macro_derive(Form, attributes(form))]
94pub fn derive_form(input: TokenStream) -> TokenStream {
95    let input = parse_macro_input!(input as DeriveInput);
96    expand_form(&input)
97        .unwrap_or_else(syn::Error::into_compile_error)
98        .into()
99}
100
101/// Derive `rustango::serializer::ModelSerializer` for a struct.
102/// (intra-doc link disabled — the macro crate doesn't depend on
103/// `rustango` itself, so rustdoc can't resolve the path.)
104///
105/// # Container attribute (required)
106/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
107///
108/// # Field attributes
109/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
110/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
111/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
112/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
113///
114/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
115#[proc_macro_derive(Serializer, attributes(serializer))]
116pub fn derive_serializer(input: TokenStream) -> TokenStream {
117    let input = parse_macro_input!(input as DeriveInput);
118    expand_serializer(&input)
119        .unwrap_or_else(syn::Error::into_compile_error)
120        .into()
121}
122
123/// Bake every `*.json` migration file in a directory into the binary
124/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
125/// of `(name, json_content)` pairs, lex-sorted by file stem.
126///
127/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
128/// behaviour as `migrate(pool, dir)` but with no filesystem access.
129/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
130/// (i.e. the crate that invokes the macro). Default is
131/// `"./migrations"` if no argument is supplied.
132///
133/// ```ignore
134/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
135/// // or:
136/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
137///
138/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
139/// ```
140///
141/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
142/// file's `name` field must equal its file stem, every `prev`
143/// reference must point to another migration in the same directory,
144/// and the JSON must parse. A broken chain — orphan `prev`, missing
145/// predecessor, malformed file — fails at macro-expansion time with
146/// a clear `compile_error!`. *No other Django-shape Rust framework
147/// validates migration chains at compile time*: Cot's migrations are
148/// imperative Rust code (no static chain), Loco's are SeaORM
149/// up/down (same), Rwf's are raw SQL (no chain at all).
150///
151/// Each migration is included via `include_str!` so cargo's rebuild
152/// detection picks up file *content* changes. **Caveat:** cargo
153/// doesn't watch directory listings, so adding or removing a
154/// migration file inside the dir won't auto-trigger a rebuild — run
155/// `cargo clean` (or just bump any other source file) when you add
156/// new migrations during embedded development.
157#[proc_macro]
158pub fn embed_migrations(input: TokenStream) -> TokenStream {
159    expand_embed_migrations(input.into())
160        .unwrap_or_else(syn::Error::into_compile_error)
161        .into()
162}
163
164/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
165/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
166/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
167/// functions are zero-boilerplate:
168///
169/// ```ignore
170/// #[rustango::main]
171/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
172///     rustango::server::Builder::from_env().await?
173///         .migrate("migrations").await?
174///         .api(my_app::urls::api())
175///         .seed_with(my_app::seed::run).await?
176///         .serve("0.0.0.0:8080").await
177/// }
178/// ```
179///
180/// Optional `flavor = "current_thread"` passes through to
181/// `#[tokio::main]`; default is the multi-threaded runtime.
182///
183/// Pulls `tracing-subscriber` into the rustango crate behind the
184/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
185/// out get plain `#[tokio::main]` ergonomics without the dependency.
186#[proc_macro_attribute]
187pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
188    expand_main(args.into(), item.into())
189        .unwrap_or_else(syn::Error::into_compile_error)
190        .into()
191}
192
193fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
194    let mut input: syn::ItemFn = syn::parse2(item)?;
195    if input.sig.asyncness.is_none() {
196        return Err(syn::Error::new(
197            input.sig.ident.span(),
198            "`#[rustango::main]` must wrap an `async fn`",
199        ));
200    }
201
202    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
203    // to `#[tokio::main]`. Tokio's proc-macro internally emits
204    // `::tokio::*` paths that resolve against the user crate's deps,
205    // so calling it through the rustango re-export still requires the
206    // user to add tokio to their own Cargo.toml. Building the
207    // runtime ourselves keeps the dep transitive through the
208    // `runtime` feature on rustango.
209    //
210    // Parse optional `flavor = "current_thread"` / `flavor =
211    // "multi_thread"` from the attribute args. Unknown args are
212    // tolerated (forward-compat with tokio's own arg surface).
213    let flavor = parse_flavor(&args);
214    let builder_call = match flavor {
215        Flavor::CurrentThread => quote! {
216            ::rustango::__private_runtime::tokio::runtime::Builder::new_current_thread()
217        },
218        Flavor::MultiThread => quote! {
219            ::rustango::__private_runtime::tokio::runtime::Builder::new_multi_thread()
220        },
221    };
222
223    // Detach the user body and rewrite `main` as a sync fn that
224    // builds the runtime and blocks on the async body.
225    let user_body = input.block.clone();
226    input.sig.asyncness = None;
227    input.block = syn::parse2(quote! {{
228        {
229            use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
230            // `try_init` so duplicate installers (e.g. tests already
231            // holding a subscriber) don't panic.
232            let _ = tracing_subscriber::fmt()
233                .with_env_filter(
234                    EnvFilter::try_from_default_env()
235                        .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
236                )
237                .try_init();
238        }
239        let __rt = #builder_call
240            .enable_all()
241            .build()
242            .expect("failed to build tokio runtime");
243        __rt.block_on(async move #user_body)
244    }})?;
245
246    Ok(quote! {
247        #input
248    })
249}
250
251enum Flavor {
252    MultiThread,
253    CurrentThread,
254}
255
256fn parse_flavor(args: &TokenStream2) -> Flavor {
257    // Cheap parser: look for the literal token sequence
258    // `flavor = "current_thread"`. Everything else (including
259    // bare `multi_thread` or no args) defaults to multi-thread.
260    let s = args.to_string();
261    if s.contains("current_thread") {
262        Flavor::CurrentThread
263    } else {
264        Flavor::MultiThread
265    }
266}
267
268fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
269    // Default to "./migrations" if invoked without args.
270    let path_str = if input.is_empty() {
271        "./migrations".to_string()
272    } else {
273        let lit: LitStr = syn::parse2(input)?;
274        lit.value()
275    };
276
277    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
278        syn::Error::new(
279            proc_macro2::Span::call_site(),
280            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
281        )
282    })?;
283    let abs = std::path::Path::new(&manifest).join(&path_str);
284
285    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
286    if abs.is_dir() {
287        let read = std::fs::read_dir(&abs).map_err(|e| {
288            syn::Error::new(
289                proc_macro2::Span::call_site(),
290                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
291            )
292        })?;
293        for entry in read.flatten() {
294            let path = entry.path();
295            if !path.is_file() {
296                continue;
297            }
298            if path.extension().and_then(|s| s.to_str()) != Some("json") {
299                continue;
300            }
301            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
302                continue;
303            };
304            entries.push((stem.to_owned(), path));
305        }
306    }
307    entries.sort_by(|a, b| a.0.cmp(&b.0));
308
309    // Compile-time chain validation: read each migration's JSON,
310    // pull `name` and `prev` (file-stem-keyed for the chain check),
311    // and verify every `prev` points to another migration in the
312    // slice. Mismatches between the file stem and the embedded
313    // `name` field — and broken `prev` chains — fail at MACRO
314    // EXPANSION time so a misshapen migration set never compiles.
315    //
316    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
317    // migrations + a Rust proc-macro that reads them is the unique
318    // combo nothing else in the Django-shape Rust camp can match
319    // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
320    // Rwf's are raw SQL — none have a static chain to validate).
321    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
322    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
323    for (stem, path) in &entries {
324        let raw = std::fs::read_to_string(path).map_err(|e| {
325            syn::Error::new(
326                proc_macro2::Span::call_site(),
327                format!(
328                    "embed_migrations!: cannot read {} for chain validation: {e}",
329                    path.display()
330                ),
331            )
332        })?;
333        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
334            syn::Error::new(
335                proc_macro2::Span::call_site(),
336                format!(
337                    "embed_migrations!: {} is not valid JSON: {e}",
338                    path.display()
339                ),
340            )
341        })?;
342        let name = json
343            .get("name")
344            .and_then(|v| v.as_str())
345            .ok_or_else(|| {
346                syn::Error::new(
347                    proc_macro2::Span::call_site(),
348                    format!(
349                        "embed_migrations!: {} is missing the `name` field",
350                        path.display()
351                    ),
352                )
353            })?
354            .to_owned();
355        if name != *stem {
356            return Err(syn::Error::new(
357                proc_macro2::Span::call_site(),
358                format!(
359                    "embed_migrations!: file stem `{stem}` does not match the migration's \
360                     `name` field `{name}` — rename the file or fix the JSON",
361                ),
362            ));
363        }
364        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
365        chain_names.push(name.clone());
366        prev_refs.push((name, prev));
367    }
368
369    let name_set: std::collections::HashSet<&str> =
370        chain_names.iter().map(String::as_str).collect();
371    for (name, prev) in &prev_refs {
372        if let Some(p) = prev {
373            if !name_set.contains(p.as_str()) {
374                return Err(syn::Error::new(
375                    proc_macro2::Span::call_site(),
376                    format!(
377                        "embed_migrations!: broken migration chain — `{name}` declares \
378                         prev=`{p}` but no migration with that name exists in {}",
379                        abs.display()
380                    ),
381                ));
382            }
383        }
384    }
385
386    let pairs: Vec<TokenStream2> = entries
387        .iter()
388        .map(|(name, path)| {
389            let path_lit = path.display().to_string();
390            quote! { (#name, ::core::include_str!(#path_lit)) }
391        })
392        .collect();
393
394    Ok(quote! {
395        {
396            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
397            __RUSTANGO_EMBEDDED
398        }
399    })
400}
401
402fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
403    let struct_name = &input.ident;
404
405    let Data::Struct(data) = &input.data else {
406        return Err(syn::Error::new_spanned(
407            struct_name,
408            "Model can only be derived on structs",
409        ));
410    };
411    let Fields::Named(named) = &data.fields else {
412        return Err(syn::Error::new_spanned(
413            struct_name,
414            "Model requires a struct with named fields",
415        ));
416    };
417
418    let container = parse_container_attrs(input)?;
419    let table = container
420        .table
421        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
422    let model_name = struct_name.to_string();
423
424    let collected = collect_fields(named, &table)?;
425
426    // Validate that #[rustango(display = "…")] names a real field.
427    if let Some((ref display, span)) = container.display {
428        if !collected.field_names.iter().any(|n| n == display) {
429            return Err(syn::Error::new(
430                span,
431                format!("`display = \"{display}\"` does not match any field on this struct"),
432            ));
433        }
434    }
435    let display = container.display.map(|(name, _)| name);
436    let app_label = container.app.clone();
437
438    // Validate admin field-name lists against declared field names.
439    // Note: `list_display` is intentionally NOT validated here. As of
440    // v0.32 it may also reference inventory-registered computed
441    // fields (via `register_admin_computed!`) whose existence the
442    // macro can't see at compile time — they're submitted from any
443    // crate that depends on rustango. The runtime list-view resolves
444    // unknown names against the inventory + silently drops the
445    // truly-bogus ones, which is the cheaper trade-off versus
446    // forcing a per-Model attr to opt out.
447    if let Some(admin) = &container.admin {
448        for (label, list) in [
449            ("search_fields", &admin.search_fields),
450            ("readonly_fields", &admin.readonly_fields),
451            ("list_filter", &admin.list_filter),
452        ] {
453            if let Some((names, span)) = list {
454                for name in names {
455                    if !collected.field_names.iter().any(|n| n == name) {
456                        return Err(syn::Error::new(
457                            *span,
458                            format!(
459                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
460                            ),
461                        ));
462                    }
463                }
464            }
465        }
466        if let Some((pairs, span)) = &admin.ordering {
467            for (name, _) in pairs {
468                if !collected.field_names.iter().any(|n| n == name) {
469                    return Err(syn::Error::new(
470                        *span,
471                        format!(
472                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
473                        ),
474                    ));
475                }
476            }
477        }
478        if let Some((groups, span)) = &admin.fieldsets {
479            for (_, fields) in groups {
480                for name in fields {
481                    if !collected.field_names.iter().any(|n| n == name) {
482                        return Err(syn::Error::new(
483                            *span,
484                            format!(
485                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
486                            ),
487                        ));
488                    }
489                }
490            }
491        }
492    }
493    if let Some(audit) = &container.audit {
494        if let Some((names, span)) = &audit.track {
495            for name in names {
496                if !collected.field_names.iter().any(|n| n == name) {
497                    return Err(syn::Error::new(
498                        *span,
499                        format!(
500                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
501                        ),
502                    ));
503                }
504            }
505        }
506    }
507
508    // Build the audit_track list for ModelSchema: None when no audit attr,
509    // Some(empty) when audit present without track, Some(names) when explicit.
510    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
511        audit
512            .track
513            .as_ref()
514            .map(|(names, _)| names.clone())
515            .unwrap_or_default()
516    });
517
518    // Merge field-level indexes into the container's index list.
519    let mut all_indexes: Vec<IndexAttr> = container.indexes;
520    for field in &named.named {
521        let ident = field.ident.as_ref().expect("named");
522        let col = to_snake_case(&ident.to_string()); // column name fallback
523                                                     // Re-parse field attrs to check for index flag
524        if let Ok(fa) = parse_field_attrs(field) {
525            if fa.index {
526                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
527                let auto_name = if fa.index_unique {
528                    format!("{table}_{col_name}_uq_idx")
529                } else {
530                    format!("{table}_{col_name}_idx")
531                };
532                all_indexes.push(IndexAttr {
533                    name: fa.index_name.or(Some(auto_name)),
534                    columns: vec![col_name],
535                    unique: fa.index_unique,
536                });
537            }
538        }
539    }
540
541    let model_impl = model_impl_tokens(
542        struct_name,
543        &model_name,
544        &table,
545        display.as_deref(),
546        app_label.as_deref(),
547        container.admin.as_ref(),
548        &collected.field_schemas,
549        collected.soft_delete_column.as_deref(),
550        container.permissions,
551        audit_track_names.as_deref(),
552        &container.m2m,
553        &all_indexes,
554        &container.checks,
555        &container.composite_fks,
556        &container.generic_fks,
557        container.scope.as_deref(),
558    );
559    let module_ident = column_module_ident(struct_name);
560    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
561    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
562        let track_set: Option<std::collections::HashSet<&str>> = audit
563            .track
564            .as_ref()
565            .map(|(names, _)| names.iter().map(String::as_str).collect());
566        collected
567            .column_entries
568            .iter()
569            .filter(|c| {
570                track_set
571                    .as_ref()
572                    .map_or(true, |s| s.contains(c.name.as_str()))
573            })
574            .collect()
575    });
576    let inherent_impl = inherent_impl_tokens(
577        struct_name,
578        &collected,
579        collected.primary_key.as_ref(),
580        &column_consts,
581        audited_fields.as_deref(),
582        &all_indexes,
583    );
584    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
585    let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
586    let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
587    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
588
589    Ok(quote! {
590        #model_impl
591        #inherent_impl
592        #from_row_impl
593        #column_module
594        #reverse_helpers
595        #m2m_accessors
596
597        ::rustango::core::inventory::submit! {
598            ::rustango::core::ModelEntry {
599                schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
600                // `module_path!()` evaluates at the registration site,
601                // so a Model declared in `crate::blog::models` records
602                // `"<crate>::blog::models"` and `resolved_app_label()`
603                // can infer "blog" without an explicit attribute.
604                module_path: ::core::module_path!(),
605            }
606        }
607    })
608}
609
610/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
611/// matches `field_name` against the model's FK fields and, for a
612/// match, decodes the FK target via the parent's macro-generated
613/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
614/// `ForeignKey::Loaded` on `self`.
615///
616/// Always emitted (with empty arms for FK-less models, which
617/// return `Ok(false)` for any field name) so the `T: LoadRelated`
618/// trait bound on `fetch_on` is universally satisfied — users
619/// never have to think about implementing it.
620fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
621    let arms = fk_relations.iter().map(|rel| {
622        let parent_ty = &rel.parent_type;
623        let fk_col = rel.fk_column.as_str();
624        // FK field's Rust ident matches its SQL column name in v0.8
625        // (no `column = "..."` rename ships on FK fields).
626        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
627        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
628        let assign = if rel.nullable {
629            quote! {
630                self.#field_ident = ::core::option::Option::Some(
631                    ::rustango::sql::ForeignKey::loaded(_pk, _parent),
632                );
633            }
634        } else {
635            quote! {
636                self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
637            }
638        };
639        quote! {
640            #fk_col => {
641                let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
642                // Loud-in-debug, default-in-release: a divergence
643                // between the FK field's declared `K` (drives the
644                // expected `SqlValue::<Variant>`) and the parent's
645                // `__rustango_pk_value` output is a macro-internal
646                // invariant break — surfacing the panic in dev
647                // catches it before users hit silent PK=0 corruption.
648                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
649                    ::rustango::core::SqlValue::#variant_ident(v) => v,
650                    _other => {
651                        ::core::debug_assert!(
652                            false,
653                            "rustango macro bug: load_related on FK `{}` expected \
654                             SqlValue::{} from parent's __rustango_pk_value but got \
655                             {:?} — file a bug at https://github.com/ujeenet/rustango",
656                            #fk_col,
657                            ::core::stringify!(#variant_ident),
658                            _other,
659                        );
660                        #default_expr
661                    }
662                };
663                #assign
664                ::core::result::Result::Ok(true)
665            }
666        }
667    });
668    quote! {
669        #[cfg(feature = "postgres")]
670        impl ::rustango::sql::LoadRelated for #struct_name {
671            #[allow(unused_variables)]
672            fn __rustango_load_related(
673                &mut self,
674                row: &::rustango::sql::sqlx::postgres::PgRow,
675                field_name: &str,
676                alias: &str,
677            ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
678                match field_name {
679                    #( #arms )*
680                    _ => ::core::result::Result::Ok(false),
681                }
682            }
683        }
684    }
685}
686
687/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
688/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
689/// which expands to a `LoadRelatedMy` impl when rustango is built with
690/// the `mysql` feature, and to nothing otherwise. The decoded parent
691/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
692/// decoder, also batch8) so the dual emission is symmetric across
693/// backends.
694fn load_related_impl_my_tokens(
695    struct_name: &syn::Ident,
696    fk_relations: &[FkRelation],
697) -> TokenStream2 {
698    let arms = fk_relations.iter().map(|rel| {
699        let parent_ty = &rel.parent_type;
700        let fk_col = rel.fk_column.as_str();
701        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
702        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
703        let assign = if rel.nullable {
704            quote! {
705                __self.#field_ident = ::core::option::Option::Some(
706                    ::rustango::sql::ForeignKey::loaded(_pk, _parent),
707                );
708            }
709        } else {
710            quote! {
711                __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
712            }
713        };
714        // `self` IS hygiene-tracked through macro_rules — emitted from
715        // a different context than the `&mut self` parameter inside
716        // the macro_rules-expanded fn. Pass it through as `__self`
717        // and let the macro_rules rebind it to the receiver.
718        quote! {
719            #fk_col => {
720                let _parent: #parent_ty =
721                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
722                // See note in `load_related_impl_tokens` (PG twin) —
723                // the same loud-in-debug invariant guard.
724                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
725                    ::rustango::core::SqlValue::#variant_ident(v) => v,
726                    _other => {
727                        ::core::debug_assert!(
728                            false,
729                            "rustango macro bug: load_related on FK `{}` expected \
730                             SqlValue::{} from parent's __rustango_pk_value but got \
731                             {:?} — file a bug at https://github.com/ujeenet/rustango",
732                            #fk_col,
733                            ::core::stringify!(#variant_ident),
734                            _other,
735                        );
736                        #default_expr
737                    }
738                };
739                #assign
740                ::core::result::Result::Ok(true)
741            }
742        }
743    });
744    quote! {
745        ::rustango::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias| {
746            #( #arms )*
747        });
748    }
749}
750
751/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
752/// Emits a call to `__impl_sqlite_load_related!` which expands to a
753/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
754fn load_related_impl_sqlite_tokens(
755    struct_name: &syn::Ident,
756    fk_relations: &[FkRelation],
757) -> TokenStream2 {
758    let arms = fk_relations.iter().map(|rel| {
759        let parent_ty = &rel.parent_type;
760        let fk_col = rel.fk_column.as_str();
761        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
762        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
763        let assign = if rel.nullable {
764            quote! {
765                __self.#field_ident = ::core::option::Option::Some(
766                    ::rustango::sql::ForeignKey::loaded(_pk, _parent),
767                );
768            }
769        } else {
770            quote! {
771                __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
772            }
773        };
774        quote! {
775            #fk_col => {
776                let _parent: #parent_ty =
777                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
778                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
779                    ::rustango::core::SqlValue::#variant_ident(v) => v,
780                    _other => {
781                        ::core::debug_assert!(
782                            false,
783                            "rustango macro bug: load_related on FK `{}` expected \
784                             SqlValue::{} from parent's __rustango_pk_value but got \
785                             {:?} — file a bug at https://github.com/ujeenet/rustango",
786                            #fk_col,
787                            ::core::stringify!(#variant_ident),
788                            _other,
789                        );
790                        #default_expr
791                    }
792                };
793                #assign
794                ::core::result::Result::Ok(true)
795            }
796        }
797    });
798    quote! {
799        ::rustango::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias| {
800            #( #arms )*
801        });
802    }
803}
804
805/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
806/// matches `field_name` against the model's FK fields and returns
807/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
808/// group children by parent PK.
809///
810/// Always emitted (with `_ => None` for FK-less models) so the
811/// trait bound on `fetch_with_prefetch` is universally satisfied.
812fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
813    let arms = fk_relations.iter().map(|rel| {
814        let fk_col = rel.fk_column.as_str();
815        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
816        if rel.pk_kind == DetectedKind::I64 {
817            // i64 FK — return the stored PK so prefetch_related can
818            // group children by it. Nullable variant unwraps via
819            // `as_ref().map(...)`: an unset (NULL) FK column yields
820            // `None` and that child sits out of the grouping (correct
821            // semantics — it has no parent to attach to).
822            if rel.nullable {
823                quote! {
824                    #fk_col => self.#field_ident
825                        .as_ref()
826                        .map(|fk| ::rustango::sql::ForeignKey::pk(fk)),
827                }
828            } else {
829                quote! {
830                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
831                }
832            }
833        } else {
834            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
835            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
836            // i64-keyed grouping path — the trait signature is
837            // `Option<i64>` and a non-i64 PK can't lower into it.
838            // The FK still works for everything else (CRUD, lazy
839            // load via `.get()`, select_related JOINs); only the
840            // bulk prefetch grouper needs the integer key.
841            quote! {
842                #fk_col => ::core::option::Option::None,
843            }
844        }
845    });
846    // PK-type-agnostic version: every FK arm emits an
847    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
848    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
849    // opt OUT of the legacy i64 method (it returns None) but opt IN
850    // here.
851    let value_arms = fk_relations.iter().map(|rel| {
852        let fk_col = rel.fk_column.as_str();
853        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
854        if rel.nullable {
855            quote! {
856                #fk_col => self.#field_ident
857                    .as_ref()
858                    .map(|fk| ::core::convert::Into::<::rustango::core::SqlValue>::into(
859                        ::rustango::sql::ForeignKey::pk(fk)
860                    )),
861            }
862        } else {
863            quote! {
864                #fk_col => ::core::option::Option::Some(
865                    ::core::convert::Into::<::rustango::core::SqlValue>::into(
866                        self.#field_ident.pk()
867                    )
868                ),
869            }
870        }
871    });
872    quote! {
873        impl ::rustango::sql::FkPkAccess for #struct_name {
874            #[allow(unused_variables)]
875            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
876                match field_name {
877                    #( #arms )*
878                    _ => ::core::option::Option::None,
879                }
880            }
881            #[allow(unused_variables)]
882            fn __rustango_fk_pk_value(
883                &self,
884                field_name: &str,
885            ) -> ::core::option::Option<::rustango::core::SqlValue> {
886                match field_name {
887                    #( #value_arms )*
888                    _ => ::core::option::Option::None,
889                }
890            }
891        }
892    }
893}
894
895/// For every `ForeignKey<Parent>` field on `Child`, emit
896/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
897/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
898/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
899/// — the canonical reverse-FK fetch. One round trip, no N+1.
900fn reverse_helper_tokens(child_ident: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
901    if fk_relations.is_empty() {
902        return TokenStream2::new();
903    }
904    // Snake-case the child struct name to derive the method suffix —
905    // `Post` → `post_set`, `BlogComment` → `blog_comment_set`. Avoids
906    // English-plural edge cases (Django's `<child>_set` convention).
907    let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
908    let method_ident = syn::Ident::new(&suffix, child_ident.span());
909    let impls = fk_relations.iter().map(|rel| {
910        let parent_ty = &rel.parent_type;
911        let fk_col = rel.fk_column.as_str();
912        let doc = format!(
913            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
914             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
915             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
916             further `{child_ident}::objects()` filters via direct queryset use."
917        );
918        quote! {
919            impl #parent_ty {
920                #[doc = #doc]
921                ///
922                /// # Errors
923                /// Returns [`::rustango::sql::ExecError`] for SQL-writing
924                /// or driver failures.
925                pub async fn #method_ident<'_c, _E>(
926                    &self,
927                    _executor: _E,
928                ) -> ::core::result::Result<
929                    ::std::vec::Vec<#child_ident>,
930                    ::rustango::sql::ExecError,
931                >
932                where
933                    _E: ::rustango::sql::sqlx::Executor<
934                        '_c,
935                        Database = ::rustango::sql::sqlx::Postgres,
936                    >,
937                {
938                    let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
939                    ::rustango::query::QuerySet::<#child_ident>::new()
940                        .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
941                        .fetch_on(_executor)
942                        .await
943                }
944            }
945        }
946    });
947    quote! { #( #impls )* }
948}
949
950/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
951/// relation declared on the model.
952fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
953    if m2m_relations.is_empty() {
954        return TokenStream2::new();
955    }
956    let methods = m2m_relations.iter().map(|rel| {
957        let method_name = format!("{}_m2m", rel.name);
958        let method_ident = syn::Ident::new(&method_name, struct_name.span());
959        let through = rel.through.as_str();
960        let src_col = rel.src.as_str();
961        let dst_col = rel.dst.as_str();
962        quote! {
963            pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
964                ::rustango::sql::M2MManager {
965                    src_pk: self.__rustango_pk_value(),
966                    through: #through,
967                    src_col: #src_col,
968                    dst_col: #dst_col,
969                }
970            }
971        }
972    });
973    quote! {
974        impl #struct_name {
975            #( #methods )*
976        }
977    }
978}
979
980struct ColumnEntry {
981    /// The struct field ident, used both for the inherent const name on
982    /// the model and for the inner column type's name.
983    ident: syn::Ident,
984    /// The struct's field type, used as `Column::Value`.
985    value_ty: Type,
986    /// Rust-side field name (e.g. `"id"`).
987    name: String,
988    /// SQL-side column name (e.g. `"user_id"`).
989    column: String,
990    /// `::rustango::core::FieldType::I64` etc.
991    field_type_tokens: TokenStream2,
992}
993
994struct CollectedFields {
995    field_schemas: Vec<TokenStream2>,
996    from_row_inits: Vec<TokenStream2>,
997    /// Aliased counterparts of `from_row_inits` — read columns via
998    /// `format!("{prefix}__{col}")` aliases so a Model can be
999    /// decoded from a JOINed row's projected target columns.
1000    from_aliased_row_inits: Vec<TokenStream2>,
1001    /// Static column-name list — used by the simple insert path
1002    /// (no `Auto<T>` fields). Aligned with `insert_values`.
1003    insert_columns: Vec<TokenStream2>,
1004    /// Static `Into<SqlValue>` expressions, one per field. Aligned
1005    /// with `insert_columns`. Used by the simple insert path only.
1006    insert_values: Vec<TokenStream2>,
1007    /// Per-field push expressions for the dynamic (Auto-aware)
1008    /// insert path. Each statement either unconditionally pushes
1009    /// `(column, value)` or, for an `Auto<T>` field, conditionally
1010    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1011    insert_pushes: Vec<TokenStream2>,
1012    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1013    /// when `has_auto == false`.
1014    returning_cols: Vec<TokenStream2>,
1015    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1016    /// field. Run after `insert_returning` to populate the model.
1017    auto_assigns: Vec<TokenStream2>,
1018    /// `(ident, column_literal)` pairs for every Auto field. Used by
1019    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1020    /// instead of `self`.
1021    auto_field_idents: Vec<(syn::Ident, String)>,
1022    /// Inner `T` of the first `Auto<T>` field, for the MySQL
1023    /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1024    first_auto_value_ty: Option<Type>,
1025    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
1026    /// by the all-Auto-Unset bulk path (Auto cols dropped from
1027    /// `columns`).
1028    bulk_pushes_no_auto: Vec<TokenStream2>,
1029    /// Bulk-insert per-row pushes for **all fields including Auto**.
1030    /// Used by the all-Auto-Set bulk path (Auto col included with the
1031    /// caller-supplied value).
1032    bulk_pushes_all: Vec<TokenStream2>,
1033    /// Column-name literals for non-Auto fields only (paired with
1034    /// `bulk_pushes_no_auto`).
1035    bulk_columns_no_auto: Vec<TokenStream2>,
1036    /// Column-name literals for every field including Auto (paired
1037    /// with `bulk_pushes_all`).
1038    bulk_columns_all: Vec<TokenStream2>,
1039    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
1040    /// + the loop that asserts every row matches. One pair per Auto
1041    /// field. Empty when `has_auto == false`.
1042    bulk_auto_uniformity: Vec<TokenStream2>,
1043    /// Identifier of the first Auto field, used as the witness for
1044    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
1045    first_auto_ident: Option<syn::Ident>,
1046    /// `true` if any field on the struct is `Auto<T>`.
1047    has_auto: bool,
1048    /// `true` when the primary-key field's Rust type is `Auto<T>`.
1049    /// Gates `save()` codegen — only Auto PKs let us infer
1050    /// insert-vs-update from the in-memory value.
1051    pk_is_auto: bool,
1052    /// `Assignment` constructors for every non-PK column. Drives the
1053    /// UPDATE branch of `save()`.
1054    update_assignments: Vec<TokenStream2>,
1055    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
1056    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
1057    upsert_update_columns: Vec<TokenStream2>,
1058    primary_key: Option<(syn::Ident, String)>,
1059    column_entries: Vec<ColumnEntry>,
1060    /// Rust-side field names, in declaration order. Used to validate
1061    /// container attributes like `display = "…"`.
1062    field_names: Vec<String>,
1063    /// FK fields on this child model. Drives the reverse-relation
1064    /// helper emit — for each FK, the macro adds an inherent
1065    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
1066    /// method on the parent type.
1067    fk_relations: Vec<FkRelation>,
1068    /// SQL column name of the `#[rustango(soft_delete)]` field, if
1069    /// the model has one. Drives emission of the `soft_delete_on` /
1070    /// `restore_on` inherent methods. At most one such column per
1071    /// model is allowed; collect_fields rejects duplicates.
1072    soft_delete_column: Option<String>,
1073}
1074
1075#[derive(Clone)]
1076struct FkRelation {
1077    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
1078    /// helper is emitted as `impl <ParentType> { … }`.
1079    parent_type: Type,
1080    /// SQL column name on the child table for this FK (e.g. `"author"`).
1081    /// Used in the generated `WHERE <fk_column> = $1` clause.
1082    fk_column: String,
1083    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
1084    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
1085    /// default `ForeignKey<T>` (no explicit K); other kinds when the
1086    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
1087    pk_kind: DetectedKind,
1088    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
1089    /// FK column). Drives the `Some(...)` wrapping in load_related
1090    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
1091    /// the codegen matches the field's declared shape.
1092    nullable: bool,
1093}
1094
1095fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
1096    let cap = named.named.len();
1097    let mut out = CollectedFields {
1098        field_schemas: Vec::with_capacity(cap),
1099        from_row_inits: Vec::with_capacity(cap),
1100        from_aliased_row_inits: Vec::with_capacity(cap),
1101        insert_columns: Vec::with_capacity(cap),
1102        insert_values: Vec::with_capacity(cap),
1103        insert_pushes: Vec::with_capacity(cap),
1104        returning_cols: Vec::new(),
1105        auto_assigns: Vec::new(),
1106        auto_field_idents: Vec::new(),
1107        first_auto_value_ty: None,
1108        bulk_pushes_no_auto: Vec::with_capacity(cap),
1109        bulk_pushes_all: Vec::with_capacity(cap),
1110        bulk_columns_no_auto: Vec::with_capacity(cap),
1111        bulk_columns_all: Vec::with_capacity(cap),
1112        bulk_auto_uniformity: Vec::new(),
1113        first_auto_ident: None,
1114        has_auto: false,
1115        pk_is_auto: false,
1116        update_assignments: Vec::with_capacity(cap),
1117        upsert_update_columns: Vec::with_capacity(cap),
1118        primary_key: None,
1119        column_entries: Vec::with_capacity(cap),
1120        field_names: Vec::with_capacity(cap),
1121        fk_relations: Vec::new(),
1122        soft_delete_column: None,
1123    };
1124
1125    for field in &named.named {
1126        let info = process_field(field, table)?;
1127        out.field_names.push(info.ident.to_string());
1128        out.field_schemas.push(info.schema);
1129        out.from_row_inits.push(info.from_row_init);
1130        out.from_aliased_row_inits.push(info.from_aliased_row_init);
1131        if let Some(parent_ty) = info.fk_inner.clone() {
1132            out.fk_relations.push(FkRelation {
1133                parent_type: parent_ty,
1134                fk_column: info.column.clone(),
1135                pk_kind: info.fk_pk_kind,
1136                nullable: info.nullable,
1137            });
1138        }
1139        if info.soft_delete {
1140            if out.soft_delete_column.is_some() {
1141                return Err(syn::Error::new_spanned(
1142                    field,
1143                    "only one field may be marked `#[rustango(soft_delete)]`",
1144                ));
1145            }
1146            out.soft_delete_column = Some(info.column.clone());
1147        }
1148        let column = info.column.as_str();
1149        let ident = info.ident;
1150        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
1151        // skip every write path — Postgres recomputes the value
1152        // from EXPR. Push only the column-entry record (so typed
1153        // column constants still exist for filtering / projection)
1154        // and the schema literal (already pushed above) and move
1155        // on. No insert_columns/values, no insert_pushes, no
1156        // bulk_*, no update_assignments, no upsert_update_columns,
1157        // no returning_cols.
1158        if info.generated_as.is_some() {
1159            out.column_entries.push(ColumnEntry {
1160                ident: ident.clone(),
1161                value_ty: info.value_ty.clone(),
1162                name: ident.to_string(),
1163                column: info.column.clone(),
1164                field_type_tokens: info.field_type_tokens,
1165            });
1166            continue;
1167        }
1168        out.insert_columns.push(quote!(#column));
1169        out.insert_values.push(quote! {
1170            ::core::convert::Into::<::rustango::core::SqlValue>::into(
1171                ::core::clone::Clone::clone(&self.#ident)
1172            )
1173        });
1174        if info.auto {
1175            out.has_auto = true;
1176            if out.first_auto_ident.is_none() {
1177                out.first_auto_ident = Some(ident.clone());
1178                out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
1179            }
1180            out.returning_cols.push(quote!(#column));
1181            out.auto_field_idents
1182                .push((ident.clone(), info.column.clone()));
1183            out.auto_assigns.push(quote! {
1184                self.#ident = ::rustango::sql::try_get_returning(_returning_row, #column)?;
1185            });
1186            out.insert_pushes.push(quote! {
1187                if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
1188                    _columns.push(#column);
1189                    _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1190                        ::core::clone::Clone::clone(_v)
1191                    ));
1192                }
1193            });
1194            // Bulk: Auto fields appear only in the all-Set path,
1195            // never in the Unset path (we drop them from `columns`).
1196            out.bulk_columns_all.push(quote!(#column));
1197            out.bulk_pushes_all.push(quote! {
1198                _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1199                    ::core::clone::Clone::clone(&_row.#ident)
1200                ));
1201            });
1202            // Uniformity check: every row's Auto state must match the
1203            // first row's. Mixed Set/Unset within one bulk_insert is
1204            // rejected here so the column list stays consistent.
1205            let ident_clone = ident.clone();
1206            out.bulk_auto_uniformity.push(quote! {
1207                for _r in rows.iter().skip(1) {
1208                    if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
1209                        return ::core::result::Result::Err(
1210                            ::rustango::sql::ExecError::Sql(
1211                                ::rustango::sql::SqlError::BulkAutoMixed
1212                            )
1213                        );
1214                    }
1215                }
1216            });
1217        } else {
1218            out.insert_pushes.push(quote! {
1219                _columns.push(#column);
1220                _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1221                    ::core::clone::Clone::clone(&self.#ident)
1222                ));
1223            });
1224            // Bulk: non-Auto fields appear in BOTH paths.
1225            out.bulk_columns_no_auto.push(quote!(#column));
1226            out.bulk_columns_all.push(quote!(#column));
1227            let push_expr = quote! {
1228                _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1229                    ::core::clone::Clone::clone(&_row.#ident)
1230                ));
1231            };
1232            out.bulk_pushes_no_auto.push(push_expr.clone());
1233            out.bulk_pushes_all.push(push_expr);
1234        }
1235        if info.primary_key {
1236            if out.primary_key.is_some() {
1237                return Err(syn::Error::new_spanned(
1238                    field,
1239                    "only one field may be marked `#[rustango(primary_key)]`",
1240                ));
1241            }
1242            out.primary_key = Some((ident.clone(), info.column.clone()));
1243            if info.auto {
1244                out.pk_is_auto = true;
1245            }
1246        } else if info.auto_now_add {
1247            // Immutable post-insert: skip from UPDATE entirely.
1248        } else if info.auto_now {
1249            // `auto_now` columns: bind `chrono::Utc::now()` on every
1250            // UPDATE so the column is always overridden with the
1251            // wall-clock at write time, regardless of what value the
1252            // user left in the struct field.
1253            out.update_assignments.push(quote! {
1254                ::rustango::core::Assignment {
1255                    column: #column,
1256                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1257                        ::chrono::Utc::now()
1258                    ),
1259                }
1260            });
1261            out.upsert_update_columns.push(quote!(#column));
1262        } else {
1263            out.update_assignments.push(quote! {
1264                ::rustango::core::Assignment {
1265                    column: #column,
1266                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1267                        ::core::clone::Clone::clone(&self.#ident)
1268                    ),
1269                }
1270            });
1271            out.upsert_update_columns.push(quote!(#column));
1272        }
1273        out.column_entries.push(ColumnEntry {
1274            ident: ident.clone(),
1275            value_ty: info.value_ty.clone(),
1276            name: ident.to_string(),
1277            column: info.column.clone(),
1278            field_type_tokens: info.field_type_tokens,
1279        });
1280    }
1281    Ok(out)
1282}
1283
1284fn model_impl_tokens(
1285    struct_name: &syn::Ident,
1286    model_name: &str,
1287    table: &str,
1288    display: Option<&str>,
1289    app_label: Option<&str>,
1290    admin: Option<&AdminAttrs>,
1291    field_schemas: &[TokenStream2],
1292    soft_delete_column: Option<&str>,
1293    permissions: bool,
1294    audit_track: Option<&[String]>,
1295    m2m_relations: &[M2MAttr],
1296    indexes: &[IndexAttr],
1297    checks: &[CheckAttr],
1298    composite_fks: &[CompositeFkAttr],
1299    generic_fks: &[GenericFkAttr],
1300    scope: Option<&str>,
1301) -> TokenStream2 {
1302    let display_tokens = if let Some(name) = display {
1303        quote!(::core::option::Option::Some(#name))
1304    } else {
1305        quote!(::core::option::Option::None)
1306    };
1307    let app_label_tokens = if let Some(name) = app_label {
1308        quote!(::core::option::Option::Some(#name))
1309    } else {
1310        quote!(::core::option::Option::None)
1311    };
1312    let soft_delete_tokens = if let Some(col) = soft_delete_column {
1313        quote!(::core::option::Option::Some(#col))
1314    } else {
1315        quote!(::core::option::Option::None)
1316    };
1317    let audit_track_tokens = match audit_track {
1318        None => quote!(::core::option::Option::None),
1319        Some(names) => {
1320            let lits = names.iter().map(|n| n.as_str());
1321            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1322        }
1323    };
1324    let admin_tokens = admin_config_tokens(admin);
1325    // Default `tenant` so single-tenant projects (no `scope` attr
1326    // anywhere) keep the v0.24.x behavior. Container-attr parser
1327    // already validated the value is "registry" or "tenant".
1328    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
1329        Some("registry") => quote!(::rustango::core::ModelScope::Registry),
1330        _ => quote!(::rustango::core::ModelScope::Tenant),
1331    };
1332    let indexes_tokens = indexes.iter().map(|idx| {
1333        let name = idx.name.as_deref().unwrap_or("unnamed_index");
1334        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1335        let unique = idx.unique;
1336        quote! {
1337            ::rustango::core::IndexSchema {
1338                name: #name,
1339                columns: &[ #(#cols),* ],
1340                unique: #unique,
1341            }
1342        }
1343    });
1344    let checks_tokens = checks.iter().map(|c| {
1345        let name = c.name.as_str();
1346        let expr = c.expr.as_str();
1347        quote! {
1348            ::rustango::core::CheckConstraint {
1349                name: #name,
1350                expr: #expr,
1351            }
1352        }
1353    });
1354    let composite_fk_tokens = composite_fks.iter().map(|rel| {
1355        let name = rel.name.as_str();
1356        let to = rel.to.as_str();
1357        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
1358        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
1359        quote! {
1360            ::rustango::core::CompositeFkRelation {
1361                name: #name,
1362                to: #to,
1363                from: &[ #(#from_cols),* ],
1364                on: &[ #(#on_cols),* ],
1365            }
1366        }
1367    });
1368    let generic_fk_tokens = generic_fks.iter().map(|rel| {
1369        let name = rel.name.as_str();
1370        let ct_col = rel.ct_column.as_str();
1371        let pk_col = rel.pk_column.as_str();
1372        quote! {
1373            ::rustango::core::GenericRelation {
1374                name: #name,
1375                ct_column: #ct_col,
1376                pk_column: #pk_col,
1377            }
1378        }
1379    });
1380    let m2m_tokens = m2m_relations.iter().map(|rel| {
1381        let name = rel.name.as_str();
1382        let to = rel.to.as_str();
1383        let through = rel.through.as_str();
1384        let src = rel.src.as_str();
1385        let dst = rel.dst.as_str();
1386        quote! {
1387            ::rustango::core::M2MRelation {
1388                name: #name,
1389                to: #to,
1390                through: #through,
1391                src_col: #src,
1392                dst_col: #dst,
1393            }
1394        }
1395    });
1396    quote! {
1397        impl ::rustango::core::Model for #struct_name {
1398            const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1399                name: #model_name,
1400                table: #table,
1401                fields: &[ #(#field_schemas),* ],
1402                display: #display_tokens,
1403                app_label: #app_label_tokens,
1404                admin: #admin_tokens,
1405                soft_delete_column: #soft_delete_tokens,
1406                permissions: #permissions,
1407                audit_track: #audit_track_tokens,
1408                m2m: &[ #(#m2m_tokens),* ],
1409                indexes: &[ #(#indexes_tokens),* ],
1410                check_constraints: &[ #(#checks_tokens),* ],
1411                composite_relations: &[ #(#composite_fk_tokens),* ],
1412                generic_relations: &[ #(#generic_fk_tokens),* ],
1413                scope: #scope_tokens,
1414            };
1415        }
1416    }
1417}
1418
1419/// Emit the `admin: Option<&'static AdminConfig>` field for the model
1420/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
1421/// otherwise a static reference to a populated `AdminConfig`.
1422fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1423    let Some(admin) = admin else {
1424        return quote!(::core::option::Option::None);
1425    };
1426
1427    let list_display = admin
1428        .list_display
1429        .as_ref()
1430        .map(|(v, _)| v.as_slice())
1431        .unwrap_or(&[]);
1432    let list_display_lits = list_display.iter().map(|s| s.as_str());
1433
1434    let search_fields = admin
1435        .search_fields
1436        .as_ref()
1437        .map(|(v, _)| v.as_slice())
1438        .unwrap_or(&[]);
1439    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1440
1441    let readonly_fields = admin
1442        .readonly_fields
1443        .as_ref()
1444        .map(|(v, _)| v.as_slice())
1445        .unwrap_or(&[]);
1446    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1447
1448    let list_filter = admin
1449        .list_filter
1450        .as_ref()
1451        .map(|(v, _)| v.as_slice())
1452        .unwrap_or(&[]);
1453    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1454
1455    let actions = admin
1456        .actions
1457        .as_ref()
1458        .map(|(v, _)| v.as_slice())
1459        .unwrap_or(&[]);
1460    let actions_lits = actions.iter().map(|s| s.as_str());
1461
1462    let fieldsets = admin
1463        .fieldsets
1464        .as_ref()
1465        .map(|(v, _)| v.as_slice())
1466        .unwrap_or(&[]);
1467    let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1468        let title = title.as_str();
1469        let field_lits = fields.iter().map(|s| s.as_str());
1470        quote!(::rustango::core::Fieldset {
1471            title: #title,
1472            fields: &[ #( #field_lits ),* ],
1473        })
1474    });
1475
1476    let list_per_page = admin.list_per_page.unwrap_or(0);
1477
1478    let ordering_pairs = admin
1479        .ordering
1480        .as_ref()
1481        .map(|(v, _)| v.as_slice())
1482        .unwrap_or(&[]);
1483    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1484        let name = name.as_str();
1485        let desc = *desc;
1486        quote!((#name, #desc))
1487    });
1488
1489    quote! {
1490        ::core::option::Option::Some(&::rustango::core::AdminConfig {
1491            list_display: &[ #( #list_display_lits ),* ],
1492            search_fields: &[ #( #search_fields_lits ),* ],
1493            list_per_page: #list_per_page,
1494            ordering: &[ #( #ordering_tokens ),* ],
1495            readonly_fields: &[ #( #readonly_fields_lits ),* ],
1496            list_filter: &[ #( #list_filter_lits ),* ],
1497            actions: &[ #( #actions_lits ),* ],
1498            fieldsets: &[ #( #fieldset_tokens ),* ],
1499        })
1500    }
1501}
1502
1503fn inherent_impl_tokens(
1504    struct_name: &syn::Ident,
1505    fields: &CollectedFields,
1506    primary_key: Option<&(syn::Ident, String)>,
1507    column_consts: &TokenStream2,
1508    audited_fields: Option<&[&ColumnEntry]>,
1509    indexes: &[IndexAttr],
1510) -> TokenStream2 {
1511    // Audit-emit fragments threaded into write paths. Non-empty only
1512    // when the model carries `#[rustango(audit(...))]`. They reborrow
1513    // `_executor` (a `&mut PgConnection` for audited models — the
1514    // macro switches the signature below) so the data write and the
1515    // audit INSERT both run on the same caller-supplied connection.
1516    let executor_passes_to_data_write = if audited_fields.is_some() {
1517        quote!(&mut *_executor)
1518    } else {
1519        quote!(_executor)
1520    };
1521    let executor_param = if audited_fields.is_some() {
1522        quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1523    } else {
1524        quote!(_executor: _E)
1525    };
1526    let executor_generics = if audited_fields.is_some() {
1527        quote!()
1528    } else {
1529        quote!(<'_c, _E>)
1530    };
1531    let executor_where = if audited_fields.is_some() {
1532        quote!()
1533    } else {
1534        quote! {
1535            where
1536                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1537        }
1538    };
1539    // For audited models the `_on` methods take `&mut PgConnection`, so
1540    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
1541    // must acquire a connection first. Non-audited models keep the
1542    // direct delegation since `&PgPool` IS an Executor.
1543    let pool_to_save_on = if audited_fields.is_some() {
1544        quote! {
1545            let mut _conn = pool.acquire().await?;
1546            self.save_on(&mut *_conn).await
1547        }
1548    } else {
1549        quote!(self.save_on(pool).await)
1550    };
1551    let pool_to_insert_on = if audited_fields.is_some() {
1552        quote! {
1553            let mut _conn = pool.acquire().await?;
1554            self.insert_on(&mut *_conn).await
1555        }
1556    } else {
1557        quote!(self.insert_on(pool).await)
1558    };
1559    let pool_to_delete_on = if audited_fields.is_some() {
1560        quote! {
1561            let mut _conn = pool.acquire().await?;
1562            self.delete_on(&mut *_conn).await
1563        }
1564    } else {
1565        quote!(self.delete_on(pool).await)
1566    };
1567    let pool_to_bulk_insert_on = if audited_fields.is_some() {
1568        quote! {
1569            let mut _conn = pool.acquire().await?;
1570            Self::bulk_insert_on(rows, &mut *_conn).await
1571        }
1572    } else {
1573        quote!(Self::bulk_insert_on(rows, pool).await)
1574    };
1575    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
1576    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
1577    // directly, but `upsert_on` for audited models takes
1578    // `&mut PgConnection` (the audit emit needs a real connection).
1579    // Add the missing acquire shim to keep audited Auto-PK upsert
1580    // compiling.
1581    let pool_to_upsert_on = if audited_fields.is_some() {
1582        quote! {
1583            let mut _conn = pool.acquire().await?;
1584            self.upsert_on(&mut *_conn).await
1585        }
1586    } else {
1587        quote!(self.upsert_on(pool).await)
1588    };
1589
1590    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
1591    // (audit-on-connection over &Pool needs a bi-dialect transaction
1592    // helper, deferred). Two body shapes:
1593    // - has_auto: build InsertQuery skipping Auto::Unset columns,
1594    //   request Auto cols in `returning`, dispatch via
1595    //   `insert_returning_pool`, then on the returned `PgRow` /
1596    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
1597    //   row OR drop the single i64 into the first Auto field on MySQL
1598    //   (multi-Auto models on MySQL error at runtime since
1599    //   `LAST_INSERT_ID()` only reports one)
1600    // - non-Auto: build InsertQuery with explicit columns/values and
1601    //   call `insert_pool` (no returning needed)
1602    // pool_insert_method body for the audited Auto-PK case is moved
1603    // to after audit_pair_tokens / audit_pk_to_string (they live
1604    // ~150 lines below). This block keeps the non-audited and
1605    // non-Auto branches in place — the audited Auto-PK arm is
1606    // computed below and merged via the dispatch helper variable.
1607    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
1608        // Audited models with explicit (non-Auto) PKs go through
1609        // the non-Auto insert path below — the audit emit is one
1610        // round-trip after the INSERT inside the same tx via
1611        // audit::save_one_with_audit_pool? No, INSERT semantics
1612        // differ. For non-Auto PK + audited, route through a
1613        // dedicated insert + audit emit on the same tx, but defer
1614        // the macro emission to the audit-bundle-aware block below
1615        // — this `quote!()` placeholder gets overwritten there.
1616        quote!()
1617    } else if audited_fields.is_some() && fields.has_auto {
1618        // Audited Auto-PK insert_pool — assembled after the audit
1619        // bundles. Placeholder; real emission below.
1620        quote!()
1621    } else if fields.has_auto {
1622        let pushes = &fields.insert_pushes;
1623        let returning_cols = &fields.returning_cols;
1624        quote! {
1625            /// Insert this row against either backend, populating any
1626            /// `Auto<T>` PK from the auto-assigned value.
1627            ///
1628            /// # Errors
1629            /// As [`Self::insert`].
1630            pub async fn insert_pool(
1631                &mut self,
1632                pool: &::rustango::sql::Pool,
1633            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1634                let mut _columns: ::std::vec::Vec<&'static str> =
1635                    ::std::vec::Vec::new();
1636                let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1637                    ::std::vec::Vec::new();
1638                #( #pushes )*
1639                let _query = ::rustango::core::InsertQuery {
1640                    model: <Self as ::rustango::core::Model>::SCHEMA,
1641                    columns: _columns,
1642                    values: _values,
1643                    returning: ::std::vec![ #( #returning_cols ),* ],
1644                    on_conflict: ::core::option::Option::None,
1645                };
1646                let _result = ::rustango::sql::insert_returning_pool(
1647                    pool, &_query,
1648                ).await?;
1649                ::rustango::sql::apply_auto_pk_pool(_result, self)
1650            }
1651        }
1652    } else {
1653        let insert_columns = &fields.insert_columns;
1654        let insert_values = &fields.insert_values;
1655        quote! {
1656            /// Insert this row into its table against either backend.
1657            /// Equivalent to [`Self::insert`] but takes
1658            /// [`::rustango::sql::Pool`].
1659            ///
1660            /// # Errors
1661            /// As [`Self::insert`].
1662            pub async fn insert_pool(
1663                &self,
1664                pool: &::rustango::sql::Pool,
1665            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1666                let _query = ::rustango::core::InsertQuery {
1667                    model: <Self as ::rustango::core::Model>::SCHEMA,
1668                    columns: ::std::vec![ #( #insert_columns ),* ],
1669                    values: ::std::vec![ #( #insert_values ),* ],
1670                    returning: ::std::vec::Vec::new(),
1671                    on_conflict: ::core::option::Option::None,
1672                };
1673                ::rustango::sql::insert_pool(pool, &_query).await
1674            }
1675        }
1676    };
1677
1678    // pool_save_method moved to after audit_pair_tokens /
1679    // audit_pk_to_string (they live ~70 lines below) — needed for
1680    // the audited branch which builds an UpdateQuery + PendingEntry
1681    // and dispatches via audit::save_one_with_audit_pool.
1682
1683    // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
1684    // are computed (they live ~80 lines below).
1685
1686    // Build the (column, JSON value) pair list used by every
1687    // snapshot-style audit emission. Reused across delete_on,
1688    // soft_delete_on, restore_on, and (later) bulk paths. Empty
1689    // when the model isn't audited.
1690    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1691        .map(|tracked| {
1692            tracked
1693                .iter()
1694                .map(|c| {
1695                    let column_lit = c.column.as_str();
1696                    let ident = &c.ident;
1697                    quote! {
1698                        (
1699                            #column_lit,
1700                            ::serde_json::to_value(&self.#ident)
1701                                .unwrap_or(::serde_json::Value::Null),
1702                        )
1703                    }
1704                })
1705                .collect()
1706        })
1707        .unwrap_or_default();
1708    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1709        if fields.pk_is_auto {
1710            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1711        } else {
1712            quote!(::std::format!("{}", &self.#pk_ident))
1713        }
1714    } else {
1715        quote!(::std::string::String::new())
1716    };
1717    let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1718        if audited_fields.is_some() {
1719            let pairs = audit_pair_tokens.iter();
1720            let pk_str = audit_pk_to_string.clone();
1721            quote! {
1722                let _audit_entry = ::rustango::audit::PendingEntry {
1723                    entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1724                    entity_pk: #pk_str,
1725                    operation: #op_path,
1726                    source: ::rustango::audit::current_source(),
1727                    changes: ::rustango::audit::snapshot_changes(&[
1728                        #( #pairs ),*
1729                    ]),
1730                };
1731                ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1732            }
1733        } else {
1734            quote!()
1735        }
1736    };
1737    let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1738    let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1739    let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1740    let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1741
1742    // `save_pool(&Pool)` — emitted for every model with a PK.
1743    // Audited Auto-PK models are deferred (the Auto::Unset →
1744    // insert_pool path needs the audited-insert flow from a future
1745    // batch). Three body shapes:
1746    // - non-audited, plain PK: build UpdateQuery + dispatch through
1747    //   sql::update_pool
1748    // - non-audited, Auto-PK: same, but Auto::Unset routes to
1749    //   self.insert_pool which already handles RETURNING / LAST_INSERT_ID
1750    // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
1751    //   through audit::save_one_with_audit_pool (per-backend tx wraps
1752    //   UPDATE + audit emit atomically). Snapshot-style audit (post-
1753    //   write field values) — diff-style audit (with pre-UPDATE
1754    //   SELECT for `before` values) needs per-tracked-column codegen
1755    //   that doesn't fit the runtime-helper pattern; legacy &PgPool
1756    //   `save` keeps the diff for now.
1757    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
1758        let pk_column_lit = pk_col.as_str();
1759        let assignments = &fields.update_assignments;
1760        if audited_fields.is_some() {
1761            if fields.pk_is_auto {
1762                // Auto-PK + audited: defer. The Auto::Unset insert
1763                // path needs a transactional INSERT + LAST_INSERT_ID
1764                // + audit emit flow — that's a follow-up batch.
1765                quote!()
1766            } else {
1767                let pairs = audit_pair_tokens.iter();
1768                let pk_str = audit_pk_to_string.clone();
1769                quote! {
1770                    /// Save (UPDATE) this row against either backend
1771                    /// with audit emission inside the same transaction.
1772                    /// Bi-dialect counterpart of [`Self::save`] for
1773                    /// audited models with non-`Auto<T>` PKs.
1774                    ///
1775                    /// Captures **post-write** field state (snapshot
1776                    /// audit). The legacy &PgPool [`Self::save`]
1777                    /// captures BEFORE+AFTER for true diff audit;
1778                    /// porting that to the &Pool path needs runtime
1779                    /// per-tracked-column decoding and is deferred.
1780                    ///
1781                    /// # Errors
1782                    /// As [`Self::save`].
1783                    pub async fn save_pool(
1784                        &mut self,
1785                        pool: &::rustango::sql::Pool,
1786                    ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1787                        let _query = ::rustango::core::UpdateQuery {
1788                            model: <Self as ::rustango::core::Model>::SCHEMA,
1789                            set: ::std::vec![ #( #assignments ),* ],
1790                            where_clause: ::rustango::core::WhereExpr::Predicate(
1791                                ::rustango::core::Filter {
1792                                    column: #pk_column_lit,
1793                                    op: ::rustango::core::Op::Eq,
1794                                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1795                                        ::core::clone::Clone::clone(&self.#pk_ident)
1796                                    ),
1797                                }
1798                            ),
1799                        };
1800                        let _audit_entry = ::rustango::audit::PendingEntry {
1801                            entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1802                            entity_pk: #pk_str,
1803                            operation: ::rustango::audit::AuditOp::Update,
1804                            source: ::rustango::audit::current_source(),
1805                            changes: ::rustango::audit::snapshot_changes(&[
1806                                #( #pairs ),*
1807                            ]),
1808                        };
1809                        let _ = ::rustango::audit::save_one_with_audit_pool(
1810                            pool, &_query, &_audit_entry,
1811                        ).await?;
1812                        ::core::result::Result::Ok(())
1813                    }
1814                }
1815            }
1816        } else {
1817            let dispatch_unset = if fields.pk_is_auto {
1818                quote! {
1819                    if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1820                        return self.insert_pool(pool).await;
1821                    }
1822                }
1823            } else {
1824                quote!()
1825            };
1826            quote! {
1827                /// Save this row to its table against either backend.
1828                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
1829                /// `UPDATE` keyed on the PK.
1830                ///
1831                /// # Errors
1832                /// As [`Self::save`].
1833                pub async fn save_pool(
1834                    &mut self,
1835                    pool: &::rustango::sql::Pool,
1836                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1837                    #dispatch_unset
1838                    let _query = ::rustango::core::UpdateQuery {
1839                        model: <Self as ::rustango::core::Model>::SCHEMA,
1840                        set: ::std::vec![ #( #assignments ),* ],
1841                        where_clause: ::rustango::core::WhereExpr::Predicate(
1842                            ::rustango::core::Filter {
1843                                column: #pk_column_lit,
1844                                op: ::rustango::core::Op::Eq,
1845                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1846                                    ::core::clone::Clone::clone(&self.#pk_ident)
1847                                ),
1848                            }
1849                        ),
1850                    };
1851                    let _ = ::rustango::sql::update_pool(pool, &_query).await?;
1852                    ::core::result::Result::Ok(())
1853                }
1854            }
1855        }
1856    } else {
1857        quote!()
1858    };
1859
1860    // Audited `insert_pool` (overrides the placeholder set higher up
1861    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
1862    // audited models get insert_pool routing through
1863    // audit::insert_one_with_audit_pool (per-backend tx wraps INSERT
1864    // + auto-PK readback + audit emit). Snapshot-style audit (the
1865    // PendingEntry's `changes` carries post-write field values).
1866    let pool_insert_method = if audited_fields.is_some() {
1867        if let Some(_) = primary_key {
1868            let pushes = if fields.has_auto {
1869                fields.insert_pushes.clone()
1870            } else {
1871                // For non-Auto-PK models, the macro normally builds
1872                // {columns, values} from fields.insert_columns +
1873                // fields.insert_values rather than insert_pushes.
1874                // Map those into the pushes shape.
1875                fields
1876                    .insert_columns
1877                    .iter()
1878                    .zip(&fields.insert_values)
1879                    .map(|(col, val)| {
1880                        quote! {
1881                            _columns.push(#col);
1882                            _values.push(#val);
1883                        }
1884                    })
1885                    .collect()
1886            };
1887            let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
1888                fields.returning_cols.clone()
1889            } else {
1890                // Non-Auto-PK: still need RETURNING something for the
1891                // audit helper's contract (it errors on empty
1892                // returning). Return the PK column so the audit row
1893                // can carry the assigned PK back. Some non-Auto PKs
1894                // are server-side-default (e.g. UUIDv4 default), so
1895                // RETURNING is genuinely useful.
1896                primary_key
1897                    .map(|(_, col)| {
1898                        let lit = col.as_str();
1899                        vec![quote!(#lit)]
1900                    })
1901                    .unwrap_or_default()
1902            };
1903            let pairs = audit_pair_tokens.iter();
1904            let pk_str = audit_pk_to_string.clone();
1905            quote! {
1906                /// Insert this row against either backend with audit
1907                /// emission inside the same transaction. Bi-dialect
1908                /// counterpart of [`Self::insert`] for audited models.
1909                ///
1910                /// Snapshot-style audit (post-write field values).
1911                ///
1912                /// # Errors
1913                /// As [`Self::insert`].
1914                pub async fn insert_pool(
1915                    &mut self,
1916                    pool: &::rustango::sql::Pool,
1917                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1918                    let mut _columns: ::std::vec::Vec<&'static str> =
1919                        ::std::vec::Vec::new();
1920                    let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1921                        ::std::vec::Vec::new();
1922                    #( #pushes )*
1923                    let _query = ::rustango::core::InsertQuery {
1924                        model: <Self as ::rustango::core::Model>::SCHEMA,
1925                        columns: _columns,
1926                        values: _values,
1927                        returning: ::std::vec![ #( #returning_cols ),* ],
1928                        on_conflict: ::core::option::Option::None,
1929                    };
1930                    let _audit_entry = ::rustango::audit::PendingEntry {
1931                        entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1932                        entity_pk: #pk_str,
1933                        operation: ::rustango::audit::AuditOp::Create,
1934                        source: ::rustango::audit::current_source(),
1935                        changes: ::rustango::audit::snapshot_changes(&[
1936                            #( #pairs ),*
1937                        ]),
1938                    };
1939                    let _result = ::rustango::audit::insert_one_with_audit_pool(
1940                        pool, &_query, &_audit_entry,
1941                    ).await?;
1942                    ::rustango::sql::apply_auto_pk_pool(_result, self)
1943                }
1944            }
1945        } else {
1946            quote!()
1947        }
1948    } else {
1949        // Keep the non-audited pool_insert_method we built earlier.
1950        pool_insert_method
1951    };
1952
1953    // Update audited save_pool: now that insert_pool is wired for
1954    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
1955    // insert_pool. Non-audited save_pool already does this.
1956    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
1957    // Replaces the snapshot-only emission with a per-backend transaction
1958    // body that:
1959    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
1960    //     column), capturing BEFORE values
1961    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
1962    //  3. builds AFTER pairs from &self
1963    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
1964    //     AuditOp::Update + diff_changes(...) on the same tx connection
1965    //  5. commits
1966    //
1967    // Per-backend arms inline the SQL string + placeholder shape, then
1968    // share the `audit_before_pair_tokens` decoder block (Row::try_get
1969    // is polymorphic over Row type — the same tokens work against
1970    // PgRow and MySqlRow as long as the field's Rust type implements
1971    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
1972    // primitives + chrono/uuid/serde_json::Value all do).
1973    let pool_save_method = if let Some(tracked) = audited_fields {
1974        if let Some((pk_ident, pk_col)) = primary_key {
1975            let pk_column_lit = pk_col.as_str();
1976            // Two iterators — quote!'s `#(#var)*` consumes the
1977            // iterator, and we need to splice the same after-pairs
1978            // sequence into both per-backend arms.
1979            let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
1980            let pk_str = audit_pk_to_string.clone();
1981            // Per-tracked-column BEFORE-pair token list. Each entry
1982            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
1983            // The Row alias resolves to PgRow / MySqlRow per call site,
1984            // so the same template generates both the PG and MySQL bodies.
1985            let mk_before_pairs =
1986                |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
1987                    tracked
1988                        .iter()
1989                        .map(|c| {
1990                            let column_lit = c.column.as_str();
1991                            let value_ty = &c.value_ty;
1992                            quote! {
1993                                (
1994                                    #column_lit,
1995                                    match #getter::<#value_ty>(
1996                                        _audit_before_row, #column_lit,
1997                                    ) {
1998                                        ::core::result::Result::Ok(v) => {
1999                                            ::serde_json::to_value(&v)
2000                                                .unwrap_or(::serde_json::Value::Null)
2001                                        }
2002                                        ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2003                                    },
2004                                )
2005                            }
2006                        })
2007                        .collect()
2008                };
2009            let before_pairs_pg: Vec<proc_macro2::TokenStream> =
2010                mk_before_pairs(quote!(::rustango::sql::try_get_returning));
2011            let before_pairs_my: Vec<proc_macro2::TokenStream> =
2012                mk_before_pairs(quote!(::rustango::sql::try_get_returning_my));
2013            let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
2014                mk_before_pairs(quote!(::rustango::sql::try_get_returning_sqlite));
2015            let pg_select_cols: String = tracked
2016                .iter()
2017                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2018                .collect::<Vec<_>>()
2019                .join(", ");
2020            let my_select_cols: String = tracked
2021                .iter()
2022                .map(|c| format!("`{}`", c.column.replace('`', "``")))
2023                .collect::<Vec<_>>()
2024                .join(", ");
2025            // SQLite uses double-quote identifier quoting (same as
2026            // Postgres in default config), so the column-list shape
2027            // matches PG.
2028            let sqlite_select_cols: String = pg_select_cols.clone();
2029            let pk_value_for_bind = if fields.pk_is_auto {
2030                quote!(self.#pk_ident.get().copied().unwrap_or_default())
2031            } else {
2032                quote!(::core::clone::Clone::clone(&self.#pk_ident))
2033            };
2034            let assignments = &fields.update_assignments;
2035            let unset_dispatch = if fields.has_auto {
2036                quote! {
2037                    if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2038                        return self.insert_pool(pool).await;
2039                    }
2040                }
2041            } else {
2042                quote!()
2043            };
2044            quote! {
2045                /// Save this row against either backend with audit
2046                /// emission (diff-style: BEFORE+AFTER) inside the
2047                /// same transaction. Auto::Unset PK routes to
2048                /// insert_pool. Bi-dialect counterpart of
2049                /// [`Self::save`] for audited models.
2050                ///
2051                /// The audit row's `changes` JSON contains one
2052                /// `{ "field": { "before": …, "after": … } }` entry
2053                /// per tracked column whose value actually changed
2054                /// — same shape as the existing &PgPool save() emits.
2055                ///
2056                /// # Errors
2057                /// As [`Self::save`].
2058                pub async fn save_pool(
2059                    &mut self,
2060                    pool: &::rustango::sql::Pool,
2061                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2062                    #unset_dispatch
2063                    let _query = ::rustango::core::UpdateQuery {
2064                        model: <Self as ::rustango::core::Model>::SCHEMA,
2065                        set: ::std::vec![ #( #assignments ),* ],
2066                        where_clause: ::rustango::core::WhereExpr::Predicate(
2067                            ::rustango::core::Filter {
2068                                column: #pk_column_lit,
2069                                op: ::rustango::core::Op::Eq,
2070                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2071                                    ::core::clone::Clone::clone(&self.#pk_ident)
2072                                ),
2073                            }
2074                        ),
2075                    };
2076                    let _after_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2077                        ::std::vec![ #( #after_pairs_pg ),* ];
2078                    ::rustango::audit::save_one_with_diff_pool(
2079                        pool,
2080                        &_query,
2081                        #pk_column_lit,
2082                        ::core::convert::Into::<::rustango::core::SqlValue>::into(
2083                            #pk_value_for_bind,
2084                        ),
2085                        <Self as ::rustango::core::Model>::SCHEMA.table,
2086                        #pk_str,
2087                        _after_pairs,
2088                        #pg_select_cols,
2089                        #my_select_cols,
2090                        #sqlite_select_cols,
2091                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
2092                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
2093                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
2094                    ).await
2095                }
2096            }
2097        } else {
2098            quote!()
2099        }
2100    } else {
2101        pool_save_method
2102    };
2103
2104    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
2105    // body shapes:
2106    // - non-audited: simple dispatch through `sql::delete_pool`
2107    // - audited: routes through `audit::delete_one_with_audit_pool`,
2108    //   which opens a per-backend transaction wrapping DELETE +
2109    //   audit emit so the data write and audit row commit atomically.
2110    let pool_delete_method = {
2111        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
2112        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
2113        if let Some(pk_ident) = pk_ident_for_pool {
2114            if audited_fields.is_some() {
2115                let pairs = audit_pair_tokens.iter();
2116                let pk_str = audit_pk_to_string.clone();
2117                quote! {
2118                    /// Delete this row against either backend with audit
2119                    /// emission inside the same transaction. Bi-dialect
2120                    /// counterpart of [`Self::delete`] for audited models.
2121                    ///
2122                    /// # Errors
2123                    /// As [`Self::delete`].
2124                    pub async fn delete_pool(
2125                        &self,
2126                        pool: &::rustango::sql::Pool,
2127                    ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2128                        let _query = ::rustango::core::DeleteQuery {
2129                            model: <Self as ::rustango::core::Model>::SCHEMA,
2130                            where_clause: ::rustango::core::WhereExpr::Predicate(
2131                                ::rustango::core::Filter {
2132                                    column: #pk_column_lit,
2133                                    op: ::rustango::core::Op::Eq,
2134                                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2135                                        ::core::clone::Clone::clone(&self.#pk_ident)
2136                                    ),
2137                                }
2138                            ),
2139                        };
2140                        let _audit_entry = ::rustango::audit::PendingEntry {
2141                            entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2142                            entity_pk: #pk_str,
2143                            operation: ::rustango::audit::AuditOp::Delete,
2144                            source: ::rustango::audit::current_source(),
2145                            changes: ::rustango::audit::snapshot_changes(&[
2146                                #( #pairs ),*
2147                            ]),
2148                        };
2149                        ::rustango::audit::delete_one_with_audit_pool(
2150                            pool, &_query, &_audit_entry,
2151                        ).await
2152                    }
2153                }
2154            } else {
2155                quote! {
2156                    /// Delete the row identified by this instance's primary key
2157                    /// against either backend. Equivalent to [`Self::delete`] but
2158                    /// takes [`::rustango::sql::Pool`] and dispatches per backend.
2159                    ///
2160                    /// # Errors
2161                    /// As [`Self::delete`].
2162                    pub async fn delete_pool(
2163                        &self,
2164                        pool: &::rustango::sql::Pool,
2165                    ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2166                        let _query = ::rustango::core::DeleteQuery {
2167                            model: <Self as ::rustango::core::Model>::SCHEMA,
2168                            where_clause: ::rustango::core::WhereExpr::Predicate(
2169                                ::rustango::core::Filter {
2170                                    column: #pk_column_lit,
2171                                    op: ::rustango::core::Op::Eq,
2172                                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2173                                        ::core::clone::Clone::clone(&self.#pk_ident)
2174                                    ),
2175                                }
2176                            ),
2177                        };
2178                        ::rustango::sql::delete_pool(pool, &_query).await
2179                    }
2180                }
2181            }
2182        } else {
2183            quote!()
2184        }
2185    };
2186
2187    // Update emission captures both BEFORE and AFTER state — runs an
2188    // extra SELECT against `_executor` BEFORE the UPDATE, captures
2189    // each tracked field's prior value, then after the UPDATE diffs
2190    // against the in-memory `&self`. `diff_changes` drops unchanged
2191    // columns so the JSON only contains the actual delta.
2192    //
2193    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
2194    // and binds `_audit_before_pairs`; `audit_update_post` runs
2195    // after the UPDATE and emits the PendingEntry.
2196    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
2197        audited_fields
2198    {
2199        if tracked.is_empty() {
2200            (quote!(), quote!())
2201        } else {
2202            let select_cols: String = tracked
2203                .iter()
2204                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2205                .collect::<Vec<_>>()
2206                .join(", ");
2207            let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
2208            let select_cols_lit = select_cols;
2209            let pk_column_lit_for_select = pk_column_for_select;
2210            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
2211                if fields.pk_is_auto {
2212                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
2213                } else {
2214                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
2215                }
2216            } else {
2217                quote!(0_i64)
2218            };
2219            let before_pairs = tracked.iter().map(|c| {
2220                let column_lit = c.column.as_str();
2221                let value_ty = &c.value_ty;
2222                quote! {
2223                    (
2224                        #column_lit,
2225                        match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
2226                            &_audit_before_row, #column_lit,
2227                        ) {
2228                            ::core::result::Result::Ok(v) => {
2229                                ::serde_json::to_value(&v)
2230                                    .unwrap_or(::serde_json::Value::Null)
2231                            }
2232                            ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2233                        },
2234                    )
2235                }
2236            });
2237            let after_pairs = tracked.iter().map(|c| {
2238                let column_lit = c.column.as_str();
2239                let ident = &c.ident;
2240                quote! {
2241                    (
2242                        #column_lit,
2243                        ::serde_json::to_value(&self.#ident)
2244                            .unwrap_or(::serde_json::Value::Null),
2245                    )
2246                }
2247            });
2248            let pk_str = audit_pk_to_string.clone();
2249            let pre = quote! {
2250                let _audit_select_sql = ::std::format!(
2251                    r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
2252                    #select_cols_lit,
2253                    <Self as ::rustango::core::Model>::SCHEMA.table,
2254                    #pk_column_lit_for_select,
2255                );
2256                let _audit_before_pairs:
2257                    ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
2258                    match ::rustango::sql::sqlx::query(&_audit_select_sql)
2259                        .bind(#pk_value_for_bind)
2260                        .fetch_optional(&mut *_executor)
2261                        .await
2262                    {
2263                        ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
2264                            ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
2265                        }
2266                        _ => ::core::option::Option::None,
2267                    };
2268            };
2269            let post = quote! {
2270                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
2271                    let _audit_after:
2272                        ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2273                        ::std::vec![ #( #after_pairs ),* ];
2274                    let _audit_entry = ::rustango::audit::PendingEntry {
2275                        entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2276                        entity_pk: #pk_str,
2277                        operation: ::rustango::audit::AuditOp::Update,
2278                        source: ::rustango::audit::current_source(),
2279                        changes: ::rustango::audit::diff_changes(
2280                            &_audit_before,
2281                            &_audit_after,
2282                        ),
2283                    };
2284                    ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
2285                }
2286            };
2287            (pre, post)
2288        }
2289    } else {
2290        (quote!(), quote!())
2291    };
2292
2293    // Bulk-insert audit: capture every row's tracked fields after the
2294    // RETURNING populates each PK, then push one batched INSERT INTO
2295    // audit_log via `emit_many`. One round-trip regardless of N rows.
2296    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
2297        let row_pk_str = if let Some((pk_ident, _)) = primary_key {
2298            if fields.pk_is_auto {
2299                quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
2300            } else {
2301                quote!(::std::format!("{}", &_row.#pk_ident))
2302            }
2303        } else {
2304            quote!(::std::string::String::new())
2305        };
2306        let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
2307            let column_lit = c.column.as_str();
2308            let ident = &c.ident;
2309            quote! {
2310                (
2311                    #column_lit,
2312                    ::serde_json::to_value(&_row.#ident)
2313                        .unwrap_or(::serde_json::Value::Null),
2314                )
2315            }
2316        });
2317        quote! {
2318            let _audit_source = ::rustango::audit::current_source();
2319            let mut _audit_entries:
2320                ::std::vec::Vec<::rustango::audit::PendingEntry> =
2321                    ::std::vec::Vec::with_capacity(rows.len());
2322            for _row in rows.iter() {
2323                _audit_entries.push(::rustango::audit::PendingEntry {
2324                    entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2325                    entity_pk: #row_pk_str,
2326                    operation: ::rustango::audit::AuditOp::Create,
2327                    source: _audit_source.clone(),
2328                    changes: ::rustango::audit::snapshot_changes(&[
2329                        #( #row_pairs ),*
2330                    ]),
2331                });
2332            }
2333            ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
2334        }
2335    } else {
2336        quote!()
2337    };
2338
2339    let save_method = if fields.pk_is_auto {
2340        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
2341        let pk_column_lit = pk_column.as_str();
2342        let assignments = &fields.update_assignments;
2343        let upsert_cols = &fields.upsert_update_columns;
2344        let upsert_pushes = &fields.insert_pushes;
2345        let upsert_returning = &fields.returning_cols;
2346        let upsert_auto_assigns = &fields.auto_assigns;
2347        // Conflict target: prefer the first declared `unique_together`
2348        // when it exists. Plain `Auto<T>` PKs are server-assigned via
2349        // `BIGSERIAL` and never collide on insert, so a PK-only target
2350        // would silently turn `upsert()` into "always-insert" for
2351        // surrogate-PK models with composite UNIQUE constraints — see
2352        // `RolePermission` / `UserRole` / `UserPermission` in the
2353        // tenancy permission engine. When no `unique_together` is
2354        // declared we keep the PK target (the original behaviour).
2355        let upsert_target_columns: Vec<String> = indexes
2356            .iter()
2357            .find(|i| i.unique && !i.columns.is_empty())
2358            .map(|i| i.columns.clone())
2359            .unwrap_or_else(|| vec![pk_column.clone()]);
2360        let upsert_target_lits = upsert_target_columns
2361            .iter()
2362            .map(String::as_str)
2363            .collect::<Vec<_>>();
2364        let conflict_clause = if fields.upsert_update_columns.is_empty() {
2365            quote!(::rustango::core::ConflictClause::DoNothing)
2366        } else {
2367            quote!(::rustango::core::ConflictClause::DoUpdate {
2368                target: ::std::vec![ #( #upsert_target_lits ),* ],
2369                update_columns: ::std::vec![ #( #upsert_cols ),* ],
2370            })
2371        };
2372        Some(quote! {
2373            /// Insert this row if its `Auto<T>` primary key is
2374            /// `Unset`, otherwise update the existing row matching the
2375            /// PK. Mirrors Django's `save()` — caller doesn't need to
2376            /// pick `insert` vs the bulk-update path manually.
2377            ///
2378            /// On the insert branch, populates the PK from `RETURNING`
2379            /// (same behavior as `insert`). On the update branch,
2380            /// writes every non-PK column back; if no row matches the
2381            /// PK, returns `Ok(())` silently.
2382            ///
2383            /// Only generated when the primary key is declared as
2384            /// `Auto<T>`. Models with a manually-managed PK must use
2385            /// `insert` or the QuerySet update builder.
2386            ///
2387            /// # Errors
2388            /// Returns [`::rustango::sql::ExecError`] for SQL-writing
2389            /// or driver failures.
2390            #[cfg(feature = "postgres")]
2391            pub async fn save(
2392                &mut self,
2393                pool: &::rustango::sql::sqlx::PgPool,
2394            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2395                #pool_to_save_on
2396            }
2397
2398            /// Like [`Self::save`] but accepts any sqlx executor —
2399            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
2400            /// escape hatch for tenant-scoped writes: schema-mode
2401            /// tenants share the registry pool but rely on a per-
2402            /// checkout `SET search_path`, so passing `&PgPool` would
2403            /// silently hit the wrong schema. Acquire a connection
2404            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
2405            ///
2406            /// # Errors
2407            /// As [`Self::save`].
2408            #[cfg(feature = "postgres")]
2409            pub async fn save_on #executor_generics (
2410                &mut self,
2411                #executor_param,
2412            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2413            #executor_where
2414            {
2415                if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2416                    return self.insert_on(#executor_passes_to_data_write).await;
2417                }
2418                #audit_update_pre
2419                let _query = ::rustango::core::UpdateQuery {
2420                    model: <Self as ::rustango::core::Model>::SCHEMA,
2421                    set: ::std::vec![ #( #assignments ),* ],
2422                    where_clause: ::rustango::core::WhereExpr::Predicate(
2423                        ::rustango::core::Filter {
2424                            column: #pk_column_lit,
2425                            op: ::rustango::core::Op::Eq,
2426                            value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2427                                ::core::clone::Clone::clone(&self.#pk_ident)
2428                            ),
2429                        }
2430                    ),
2431                };
2432                let _ = ::rustango::sql::update_on(
2433                    #executor_passes_to_data_write,
2434                    &_query,
2435                ).await?;
2436                #audit_update_post
2437                ::core::result::Result::Ok(())
2438            }
2439
2440            /// Per-call override for the audit source. Runs
2441            /// [`Self::save_on`] inside an [`::rustango::audit::with_source`]
2442            /// scope so the resulting audit entry records `source`
2443            /// instead of the task-local default. Useful for seed
2444            /// scripts and one-off CLI tools that don't sit inside an
2445            /// admin handler. The override applies only to this call;
2446            /// no global state changes.
2447            ///
2448            /// # Errors
2449            /// As [`Self::save_on`].
2450            #[cfg(feature = "postgres")]
2451            pub async fn save_on_with #executor_generics (
2452                &mut self,
2453                #executor_param,
2454                source: ::rustango::audit::AuditSource,
2455            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2456            #executor_where
2457            {
2458                ::rustango::audit::with_source(source, self.save_on(_executor)).await
2459            }
2460
2461            /// Insert this row or update it in-place if the primary key already
2462            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
2463            ///
2464            /// With `Auto::Unset` PK the server assigns a new key and no conflict
2465            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
2466            /// inserted if absent or all non-PK columns are overwritten if present.
2467            ///
2468            /// # Errors
2469            /// As [`Self::insert_on`].
2470            #[cfg(feature = "postgres")]
2471            pub async fn upsert(
2472                &mut self,
2473                pool: &::rustango::sql::sqlx::PgPool,
2474            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2475                #pool_to_upsert_on
2476            }
2477
2478            /// Like [`Self::upsert`] but accepts any sqlx executor.
2479            /// See [`Self::save_on`] for tenancy-scoped rationale.
2480            ///
2481            /// # Errors
2482            /// As [`Self::upsert`].
2483            #[cfg(feature = "postgres")]
2484            pub async fn upsert_on #executor_generics (
2485                &mut self,
2486                #executor_param,
2487            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2488            #executor_where
2489            {
2490                let mut _columns: ::std::vec::Vec<&'static str> =
2491                    ::std::vec::Vec::new();
2492                let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2493                    ::std::vec::Vec::new();
2494                #( #upsert_pushes )*
2495                let query = ::rustango::core::InsertQuery {
2496                    model: <Self as ::rustango::core::Model>::SCHEMA,
2497                    columns: _columns,
2498                    values: _values,
2499                    returning: ::std::vec![ #( #upsert_returning ),* ],
2500                    on_conflict: ::core::option::Option::Some(#conflict_clause),
2501                };
2502                let _returning_row_v = ::rustango::sql::insert_returning_on(
2503                    #executor_passes_to_data_write,
2504                    &query,
2505                ).await?;
2506                let _returning_row = &_returning_row_v;
2507                #( #upsert_auto_assigns )*
2508                ::core::result::Result::Ok(())
2509            }
2510        })
2511    } else {
2512        None
2513    };
2514
2515    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
2516        let pk_column_lit = pk_column.as_str();
2517        // Optional `soft_delete_on` / `restore_on` companions when the
2518        // model has a `#[rustango(soft_delete)]` column. They land
2519        // alongside the regular `delete_on` so callers have both
2520        // options — a hard delete (audit-tracked as a real DELETE) and
2521        // a logical delete (audit-tracked as an UPDATE setting the
2522        // deleted_at column to NOW()).
2523        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
2524            let col_lit = col;
2525            quote! {
2526                /// Soft-delete this row by setting its
2527                /// `#[rustango(soft_delete)]` column to `NOW()`.
2528                /// Mirrors Django's `SoftDeleteModel.delete()` shape:
2529                /// the row stays in the table; query helpers can
2530                /// filter it out by checking the column for `IS NOT
2531                /// NULL`.
2532                ///
2533                /// # Errors
2534                /// As [`Self::delete`].
2535                pub async fn soft_delete_on #executor_generics (
2536                    &self,
2537                    #executor_param,
2538                ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2539                #executor_where
2540                {
2541                    let _query = ::rustango::core::UpdateQuery {
2542                        model: <Self as ::rustango::core::Model>::SCHEMA,
2543                        set: ::std::vec![
2544                            ::rustango::core::Assignment {
2545                                column: #col_lit,
2546                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2547                                    ::chrono::Utc::now()
2548                                ),
2549                            },
2550                        ],
2551                        where_clause: ::rustango::core::WhereExpr::Predicate(
2552                            ::rustango::core::Filter {
2553                                column: #pk_column_lit,
2554                                op: ::rustango::core::Op::Eq,
2555                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2556                                    ::core::clone::Clone::clone(&self.#pk_ident)
2557                                ),
2558                            }
2559                        ),
2560                    };
2561                    let _affected = ::rustango::sql::update_on(
2562                        #executor_passes_to_data_write,
2563                        &_query,
2564                    ).await?;
2565                    #audit_softdelete_emit
2566                    ::core::result::Result::Ok(_affected)
2567                }
2568
2569                /// Inverse of [`Self::soft_delete_on`] — clears the
2570                /// soft-delete column back to NULL so the row is
2571                /// considered live again.
2572                ///
2573                /// # Errors
2574                /// As [`Self::delete`].
2575                pub async fn restore_on #executor_generics (
2576                    &self,
2577                    #executor_param,
2578                ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2579                #executor_where
2580                {
2581                    let _query = ::rustango::core::UpdateQuery {
2582                        model: <Self as ::rustango::core::Model>::SCHEMA,
2583                        set: ::std::vec![
2584                            ::rustango::core::Assignment {
2585                                column: #col_lit,
2586                                value: ::rustango::core::SqlValue::Null,
2587                            },
2588                        ],
2589                        where_clause: ::rustango::core::WhereExpr::Predicate(
2590                            ::rustango::core::Filter {
2591                                column: #pk_column_lit,
2592                                op: ::rustango::core::Op::Eq,
2593                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2594                                    ::core::clone::Clone::clone(&self.#pk_ident)
2595                                ),
2596                            }
2597                        ),
2598                    };
2599                    let _affected = ::rustango::sql::update_on(
2600                        #executor_passes_to_data_write,
2601                        &_query,
2602                    ).await?;
2603                    #audit_restore_emit
2604                    ::core::result::Result::Ok(_affected)
2605                }
2606            }
2607        } else {
2608            quote!()
2609        };
2610        quote! {
2611            /// Delete the row identified by this instance's primary key.
2612            ///
2613            /// Returns the number of rows affected (0 or 1).
2614            ///
2615            /// # Errors
2616            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
2617            /// driver failures.
2618            #[cfg(feature = "postgres")]
2619            pub async fn delete(
2620                &self,
2621                pool: &::rustango::sql::sqlx::PgPool,
2622            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2623                #pool_to_delete_on
2624            }
2625
2626            /// Like [`Self::delete`] but accepts any sqlx executor —
2627            /// for tenant-scoped deletes against an explicitly-acquired
2628            /// connection. See [`Self::save_on`] for the rationale.
2629            ///
2630            /// # Errors
2631            /// As [`Self::delete`].
2632            #[cfg(feature = "postgres")]
2633            pub async fn delete_on #executor_generics (
2634                &self,
2635                #executor_param,
2636            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2637            #executor_where
2638            {
2639                let query = ::rustango::core::DeleteQuery {
2640                    model: <Self as ::rustango::core::Model>::SCHEMA,
2641                    where_clause: ::rustango::core::WhereExpr::Predicate(
2642                        ::rustango::core::Filter {
2643                            column: #pk_column_lit,
2644                            op: ::rustango::core::Op::Eq,
2645                            value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2646                                ::core::clone::Clone::clone(&self.#pk_ident)
2647                            ),
2648                        }
2649                    ),
2650                };
2651                let _affected = ::rustango::sql::delete_on(
2652                    #executor_passes_to_data_write,
2653                    &query,
2654                ).await?;
2655                #audit_delete_emit
2656                ::core::result::Result::Ok(_affected)
2657            }
2658
2659            /// Per-call audit-source override for [`Self::delete_on`].
2660            /// See [`Self::save_on_with`] for shape rationale.
2661            ///
2662            /// # Errors
2663            /// As [`Self::delete_on`].
2664            #[cfg(feature = "postgres")]
2665            pub async fn delete_on_with #executor_generics (
2666                &self,
2667                #executor_param,
2668                source: ::rustango::audit::AuditSource,
2669            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2670            #executor_where
2671            {
2672                ::rustango::audit::with_source(source, self.delete_on(_executor)).await
2673            }
2674            #pool_delete_method
2675            #pool_insert_method
2676            #pool_save_method
2677            #soft_delete_methods
2678        }
2679    });
2680
2681    let insert_method = if fields.has_auto {
2682        let pushes = &fields.insert_pushes;
2683        let returning_cols = &fields.returning_cols;
2684        let auto_assigns = &fields.auto_assigns;
2685        quote! {
2686            /// Insert this row into its table. Skips columns whose
2687            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
2688            /// sequence fills them in, then reads each `Auto` column
2689            /// back via `RETURNING` and stores it on `self`.
2690            ///
2691            /// # Errors
2692            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
2693            /// driver failures.
2694            #[cfg(feature = "postgres")]
2695            pub async fn insert(
2696                &mut self,
2697                pool: &::rustango::sql::sqlx::PgPool,
2698            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2699                #pool_to_insert_on
2700            }
2701
2702            /// Like [`Self::insert`] but accepts any sqlx executor.
2703            /// See [`Self::save_on`] for tenancy-scoped rationale.
2704            ///
2705            /// # Errors
2706            /// As [`Self::insert`].
2707            #[cfg(feature = "postgres")]
2708            pub async fn insert_on #executor_generics (
2709                &mut self,
2710                #executor_param,
2711            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2712            #executor_where
2713            {
2714                let mut _columns: ::std::vec::Vec<&'static str> =
2715                    ::std::vec::Vec::new();
2716                let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2717                    ::std::vec::Vec::new();
2718                #( #pushes )*
2719                let query = ::rustango::core::InsertQuery {
2720                    model: <Self as ::rustango::core::Model>::SCHEMA,
2721                    columns: _columns,
2722                    values: _values,
2723                    returning: ::std::vec![ #( #returning_cols ),* ],
2724                    on_conflict: ::core::option::Option::None,
2725                };
2726                let _returning_row_v = ::rustango::sql::insert_returning_on(
2727                    #executor_passes_to_data_write,
2728                    &query,
2729                ).await?;
2730                let _returning_row = &_returning_row_v;
2731                #( #auto_assigns )*
2732                #audit_insert_emit
2733                ::core::result::Result::Ok(())
2734            }
2735
2736            /// Per-call audit-source override for [`Self::insert_on`].
2737            /// See [`Self::save_on_with`] for shape rationale.
2738            ///
2739            /// # Errors
2740            /// As [`Self::insert_on`].
2741            #[cfg(feature = "postgres")]
2742            pub async fn insert_on_with #executor_generics (
2743                &mut self,
2744                #executor_param,
2745                source: ::rustango::audit::AuditSource,
2746            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2747            #executor_where
2748            {
2749                ::rustango::audit::with_source(source, self.insert_on(_executor)).await
2750            }
2751        }
2752    } else {
2753        let insert_columns = &fields.insert_columns;
2754        let insert_values = &fields.insert_values;
2755        quote! {
2756            /// Insert this row into its table.
2757            ///
2758            /// # Errors
2759            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
2760            /// driver failures.
2761            #[cfg(feature = "postgres")]
2762            pub async fn insert(
2763                &self,
2764                pool: &::rustango::sql::sqlx::PgPool,
2765            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2766                self.insert_on(pool).await
2767            }
2768
2769            /// Like [`Self::insert`] but accepts any sqlx executor.
2770            /// See [`Self::save_on`] for tenancy-scoped rationale.
2771            ///
2772            /// # Errors
2773            /// As [`Self::insert`].
2774            #[cfg(feature = "postgres")]
2775            pub async fn insert_on<'_c, _E>(
2776                &self,
2777                _executor: _E,
2778            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2779            where
2780                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2781            {
2782                let query = ::rustango::core::InsertQuery {
2783                    model: <Self as ::rustango::core::Model>::SCHEMA,
2784                    columns: ::std::vec![ #( #insert_columns ),* ],
2785                    values: ::std::vec![ #( #insert_values ),* ],
2786                    returning: ::std::vec::Vec::new(),
2787                    on_conflict: ::core::option::Option::None,
2788                };
2789                ::rustango::sql::insert_on(_executor, &query).await
2790            }
2791        }
2792    };
2793
2794    let bulk_insert_method = if fields.has_auto {
2795        let cols_no_auto = &fields.bulk_columns_no_auto;
2796        let cols_all = &fields.bulk_columns_all;
2797        let pushes_no_auto = &fields.bulk_pushes_no_auto;
2798        let pushes_all = &fields.bulk_pushes_all;
2799        let returning_cols = &fields.returning_cols;
2800        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
2801        let uniformity = &fields.bulk_auto_uniformity;
2802        let first_auto_ident = fields
2803            .first_auto_ident
2804            .as_ref()
2805            .expect("has_auto implies first_auto_ident is Some");
2806        quote! {
2807            /// Bulk-insert `rows` in a single round-trip. Every row's
2808            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
2809            /// (sequence fills them in) or uniformly `Auto::Set(_)`
2810            /// (caller-supplied values). Mixed Set/Unset is rejected
2811            /// — call `insert` per row for that case.
2812            ///
2813            /// Empty slice is a no-op. Each row's `Auto` fields are
2814            /// populated from the `RETURNING` clause in input order
2815            /// before this returns.
2816            ///
2817            /// # Errors
2818            /// Returns [`::rustango::sql::ExecError`] for validation,
2819            /// SQL-writing, mixed-Auto rejection, or driver failures.
2820            #[cfg(feature = "postgres")]
2821            pub async fn bulk_insert(
2822                rows: &mut [Self],
2823                pool: &::rustango::sql::sqlx::PgPool,
2824            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2825                #pool_to_bulk_insert_on
2826            }
2827
2828            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
2829            /// See [`Self::save_on`] for tenancy-scoped rationale.
2830            ///
2831            /// # Errors
2832            /// As [`Self::bulk_insert`].
2833            #[cfg(feature = "postgres")]
2834            pub async fn bulk_insert_on #executor_generics (
2835                rows: &mut [Self],
2836                #executor_param,
2837            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2838            #executor_where
2839            {
2840                if rows.is_empty() {
2841                    return ::core::result::Result::Ok(());
2842                }
2843                let _first_unset = matches!(
2844                    rows[0].#first_auto_ident,
2845                    ::rustango::sql::Auto::Unset
2846                );
2847                #( #uniformity )*
2848
2849                let mut _all_rows: ::std::vec::Vec<
2850                    ::std::vec::Vec<::rustango::core::SqlValue>,
2851                > = ::std::vec::Vec::with_capacity(rows.len());
2852                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
2853                    for _row in rows.iter() {
2854                        let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2855                            ::std::vec::Vec::new();
2856                        #( #pushes_no_auto )*
2857                        _all_rows.push(_row_vals);
2858                    }
2859                    ::std::vec![ #( #cols_no_auto ),* ]
2860                } else {
2861                    for _row in rows.iter() {
2862                        let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2863                            ::std::vec::Vec::new();
2864                        #( #pushes_all )*
2865                        _all_rows.push(_row_vals);
2866                    }
2867                    ::std::vec![ #( #cols_all ),* ]
2868                };
2869
2870                let _query = ::rustango::core::BulkInsertQuery {
2871                    model: <Self as ::rustango::core::Model>::SCHEMA,
2872                    columns: _columns,
2873                    rows: _all_rows,
2874                    returning: ::std::vec![ #( #returning_cols ),* ],
2875                    on_conflict: ::core::option::Option::None,
2876                };
2877                let _returned = ::rustango::sql::bulk_insert_on(
2878                    #executor_passes_to_data_write,
2879                    &_query,
2880                ).await?;
2881                if _returned.len() != rows.len() {
2882                    return ::core::result::Result::Err(
2883                        ::rustango::sql::ExecError::Sql(
2884                            ::rustango::sql::SqlError::BulkInsertReturningMismatch {
2885                                expected: rows.len(),
2886                                actual: _returned.len(),
2887                            }
2888                        )
2889                    );
2890                }
2891                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
2892                    #auto_assigns_for_row
2893                }
2894                #audit_bulk_insert_emit
2895                ::core::result::Result::Ok(())
2896            }
2897        }
2898    } else {
2899        let cols_all = &fields.bulk_columns_all;
2900        let pushes_all = &fields.bulk_pushes_all;
2901        quote! {
2902            /// Bulk-insert `rows` in a single round-trip. Every row's
2903            /// fields are written verbatim — there are no `Auto<T>`
2904            /// fields on this model.
2905            ///
2906            /// Empty slice is a no-op.
2907            ///
2908            /// # Errors
2909            /// Returns [`::rustango::sql::ExecError`] for validation,
2910            /// SQL-writing, or driver failures.
2911            #[cfg(feature = "postgres")]
2912            pub async fn bulk_insert(
2913                rows: &[Self],
2914                pool: &::rustango::sql::sqlx::PgPool,
2915            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2916                Self::bulk_insert_on(rows, pool).await
2917            }
2918
2919            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
2920            /// See [`Self::save_on`] for tenancy-scoped rationale.
2921            ///
2922            /// # Errors
2923            /// As [`Self::bulk_insert`].
2924            #[cfg(feature = "postgres")]
2925            pub async fn bulk_insert_on<'_c, _E>(
2926                rows: &[Self],
2927                _executor: _E,
2928            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2929            where
2930                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2931            {
2932                if rows.is_empty() {
2933                    return ::core::result::Result::Ok(());
2934                }
2935                let mut _all_rows: ::std::vec::Vec<
2936                    ::std::vec::Vec<::rustango::core::SqlValue>,
2937                > = ::std::vec::Vec::with_capacity(rows.len());
2938                for _row in rows.iter() {
2939                    let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2940                        ::std::vec::Vec::new();
2941                    #( #pushes_all )*
2942                    _all_rows.push(_row_vals);
2943                }
2944                let _query = ::rustango::core::BulkInsertQuery {
2945                    model: <Self as ::rustango::core::Model>::SCHEMA,
2946                    columns: ::std::vec![ #( #cols_all ),* ],
2947                    rows: _all_rows,
2948                    returning: ::std::vec::Vec::new(),
2949                    on_conflict: ::core::option::Option::None,
2950                };
2951                let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
2952                ::core::result::Result::Ok(())
2953            }
2954        }
2955    };
2956
2957    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
2958        quote! {
2959            /// Hidden runtime accessor for the primary-key value as a
2960            /// [`SqlValue`]. Used by reverse-relation helpers
2961            /// (`<parent>::<child>_set`) emitted from sibling models'
2962            /// FK fields. Not part of the public API.
2963            #[doc(hidden)]
2964            pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
2965                ::core::convert::Into::<::rustango::core::SqlValue>::into(
2966                    ::core::clone::Clone::clone(&self.#pk_ident)
2967                )
2968            }
2969        }
2970    });
2971
2972    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
2973        quote! {
2974            impl ::rustango::sql::HasPkValue for #struct_name {
2975                fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
2976                    ::core::convert::Into::<::rustango::core::SqlValue>::into(
2977                        ::core::clone::Clone::clone(&self.#pk_ident)
2978                    )
2979                }
2980            }
2981        }
2982    });
2983
2984    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
2985
2986    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk_pool`
2987    // dispatch to the right per-backend body without the macro emitting
2988    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
2989    // so audited models with non-Auto PKs (which still go through
2990    // `insert_one_with_audit_pool` → `apply_auto_pk_pool`) link.
2991    let assign_auto_pk_pool_impl = {
2992        let auto_assigns = &fields.auto_assigns;
2993        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
2994        // so the body is structurally identical to `auto_assigns` —
2995        // only the helper name swaps from `try_get_returning` to
2996        // `try_get_returning_sqlite` so the closure typechecks against
2997        // a `SqliteRow` instead of a `PgRow`.
2998        let auto_assigns_sqlite: Vec<TokenStream2> = fields
2999            .auto_field_idents
3000            .iter()
3001            .map(|(ident, column)| {
3002                quote! {
3003                    self.#ident = ::rustango::sql::try_get_returning_sqlite(
3004                        _returning_row, #column
3005                    )?;
3006                }
3007            })
3008            .collect();
3009        let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
3010            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
3011            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
3012            // Auto<Uuid>/etc. fail to link against MySQL (intended —
3013            // those models can't use AUTO_INCREMENT). The trait is only
3014            // touched on the MySQL arm at runtime, so PG-only consumers
3015            // never see the bound failure.
3016            //
3017            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
3018            // Auto<i64> PK + auto_now_add timestamp) errored hard at
3019            // runtime with "multi-column RETURNING". MySQL has no
3020            // multi-column RETURNING semantic and a follow-up SELECT
3021            // would need cross-trait plumbing. Pragmatic shape: succeed
3022            // with the FIRST Auto field populated from LAST_INSERT_ID();
3023            // any other Auto fields stay `Auto::Unset`. Callers that
3024            // need the DB-defaulted timestamp / UUID can re-fetch the
3025            // row by PK after `save_pool`. Fixes the cookbook chapter
3026            // 12 dialect divergence.
3027            let value_ty = fields
3028                .first_auto_value_ty
3029                .as_ref()
3030                .expect("first_auto_value_ty set whenever first_auto_ident is");
3031            quote! {
3032                let _converted = <#value_ty as ::rustango::sql::MysqlAutoIdSet>
3033                    ::rustango_from_mysql_auto_id(_id)?;
3034                self.#first = ::rustango::sql::Auto::Set(_converted);
3035                ::core::result::Result::Ok(())
3036            }
3037        } else {
3038            quote! {
3039                let _ = _id;
3040                ::core::result::Result::Ok(())
3041            }
3042        };
3043        quote! {
3044            impl ::rustango::sql::AssignAutoPkPool for #struct_name {
3045                fn __rustango_assign_from_pg_row(
3046                    &mut self,
3047                    _returning_row: &::rustango::sql::PgReturningRow,
3048                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3049                    #( #auto_assigns )*
3050                    ::core::result::Result::Ok(())
3051                }
3052                fn __rustango_assign_from_mysql_id(
3053                    &mut self,
3054                    _id: i64,
3055                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3056                    #mysql_body
3057                }
3058                fn __rustango_assign_from_sqlite_row(
3059                    &mut self,
3060                    _returning_row: &::rustango::sql::SqliteReturningRow,
3061                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3062                    #( #auto_assigns_sqlite )*
3063                    ::core::result::Result::Ok(())
3064                }
3065            }
3066        }
3067    };
3068
3069    let from_aliased_row_inits = &fields.from_aliased_row_inits;
3070    let aliased_row_helper = quote! {
3071        /// Decode a row's aliased target columns (produced by
3072        /// `select_related`'s LEFT JOIN) into a fresh instance of
3073        /// this model. Reads each column via
3074        /// `format!("{prefix}__{col}")`, matching the alias the
3075        /// SELECT writer emitted. Slice 9.0d.
3076        #[doc(hidden)]
3077        #[cfg(feature = "postgres")]
3078        pub fn __rustango_from_aliased_row(
3079            row: &::rustango::sql::sqlx::postgres::PgRow,
3080            prefix: &str,
3081        ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3082            ::core::result::Result::Ok(Self {
3083                #( #from_aliased_row_inits ),*
3084            })
3085        }
3086    };
3087    // v0.23.0-batch8 — MySQL counterpart, gated through the
3088    // cfg-aware macro_rules so PG-only builds expand to nothing.
3089    let aliased_row_helper_my = quote! {
3090        ::rustango::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
3091            #( #from_aliased_row_inits ),*
3092        });
3093    };
3094
3095    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
3096    // pattern + cfg gate on the `sqlite` feature.
3097    let aliased_row_helper_sqlite = quote! {
3098        ::rustango::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
3099            #( #from_aliased_row_inits ),*
3100        });
3101    };
3102
3103    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
3104    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
3105    let load_related_impl_sqlite =
3106        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
3107
3108    quote! {
3109        impl #struct_name {
3110            /// Start a new `QuerySet` over this model.
3111            #[must_use]
3112            pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
3113                ::rustango::query::QuerySet::new()
3114            }
3115
3116            #insert_method
3117
3118            #bulk_insert_method
3119
3120            #save_method
3121
3122            #pk_methods
3123
3124            #pk_value_helper
3125
3126            #aliased_row_helper
3127
3128            #column_consts
3129        }
3130
3131        #aliased_row_helper_my
3132
3133        #aliased_row_helper_sqlite
3134
3135        #load_related_impl
3136
3137        #load_related_impl_my
3138
3139        #load_related_impl_sqlite
3140
3141        #has_pk_value_impl
3142
3143        #fk_pk_access_impl
3144
3145        #assign_auto_pk_pool_impl
3146    }
3147}
3148
3149/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
3150/// `auto_assigns` but reading from `_returning_row` and writing to
3151/// `_row_mut` instead of `self`.
3152fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
3153    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
3154        let col_lit = column.as_str();
3155        quote! {
3156            _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
3157                _returning_row,
3158                #col_lit,
3159            )?;
3160        }
3161    });
3162    quote! { #( #lines )* }
3163}
3164
3165/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
3166fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
3167    let lines = entries.iter().map(|e| {
3168        let ident = &e.ident;
3169        let col_ty = column_type_ident(ident);
3170        quote! {
3171            #[allow(non_upper_case_globals)]
3172            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
3173        }
3174    });
3175    quote! { #(#lines)* }
3176}
3177
3178/// Emit a hidden per-model module carrying one zero-sized type per field,
3179/// each with a `Column` impl pointing back at the model.
3180fn column_module_tokens(
3181    module_ident: &syn::Ident,
3182    struct_name: &syn::Ident,
3183    entries: &[ColumnEntry],
3184) -> TokenStream2 {
3185    let items = entries.iter().map(|e| {
3186        let col_ty = column_type_ident(&e.ident);
3187        let value_ty = &e.value_ty;
3188        let name = &e.name;
3189        let column = &e.column;
3190        let field_type_tokens = &e.field_type_tokens;
3191        quote! {
3192            #[derive(::core::clone::Clone, ::core::marker::Copy)]
3193            pub struct #col_ty;
3194
3195            impl ::rustango::core::Column for #col_ty {
3196                type Model = super::#struct_name;
3197                type Value = #value_ty;
3198                const NAME: &'static str = #name;
3199                const COLUMN: &'static str = #column;
3200                const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
3201            }
3202        }
3203    });
3204    quote! {
3205        #[doc(hidden)]
3206        #[allow(non_camel_case_types, non_snake_case)]
3207        pub mod #module_ident {
3208            // Re-import the parent scope so field types referencing
3209            // sibling models (e.g. `ForeignKey<Author>`) resolve
3210            // inside this submodule. Without this we'd hit
3211            // `proc_macro_derive_resolution_fallback` warnings.
3212            #[allow(unused_imports)]
3213            use super::*;
3214            #(#items)*
3215        }
3216    }
3217}
3218
3219fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
3220    syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
3221}
3222
3223fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
3224    syn::Ident::new(
3225        &format!("__rustango_cols_{struct_name}"),
3226        struct_name.span(),
3227    )
3228}
3229
3230fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
3231    // The Postgres impl is always emitted — every rustango build pulls in
3232    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
3233    // routed through `::rustango::__impl_my_from_row!`, a cfg-gated
3234    // macro_rules whose body collapses to nothing when rustango is built
3235    // without the `mysql` feature. No user-facing feature shim required.
3236    //
3237    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
3238    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
3239    // back into a comma-separated list inside square brackets. Since each
3240    // entry is already in `field: expr` shape, the existing tokens slot in.
3241    quote! {
3242        #[cfg(feature = "postgres")]
3243        impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
3244            for #struct_name
3245        {
3246            fn from_row(
3247                row: &'r ::rustango::sql::sqlx::postgres::PgRow,
3248            ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3249                ::core::result::Result::Ok(Self {
3250                    #( #from_row_inits ),*
3251                })
3252            }
3253        }
3254
3255        ::rustango::__impl_my_from_row!(#struct_name, |row| {
3256            #( #from_row_inits ),*
3257        });
3258
3259        ::rustango::__impl_sqlite_from_row!(#struct_name, |row| {
3260            #( #from_row_inits ),*
3261        });
3262    }
3263}
3264
3265struct ContainerAttrs {
3266    table: Option<String>,
3267    display: Option<(String, proc_macro2::Span)>,
3268    /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
3269    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
3270    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
3271    /// at runtime — this attribute is the override for cases where
3272    /// the inference is wrong (e.g. a model that conceptually belongs
3273    /// to one app but is physically in another module).
3274    app: Option<String>,
3275    /// Django ModelAdmin-shape per-model knobs from
3276    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
3277    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
3278    /// admin code falls back to `AdminConfig::DEFAULT`.
3279    admin: Option<AdminAttrs>,
3280    /// Per-model audit configuration from `#[rustango(audit(...))]`.
3281    /// `None` when the model isn't audited — write paths emit no
3282    /// audit entries. When present, single-row writes capture
3283    /// before/after for the listed fields and bulk writes batch
3284    /// snapshots into one INSERT into `rustango_audit_log`.
3285    audit: Option<AuditAttrs>,
3286    /// `true` when `#[rustango(permissions)]` is present. Signals that
3287    /// `auto_create_permissions` should seed the four CRUD codenames for
3288    /// this model.
3289    permissions: bool,
3290    /// Many-to-many relations declared via
3291    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
3292    ///                 src = "post_id", dst = "tag_id"))]`.
3293    m2m: Vec<M2MAttr>,
3294    /// Composite indexes declared via
3295    /// `#[rustango(index("col1, col2"))]` or
3296    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
3297    /// Single-column indexes from `#[rustango(index)]` on fields are
3298    /// accumulated here during field collection.
3299    indexes: Vec<IndexAttr>,
3300    /// Table-level CHECK constraints declared via
3301    /// `#[rustango(check(name = "…", expr = "…"))]`.
3302    checks: Vec<CheckAttr>,
3303    /// Composite (multi-column) FKs declared via
3304    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
3305    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
3306    composite_fks: Vec<CompositeFkAttr>,
3307    /// Generic ("any model") FKs declared via
3308    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
3309    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
3310    generic_fks: Vec<GenericFkAttr>,
3311    /// Where this model lives in a tenancy deployment, declared via
3312    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
3313    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
3314    /// to partition diff output between registry-scoped and
3315    /// tenant-scoped migration files.
3316    scope: Option<String>,
3317}
3318
3319/// Parsed form of one index declaration (field-level or container-level).
3320struct IndexAttr {
3321    /// Index name; auto-derived when `None` at parse time.
3322    name: Option<String>,
3323    /// Column names in the index.
3324    columns: Vec<String>,
3325    /// `true` for `CREATE UNIQUE INDEX`.
3326    unique: bool,
3327}
3328
3329/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
3330struct CheckAttr {
3331    name: String,
3332    expr: String,
3333}
3334
3335/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
3336/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
3337/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
3338/// the v0.15.0 ContentType plan — multi-column foreign keys live on
3339/// the model, not the field.
3340struct CompositeFkAttr {
3341    /// Logical relation name (free-form Rust identifier).
3342    name: String,
3343    /// SQL table name of the target.
3344    to: String,
3345    /// Source-side column names, in declaration order.
3346    from: Vec<String>,
3347    /// Target-side column names, same length / order as `from`.
3348    on: Vec<String>,
3349}
3350
3351/// Parsed form of one `#[rustango(generic_fk(name = "target",
3352/// ct_column = "content_type_id", pk_column = "object_pk"))]`
3353/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
3354/// generic ("any model") FKs live on the model, not the field.
3355struct GenericFkAttr {
3356    /// Logical relation name (free-form Rust identifier).
3357    name: String,
3358    /// Source-side column carrying the `content_type_id` value.
3359    ct_column: String,
3360    /// Source-side column carrying the target row's primary key.
3361    pk_column: String,
3362}
3363
3364/// Parsed form of one `#[rustango(m2m(...))]` declaration.
3365struct M2MAttr {
3366    /// Accessor suffix: `tags` → generates `tags_m2m()`.
3367    name: String,
3368    /// Target table (e.g. `"app_tags"`).
3369    to: String,
3370    /// Junction table (e.g. `"post_tags"`).
3371    through: String,
3372    /// Source FK column in the junction table (e.g. `"post_id"`).
3373    src: String,
3374    /// Destination FK column in the junction table (e.g. `"tag_id"`).
3375    dst: String,
3376}
3377
3378/// Parsed shape of `#[rustango(audit(track = "name, body", source =
3379/// "user"))]`. `track` is a comma-separated list of field names whose
3380/// before/after values land in the JSONB `changes` column. `source`
3381/// is informational only — it pins a default source when the model
3382/// is written outside any `audit::with_source(...)` scope (rare).
3383#[derive(Default)]
3384struct AuditAttrs {
3385    /// Field names to capture in the `changes` JSONB. Validated
3386    /// against declared scalar fields at compile time. Empty means
3387    /// "track every scalar field" — Django's audit-everything default.
3388    track: Option<(Vec<String>, proc_macro2::Span)>,
3389}
3390
3391/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
3392/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
3393/// comma-separated strings; we validate each ident against the model's
3394/// declared fields at compile time.
3395#[derive(Default)]
3396struct AdminAttrs {
3397    list_display: Option<(Vec<String>, proc_macro2::Span)>,
3398    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
3399    list_per_page: Option<usize>,
3400    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
3401    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
3402    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
3403    /// Bulk action names. No field-validation against model fields —
3404    /// these are action handlers, not column references.
3405    actions: Option<(Vec<String>, proc_macro2::Span)>,
3406    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
3407    /// sections, comma-separated fields per section, optional
3408    /// `Title:` prefix. Empty title omits the `<legend>`.
3409    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
3410}
3411
3412fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
3413    let mut out = ContainerAttrs {
3414        table: None,
3415        display: None,
3416        app: None,
3417        admin: None,
3418        audit: None,
3419        // Default `permissions = true` so every `#[derive(Model)]`
3420        // gets the four CRUD codenames seeded by `auto_create_permissions`
3421        // and is visible to non-superusers in the tenant admin without
3422        // manual per-model annotation. Models that intentionally don't
3423        // want permission rows (registry-internal types, framework
3424        // tables operators shouldn't manage directly) opt out via
3425        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
3426        // out-of-the-box admin invisibility regression (#62).
3427        permissions: true,
3428        m2m: Vec::new(),
3429        indexes: Vec::new(),
3430        checks: Vec::new(),
3431        composite_fks: Vec::new(),
3432        generic_fks: Vec::new(),
3433        scope: None,
3434    };
3435    for attr in &input.attrs {
3436        if !attr.path().is_ident("rustango") {
3437            continue;
3438        }
3439        attr.parse_nested_meta(|meta| {
3440            if meta.path.is_ident("table") {
3441                let s: LitStr = meta.value()?.parse()?;
3442                let name = s.value();
3443                // v0.27.3 (#65) — macro-time guard against table names
3444                // that compile but break SQL downstream. Hyphens are
3445                // the common footgun: PostgreSQL accepts a quoted
3446                // `"intermediate-region"` in CREATE TABLE, but the
3447                // FK / index name derivation in `migrate::ddl`
3448                // emits `intermediate-region_field_fkey` unquoted,
3449                // which then fails the SQL parser. Same shape rule
3450                // as Postgres regular identifiers so the safe path
3451                // is the only path.
3452                validate_table_name(&name, s.span())?;
3453                out.table = Some(name);
3454                return Ok(());
3455            }
3456            if meta.path.is_ident("display") {
3457                let s: LitStr = meta.value()?.parse()?;
3458                out.display = Some((s.value(), s.span()));
3459                return Ok(());
3460            }
3461            if meta.path.is_ident("app") {
3462                let s: LitStr = meta.value()?.parse()?;
3463                out.app = Some(s.value());
3464                return Ok(());
3465            }
3466            if meta.path.is_ident("scope") {
3467                let s: LitStr = meta.value()?.parse()?;
3468                let val = s.value();
3469                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
3470                    return Err(meta.error(format!(
3471                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
3472                    )));
3473                }
3474                out.scope = Some(val);
3475                return Ok(());
3476            }
3477            if meta.path.is_ident("admin") {
3478                let mut admin = AdminAttrs::default();
3479                meta.parse_nested_meta(|inner| {
3480                    if inner.path.is_ident("list_display") {
3481                        let s: LitStr = inner.value()?.parse()?;
3482                        admin.list_display =
3483                            Some((split_field_list(&s.value()), s.span()));
3484                        return Ok(());
3485                    }
3486                    if inner.path.is_ident("search_fields") {
3487                        let s: LitStr = inner.value()?.parse()?;
3488                        admin.search_fields =
3489                            Some((split_field_list(&s.value()), s.span()));
3490                        return Ok(());
3491                    }
3492                    if inner.path.is_ident("readonly_fields") {
3493                        let s: LitStr = inner.value()?.parse()?;
3494                        admin.readonly_fields =
3495                            Some((split_field_list(&s.value()), s.span()));
3496                        return Ok(());
3497                    }
3498                    if inner.path.is_ident("list_per_page") {
3499                        let lit: syn::LitInt = inner.value()?.parse()?;
3500                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
3501                        return Ok(());
3502                    }
3503                    if inner.path.is_ident("ordering") {
3504                        let s: LitStr = inner.value()?.parse()?;
3505                        admin.ordering = Some((
3506                            parse_ordering_list(&s.value()),
3507                            s.span(),
3508                        ));
3509                        return Ok(());
3510                    }
3511                    if inner.path.is_ident("list_filter") {
3512                        let s: LitStr = inner.value()?.parse()?;
3513                        admin.list_filter =
3514                            Some((split_field_list(&s.value()), s.span()));
3515                        return Ok(());
3516                    }
3517                    if inner.path.is_ident("actions") {
3518                        let s: LitStr = inner.value()?.parse()?;
3519                        admin.actions =
3520                            Some((split_field_list(&s.value()), s.span()));
3521                        return Ok(());
3522                    }
3523                    if inner.path.is_ident("fieldsets") {
3524                        let s: LitStr = inner.value()?.parse()?;
3525                        admin.fieldsets =
3526                            Some((parse_fieldset_list(&s.value()), s.span()));
3527                        return Ok(());
3528                    }
3529                    Err(inner.error(
3530                        "unknown admin attribute (supported: \
3531                         `list_display`, `search_fields`, `readonly_fields`, \
3532                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
3533                         `fieldsets`)",
3534                    ))
3535                })?;
3536                out.admin = Some(admin);
3537                return Ok(());
3538            }
3539            if meta.path.is_ident("audit") {
3540                let mut audit = AuditAttrs::default();
3541                meta.parse_nested_meta(|inner| {
3542                    if inner.path.is_ident("track") {
3543                        let s: LitStr = inner.value()?.parse()?;
3544                        audit.track =
3545                            Some((split_field_list(&s.value()), s.span()));
3546                        return Ok(());
3547                    }
3548                    Err(inner.error(
3549                        "unknown audit attribute (supported: `track`)",
3550                    ))
3551                })?;
3552                out.audit = Some(audit);
3553                return Ok(());
3554            }
3555            if meta.path.is_ident("permissions") {
3556                // Two forms accepted:
3557                //   #[rustango(permissions)]          — flag form, true
3558                //   #[rustango(permissions = false)]  — explicit opt-out
3559                //   #[rustango(permissions = true)]   — explicit opt-in
3560                if let Ok(v) = meta.value() {
3561                    let lit: syn::LitBool = v.parse()?;
3562                    out.permissions = lit.value;
3563                } else {
3564                    out.permissions = true;
3565                }
3566                return Ok(());
3567            }
3568            if meta.path.is_ident("unique_together") {
3569                // Django-shape composite UNIQUE index. Two syntaxes:
3570                //
3571                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
3572                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
3573                //
3574                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
3575                // (col1, col2)`, where <name> defaults to
3576                // `<table>_<col1>_<col2>_uq` when not supplied.
3577                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
3578                out.indexes.push(IndexAttr { name, columns, unique: true });
3579                return Ok(());
3580            }
3581            if meta.path.is_ident("index_together") {
3582                // Django-shape composite (non-unique) index. Two syntaxes
3583                // mirroring `unique_together`.
3584                //
3585                //   #[rustango(index_together = "created_at, status")]
3586                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
3587                let (columns, name) = parse_together_attr(&meta, "index_together")?;
3588                out.indexes.push(IndexAttr { name, columns, unique: false });
3589                return Ok(());
3590            }
3591            if meta.path.is_ident("index") {
3592                // Container-level composite index — legacy entry that
3593                // was advertised with a trailing `, unique, name = ...`
3594                // flag block which doesn't actually compose under
3595                // `parse_nested_meta`. Prefer `unique_together` /
3596                // `index_together` (above) for new code. The bare
3597                // `index = "..."` form is kept for back-compat: it
3598                // emits a non-unique composite index.
3599                let cols_lit: LitStr = meta.value()?.parse()?;
3600                let columns = split_field_list(&cols_lit.value());
3601                out.indexes.push(IndexAttr { name: None, columns, unique: false });
3602                return Ok(());
3603            }
3604            if meta.path.is_ident("check") {
3605                // #[rustango(check(name = "…", expr = "…"))]
3606                let mut name: Option<String> = None;
3607                let mut expr: Option<String> = None;
3608                meta.parse_nested_meta(|inner| {
3609                    if inner.path.is_ident("name") {
3610                        let s: LitStr = inner.value()?.parse()?;
3611                        name = Some(s.value());
3612                        return Ok(());
3613                    }
3614                    if inner.path.is_ident("expr") {
3615                        let s: LitStr = inner.value()?.parse()?;
3616                        expr = Some(s.value());
3617                        return Ok(());
3618                    }
3619                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
3620                })?;
3621                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
3622                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
3623                out.checks.push(CheckAttr { name, expr });
3624                return Ok(());
3625            }
3626            if meta.path.is_ident("generic_fk") {
3627                let mut gfk = GenericFkAttr {
3628                    name: String::new(),
3629                    ct_column: String::new(),
3630                    pk_column: String::new(),
3631                };
3632                meta.parse_nested_meta(|inner| {
3633                    if inner.path.is_ident("name") {
3634                        let s: LitStr = inner.value()?.parse()?;
3635                        gfk.name = s.value();
3636                        return Ok(());
3637                    }
3638                    if inner.path.is_ident("ct_column") {
3639                        let s: LitStr = inner.value()?.parse()?;
3640                        gfk.ct_column = s.value();
3641                        return Ok(());
3642                    }
3643                    if inner.path.is_ident("pk_column") {
3644                        let s: LitStr = inner.value()?.parse()?;
3645                        gfk.pk_column = s.value();
3646                        return Ok(());
3647                    }
3648                    Err(inner.error(
3649                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
3650                    ))
3651                })?;
3652                if gfk.name.is_empty() {
3653                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
3654                }
3655                if gfk.ct_column.is_empty() {
3656                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
3657                }
3658                if gfk.pk_column.is_empty() {
3659                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
3660                }
3661                out.generic_fks.push(gfk);
3662                return Ok(());
3663            }
3664            if meta.path.is_ident("fk_composite") {
3665                let mut fk = CompositeFkAttr {
3666                    name: String::new(),
3667                    to: String::new(),
3668                    from: Vec::new(),
3669                    on: Vec::new(),
3670                };
3671                meta.parse_nested_meta(|inner| {
3672                    if inner.path.is_ident("name") {
3673                        let s: LitStr = inner.value()?.parse()?;
3674                        fk.name = s.value();
3675                        return Ok(());
3676                    }
3677                    if inner.path.is_ident("to") {
3678                        let s: LitStr = inner.value()?.parse()?;
3679                        fk.to = s.value();
3680                        return Ok(());
3681                    }
3682                    // `on = ("col1", "col2", ...)` — parse a parenthesised
3683                    // comma-list of string literals.
3684                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
3685                        let value = inner.value()?;
3686                        let content;
3687                        syn::parenthesized!(content in value);
3688                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
3689                            content.parse_terminated(
3690                                |p| p.parse::<syn::LitStr>(),
3691                                syn::Token![,],
3692                            )?;
3693                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
3694                        if inner.path.is_ident("on") {
3695                            fk.on = cols;
3696                        } else {
3697                            fk.from = cols;
3698                        }
3699                        return Ok(());
3700                    }
3701                    Err(inner.error(
3702                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
3703                    ))
3704                })?;
3705                if fk.name.is_empty() {
3706                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
3707                }
3708                if fk.to.is_empty() {
3709                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
3710                }
3711                if fk.from.is_empty() || fk.on.is_empty() {
3712                    return Err(meta.error(
3713                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
3714                    ));
3715                }
3716                if fk.from.len() != fk.on.len() {
3717                    return Err(meta.error(format!(
3718                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
3719                        fk.from.len(),
3720                        fk.on.len(),
3721                    )));
3722                }
3723                out.composite_fks.push(fk);
3724                return Ok(());
3725            }
3726            if meta.path.is_ident("m2m") {
3727                let mut m2m = M2MAttr {
3728                    name: String::new(),
3729                    to: String::new(),
3730                    through: String::new(),
3731                    src: String::new(),
3732                    dst: String::new(),
3733                };
3734                meta.parse_nested_meta(|inner| {
3735                    if inner.path.is_ident("name") {
3736                        let s: LitStr = inner.value()?.parse()?;
3737                        m2m.name = s.value();
3738                        return Ok(());
3739                    }
3740                    if inner.path.is_ident("to") {
3741                        let s: LitStr = inner.value()?.parse()?;
3742                        m2m.to = s.value();
3743                        return Ok(());
3744                    }
3745                    if inner.path.is_ident("through") {
3746                        let s: LitStr = inner.value()?.parse()?;
3747                        m2m.through = s.value();
3748                        return Ok(());
3749                    }
3750                    if inner.path.is_ident("src") {
3751                        let s: LitStr = inner.value()?.parse()?;
3752                        m2m.src = s.value();
3753                        return Ok(());
3754                    }
3755                    if inner.path.is_ident("dst") {
3756                        let s: LitStr = inner.value()?.parse()?;
3757                        m2m.dst = s.value();
3758                        return Ok(());
3759                    }
3760                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
3761                })?;
3762                if m2m.name.is_empty() {
3763                    return Err(meta.error("m2m requires `name = \"...\"`"));
3764                }
3765                if m2m.to.is_empty() {
3766                    return Err(meta.error("m2m requires `to = \"...\"`"));
3767                }
3768                if m2m.through.is_empty() {
3769                    return Err(meta.error("m2m requires `through = \"...\"`"));
3770                }
3771                if m2m.src.is_empty() {
3772                    return Err(meta.error("m2m requires `src = \"...\"`"));
3773                }
3774                if m2m.dst.is_empty() {
3775                    return Err(meta.error("m2m requires `dst = \"...\"`"));
3776                }
3777                out.m2m.push(m2m);
3778                return Ok(());
3779            }
3780            Err(meta.error("unknown rustango container attribute"))
3781        })?;
3782    }
3783    Ok(out)
3784}
3785
3786/// Split a comma-separated field-name list (e.g. `"name, office"`) into
3787/// owned field names, trimming whitespace and skipping empty entries.
3788/// Field-name validation against the model is done by the caller.
3789fn split_field_list(raw: &str) -> Vec<String> {
3790    raw.split(',')
3791        .map(str::trim)
3792        .filter(|s| !s.is_empty())
3793        .map(str::to_owned)
3794        .collect()
3795}
3796
3797/// Shared parser for `unique_together` and `index_together` container
3798/// attrs. Accepts both shapes:
3799///
3800///   * `attr = "col1, col2"`              — auto-derived index name.
3801///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
3802///
3803/// Returns `(columns, name)`.
3804fn parse_together_attr(
3805    meta: &syn::meta::ParseNestedMeta<'_>,
3806    attr: &str,
3807) -> syn::Result<(Vec<String>, Option<String>)> {
3808    // Disambiguate by whether the next token is `=` (key-value) or
3809    // `(` (parenthesized).
3810    if meta.input.peek(syn::Token![=]) {
3811        let cols_lit: LitStr = meta.value()?.parse()?;
3812        let columns = split_field_list(&cols_lit.value());
3813        check_together_columns(meta, attr, &columns)?;
3814        return Ok((columns, None));
3815    }
3816    let mut columns: Option<Vec<String>> = None;
3817    let mut name: Option<String> = None;
3818    meta.parse_nested_meta(|inner| {
3819        if inner.path.is_ident("columns") {
3820            let s: LitStr = inner.value()?.parse()?;
3821            columns = Some(split_field_list(&s.value()));
3822            return Ok(());
3823        }
3824        if inner.path.is_ident("name") {
3825            let s: LitStr = inner.value()?.parse()?;
3826            name = Some(s.value());
3827            return Ok(());
3828        }
3829        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
3830    })?;
3831    let columns = columns.ok_or_else(|| {
3832        meta.error(format!(
3833            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
3834        ))
3835    })?;
3836    check_together_columns(meta, attr, &columns)?;
3837    Ok((columns, name))
3838}
3839
3840fn check_together_columns(
3841    meta: &syn::meta::ParseNestedMeta<'_>,
3842    attr: &str,
3843    columns: &[String],
3844) -> syn::Result<()> {
3845    if columns.len() < 2 {
3846        let single = if attr == "unique_together" {
3847            "#[rustango(unique)] on the field"
3848        } else {
3849            "#[rustango(index)] on the field"
3850        };
3851        return Err(meta.error(format!(
3852            "{attr} expects two or more columns; for a single-column equivalent use {single}",
3853        )));
3854    }
3855    Ok(())
3856}
3857
3858/// Parse the fieldsets DSL: pipe-separated sections, optional
3859/// `"Title:"` prefix on each, comma-separated field names after.
3860/// Examples:
3861/// * `"name, office"` → one untitled section with two fields
3862/// * `"Identity: name, office | Metadata: created_at"` → two titled
3863///   sections
3864///
3865/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
3866fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
3867    raw.split('|')
3868        .map(str::trim)
3869        .filter(|s| !s.is_empty())
3870        .map(|section| {
3871            // Split off an optional `Title:` prefix (first colon).
3872            let (title, rest) = match section.split_once(':') {
3873                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
3874                _ => (String::new(), section),
3875            };
3876            let fields = split_field_list(rest);
3877            (title, fields)
3878        })
3879        .collect()
3880}
3881
3882/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
3883/// Returns `(field_name, desc)` pairs in the same order as the input.
3884fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
3885    raw.split(',')
3886        .map(str::trim)
3887        .filter(|s| !s.is_empty())
3888        .map(|spec| {
3889            spec.strip_prefix('-')
3890                .map_or((spec.to_owned(), false), |rest| {
3891                    (rest.trim().to_owned(), true)
3892                })
3893        })
3894        .collect()
3895}
3896
3897struct FieldAttrs {
3898    column: Option<String>,
3899    primary_key: bool,
3900    fk: Option<String>,
3901    o2o: Option<String>,
3902    on: Option<String>,
3903    max_length: Option<u32>,
3904    min: Option<i64>,
3905    max: Option<i64>,
3906    default: Option<String>,
3907    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
3908    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
3909    /// "gen_random_uuid()"`. The Rust field type must be
3910    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
3911    /// INSERTs so the DB DEFAULT fires.
3912    auto_uuid: bool,
3913    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
3914    /// Server-set on insert, immutable from app code afterwards.
3915    /// Implies `auto + default = "now()"`. Field type must be
3916    /// `DateTime<Utc>`.
3917    auto_now_add: bool,
3918    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
3919    /// every insert AND every update. Implies `auto + default =
3920    /// "now()"`; the macro additionally rewrites `update_on` /
3921    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
3922    /// field value.
3923    auto_now: bool,
3924    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
3925    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
3926    /// `soft_delete_on(executor)` and `restore_on(executor)`
3927    /// methods on the model.
3928    soft_delete: bool,
3929    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
3930    /// the column in the generated DDL.
3931    unique: bool,
3932    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
3933    /// generates a `CREATE INDEX` for this column. `unique` here means
3934    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
3935    index: bool,
3936    index_unique: bool,
3937    index_name: Option<String>,
3938    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
3939    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
3940    /// the macro skips this column from every INSERT and UPDATE
3941    /// path, so the database always recomputes the value from
3942    /// `EXPR`. Backlog item #35.
3943    generated_as: Option<String>,
3944}
3945
3946fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
3947    let mut out = FieldAttrs {
3948        column: None,
3949        primary_key: false,
3950        fk: None,
3951        o2o: None,
3952        on: None,
3953        max_length: None,
3954        min: None,
3955        max: None,
3956        default: None,
3957        auto_uuid: false,
3958        auto_now_add: false,
3959        auto_now: false,
3960        soft_delete: false,
3961        unique: false,
3962        index: false,
3963        index_unique: false,
3964        index_name: None,
3965        generated_as: None,
3966    };
3967    for attr in &field.attrs {
3968        if !attr.path().is_ident("rustango") {
3969            continue;
3970        }
3971        attr.parse_nested_meta(|meta| {
3972            if meta.path.is_ident("column") {
3973                let s: LitStr = meta.value()?.parse()?;
3974                let name = s.value();
3975                validate_sql_identifier(&name, "column", s.span())?;
3976                out.column = Some(name);
3977                return Ok(());
3978            }
3979            if meta.path.is_ident("primary_key") {
3980                out.primary_key = true;
3981                return Ok(());
3982            }
3983            if meta.path.is_ident("fk") {
3984                let s: LitStr = meta.value()?.parse()?;
3985                out.fk = Some(s.value());
3986                return Ok(());
3987            }
3988            if meta.path.is_ident("o2o") {
3989                let s: LitStr = meta.value()?.parse()?;
3990                out.o2o = Some(s.value());
3991                return Ok(());
3992            }
3993            if meta.path.is_ident("on") {
3994                let s: LitStr = meta.value()?.parse()?;
3995                out.on = Some(s.value());
3996                return Ok(());
3997            }
3998            if meta.path.is_ident("max_length") {
3999                let lit: syn::LitInt = meta.value()?.parse()?;
4000                out.max_length = Some(lit.base10_parse::<u32>()?);
4001                return Ok(());
4002            }
4003            if meta.path.is_ident("min") {
4004                out.min = Some(parse_signed_i64(&meta)?);
4005                return Ok(());
4006            }
4007            if meta.path.is_ident("max") {
4008                out.max = Some(parse_signed_i64(&meta)?);
4009                return Ok(());
4010            }
4011            if meta.path.is_ident("default") {
4012                let s: LitStr = meta.value()?.parse()?;
4013                out.default = Some(s.value());
4014                return Ok(());
4015            }
4016            if meta.path.is_ident("generated_as") {
4017                let s: LitStr = meta.value()?.parse()?;
4018                out.generated_as = Some(s.value());
4019                return Ok(());
4020            }
4021            if meta.path.is_ident("auto_uuid") {
4022                out.auto_uuid = true;
4023                // Implied: PK + auto + DEFAULT gen_random_uuid().
4024                // Each is also explicitly settable; the explicit
4025                // value wins if conflicting.
4026                out.primary_key = true;
4027                if out.default.is_none() {
4028                    out.default = Some("gen_random_uuid()".into());
4029                }
4030                return Ok(());
4031            }
4032            if meta.path.is_ident("auto_now_add") {
4033                out.auto_now_add = true;
4034                if out.default.is_none() {
4035                    out.default = Some("now()".into());
4036                }
4037                return Ok(());
4038            }
4039            if meta.path.is_ident("auto_now") {
4040                out.auto_now = true;
4041                if out.default.is_none() {
4042                    out.default = Some("now()".into());
4043                }
4044                return Ok(());
4045            }
4046            if meta.path.is_ident("soft_delete") {
4047                out.soft_delete = true;
4048                return Ok(());
4049            }
4050            if meta.path.is_ident("unique") {
4051                out.unique = true;
4052                return Ok(());
4053            }
4054            if meta.path.is_ident("index") {
4055                out.index = true;
4056                // Optional sub-attrs: #[rustango(index(unique, name = "…"))]
4057                if meta.input.peek(syn::token::Paren) {
4058                    meta.parse_nested_meta(|inner| {
4059                        if inner.path.is_ident("unique") {
4060                            out.index_unique = true;
4061                            return Ok(());
4062                        }
4063                        if inner.path.is_ident("name") {
4064                            let s: LitStr = inner.value()?.parse()?;
4065                            out.index_name = Some(s.value());
4066                            return Ok(());
4067                        }
4068                        Err(inner
4069                            .error("unknown index sub-attribute (supported: `unique`, `name`)"))
4070                    })?;
4071                }
4072                return Ok(());
4073            }
4074            Err(meta.error("unknown rustango field attribute"))
4075        })?;
4076    }
4077    Ok(out)
4078}
4079
4080/// Parse a signed integer literal, accepting optional leading `-`.
4081fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
4082    let expr: syn::Expr = meta.value()?.parse()?;
4083    match expr {
4084        syn::Expr::Lit(syn::ExprLit {
4085            lit: syn::Lit::Int(lit),
4086            ..
4087        }) => lit.base10_parse::<i64>(),
4088        syn::Expr::Unary(syn::ExprUnary {
4089            op: syn::UnOp::Neg(_),
4090            expr,
4091            ..
4092        }) => {
4093            if let syn::Expr::Lit(syn::ExprLit {
4094                lit: syn::Lit::Int(lit),
4095                ..
4096            }) = *expr
4097            {
4098                let v: i64 = lit.base10_parse()?;
4099                Ok(-v)
4100            } else {
4101                Err(syn::Error::new_spanned(expr, "expected integer literal"))
4102            }
4103        }
4104        other => Err(syn::Error::new_spanned(
4105            other,
4106            "expected integer literal (signed)",
4107        )),
4108    }
4109}
4110
4111struct FieldInfo<'a> {
4112    ident: &'a syn::Ident,
4113    column: String,
4114    primary_key: bool,
4115    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
4116    /// skip this column when `Auto::Unset` and emit it under
4117    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
4118    auto: bool,
4119    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
4120    /// the `Column::Value` associated type for typed-column tokens.
4121    value_ty: &'a Type,
4122    /// `FieldType` variant tokens (`::rustango::core::FieldType::I64`).
4123    field_type_tokens: TokenStream2,
4124    schema: TokenStream2,
4125    from_row_init: TokenStream2,
4126    /// Variant of [`Self::from_row_init`] that reads the column via
4127    /// `format!("{prefix}__{col}")` so a model can be decoded out of
4128    /// the aliased columns of a JOINed row. Drives slice 9.0d's
4129    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
4130    /// helper that `select_related` calls when stitching loaded FKs.
4131    from_aliased_row_init: TokenStream2,
4132    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
4133    /// relation helper emit (`Author::<child>_set`) needs to know `T`
4134    /// to point the generated method at the right child model.
4135    fk_inner: Option<Type>,
4136    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
4137    /// `kind` (since ForeignKey detection sets `kind` to K's
4138    /// underlying type) but stored separately for clarity at the
4139    /// `FkRelation` construction site, which only sees the FK's
4140    /// surface fields.
4141    fk_pk_kind: DetectedKind,
4142    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
4143    /// the bare `ForeignKey<T, K>`. Routes the load_related and
4144    /// fk_pk_access emitters to wrap assignments / accessors in
4145    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
4146    /// FK column compiles end-to-end. The DDL writer reads this off
4147    /// the field schema (`nullable` flag); the macro just needs to
4148    /// keep the Rust-side codegen consistent.
4149    nullable: bool,
4150    /// `true` when this column was marked `#[rustango(auto_now)]` —
4151    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
4152    /// column instead of the user-supplied value, so `updated_at`
4153    /// always reflects the latest write without the caller having
4154    /// to remember to set it.
4155    auto_now: bool,
4156    /// `true` when this column was marked `#[rustango(auto_now_add)]`
4157    /// — the column is server-set on INSERT (DB DEFAULT) and
4158    /// **immutable** afterwards. `update_on` / `save_on` skip the
4159    /// column entirely so a stale `created_at` value in memory never
4160    /// rewrites the persisted timestamp.
4161    auto_now_add: bool,
4162    /// `true` when this column was marked `#[rustango(soft_delete)]`.
4163    /// Triggers emission of `soft_delete_on(executor)` and
4164    /// `restore_on(executor)` on the model's inherent impl. There is
4165    /// at most one such column per model — emission asserts this.
4166    soft_delete: bool,
4167    /// `Some` when this column was marked
4168    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
4169    /// every INSERT and UPDATE path; the database recomputes the
4170    /// value from `EXPR`. Backlog item #35.
4171    generated_as: Option<String>,
4172}
4173
4174/// Reject table names that won't survive SQL identifier
4175/// derivation downstream. Postgres' regular-identifier rule
4176/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
4177/// through the framework's unquoted FK / index / constraint name
4178/// emission without surprises. We also disallow leading-digit and
4179/// the empty string for clarity.
4180///
4181/// Reserved-word collisions (`select`, `from`, …) aren't flagged
4182/// here — those produce a runtime error from the SQL parser,
4183/// which is loud enough; statically enumerating reserved words
4184/// across the three supported dialects is more friction than help.
4185///
4186/// Backlog item #65.
4187fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
4188    validate_sql_identifier(name, "table", span)
4189}
4190
4191/// Reject SQL identifiers that compile but break downstream SQL
4192/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
4193/// `kind` is "table" / "column" — used for the error message so users
4194/// see which attribute caused the failure.
4195fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
4196    if name.is_empty() {
4197        return Err(syn::Error::new(
4198            span,
4199            format!("`{kind} = \"\"` is not a valid SQL identifier"),
4200        ));
4201    }
4202    let mut chars = name.chars();
4203    let first = chars.next().unwrap();
4204    if !(first.is_ascii_alphabetic() || first == '_') {
4205        return Err(syn::Error::new(
4206            span,
4207            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
4208        ));
4209    }
4210    for c in chars {
4211        if !(c.is_ascii_alphanumeric() || c == '_') {
4212            return Err(syn::Error::new(
4213                span,
4214                format!(
4215                    "{kind} name `{name}` contains invalid character {c:?} — \
4216                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
4217                     Hyphens in particular break FK / index name derivation \
4218                     downstream; use underscores instead (e.g. `{}`)",
4219                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
4220                ),
4221            ));
4222        }
4223    }
4224    Ok(())
4225}
4226
4227fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
4228    let attrs = parse_field_attrs(field)?;
4229    let ident = field
4230        .ident
4231        .as_ref()
4232        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4233    let name = ident.to_string();
4234    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
4235    let primary_key = attrs.primary_key;
4236    let DetectedType {
4237        kind,
4238        nullable,
4239        auto: detected_auto,
4240        fk_inner,
4241    } = detect_type(&field.ty)?;
4242    check_bound_compatibility(field, &attrs, kind)?;
4243    let auto = detected_auto;
4244    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
4245    // INSERT path: the user must wrap the field in `Auto<T>`, which
4246    // marks the column as DB-default-supplied. The mixin attrs then
4247    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
4248    // for `auto_now`, force the value on UPDATE too.
4249    if attrs.auto_uuid {
4250        if kind != DetectedKind::Uuid {
4251            return Err(syn::Error::new_spanned(
4252                field,
4253                "`#[rustango(auto_uuid)]` requires the field type to be \
4254                 `Auto<uuid::Uuid>`",
4255            ));
4256        }
4257        if !detected_auto {
4258            return Err(syn::Error::new_spanned(
4259                field,
4260                "`#[rustango(auto_uuid)]` requires the field type to be \
4261                 wrapped in `Auto<...>` so the macro skips the column on \
4262                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
4263            ));
4264        }
4265    }
4266    if attrs.auto_now_add || attrs.auto_now {
4267        if kind != DetectedKind::DateTime {
4268            return Err(syn::Error::new_spanned(
4269                field,
4270                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4271                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
4272            ));
4273        }
4274        if !detected_auto {
4275            return Err(syn::Error::new_spanned(
4276                field,
4277                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4278                 the field type to be wrapped in `Auto<...>` so the macro skips \
4279                 the column on INSERT and the DB DEFAULT (`now()`) fires",
4280            ));
4281        }
4282    }
4283    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
4284        return Err(syn::Error::new_spanned(
4285            field,
4286            "`#[rustango(soft_delete)]` requires the field type to be \
4287             `Option<chrono::DateTime<chrono::Utc>>`",
4288        ));
4289    }
4290    let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
4291    if detected_auto && !primary_key && !is_mixin_auto {
4292        return Err(syn::Error::new_spanned(
4293            field,
4294            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
4295             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
4296             `auto_now`",
4297        ));
4298    }
4299    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
4300        return Err(syn::Error::new_spanned(
4301            field,
4302            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
4303             SERIAL / BIGSERIAL already supplies a default sequence.",
4304        ));
4305    }
4306    if fk_inner.is_some() && primary_key {
4307        return Err(syn::Error::new_spanned(
4308            field,
4309            "`ForeignKey<T>` is not allowed on a primary-key field — \
4310             a row's PK is its own identity, not a reference to a parent.",
4311        ));
4312    }
4313    if attrs.generated_as.is_some() {
4314        if primary_key {
4315            return Err(syn::Error::new_spanned(
4316                field,
4317                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4318                 primary-key field — a PK must be writable so the row \
4319                 has an identity at INSERT time.",
4320            ));
4321        }
4322        if attrs.default.is_some() {
4323            return Err(syn::Error::new_spanned(
4324                field,
4325                "`#[rustango(generated_as = \"…\")]` cannot combine with \
4326                 `default = \"…\"` — Postgres rejects DEFAULT on \
4327                 generated columns. The expression IS the default.",
4328            ));
4329        }
4330        if detected_auto {
4331            return Err(syn::Error::new_spanned(
4332                field,
4333                "`#[rustango(generated_as = \"…\")]` is not allowed on \
4334                 an `Auto<T>` field — generated columns are computed \
4335                 by the DB, not server-assigned via a sequence. Use a \
4336                 plain Rust type (e.g. `f64`).",
4337            ));
4338        }
4339        if fk_inner.is_some() {
4340            return Err(syn::Error::new_spanned(
4341                field,
4342                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4343                 ForeignKey field.",
4344            ));
4345        }
4346    }
4347    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
4348    let column_lit = column.as_str();
4349    let field_type_tokens = kind.variant_tokens();
4350    let max_length = optional_u32(attrs.max_length);
4351    let min = optional_i64(attrs.min);
4352    let max = optional_i64(attrs.max);
4353    let default = optional_str(attrs.default.as_deref());
4354
4355    let unique = attrs.unique;
4356    let generated_as = optional_str(attrs.generated_as.as_deref());
4357    let schema = quote! {
4358        ::rustango::core::FieldSchema {
4359            name: #name,
4360            column: #column_lit,
4361            ty: #field_type_tokens,
4362            nullable: #nullable,
4363            primary_key: #primary_key,
4364            relation: #relation,
4365            max_length: #max_length,
4366            min: #min,
4367            max: #max,
4368            default: #default,
4369            auto: #auto,
4370            unique: #unique,
4371            generated_as: #generated_as,
4372        }
4373    };
4374
4375    let from_row_init = quote! {
4376        #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
4377    };
4378    let from_aliased_row_init = quote! {
4379        #ident: ::rustango::sql::sqlx::Row::try_get(
4380            row,
4381            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
4382        )?
4383    };
4384
4385    Ok(FieldInfo {
4386        ident,
4387        column,
4388        primary_key,
4389        auto,
4390        value_ty: &field.ty,
4391        field_type_tokens,
4392        schema,
4393        from_row_init,
4394        from_aliased_row_init,
4395        fk_inner: fk_inner.cloned(),
4396        fk_pk_kind: kind,
4397        nullable,
4398        auto_now: attrs.auto_now,
4399        auto_now_add: attrs.auto_now_add,
4400        soft_delete: attrs.soft_delete,
4401        generated_as: attrs.generated_as.clone(),
4402    })
4403}
4404
4405fn check_bound_compatibility(
4406    field: &syn::Field,
4407    attrs: &FieldAttrs,
4408    kind: DetectedKind,
4409) -> syn::Result<()> {
4410    if attrs.max_length.is_some() && kind != DetectedKind::String {
4411        return Err(syn::Error::new_spanned(
4412            field,
4413            "`max_length` is only valid on `String` fields (or `Option<String>`)",
4414        ));
4415    }
4416    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
4417        return Err(syn::Error::new_spanned(
4418            field,
4419            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
4420        ));
4421    }
4422    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
4423        if min > max {
4424            return Err(syn::Error::new_spanned(
4425                field,
4426                format!("`min` ({min}) is greater than `max` ({max})"),
4427            ));
4428        }
4429    }
4430    Ok(())
4431}
4432
4433fn optional_u32(value: Option<u32>) -> TokenStream2 {
4434    if let Some(v) = value {
4435        quote!(::core::option::Option::Some(#v))
4436    } else {
4437        quote!(::core::option::Option::None)
4438    }
4439}
4440
4441fn optional_i64(value: Option<i64>) -> TokenStream2 {
4442    if let Some(v) = value {
4443        quote!(::core::option::Option::Some(#v))
4444    } else {
4445        quote!(::core::option::Option::None)
4446    }
4447}
4448
4449fn optional_str(value: Option<&str>) -> TokenStream2 {
4450    if let Some(v) = value {
4451        quote!(::core::option::Option::Some(#v))
4452    } else {
4453        quote!(::core::option::Option::None)
4454    }
4455}
4456
4457fn relation_tokens(
4458    field: &syn::Field,
4459    attrs: &FieldAttrs,
4460    fk_inner: Option<&syn::Type>,
4461    table: &str,
4462) -> syn::Result<TokenStream2> {
4463    if let Some(inner) = fk_inner {
4464        if attrs.fk.is_some() || attrs.o2o.is_some() {
4465            return Err(syn::Error::new_spanned(
4466                field,
4467                "`ForeignKey<T>` already declares the FK target via the type parameter — \
4468                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
4469            ));
4470        }
4471        let on = attrs.on.as_deref().unwrap_or("id");
4472        return Ok(quote! {
4473            ::core::option::Option::Some(::rustango::core::Relation::Fk {
4474                to: <#inner as ::rustango::core::Model>::SCHEMA.table,
4475                on: #on,
4476            })
4477        });
4478    }
4479    match (&attrs.fk, &attrs.o2o) {
4480        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
4481            field,
4482            "`fk` and `o2o` are mutually exclusive",
4483        )),
4484        (Some(to), None) => {
4485            let on = attrs.on.as_deref().unwrap_or("id");
4486            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
4487            // the model's own table. Threaded as a literal string at
4488            // macro-expansion time to sidestep the const-eval cycle
4489            // that `Self::SCHEMA.table` would create when referenced
4490            // inside Self::SCHEMA's own initializer.
4491            let resolved = if to == "self" { table } else { to };
4492            Ok(quote! {
4493                ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #resolved, on: #on })
4494            })
4495        }
4496        (None, Some(to)) => {
4497            let on = attrs.on.as_deref().unwrap_or("id");
4498            let resolved = if to == "self" { table } else { to };
4499            Ok(quote! {
4500                ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #resolved, on: #on })
4501            })
4502        }
4503        (None, None) => {
4504            if attrs.on.is_some() {
4505                return Err(syn::Error::new_spanned(
4506                    field,
4507                    "`on` requires `fk` or `o2o`",
4508                ));
4509            }
4510            Ok(quote!(::core::option::Option::None))
4511        }
4512    }
4513}
4514
4515/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
4516/// about kinds without depending on `rustango-core` (which would require a
4517/// proc-macro/normal split it doesn't have today).
4518#[derive(Clone, Copy, PartialEq, Eq)]
4519enum DetectedKind {
4520    I16,
4521    I32,
4522    I64,
4523    F32,
4524    F64,
4525    Bool,
4526    String,
4527    DateTime,
4528    Date,
4529    Uuid,
4530    Json,
4531}
4532
4533impl DetectedKind {
4534    fn variant_tokens(self) -> TokenStream2 {
4535        match self {
4536            Self::I16 => quote!(::rustango::core::FieldType::I16),
4537            Self::I32 => quote!(::rustango::core::FieldType::I32),
4538            Self::I64 => quote!(::rustango::core::FieldType::I64),
4539            Self::F32 => quote!(::rustango::core::FieldType::F32),
4540            Self::F64 => quote!(::rustango::core::FieldType::F64),
4541            Self::Bool => quote!(::rustango::core::FieldType::Bool),
4542            Self::String => quote!(::rustango::core::FieldType::String),
4543            Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
4544            Self::Date => quote!(::rustango::core::FieldType::Date),
4545            Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
4546            Self::Json => quote!(::rustango::core::FieldType::Json),
4547        }
4548    }
4549
4550    fn is_integer(self) -> bool {
4551        matches!(self, Self::I16 | Self::I32 | Self::I64)
4552    }
4553
4554    /// `(SqlValue::<Variant>, default expr)` for emitting the
4555    /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
4556    /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
4557    /// fires only when the parent's `__rustango_pk_value` returns a
4558    /// different variant than expected, which is a compile-time bug —
4559    /// but we still need a value-typed fallback to keep the match
4560    /// total.
4561    fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
4562        match self {
4563            Self::I16 => (quote!(I16), quote!(0i16)),
4564            Self::I32 => (quote!(I32), quote!(0i32)),
4565            Self::I64 => (quote!(I64), quote!(0i64)),
4566            Self::F32 => (quote!(F32), quote!(0f32)),
4567            Self::F64 => (quote!(F64), quote!(0f64)),
4568            Self::Bool => (quote!(Bool), quote!(false)),
4569            Self::String => (quote!(String), quote!(::std::string::String::new())),
4570            Self::DateTime => (
4571                quote!(DateTime),
4572                quote!(<::chrono::DateTime<::chrono::Utc> as ::std::default::Default>::default()),
4573            ),
4574            Self::Date => (
4575                quote!(Date),
4576                quote!(<::chrono::NaiveDate as ::std::default::Default>::default()),
4577            ),
4578            Self::Uuid => (quote!(Uuid), quote!(::uuid::Uuid::nil())),
4579            Self::Json => (quote!(Json), quote!(::serde_json::Value::Null)),
4580        }
4581    }
4582}
4583
4584/// Result of walking a field's Rust type. `kind` is the underlying
4585/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
4586/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
4587/// `Some(<T>)` when the field was `ForeignKey<T>` (or
4588/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
4589#[derive(Clone, Copy)]
4590struct DetectedType<'a> {
4591    kind: DetectedKind,
4592    nullable: bool,
4593    auto: bool,
4594    fk_inner: Option<&'a syn::Type>,
4595}
4596
4597/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
4598/// non-`Auto` types — the caller should already have routed Auto-only
4599/// codegen through this helper, so a `None` indicates a macro-internal
4600/// invariant break.
4601fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
4602    let Type::Path(TypePath { path, qself: None }) = ty else {
4603        return None;
4604    };
4605    let last = path.segments.last()?;
4606    if last.ident != "Auto" {
4607        return None;
4608    }
4609    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
4610        return None;
4611    };
4612    args.args.iter().find_map(|a| match a {
4613        syn::GenericArgument::Type(t) => Some(t),
4614        _ => None,
4615    })
4616}
4617
4618fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
4619    let Type::Path(TypePath { path, qself: None }) = ty else {
4620        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
4621    };
4622    let last = path
4623        .segments
4624        .last()
4625        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
4626
4627    if last.ident == "Option" {
4628        let inner = generic_inner(ty, &last.arguments, "Option")?;
4629        let inner_det = detect_type(inner)?;
4630        if inner_det.nullable {
4631            return Err(syn::Error::new_spanned(
4632                ty,
4633                "nested Option is not supported",
4634            ));
4635        }
4636        if inner_det.auto {
4637            return Err(syn::Error::new_spanned(
4638                ty,
4639                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4640            ));
4641        }
4642        return Ok(DetectedType {
4643            nullable: true,
4644            ..inner_det
4645        });
4646    }
4647
4648    if last.ident == "Auto" {
4649        let inner = generic_inner(ty, &last.arguments, "Auto")?;
4650        let inner_det = detect_type(inner)?;
4651        if inner_det.auto {
4652            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
4653        }
4654        if inner_det.nullable {
4655            return Err(syn::Error::new_spanned(
4656                ty,
4657                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4658            ));
4659        }
4660        if inner_det.fk_inner.is_some() {
4661            return Err(syn::Error::new_spanned(
4662                ty,
4663                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
4664            ));
4665        }
4666        if !matches!(
4667            inner_det.kind,
4668            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
4669        ) {
4670            return Err(syn::Error::new_spanned(
4671                ty,
4672                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
4673                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
4674                 (DEFAULT now())",
4675            ));
4676        }
4677        return Ok(DetectedType {
4678            auto: true,
4679            ..inner_det
4680        });
4681    }
4682
4683    if last.ident == "ForeignKey" {
4684        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
4685        // Resolve the FK column's underlying SQL type from `K`. When the
4686        // user wrote `ForeignKey<T>` without a key parameter, the type
4687        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
4688        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
4689        // recurse into K so the column DDL emits the right SQL type
4690        // (VARCHAR for String, UUID for Uuid, …) and the load_related
4691        // emitter knows which `SqlValue` variant to match.
4692        let kind = match key_ty {
4693            Some(k) => detect_type(k)?.kind,
4694            None => DetectedKind::I64,
4695        };
4696        return Ok(DetectedType {
4697            kind,
4698            nullable: false,
4699            auto: false,
4700            fk_inner: Some(inner),
4701        });
4702    }
4703
4704    let kind = match last.ident.to_string().as_str() {
4705        "i16" => DetectedKind::I16,
4706        "i32" => DetectedKind::I32,
4707        "i64" => DetectedKind::I64,
4708        "f32" => DetectedKind::F32,
4709        "f64" => DetectedKind::F64,
4710        "bool" => DetectedKind::Bool,
4711        "String" => DetectedKind::String,
4712        "DateTime" => DetectedKind::DateTime,
4713        "NaiveDate" => DetectedKind::Date,
4714        "Uuid" => DetectedKind::Uuid,
4715        "Value" => DetectedKind::Json,
4716        other => {
4717            return Err(syn::Error::new_spanned(
4718                ty,
4719                format!("unsupported field type `{other}`; v0.1 supports i32/i64/f32/f64/bool/String/DateTime/NaiveDate/Uuid/serde_json::Value, optionally wrapped in Option or Auto (Auto only on integers)"),
4720            ));
4721        }
4722    };
4723    Ok(DetectedType {
4724        kind,
4725        nullable: false,
4726        auto: false,
4727        fk_inner: None,
4728    })
4729}
4730
4731fn generic_inner<'a>(
4732    ty: &'a Type,
4733    arguments: &'a PathArguments,
4734    wrapper: &str,
4735) -> syn::Result<&'a Type> {
4736    let PathArguments::AngleBracketed(args) = arguments else {
4737        return Err(syn::Error::new_spanned(
4738            ty,
4739            format!("{wrapper} requires a generic argument"),
4740        ));
4741    };
4742    args.args
4743        .iter()
4744        .find_map(|a| match a {
4745            GenericArgument::Type(t) => Some(t),
4746            _ => None,
4747        })
4748        .ok_or_else(|| {
4749            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4750        })
4751}
4752
4753/// Like [`generic_inner`] but pulls *two* type args — the first is
4754/// required, the second is optional. Used by the `ForeignKey<T, K>`
4755/// detection where K defaults to `i64` when omitted.
4756fn generic_pair<'a>(
4757    ty: &'a Type,
4758    arguments: &'a PathArguments,
4759    wrapper: &str,
4760) -> syn::Result<(&'a Type, Option<&'a Type>)> {
4761    let PathArguments::AngleBracketed(args) = arguments else {
4762        return Err(syn::Error::new_spanned(
4763            ty,
4764            format!("{wrapper} requires a generic argument"),
4765        ));
4766    };
4767    let mut types = args.args.iter().filter_map(|a| match a {
4768        GenericArgument::Type(t) => Some(t),
4769        _ => None,
4770    });
4771    let first = types.next().ok_or_else(|| {
4772        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4773    })?;
4774    let second = types.next();
4775    Ok((first, second))
4776}
4777
4778fn to_snake_case(s: &str) -> String {
4779    let mut out = String::with_capacity(s.len() + 4);
4780    for (i, ch) in s.chars().enumerate() {
4781        if ch.is_ascii_uppercase() {
4782            if i > 0 {
4783                out.push('_');
4784            }
4785            out.push(ch.to_ascii_lowercase());
4786        } else {
4787            out.push(ch);
4788        }
4789    }
4790    out
4791}
4792
4793// ============================================================
4794//  #[derive(Form)]  —  slice 8.4B
4795// ============================================================
4796
4797/// Per-field `#[form(...)]` attributes recognised by the derive.
4798#[derive(Default)]
4799struct FormFieldAttrs {
4800    min: Option<i64>,
4801    max: Option<i64>,
4802    min_length: Option<u32>,
4803    max_length: Option<u32>,
4804}
4805
4806/// Detected shape of a form field's Rust type.
4807#[derive(Clone, Copy)]
4808enum FormFieldKind {
4809    String,
4810    I16,
4811    I32,
4812    I64,
4813    F32,
4814    F64,
4815    Bool,
4816}
4817
4818impl FormFieldKind {
4819    fn parse_method(self) -> &'static str {
4820        match self {
4821            Self::I16 => "i16",
4822            Self::I32 => "i32",
4823            Self::I64 => "i64",
4824            Self::F32 => "f32",
4825            Self::F64 => "f64",
4826            // String + Bool don't go through `str::parse`; the codegen
4827            // handles them inline.
4828            Self::String | Self::Bool => "",
4829        }
4830    }
4831}
4832
4833fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
4834    let struct_name = &input.ident;
4835
4836    let Data::Struct(data) = &input.data else {
4837        return Err(syn::Error::new_spanned(
4838            struct_name,
4839            "Form can only be derived on structs",
4840        ));
4841    };
4842    let Fields::Named(named) = &data.fields else {
4843        return Err(syn::Error::new_spanned(
4844            struct_name,
4845            "Form requires a struct with named fields",
4846        ));
4847    };
4848
4849    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
4850    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
4851
4852    for field in &named.named {
4853        let ident = field
4854            .ident
4855            .as_ref()
4856            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4857        let attrs = parse_form_field_attrs(field)?;
4858        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
4859
4860        let name_lit = ident.to_string();
4861        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
4862        field_blocks.push(parse_block);
4863        field_idents.push(ident);
4864    }
4865
4866    Ok(quote! {
4867        impl ::rustango::forms::Form for #struct_name {
4868            fn parse(
4869                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
4870            ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
4871                let mut __errors = ::rustango::forms::FormErrors::default();
4872                #( #field_blocks )*
4873                if !__errors.is_empty() {
4874                    return ::core::result::Result::Err(__errors);
4875                }
4876                ::core::result::Result::Ok(Self {
4877                    #( #field_idents ),*
4878                })
4879            }
4880        }
4881    })
4882}
4883
4884fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
4885    let mut out = FormFieldAttrs::default();
4886    for attr in &field.attrs {
4887        if !attr.path().is_ident("form") {
4888            continue;
4889        }
4890        attr.parse_nested_meta(|meta| {
4891            if meta.path.is_ident("min") {
4892                let lit: syn::LitInt = meta.value()?.parse()?;
4893                out.min = Some(lit.base10_parse::<i64>()?);
4894                return Ok(());
4895            }
4896            if meta.path.is_ident("max") {
4897                let lit: syn::LitInt = meta.value()?.parse()?;
4898                out.max = Some(lit.base10_parse::<i64>()?);
4899                return Ok(());
4900            }
4901            if meta.path.is_ident("min_length") {
4902                let lit: syn::LitInt = meta.value()?.parse()?;
4903                out.min_length = Some(lit.base10_parse::<u32>()?);
4904                return Ok(());
4905            }
4906            if meta.path.is_ident("max_length") {
4907                let lit: syn::LitInt = meta.value()?.parse()?;
4908                out.max_length = Some(lit.base10_parse::<u32>()?);
4909                return Ok(());
4910            }
4911            Err(meta.error(
4912                "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
4913            ))
4914        })?;
4915    }
4916    Ok(out)
4917}
4918
4919fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
4920    let Type::Path(TypePath { path, qself: None }) = ty else {
4921        return Err(syn::Error::new(
4922            span,
4923            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
4924        ));
4925    };
4926    let last = path
4927        .segments
4928        .last()
4929        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
4930
4931    if last.ident == "Option" {
4932        let inner = generic_inner(ty, &last.arguments, "Option")?;
4933        let (kind, nested) = detect_form_field(inner, span)?;
4934        if nested {
4935            return Err(syn::Error::new(
4936                span,
4937                "nested Option in Form fields is not supported",
4938            ));
4939        }
4940        return Ok((kind, true));
4941    }
4942
4943    let kind = match last.ident.to_string().as_str() {
4944        "String" => FormFieldKind::String,
4945        "i16" => FormFieldKind::I16,
4946        "i32" => FormFieldKind::I32,
4947        "i64" => FormFieldKind::I64,
4948        "f32" => FormFieldKind::F32,
4949        "f64" => FormFieldKind::F64,
4950        "bool" => FormFieldKind::Bool,
4951        other => {
4952            return Err(syn::Error::new(
4953                span,
4954                format!(
4955                    "Form field type `{other}` is not supported in v0.8 — use String / \
4956                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
4957                ),
4958            ));
4959        }
4960    };
4961    Ok((kind, false))
4962}
4963
4964#[allow(clippy::too_many_lines)]
4965fn render_form_field_parse(
4966    ident: &syn::Ident,
4967    name_lit: &str,
4968    kind: FormFieldKind,
4969    nullable: bool,
4970    attrs: &FormFieldAttrs,
4971) -> TokenStream2 {
4972    // Pull the raw &str from the payload. Uses variable name `data` to
4973    // match the new `Form::parse(data: &HashMap<…>)` signature.
4974    let lookup = quote! {
4975        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
4976    };
4977
4978    let parsed_value = match kind {
4979        FormFieldKind::Bool => quote! {
4980            let __v: bool = match __raw {
4981                ::core::option::Option::None => false,
4982                ::core::option::Option::Some(__s) => !matches!(
4983                    __s.to_ascii_lowercase().as_str(),
4984                    "" | "false" | "0" | "off" | "no"
4985                ),
4986            };
4987        },
4988        FormFieldKind::String => {
4989            if nullable {
4990                quote! {
4991                    let __v: ::core::option::Option<::std::string::String> = match __raw {
4992                        ::core::option::Option::None => ::core::option::Option::None,
4993                        ::core::option::Option::Some(__s) if __s.is_empty() => {
4994                            ::core::option::Option::None
4995                        }
4996                        ::core::option::Option::Some(__s) => {
4997                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
4998                        }
4999                    };
5000                }
5001            } else {
5002                quote! {
5003                    let __v: ::std::string::String = match __raw {
5004                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
5005                            ::core::clone::Clone::clone(__s)
5006                        }
5007                        _ => {
5008                            __errors.add(#name_lit, "This field is required.");
5009                            ::std::string::String::new()
5010                        }
5011                    };
5012                }
5013            }
5014        }
5015        FormFieldKind::I16
5016        | FormFieldKind::I32
5017        | FormFieldKind::I64
5018        | FormFieldKind::F32
5019        | FormFieldKind::F64 => {
5020            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
5021            let ty_lit = kind.parse_method();
5022            let default_val = match kind {
5023                FormFieldKind::I16 => quote! { 0i16 },
5024                FormFieldKind::I32 => quote! { 0i32 },
5025                FormFieldKind::I64 => quote! { 0i64 },
5026                FormFieldKind::F32 => quote! { 0f32 },
5027                FormFieldKind::F64 => quote! { 0f64 },
5028                _ => quote! { Default::default() },
5029            };
5030            if nullable {
5031                quote! {
5032                    let __v: ::core::option::Option<#parse_ty> = match __raw {
5033                        ::core::option::Option::None => ::core::option::Option::None,
5034                        ::core::option::Option::Some(__s) if __s.is_empty() => {
5035                            ::core::option::Option::None
5036                        }
5037                        ::core::option::Option::Some(__s) => {
5038                            match __s.parse::<#parse_ty>() {
5039                                ::core::result::Result::Ok(__n) => {
5040                                    ::core::option::Option::Some(__n)
5041                                }
5042                                ::core::result::Result::Err(__e) => {
5043                                    __errors.add(
5044                                        #name_lit,
5045                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
5046                                    );
5047                                    ::core::option::Option::None
5048                                }
5049                            }
5050                        }
5051                    };
5052                }
5053            } else {
5054                quote! {
5055                    let __v: #parse_ty = match __raw {
5056                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
5057                            match __s.parse::<#parse_ty>() {
5058                                ::core::result::Result::Ok(__n) => __n,
5059                                ::core::result::Result::Err(__e) => {
5060                                    __errors.add(
5061                                        #name_lit,
5062                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
5063                                    );
5064                                    #default_val
5065                                }
5066                            }
5067                        }
5068                        _ => {
5069                            __errors.add(#name_lit, "This field is required.");
5070                            #default_val
5071                        }
5072                    };
5073                }
5074            }
5075        }
5076    };
5077
5078    let validators = render_form_validators(name_lit, kind, nullable, attrs);
5079
5080    quote! {
5081        let #ident = {
5082            #lookup
5083            #parsed_value
5084            #validators
5085            __v
5086        };
5087    }
5088}
5089
5090fn render_form_validators(
5091    name_lit: &str,
5092    kind: FormFieldKind,
5093    nullable: bool,
5094    attrs: &FormFieldAttrs,
5095) -> TokenStream2 {
5096    let mut checks: Vec<TokenStream2> = Vec::new();
5097
5098    let val_ref = if nullable {
5099        quote! { __v.as_ref() }
5100    } else {
5101        quote! { ::core::option::Option::Some(&__v) }
5102    };
5103
5104    let is_string = matches!(kind, FormFieldKind::String);
5105    let is_numeric = matches!(
5106        kind,
5107        FormFieldKind::I16
5108            | FormFieldKind::I32
5109            | FormFieldKind::I64
5110            | FormFieldKind::F32
5111            | FormFieldKind::F64
5112    );
5113
5114    if is_string {
5115        if let Some(min_len) = attrs.min_length {
5116            let min_len_usize = min_len as usize;
5117            checks.push(quote! {
5118                if let ::core::option::Option::Some(__s) = #val_ref {
5119                    if __s.len() < #min_len_usize {
5120                        __errors.add(
5121                            #name_lit,
5122                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
5123                        );
5124                    }
5125                }
5126            });
5127        }
5128        if let Some(max_len) = attrs.max_length {
5129            let max_len_usize = max_len as usize;
5130            checks.push(quote! {
5131                if let ::core::option::Option::Some(__s) = #val_ref {
5132                    if __s.len() > #max_len_usize {
5133                        __errors.add(
5134                            #name_lit,
5135                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
5136                        );
5137                    }
5138                }
5139            });
5140        }
5141    }
5142
5143    if is_numeric {
5144        if let Some(min) = attrs.min {
5145            checks.push(quote! {
5146                if let ::core::option::Option::Some(__n) = #val_ref {
5147                    if (*__n as f64) < (#min as f64) {
5148                        __errors.add(
5149                            #name_lit,
5150                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
5151                        );
5152                    }
5153                }
5154            });
5155        }
5156        if let Some(max) = attrs.max {
5157            checks.push(quote! {
5158                if let ::core::option::Option::Some(__n) = #val_ref {
5159                    if (*__n as f64) > (#max as f64) {
5160                        __errors.add(
5161                            #name_lit,
5162                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
5163                        );
5164                    }
5165                }
5166            });
5167        }
5168    }
5169
5170    quote! { #( #checks )* }
5171}
5172
5173// ============================================================
5174//  #[derive(ViewSet)]
5175// ============================================================
5176
5177struct ViewSetAttrs {
5178    model: syn::Path,
5179    fields: Option<Vec<String>>,
5180    filter_fields: Vec<String>,
5181    search_fields: Vec<String>,
5182    /// (field_name, desc)
5183    ordering: Vec<(String, bool)>,
5184    page_size: Option<usize>,
5185    read_only: bool,
5186    perms: ViewSetPermsAttrs,
5187}
5188
5189#[derive(Default)]
5190struct ViewSetPermsAttrs {
5191    list: Vec<String>,
5192    retrieve: Vec<String>,
5193    create: Vec<String>,
5194    update: Vec<String>,
5195    destroy: Vec<String>,
5196}
5197
5198fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
5199    let struct_name = &input.ident;
5200
5201    // Must be a unit struct or an empty named struct.
5202    match &input.data {
5203        Data::Struct(s) => match &s.fields {
5204            Fields::Unit | Fields::Named(_) => {}
5205            Fields::Unnamed(_) => {
5206                return Err(syn::Error::new_spanned(
5207                    struct_name,
5208                    "ViewSet can only be derived on a unit struct or an empty named struct",
5209                ));
5210            }
5211        },
5212        _ => {
5213            return Err(syn::Error::new_spanned(
5214                struct_name,
5215                "ViewSet can only be derived on a struct",
5216            ));
5217        }
5218    }
5219
5220    let attrs = parse_viewset_attrs(input)?;
5221    let model_path = &attrs.model;
5222
5223    // `.fields(&[...])` call — None means skip (use all scalar fields).
5224    let fields_call = if let Some(ref fields) = attrs.fields {
5225        let lits = fields.iter().map(|f| f.as_str());
5226        quote!(.fields(&[ #(#lits),* ]))
5227    } else {
5228        quote!()
5229    };
5230
5231    let filter_fields_call = if attrs.filter_fields.is_empty() {
5232        quote!()
5233    } else {
5234        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
5235        quote!(.filter_fields(&[ #(#lits),* ]))
5236    };
5237
5238    let search_fields_call = if attrs.search_fields.is_empty() {
5239        quote!()
5240    } else {
5241        let lits = attrs.search_fields.iter().map(|f| f.as_str());
5242        quote!(.search_fields(&[ #(#lits),* ]))
5243    };
5244
5245    let ordering_call = if attrs.ordering.is_empty() {
5246        quote!()
5247    } else {
5248        let pairs = attrs.ordering.iter().map(|(f, desc)| {
5249            let f = f.as_str();
5250            quote!((#f, #desc))
5251        });
5252        quote!(.ordering(&[ #(#pairs),* ]))
5253    };
5254
5255    let page_size_call = if let Some(n) = attrs.page_size {
5256        quote!(.page_size(#n))
5257    } else {
5258        quote!()
5259    };
5260
5261    let read_only_call = if attrs.read_only {
5262        quote!(.read_only())
5263    } else {
5264        quote!()
5265    };
5266
5267    let perms = &attrs.perms;
5268    let perms_call = if perms.list.is_empty()
5269        && perms.retrieve.is_empty()
5270        && perms.create.is_empty()
5271        && perms.update.is_empty()
5272        && perms.destroy.is_empty()
5273    {
5274        quote!()
5275    } else {
5276        let list_lits = perms.list.iter().map(|s| s.as_str());
5277        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
5278        let create_lits = perms.create.iter().map(|s| s.as_str());
5279        let update_lits = perms.update.iter().map(|s| s.as_str());
5280        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
5281        quote! {
5282            .permissions(::rustango::viewset::ViewSetPerms {
5283                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
5284                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
5285                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
5286                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
5287                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
5288            })
5289        }
5290    };
5291
5292    Ok(quote! {
5293        impl #struct_name {
5294            /// Build an `axum::Router` with the six standard REST endpoints
5295            /// for this ViewSet, mounted at `prefix`.
5296            pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
5297                ::rustango::viewset::ViewSet::for_model(
5298                    <#model_path as ::rustango::core::Model>::SCHEMA
5299                )
5300                    #fields_call
5301                    #filter_fields_call
5302                    #search_fields_call
5303                    #ordering_call
5304                    #page_size_call
5305                    #perms_call
5306                    #read_only_call
5307                    .router(prefix, pool)
5308            }
5309        }
5310    })
5311}
5312
5313fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
5314    let mut model: Option<syn::Path> = None;
5315    let mut fields: Option<Vec<String>> = None;
5316    let mut filter_fields: Vec<String> = Vec::new();
5317    let mut search_fields: Vec<String> = Vec::new();
5318    let mut ordering: Vec<(String, bool)> = Vec::new();
5319    let mut page_size: Option<usize> = None;
5320    let mut read_only = false;
5321    let mut perms = ViewSetPermsAttrs::default();
5322
5323    for attr in &input.attrs {
5324        if !attr.path().is_ident("viewset") {
5325            continue;
5326        }
5327        attr.parse_nested_meta(|meta| {
5328            if meta.path.is_ident("model") {
5329                let path: syn::Path = meta.value()?.parse()?;
5330                model = Some(path);
5331                return Ok(());
5332            }
5333            if meta.path.is_ident("fields") {
5334                let s: LitStr = meta.value()?.parse()?;
5335                fields = Some(split_field_list(&s.value()));
5336                return Ok(());
5337            }
5338            if meta.path.is_ident("filter_fields") {
5339                let s: LitStr = meta.value()?.parse()?;
5340                filter_fields = split_field_list(&s.value());
5341                return Ok(());
5342            }
5343            if meta.path.is_ident("search_fields") {
5344                let s: LitStr = meta.value()?.parse()?;
5345                search_fields = split_field_list(&s.value());
5346                return Ok(());
5347            }
5348            if meta.path.is_ident("ordering") {
5349                let s: LitStr = meta.value()?.parse()?;
5350                ordering = parse_ordering_list(&s.value());
5351                return Ok(());
5352            }
5353            if meta.path.is_ident("page_size") {
5354                let lit: syn::LitInt = meta.value()?.parse()?;
5355                page_size = Some(lit.base10_parse::<usize>()?);
5356                return Ok(());
5357            }
5358            if meta.path.is_ident("read_only") {
5359                read_only = true;
5360                return Ok(());
5361            }
5362            if meta.path.is_ident("permissions") {
5363                meta.parse_nested_meta(|inner| {
5364                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
5365                        let s: LitStr = inner.value()?.parse()?;
5366                        Ok(split_field_list(&s.value()))
5367                    };
5368                    if inner.path.is_ident("list") {
5369                        perms.list = parse_codenames(&inner)?;
5370                    } else if inner.path.is_ident("retrieve") {
5371                        perms.retrieve = parse_codenames(&inner)?;
5372                    } else if inner.path.is_ident("create") {
5373                        perms.create = parse_codenames(&inner)?;
5374                    } else if inner.path.is_ident("update") {
5375                        perms.update = parse_codenames(&inner)?;
5376                    } else if inner.path.is_ident("destroy") {
5377                        perms.destroy = parse_codenames(&inner)?;
5378                    } else {
5379                        return Err(inner.error(
5380                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
5381                        ));
5382                    }
5383                    Ok(())
5384                })?;
5385                return Ok(());
5386            }
5387            Err(meta.error(
5388                "unknown viewset attribute (supported: model, fields, filter_fields, \
5389                 search_fields, ordering, page_size, read_only, permissions(...))",
5390            ))
5391        })?;
5392    }
5393
5394    let model = model.ok_or_else(|| {
5395        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
5396    })?;
5397
5398    Ok(ViewSetAttrs {
5399        model,
5400        fields,
5401        filter_fields,
5402        search_fields,
5403        ordering,
5404        page_size,
5405        read_only,
5406        perms,
5407    })
5408}
5409
5410// ============================================================ #[derive(Serializer)]
5411
5412struct SerializerContainerAttrs {
5413    model: syn::Path,
5414}
5415
5416#[derive(Default)]
5417struct SerializerFieldAttrs {
5418    read_only: bool,
5419    write_only: bool,
5420    source: Option<String>,
5421    skip: bool,
5422    /// `#[serializer(method = "fn_name")]` — DRF SerializerMethodField
5423    /// analog. The macro emits `from_model` initializer that calls
5424    /// `Self::fn_name(&model)` and stores the return value.
5425    method: Option<String>,
5426    /// `#[serializer(validate = "fn_name")]` — per-field validator
5427    /// callable run by `Self::validate(&self)`. Must return
5428    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
5429    /// the field name.
5430    validate: Option<String>,
5431    /// `#[serializer(nested)]` on a field whose type is another
5432    /// `Serializer` — the macro emits `from_model` initializer that
5433    /// reads the parent via `model.<source>.value()` then calls the
5434    /// child serializer's `from_model(parent)`. When the FK is
5435    /// unloaded the field falls back to `Default::default()` (does
5436    /// NOT panic) so a missing prefetch in prod degrades gracefully.
5437    /// Source field on the model defaults to the field name; override
5438    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
5439    /// panic-on-unloaded behavior for tests.
5440    nested: bool,
5441    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
5442    /// strict behavior: panic when the FK isn't loaded. Useful in
5443    /// test code where forgetting select_related must trip a hard
5444    /// failure rather than render a blank nested object.
5445    nested_strict: bool,
5446    /// `#[serializer(many = TagSerializer)]` — declare the field as
5447    /// a list of nested serializers. Field type must be `Vec<S>`
5448    /// where `S` is the inner serializer. The macro initializes the
5449    /// field to `Vec::new()` in `from_model` and emits a typed
5450    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
5451    /// maps each model row through `S::from_model`. Auto-load isn't
5452    /// possible (the M2M / one-to-many accessor is async); callers
5453    /// fetch the children + call the setter post-from_model.
5454    many: Option<syn::Type>,
5455}
5456
5457fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
5458    let mut model: Option<syn::Path> = None;
5459    for attr in &input.attrs {
5460        if !attr.path().is_ident("serializer") {
5461            continue;
5462        }
5463        attr.parse_nested_meta(|meta| {
5464            if meta.path.is_ident("model") {
5465                let _eq: syn::Token![=] = meta.input.parse()?;
5466                model = Some(meta.input.parse()?);
5467                return Ok(());
5468            }
5469            Err(meta.error("unknown serializer container attribute (supported: `model`)"))
5470        })?;
5471    }
5472    let model = model.ok_or_else(|| {
5473        syn::Error::new_spanned(
5474            &input.ident,
5475            "`#[serializer(model = SomeModel)]` is required",
5476        )
5477    })?;
5478    Ok(SerializerContainerAttrs { model })
5479}
5480
5481fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
5482    let mut out = SerializerFieldAttrs::default();
5483    for attr in &field.attrs {
5484        if !attr.path().is_ident("serializer") {
5485            continue;
5486        }
5487        attr.parse_nested_meta(|meta| {
5488            if meta.path.is_ident("read_only") {
5489                out.read_only = true;
5490                return Ok(());
5491            }
5492            if meta.path.is_ident("write_only") {
5493                out.write_only = true;
5494                return Ok(());
5495            }
5496            if meta.path.is_ident("skip") {
5497                out.skip = true;
5498                return Ok(());
5499            }
5500            if meta.path.is_ident("source") {
5501                let s: LitStr = meta.value()?.parse()?;
5502                out.source = Some(s.value());
5503                return Ok(());
5504            }
5505            if meta.path.is_ident("method") {
5506                let s: LitStr = meta.value()?.parse()?;
5507                out.method = Some(s.value());
5508                return Ok(());
5509            }
5510            if meta.path.is_ident("validate") {
5511                let s: LitStr = meta.value()?.parse()?;
5512                out.validate = Some(s.value());
5513                return Ok(());
5514            }
5515            if meta.path.is_ident("many") {
5516                let _eq: syn::Token![=] = meta.input.parse()?;
5517                out.many = Some(meta.input.parse()?);
5518                return Ok(());
5519            }
5520            if meta.path.is_ident("nested") {
5521                out.nested = true;
5522                // Optional strict flag inside parentheses:
5523                //   #[serializer(nested(strict))]
5524                if meta.input.peek(syn::token::Paren) {
5525                    meta.parse_nested_meta(|inner| {
5526                        if inner.path.is_ident("strict") {
5527                            out.nested_strict = true;
5528                            return Ok(());
5529                        }
5530                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
5531                    })?;
5532                }
5533                return Ok(());
5534            }
5535            Err(meta.error(
5536                "unknown serializer field attribute (supported: \
5537                 `read_only`, `write_only`, `source`, `skip`, `method`, `validate`, `nested`)",
5538            ))
5539        })?;
5540    }
5541    // Validate: read_only + write_only is nonsensical
5542    if out.read_only && out.write_only {
5543        return Err(syn::Error::new_spanned(
5544            field,
5545            "a field cannot be both `read_only` and `write_only`",
5546        ));
5547    }
5548    if out.method.is_some() && out.source.is_some() {
5549        return Err(syn::Error::new_spanned(
5550            field,
5551            "`method` and `source` are mutually exclusive — `method` computes \
5552             the value from a method, `source` reads it from a different model field",
5553        ));
5554    }
5555    Ok(out)
5556}
5557
5558fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
5559    let struct_name = &input.ident;
5560    let struct_name_lit = struct_name.to_string();
5561
5562    let Data::Struct(data) = &input.data else {
5563        return Err(syn::Error::new_spanned(
5564            struct_name,
5565            "Serializer can only be derived on structs",
5566        ));
5567    };
5568    let Fields::Named(named) = &data.fields else {
5569        return Err(syn::Error::new_spanned(
5570            struct_name,
5571            "Serializer requires a struct with named fields",
5572        ));
5573    };
5574
5575    let container = parse_serializer_container_attrs(input)?;
5576    let model_path = &container.model;
5577
5578    // Classify each field. `ty` is only consumed by the
5579    // `#[cfg(feature = "openapi")]` block below, but we always
5580    // capture it to keep the field-info build a single pass.
5581    #[allow(dead_code)]
5582    struct FieldInfo {
5583        ident: syn::Ident,
5584        ty: syn::Type,
5585        attrs: SerializerFieldAttrs,
5586    }
5587    let mut fields_info: Vec<FieldInfo> = Vec::new();
5588    for field in &named.named {
5589        let ident = field.ident.clone().expect("named field has ident");
5590        let attrs = parse_serializer_field_attrs(field)?;
5591        fields_info.push(FieldInfo {
5592            ident,
5593            ty: field.ty.clone(),
5594            attrs,
5595        });
5596    }
5597
5598    // Generate from_model body: struct literal with each field assigned.
5599    let from_model_fields = fields_info.iter().map(|fi| {
5600        let ident = &fi.ident;
5601        let ty = &fi.ty;
5602        if let Some(_inner) = &fi.attrs.many {
5603            // Many — collection field. Initialize empty; caller
5604            // populates via the macro-emitted set_<field> helper
5605            // after fetching the M2M children.
5606            quote! { #ident: ::std::vec::Vec::new() }
5607        } else if let Some(method) = &fi.attrs.method {
5608            // SerializerMethodField: call Self::<method>(&model) to
5609            // compute the value. Method signature must be
5610            // `fn <method>(model: &T) -> <field type>`.
5611            let method_ident = syn::Ident::new(method, ident.span());
5612            quote! { #ident: Self::#method_ident(model) }
5613        } else if fi.attrs.nested {
5614            // Nested serializer. Source defaults to the field name on
5615            // this struct; override via `source = "..."`. The source
5616            // field on the model is expected to be a `ForeignKey<T>`
5617            // whose `.value()` returns `Option<&T>` after lazy-load.
5618            //
5619            // Behavior matrix (tweakable per-field):
5620            //   * FK loaded   → nested object materializes via
5621            //                   ChildSerializer::from_model(parent).
5622            //   * FK unloaded → fall back to ChildSerializer::default()
5623            //                   (so prod doesn't crash on a missing
5624            //                   prefetch — just renders a blank nested
5625            //                   object). Add `#[serializer(nested,
5626            //                   strict)]` to keep the v0.18.1
5627            //                   panic-on-unloaded behavior for tests
5628            //                   that want hard guardrails.
5629            let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
5630            let src_ident = syn::Ident::new(&src_name, ident.span());
5631            if fi.attrs.nested_strict {
5632                let panic_msg = format!(
5633                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
5634                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
5635                );
5636                quote! {
5637                    #ident: <#ty as ::rustango::serializer::ModelSerializer>::from_model(
5638                        model.#src_ident.value().expect(#panic_msg),
5639                    )
5640                }
5641            } else {
5642                quote! {
5643                    #ident: match model.#src_ident.value() {
5644                        ::core::option::Option::Some(__loaded) =>
5645                            <#ty as ::rustango::serializer::ModelSerializer>::from_model(__loaded),
5646                        ::core::option::Option::None =>
5647                            ::core::default::Default::default(),
5648                    }
5649                }
5650            }
5651        } else if fi.attrs.write_only || fi.attrs.skip {
5652            // Not read from model — use default
5653            quote! { #ident: ::core::default::Default::default() }
5654        } else if let Some(src) = &fi.attrs.source {
5655            let src_ident = syn::Ident::new(src, ident.span());
5656            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
5657        } else {
5658            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
5659        }
5660    });
5661
5662    // Per-field validators (DRF-shape `validators=[...]`). Emit a
5663    // `validate(&self)` method that runs each user-defined validator
5664    // and aggregates errors into `FormErrors`.
5665    let validator_calls: Vec<_> = fields_info
5666        .iter()
5667        .filter_map(|fi| {
5668            let ident = &fi.ident;
5669            let name_lit = ident.to_string();
5670            let method = fi.attrs.validate.as_ref()?;
5671            let method_ident = syn::Ident::new(method, ident.span());
5672            Some(quote! {
5673                if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
5674                    __errors.add(#name_lit.to_owned(), __e);
5675                }
5676            })
5677        })
5678        .collect();
5679    let validate_method = if validator_calls.is_empty() {
5680        quote! {}
5681    } else {
5682        quote! {
5683            impl #struct_name {
5684                /// Run every `#[serializer(validate = "...")]` per-field
5685                /// validator. Aggregates errors into `FormErrors` keyed
5686                /// by the field name. Returns `Ok(())` when all pass.
5687                pub fn validate(&self) -> ::core::result::Result<(), ::rustango::forms::FormErrors> {
5688                    let mut __errors = ::rustango::forms::FormErrors::default();
5689                    #( #validator_calls )*
5690                    if __errors.is_empty() {
5691                        ::core::result::Result::Ok(())
5692                    } else {
5693                        ::core::result::Result::Err(__errors)
5694                    }
5695                }
5696            }
5697        }
5698    };
5699
5700    // For every `#[serializer(many = S)]` field, emit a
5701    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
5702    // helper that maps the parents through `S::from_model`.
5703    let many_setters: Vec<_> = fields_info
5704        .iter()
5705        .filter_map(|fi| {
5706            let many_ty = fi.attrs.many.as_ref()?;
5707            let ident = &fi.ident;
5708            let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
5709            Some(quote! {
5710                /// Populate this `many` field by mapping each parent model
5711                /// through the inner serializer's `from_model`. Call after
5712                /// fetching the M2M / one-to-many children since
5713                /// `from_model` itself can't await an SQL query.
5714                pub fn #setter(
5715                    &mut self,
5716                    models: &[<#many_ty as ::rustango::serializer::ModelSerializer>::Model],
5717                ) -> &mut Self {
5718                    self.#ident = models.iter()
5719                        .map(<#many_ty as ::rustango::serializer::ModelSerializer>::from_model)
5720                        .collect();
5721                    self
5722                }
5723            })
5724        })
5725        .collect();
5726    let many_setters_impl = if many_setters.is_empty() {
5727        quote! {}
5728    } else {
5729        quote! {
5730            impl #struct_name {
5731                #( #many_setters )*
5732            }
5733        }
5734    };
5735
5736    // Generate custom Serialize: skip write_only fields
5737    let output_fields: Vec<_> = fields_info
5738        .iter()
5739        .filter(|fi| !fi.attrs.write_only)
5740        .collect();
5741    let output_field_count = output_fields.len();
5742    let serialize_fields = output_fields.iter().map(|fi| {
5743        let ident = &fi.ident;
5744        let name_lit = ident.to_string();
5745        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
5746    });
5747
5748    // writable_fields: normal + write_only (not read_only, not skip)
5749    let writable_lits: Vec<_> = fields_info
5750        .iter()
5751        .filter(|fi| !fi.attrs.read_only && !fi.attrs.skip)
5752        .map(|fi| fi.ident.to_string())
5753        .collect();
5754
5755    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
5756    // Only includes fields shown in JSON output (skips write_only). For each
5757    // `Option<T>` field, omit from `required` and add `.nullable()`.
5758    let openapi_impl = {
5759        #[cfg(feature = "openapi")]
5760        {
5761            let property_calls = output_fields.iter().map(|fi| {
5762                let ident = &fi.ident;
5763                let name_lit = ident.to_string();
5764                let ty = &fi.ty;
5765                let nullable_call = if is_option(ty) {
5766                    quote! { .nullable() }
5767                } else {
5768                    quote! {}
5769                };
5770                quote! {
5771                    .property(
5772                        #name_lit,
5773                        <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
5774                            #nullable_call,
5775                    )
5776                }
5777            });
5778            let required_lits: Vec<_> = output_fields
5779                .iter()
5780                .filter(|fi| !is_option(&fi.ty))
5781                .map(|fi| fi.ident.to_string())
5782                .collect();
5783            quote! {
5784                impl ::rustango::openapi::OpenApiSchema for #struct_name {
5785                    fn openapi_schema() -> ::rustango::openapi::Schema {
5786                        ::rustango::openapi::Schema::object()
5787                            #( #property_calls )*
5788                            .required([ #( #required_lits ),* ])
5789                    }
5790                }
5791            }
5792        }
5793        #[cfg(not(feature = "openapi"))]
5794        {
5795            quote! {}
5796        }
5797    };
5798
5799    Ok(quote! {
5800        impl ::rustango::serializer::ModelSerializer for #struct_name {
5801            type Model = #model_path;
5802
5803            fn from_model(model: &Self::Model) -> Self {
5804                Self {
5805                    #( #from_model_fields ),*
5806                }
5807            }
5808
5809            fn writable_fields() -> &'static [&'static str] {
5810                &[ #( #writable_lits ),* ]
5811            }
5812        }
5813
5814        impl ::serde::Serialize for #struct_name {
5815            fn serialize<S>(&self, serializer: S)
5816                -> ::core::result::Result<S::Ok, S::Error>
5817            where
5818                S: ::serde::Serializer,
5819            {
5820                use ::serde::ser::SerializeStruct;
5821                let mut __state = serializer.serialize_struct(
5822                    #struct_name_lit,
5823                    #output_field_count,
5824                )?;
5825                #( #serialize_fields )*
5826                __state.end()
5827            }
5828        }
5829
5830        #openapi_impl
5831
5832        #validate_method
5833
5834        #many_setters_impl
5835    })
5836}
5837
5838/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
5839/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
5840/// when the feature is off.
5841#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
5842fn is_option(ty: &syn::Type) -> bool {
5843    if let syn::Type::Path(p) = ty {
5844        if let Some(last) = p.path.segments.last() {
5845            return last.ident == "Option";
5846        }
5847    }
5848    false
5849}