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