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