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///
103/// # Container attribute (required)
104/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
105///
106/// # Field attributes
107/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
108/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
109/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
110/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
111///
112/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
113#[proc_macro_derive(Serializer, attributes(serializer))]
114pub fn derive_serializer(input: TokenStream) -> TokenStream {
115    let input = parse_macro_input!(input as DeriveInput);
116    expand_serializer(&input)
117        .unwrap_or_else(syn::Error::into_compile_error)
118        .into()
119}
120
121/// Bake every `*.json` migration file in a directory into the binary
122/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
123/// of `(name, json_content)` pairs, lex-sorted by file stem.
124///
125/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
126/// behaviour as `migrate(pool, dir)` but with no filesystem access.
127/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
128/// (i.e. the crate that invokes the macro). Default is
129/// `"./migrations"` if no argument is supplied.
130///
131/// ```ignore
132/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
133/// // or:
134/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
135///
136/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
137/// ```
138///
139/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
140/// file's `name` field must equal its file stem, every `prev`
141/// reference must point to another migration in the same directory,
142/// and the JSON must parse. A broken chain — orphan `prev`, missing
143/// predecessor, malformed file — fails at macro-expansion time with
144/// a clear `compile_error!`. *No other Django-shape Rust framework
145/// validates migration chains at compile time*: Cot's migrations are
146/// imperative Rust code (no static chain), Loco's are SeaORM
147/// up/down (same), Rwf's are raw SQL (no chain at all).
148///
149/// Each migration is included via `include_str!` so cargo's rebuild
150/// detection picks up file *content* changes. **Caveat:** cargo
151/// doesn't watch directory listings, so adding or removing a
152/// migration file inside the dir won't auto-trigger a rebuild — run
153/// `cargo clean` (or just bump any other source file) when you add
154/// new migrations during embedded development.
155#[proc_macro]
156pub fn embed_migrations(input: TokenStream) -> TokenStream {
157    expand_embed_migrations(input.into())
158        .unwrap_or_else(syn::Error::into_compile_error)
159        .into()
160}
161
162/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
163/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
164/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
165/// functions are zero-boilerplate:
166///
167/// ```ignore
168/// #[rustango::main]
169/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
170///     rustango::server::Builder::from_env().await?
171///         .migrate("migrations").await?
172///         .api(my_app::urls::api())
173///         .seed_with(my_app::seed::run).await?
174///         .serve("0.0.0.0:8080").await
175/// }
176/// ```
177///
178/// Optional `flavor = "current_thread"` passes through to
179/// `#[tokio::main]`; default is the multi-threaded runtime.
180///
181/// Pulls `tracing-subscriber` into the rustango crate behind the
182/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
183/// out get plain `#[tokio::main]` ergonomics without the dependency.
184#[proc_macro_attribute]
185pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
186    expand_main(args.into(), item.into())
187        .unwrap_or_else(syn::Error::into_compile_error)
188        .into()
189}
190
191fn expand_main(
192    args: TokenStream2,
193    item: TokenStream2,
194) -> syn::Result<TokenStream2> {
195    let mut input: syn::ItemFn = syn::parse2(item)?;
196    if input.sig.asyncness.is_none() {
197        return Err(syn::Error::new(
198            input.sig.ident.span(),
199            "`#[rustango::main]` must wrap an `async fn`",
200        ));
201    }
202
203    // Parse optional `flavor = "..."` etc. from the attribute args
204    // and pass them straight through to `#[tokio::main(...)]`.
205    let tokio_attr = if args.is_empty() {
206        quote! { #[::tokio::main] }
207    } else {
208        quote! { #[::tokio::main(#args)] }
209    };
210
211    // Re-block the body so the tracing init runs before user code.
212    let body = input.block.clone();
213    input.block = syn::parse2(quote! {{
214        {
215            use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
216            // `try_init` so duplicate installers (e.g. tests already
217            // holding a subscriber) don't panic.
218            let _ = tracing_subscriber::fmt()
219                .with_env_filter(
220                    EnvFilter::try_from_default_env()
221                        .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
222                )
223                .try_init();
224        }
225        #body
226    }})?;
227
228    Ok(quote! {
229        #tokio_attr
230        #input
231    })
232}
233
234fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
235    // Default to "./migrations" if invoked without args.
236    let path_str = if input.is_empty() {
237        "./migrations".to_string()
238    } else {
239        let lit: LitStr = syn::parse2(input)?;
240        lit.value()
241    };
242
243    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
244        syn::Error::new(
245            proc_macro2::Span::call_site(),
246            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
247        )
248    })?;
249    let abs = std::path::Path::new(&manifest).join(&path_str);
250
251    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
252    if abs.is_dir() {
253        let read = std::fs::read_dir(&abs).map_err(|e| {
254            syn::Error::new(
255                proc_macro2::Span::call_site(),
256                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
257            )
258        })?;
259        for entry in read.flatten() {
260            let path = entry.path();
261            if !path.is_file() {
262                continue;
263            }
264            if path.extension().and_then(|s| s.to_str()) != Some("json") {
265                continue;
266            }
267            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
268                continue;
269            };
270            entries.push((stem.to_owned(), path));
271        }
272    }
273    entries.sort_by(|a, b| a.0.cmp(&b.0));
274
275    // Compile-time chain validation: read each migration's JSON,
276    // pull `name` and `prev` (file-stem-keyed for the chain check),
277    // and verify every `prev` points to another migration in the
278    // slice. Mismatches between the file stem and the embedded
279    // `name` field — and broken `prev` chains — fail at MACRO
280    // EXPANSION time so a misshapen migration set never compiles.
281    //
282    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
283    // migrations + a Rust proc-macro that reads them is the unique
284    // combo nothing else in the Django-shape Rust camp can match
285    // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
286    // Rwf's are raw SQL — none have a static chain to validate).
287    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
288    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
289    for (stem, path) in &entries {
290        let raw = std::fs::read_to_string(path).map_err(|e| {
291            syn::Error::new(
292                proc_macro2::Span::call_site(),
293                format!(
294                    "embed_migrations!: cannot read {} for chain validation: {e}",
295                    path.display()
296                ),
297            )
298        })?;
299        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
300            syn::Error::new(
301                proc_macro2::Span::call_site(),
302                format!(
303                    "embed_migrations!: {} is not valid JSON: {e}",
304                    path.display()
305                ),
306            )
307        })?;
308        let name = json
309            .get("name")
310            .and_then(|v| v.as_str())
311            .ok_or_else(|| {
312                syn::Error::new(
313                    proc_macro2::Span::call_site(),
314                    format!(
315                        "embed_migrations!: {} is missing the `name` field",
316                        path.display()
317                    ),
318                )
319            })?
320            .to_owned();
321        if name != *stem {
322            return Err(syn::Error::new(
323                proc_macro2::Span::call_site(),
324                format!(
325                    "embed_migrations!: file stem `{stem}` does not match the migration's \
326                     `name` field `{name}` — rename the file or fix the JSON",
327                ),
328            ));
329        }
330        let prev = json
331            .get("prev")
332            .and_then(|v| v.as_str())
333            .map(str::to_owned);
334        chain_names.push(name.clone());
335        prev_refs.push((name, prev));
336    }
337
338    let name_set: std::collections::HashSet<&str> =
339        chain_names.iter().map(String::as_str).collect();
340    for (name, prev) in &prev_refs {
341        if let Some(p) = prev {
342            if !name_set.contains(p.as_str()) {
343                return Err(syn::Error::new(
344                    proc_macro2::Span::call_site(),
345                    format!(
346                        "embed_migrations!: broken migration chain — `{name}` declares \
347                         prev=`{p}` but no migration with that name exists in {}",
348                        abs.display()
349                    ),
350                ));
351            }
352        }
353    }
354
355    let pairs: Vec<TokenStream2> = entries
356        .iter()
357        .map(|(name, path)| {
358            let path_lit = path.display().to_string();
359            quote! { (#name, ::core::include_str!(#path_lit)) }
360        })
361        .collect();
362
363    Ok(quote! {
364        {
365            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
366            __RUSTANGO_EMBEDDED
367        }
368    })
369}
370
371fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
372    let struct_name = &input.ident;
373
374    let Data::Struct(data) = &input.data else {
375        return Err(syn::Error::new_spanned(
376            struct_name,
377            "Model can only be derived on structs",
378        ));
379    };
380    let Fields::Named(named) = &data.fields else {
381        return Err(syn::Error::new_spanned(
382            struct_name,
383            "Model requires a struct with named fields",
384        ));
385    };
386
387    let container = parse_container_attrs(input)?;
388    let table = container
389        .table
390        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
391    let model_name = struct_name.to_string();
392
393    let collected = collect_fields(named)?;
394
395    // Validate that #[rustango(display = "…")] names a real field.
396    if let Some((ref display, span)) = container.display {
397        if !collected.field_names.iter().any(|n| n == display) {
398            return Err(syn::Error::new(
399                span,
400                format!("`display = \"{display}\"` does not match any field on this struct"),
401            ));
402        }
403    }
404    let display = container.display.map(|(name, _)| name);
405    let app_label = container.app.clone();
406
407    // Validate admin field-name lists against declared field names.
408    if let Some(admin) = &container.admin {
409        for (label, list) in [
410            ("list_display", &admin.list_display),
411            ("search_fields", &admin.search_fields),
412            ("readonly_fields", &admin.readonly_fields),
413            ("list_filter", &admin.list_filter),
414        ] {
415            if let Some((names, span)) = list {
416                for name in names {
417                    if !collected.field_names.iter().any(|n| n == name) {
418                        return Err(syn::Error::new(
419                            *span,
420                            format!(
421                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
422                            ),
423                        ));
424                    }
425                }
426            }
427        }
428        if let Some((pairs, span)) = &admin.ordering {
429            for (name, _) in pairs {
430                if !collected.field_names.iter().any(|n| n == name) {
431                    return Err(syn::Error::new(
432                        *span,
433                        format!(
434                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
435                        ),
436                    ));
437                }
438            }
439        }
440        if let Some((groups, span)) = &admin.fieldsets {
441            for (_, fields) in groups {
442                for name in fields {
443                    if !collected.field_names.iter().any(|n| n == name) {
444                        return Err(syn::Error::new(
445                            *span,
446                            format!(
447                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
448                            ),
449                        ));
450                    }
451                }
452            }
453        }
454    }
455    if let Some(audit) = &container.audit {
456        if let Some((names, span)) = &audit.track {
457            for name in names {
458                if !collected.field_names.iter().any(|n| n == name) {
459                    return Err(syn::Error::new(
460                        *span,
461                        format!(
462                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
463                        ),
464                    ));
465                }
466            }
467        }
468    }
469
470    // Build the audit_track list for ModelSchema: None when no audit attr,
471    // Some(empty) when audit present without track, Some(names) when explicit.
472    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
473        audit
474            .track
475            .as_ref()
476            .map(|(names, _)| names.clone())
477            .unwrap_or_default()
478    });
479
480    // Merge field-level indexes into the container's index list.
481    let mut all_indexes: Vec<IndexAttr> = container.indexes;
482    for field in &named.named {
483        let ident = field.ident.as_ref().expect("named");
484        let col = to_snake_case(&ident.to_string()); // column name fallback
485        // Re-parse field attrs to check for index flag
486        if let Ok(fa) = parse_field_attrs(field) {
487            if fa.index {
488                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
489                let auto_name = if fa.index_unique {
490                    format!("{table}_{col_name}_uq_idx")
491                } else {
492                    format!("{table}_{col_name}_idx")
493                };
494                all_indexes.push(IndexAttr {
495                    name: fa.index_name.or(Some(auto_name)),
496                    columns: vec![col_name],
497                    unique: fa.index_unique,
498                });
499            }
500        }
501    }
502
503    let model_impl = model_impl_tokens(
504        struct_name,
505        &model_name,
506        &table,
507        display.as_deref(),
508        app_label.as_deref(),
509        container.admin.as_ref(),
510        &collected.field_schemas,
511        collected.soft_delete_column.as_deref(),
512        container.permissions,
513        audit_track_names.as_deref(),
514        &container.m2m,
515        &all_indexes,
516        &container.checks,
517    );
518    let module_ident = column_module_ident(struct_name);
519    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
520    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
521        let track_set: Option<std::collections::HashSet<&str>> = audit
522            .track
523            .as_ref()
524            .map(|(names, _)| names.iter().map(String::as_str).collect());
525        collected
526            .column_entries
527            .iter()
528            .filter(|c| {
529                track_set
530                    .as_ref()
531                    .map_or(true, |s| s.contains(c.name.as_str()))
532            })
533            .collect()
534    });
535    let inherent_impl = inherent_impl_tokens(
536        struct_name,
537        &collected,
538        collected.primary_key.as_ref(),
539        &column_consts,
540        audited_fields.as_deref(),
541    );
542    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
543    let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
544    let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
545    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
546
547    Ok(quote! {
548        #model_impl
549        #inherent_impl
550        #from_row_impl
551        #column_module
552        #reverse_helpers
553        #m2m_accessors
554
555        ::rustango::core::inventory::submit! {
556            ::rustango::core::ModelEntry {
557                schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
558                // `module_path!()` evaluates at the registration site,
559                // so a Model declared in `crate::blog::models` records
560                // `"<crate>::blog::models"` and `resolved_app_label()`
561                // can infer "blog" without an explicit attribute.
562                module_path: ::core::module_path!(),
563            }
564        }
565    })
566}
567
568/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
569/// matches `field_name` against the model's FK fields and, for a
570/// match, decodes the FK target via the parent's macro-generated
571/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
572/// `ForeignKey::Loaded` on `self`.
573///
574/// Always emitted (with empty arms for FK-less models, which
575/// return `Ok(false)` for any field name) so the `T: LoadRelated`
576/// trait bound on `fetch_on` is universally satisfied — users
577/// never have to think about implementing it.
578fn load_related_impl_tokens(
579    struct_name: &syn::Ident,
580    fk_relations: &[FkRelation],
581) -> TokenStream2 {
582    let arms = fk_relations.iter().map(|rel| {
583        let parent_ty = &rel.parent_type;
584        let fk_col = rel.fk_column.as_str();
585        // FK field's Rust ident matches its SQL column name in v0.8
586        // (no `column = "..."` rename ships on FK fields).
587        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
588        quote! {
589            #fk_col => {
590                let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
591                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
592                    ::rustango::core::SqlValue::I64(v) => v,
593                    _ => 0i64,
594                };
595                self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
596                ::core::result::Result::Ok(true)
597            }
598        }
599    });
600    quote! {
601        impl ::rustango::sql::LoadRelated for #struct_name {
602            #[allow(unused_variables)]
603            fn __rustango_load_related(
604                &mut self,
605                row: &::rustango::sql::sqlx::postgres::PgRow,
606                field_name: &str,
607                alias: &str,
608            ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
609                match field_name {
610                    #( #arms )*
611                    _ => ::core::result::Result::Ok(false),
612                }
613            }
614        }
615    }
616}
617
618/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
619/// matches `field_name` against the model's FK fields and returns
620/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
621/// group children by parent PK.
622///
623/// Always emitted (with `_ => None` for FK-less models) so the
624/// trait bound on `fetch_with_prefetch` is universally satisfied.
625fn fk_pk_access_impl_tokens(
626    struct_name: &syn::Ident,
627    fk_relations: &[FkRelation],
628) -> TokenStream2 {
629    let arms = fk_relations.iter().map(|rel| {
630        let fk_col = rel.fk_column.as_str();
631        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
632        quote! {
633            #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
634        }
635    });
636    quote! {
637        impl ::rustango::sql::FkPkAccess for #struct_name {
638            #[allow(unused_variables)]
639            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
640                match field_name {
641                    #( #arms )*
642                    _ => ::core::option::Option::None,
643                }
644            }
645        }
646    }
647}
648
649/// For every `ForeignKey<Parent>` field on `Child`, emit
650/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
651/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
652/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
653/// — the canonical reverse-FK fetch. One round trip, no N+1.
654fn reverse_helper_tokens(
655    child_ident: &syn::Ident,
656    fk_relations: &[FkRelation],
657) -> TokenStream2 {
658    if fk_relations.is_empty() {
659        return TokenStream2::new();
660    }
661    // Snake-case the child struct name to derive the method suffix —
662    // `Post` → `post_set`, `BlogComment` → `blog_comment_set`. Avoids
663    // English-plural edge cases (Django's `<child>_set` convention).
664    let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
665    let method_ident = syn::Ident::new(&suffix, child_ident.span());
666    let impls = fk_relations.iter().map(|rel| {
667        let parent_ty = &rel.parent_type;
668        let fk_col = rel.fk_column.as_str();
669        let doc = format!(
670            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
671             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
672             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
673             further `{child_ident}::objects()` filters via direct queryset use."
674        );
675        quote! {
676            impl #parent_ty {
677                #[doc = #doc]
678                ///
679                /// # Errors
680                /// Returns [`::rustango::sql::ExecError`] for SQL-writing
681                /// or driver failures.
682                pub async fn #method_ident<'_c, _E>(
683                    &self,
684                    _executor: _E,
685                ) -> ::core::result::Result<
686                    ::std::vec::Vec<#child_ident>,
687                    ::rustango::sql::ExecError,
688                >
689                where
690                    _E: ::rustango::sql::sqlx::Executor<
691                        '_c,
692                        Database = ::rustango::sql::sqlx::Postgres,
693                    >,
694                {
695                    let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
696                    ::rustango::query::QuerySet::<#child_ident>::new()
697                        .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
698                        .fetch_on(_executor)
699                        .await
700                }
701            }
702        }
703    });
704    quote! { #( #impls )* }
705}
706
707/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
708/// relation declared on the model.
709fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
710    if m2m_relations.is_empty() {
711        return TokenStream2::new();
712    }
713    let methods = m2m_relations.iter().map(|rel| {
714        let method_name = format!("{}_m2m", rel.name);
715        let method_ident = syn::Ident::new(&method_name, struct_name.span());
716        let through = rel.through.as_str();
717        let src_col = rel.src.as_str();
718        let dst_col = rel.dst.as_str();
719        quote! {
720            pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
721                ::rustango::sql::M2MManager {
722                    src_pk: self.__rustango_pk_value(),
723                    through: #through,
724                    src_col: #src_col,
725                    dst_col: #dst_col,
726                }
727            }
728        }
729    });
730    quote! {
731        impl #struct_name {
732            #( #methods )*
733        }
734    }
735}
736
737struct ColumnEntry {
738    /// The struct field ident, used both for the inherent const name on
739    /// the model and for the inner column type's name.
740    ident: syn::Ident,
741    /// The struct's field type, used as `Column::Value`.
742    value_ty: Type,
743    /// Rust-side field name (e.g. `"id"`).
744    name: String,
745    /// SQL-side column name (e.g. `"user_id"`).
746    column: String,
747    /// `::rustango::core::FieldType::I64` etc.
748    field_type_tokens: TokenStream2,
749}
750
751struct CollectedFields {
752    field_schemas: Vec<TokenStream2>,
753    from_row_inits: Vec<TokenStream2>,
754    /// Aliased counterparts of `from_row_inits` — read columns via
755    /// `format!("{prefix}__{col}")` aliases so a Model can be
756    /// decoded from a JOINed row's projected target columns.
757    from_aliased_row_inits: Vec<TokenStream2>,
758    /// Static column-name list — used by the simple insert path
759    /// (no `Auto<T>` fields). Aligned with `insert_values`.
760    insert_columns: Vec<TokenStream2>,
761    /// Static `Into<SqlValue>` expressions, one per field. Aligned
762    /// with `insert_columns`. Used by the simple insert path only.
763    insert_values: Vec<TokenStream2>,
764    /// Per-field push expressions for the dynamic (Auto-aware)
765    /// insert path. Each statement either unconditionally pushes
766    /// `(column, value)` or, for an `Auto<T>` field, conditionally
767    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
768    insert_pushes: Vec<TokenStream2>,
769    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
770    /// when `has_auto == false`.
771    returning_cols: Vec<TokenStream2>,
772    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
773    /// field. Run after `insert_returning` to populate the model.
774    auto_assigns: Vec<TokenStream2>,
775    /// `(ident, column_literal)` pairs for every Auto field. Used by
776    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
777    /// instead of `self`.
778    auto_field_idents: Vec<(syn::Ident, String)>,
779    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
780    /// by the all-Auto-Unset bulk path (Auto cols dropped from
781    /// `columns`).
782    bulk_pushes_no_auto: Vec<TokenStream2>,
783    /// Bulk-insert per-row pushes for **all fields including Auto**.
784    /// Used by the all-Auto-Set bulk path (Auto col included with the
785    /// caller-supplied value).
786    bulk_pushes_all: Vec<TokenStream2>,
787    /// Column-name literals for non-Auto fields only (paired with
788    /// `bulk_pushes_no_auto`).
789    bulk_columns_no_auto: Vec<TokenStream2>,
790    /// Column-name literals for every field including Auto (paired
791    /// with `bulk_pushes_all`).
792    bulk_columns_all: Vec<TokenStream2>,
793    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
794    /// + the loop that asserts every row matches. One pair per Auto
795    /// field. Empty when `has_auto == false`.
796    bulk_auto_uniformity: Vec<TokenStream2>,
797    /// Identifier of the first Auto field, used as the witness for
798    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
799    first_auto_ident: Option<syn::Ident>,
800    /// `true` if any field on the struct is `Auto<T>`.
801    has_auto: bool,
802    /// `true` when the primary-key field's Rust type is `Auto<T>`.
803    /// Gates `save()` codegen — only Auto PKs let us infer
804    /// insert-vs-update from the in-memory value.
805    pk_is_auto: bool,
806    /// `Assignment` constructors for every non-PK column. Drives the
807    /// UPDATE branch of `save()`.
808    update_assignments: Vec<TokenStream2>,
809    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
810    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
811    upsert_update_columns: Vec<TokenStream2>,
812    primary_key: Option<(syn::Ident, String)>,
813    column_entries: Vec<ColumnEntry>,
814    /// Rust-side field names, in declaration order. Used to validate
815    /// container attributes like `display = "…"`.
816    field_names: Vec<String>,
817    /// FK fields on this child model. Drives the reverse-relation
818    /// helper emit — for each FK, the macro adds an inherent
819    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
820    /// method on the parent type.
821    fk_relations: Vec<FkRelation>,
822    /// SQL column name of the `#[rustango(soft_delete)]` field, if
823    /// the model has one. Drives emission of the `soft_delete_on` /
824    /// `restore_on` inherent methods. At most one such column per
825    /// model is allowed; collect_fields rejects duplicates.
826    soft_delete_column: Option<String>,
827}
828
829#[derive(Clone)]
830struct FkRelation {
831    /// Inner type of `ForeignKey<T>` — the parent model. The reverse
832    /// helper is emitted as `impl <ParentType> { … }`.
833    parent_type: Type,
834    /// SQL column name on the child table for this FK (e.g. `"author"`).
835    /// Used in the generated `WHERE <fk_column> = $1` clause.
836    fk_column: String,
837}
838
839fn collect_fields(named: &syn::FieldsNamed) -> syn::Result<CollectedFields> {
840    let cap = named.named.len();
841    let mut out = CollectedFields {
842        field_schemas: Vec::with_capacity(cap),
843        from_row_inits: Vec::with_capacity(cap),
844        from_aliased_row_inits: Vec::with_capacity(cap),
845        insert_columns: Vec::with_capacity(cap),
846        insert_values: Vec::with_capacity(cap),
847        insert_pushes: Vec::with_capacity(cap),
848        returning_cols: Vec::new(),
849        auto_assigns: Vec::new(),
850        auto_field_idents: Vec::new(),
851        bulk_pushes_no_auto: Vec::with_capacity(cap),
852        bulk_pushes_all: Vec::with_capacity(cap),
853        bulk_columns_no_auto: Vec::with_capacity(cap),
854        bulk_columns_all: Vec::with_capacity(cap),
855        bulk_auto_uniformity: Vec::new(),
856        first_auto_ident: None,
857        has_auto: false,
858        pk_is_auto: false,
859        update_assignments: Vec::with_capacity(cap),
860        upsert_update_columns: Vec::with_capacity(cap),
861        primary_key: None,
862        column_entries: Vec::with_capacity(cap),
863        field_names: Vec::with_capacity(cap),
864        fk_relations: Vec::new(),
865        soft_delete_column: None,
866    };
867
868    for field in &named.named {
869        let info = process_field(field)?;
870        out.field_names.push(info.ident.to_string());
871        out.field_schemas.push(info.schema);
872        out.from_row_inits.push(info.from_row_init);
873        out.from_aliased_row_inits.push(info.from_aliased_row_init);
874        if let Some(parent_ty) = info.fk_inner.clone() {
875            out.fk_relations.push(FkRelation {
876                parent_type: parent_ty,
877                fk_column: info.column.clone(),
878            });
879        }
880        if info.soft_delete {
881            if out.soft_delete_column.is_some() {
882                return Err(syn::Error::new_spanned(
883                    field,
884                    "only one field may be marked `#[rustango(soft_delete)]`",
885                ));
886            }
887            out.soft_delete_column = Some(info.column.clone());
888        }
889        let column = info.column.as_str();
890        let ident = info.ident;
891        out.insert_columns.push(quote!(#column));
892        out.insert_values.push(quote! {
893            ::core::convert::Into::<::rustango::core::SqlValue>::into(
894                ::core::clone::Clone::clone(&self.#ident)
895            )
896        });
897        if info.auto {
898            out.has_auto = true;
899            if out.first_auto_ident.is_none() {
900                out.first_auto_ident = Some(ident.clone());
901            }
902            out.returning_cols.push(quote!(#column));
903            out.auto_field_idents
904                .push((ident.clone(), info.column.clone()));
905            out.auto_assigns.push(quote! {
906                self.#ident = ::rustango::sql::sqlx::Row::try_get(&_returning_row, #column)?;
907            });
908            out.insert_pushes.push(quote! {
909                if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
910                    _columns.push(#column);
911                    _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
912                        ::core::clone::Clone::clone(_v)
913                    ));
914                }
915            });
916            // Bulk: Auto fields appear only in the all-Set path,
917            // never in the Unset path (we drop them from `columns`).
918            out.bulk_columns_all.push(quote!(#column));
919            out.bulk_pushes_all.push(quote! {
920                _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
921                    ::core::clone::Clone::clone(&_row.#ident)
922                ));
923            });
924            // Uniformity check: every row's Auto state must match the
925            // first row's. Mixed Set/Unset within one bulk_insert is
926            // rejected here so the column list stays consistent.
927            let ident_clone = ident.clone();
928            out.bulk_auto_uniformity.push(quote! {
929                for _r in rows.iter().skip(1) {
930                    if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
931                        return ::core::result::Result::Err(
932                            ::rustango::sql::ExecError::Sql(
933                                ::rustango::sql::SqlError::BulkAutoMixed
934                            )
935                        );
936                    }
937                }
938            });
939        } else {
940            out.insert_pushes.push(quote! {
941                _columns.push(#column);
942                _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
943                    ::core::clone::Clone::clone(&self.#ident)
944                ));
945            });
946            // Bulk: non-Auto fields appear in BOTH paths.
947            out.bulk_columns_no_auto.push(quote!(#column));
948            out.bulk_columns_all.push(quote!(#column));
949            let push_expr = quote! {
950                _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
951                    ::core::clone::Clone::clone(&_row.#ident)
952                ));
953            };
954            out.bulk_pushes_no_auto.push(push_expr.clone());
955            out.bulk_pushes_all.push(push_expr);
956        }
957        if info.primary_key {
958            if out.primary_key.is_some() {
959                return Err(syn::Error::new_spanned(
960                    field,
961                    "only one field may be marked `#[rustango(primary_key)]`",
962                ));
963            }
964            out.primary_key = Some((ident.clone(), info.column.clone()));
965            if info.auto {
966                out.pk_is_auto = true;
967            }
968        } else if info.auto_now_add {
969            // Immutable post-insert: skip from UPDATE entirely.
970        } else if info.auto_now {
971            // `auto_now` columns: bind `chrono::Utc::now()` on every
972            // UPDATE so the column is always overridden with the
973            // wall-clock at write time, regardless of what value the
974            // user left in the struct field.
975            out.update_assignments.push(quote! {
976                ::rustango::core::Assignment {
977                    column: #column,
978                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
979                        ::chrono::Utc::now()
980                    ),
981                }
982            });
983            out.upsert_update_columns.push(quote!(#column));
984        } else {
985            out.update_assignments.push(quote! {
986                ::rustango::core::Assignment {
987                    column: #column,
988                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
989                        ::core::clone::Clone::clone(&self.#ident)
990                    ),
991                }
992            });
993            out.upsert_update_columns.push(quote!(#column));
994        }
995        out.column_entries.push(ColumnEntry {
996            ident: ident.clone(),
997            value_ty: info.value_ty.clone(),
998            name: ident.to_string(),
999            column: info.column.clone(),
1000            field_type_tokens: info.field_type_tokens,
1001        });
1002    }
1003    Ok(out)
1004}
1005
1006fn model_impl_tokens(
1007    struct_name: &syn::Ident,
1008    model_name: &str,
1009    table: &str,
1010    display: Option<&str>,
1011    app_label: Option<&str>,
1012    admin: Option<&AdminAttrs>,
1013    field_schemas: &[TokenStream2],
1014    soft_delete_column: Option<&str>,
1015    permissions: bool,
1016    audit_track: Option<&[String]>,
1017    m2m_relations: &[M2MAttr],
1018    indexes: &[IndexAttr],
1019    checks: &[CheckAttr],
1020) -> TokenStream2 {
1021    let display_tokens = if let Some(name) = display {
1022        quote!(::core::option::Option::Some(#name))
1023    } else {
1024        quote!(::core::option::Option::None)
1025    };
1026    let app_label_tokens = if let Some(name) = app_label {
1027        quote!(::core::option::Option::Some(#name))
1028    } else {
1029        quote!(::core::option::Option::None)
1030    };
1031    let soft_delete_tokens = if let Some(col) = soft_delete_column {
1032        quote!(::core::option::Option::Some(#col))
1033    } else {
1034        quote!(::core::option::Option::None)
1035    };
1036    let audit_track_tokens = match audit_track {
1037        None => quote!(::core::option::Option::None),
1038        Some(names) => {
1039            let lits = names.iter().map(|n| n.as_str());
1040            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1041        }
1042    };
1043    let admin_tokens = admin_config_tokens(admin);
1044    let indexes_tokens = indexes.iter().map(|idx| {
1045        let name = idx.name.as_deref().unwrap_or("unnamed_index");
1046        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1047        let unique = idx.unique;
1048        quote! {
1049            ::rustango::core::IndexSchema {
1050                name: #name,
1051                columns: &[ #(#cols),* ],
1052                unique: #unique,
1053            }
1054        }
1055    });
1056    let checks_tokens = checks.iter().map(|c| {
1057        let name = c.name.as_str();
1058        let expr = c.expr.as_str();
1059        quote! {
1060            ::rustango::core::CheckConstraint {
1061                name: #name,
1062                expr: #expr,
1063            }
1064        }
1065    });
1066    let m2m_tokens = m2m_relations.iter().map(|rel| {
1067        let name = rel.name.as_str();
1068        let to = rel.to.as_str();
1069        let through = rel.through.as_str();
1070        let src = rel.src.as_str();
1071        let dst = rel.dst.as_str();
1072        quote! {
1073            ::rustango::core::M2MRelation {
1074                name: #name,
1075                to: #to,
1076                through: #through,
1077                src_col: #src,
1078                dst_col: #dst,
1079            }
1080        }
1081    });
1082    quote! {
1083        impl ::rustango::core::Model for #struct_name {
1084            const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1085                name: #model_name,
1086                table: #table,
1087                fields: &[ #(#field_schemas),* ],
1088                display: #display_tokens,
1089                app_label: #app_label_tokens,
1090                admin: #admin_tokens,
1091                soft_delete_column: #soft_delete_tokens,
1092                permissions: #permissions,
1093                audit_track: #audit_track_tokens,
1094                m2m: &[ #(#m2m_tokens),* ],
1095                indexes: &[ #(#indexes_tokens),* ],
1096                check_constraints: &[ #(#checks_tokens),* ],
1097            };
1098        }
1099    }
1100}
1101
1102/// Emit the `admin: Option<&'static AdminConfig>` field for the model
1103/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
1104/// otherwise a static reference to a populated `AdminConfig`.
1105fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1106    let Some(admin) = admin else {
1107        return quote!(::core::option::Option::None);
1108    };
1109
1110    let list_display = admin
1111        .list_display
1112        .as_ref()
1113        .map(|(v, _)| v.as_slice())
1114        .unwrap_or(&[]);
1115    let list_display_lits = list_display.iter().map(|s| s.as_str());
1116
1117    let search_fields = admin
1118        .search_fields
1119        .as_ref()
1120        .map(|(v, _)| v.as_slice())
1121        .unwrap_or(&[]);
1122    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1123
1124    let readonly_fields = admin
1125        .readonly_fields
1126        .as_ref()
1127        .map(|(v, _)| v.as_slice())
1128        .unwrap_or(&[]);
1129    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1130
1131    let list_filter = admin
1132        .list_filter
1133        .as_ref()
1134        .map(|(v, _)| v.as_slice())
1135        .unwrap_or(&[]);
1136    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1137
1138    let actions = admin
1139        .actions
1140        .as_ref()
1141        .map(|(v, _)| v.as_slice())
1142        .unwrap_or(&[]);
1143    let actions_lits = actions.iter().map(|s| s.as_str());
1144
1145    let fieldsets = admin
1146        .fieldsets
1147        .as_ref()
1148        .map(|(v, _)| v.as_slice())
1149        .unwrap_or(&[]);
1150    let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1151        let title = title.as_str();
1152        let field_lits = fields.iter().map(|s| s.as_str());
1153        quote!(::rustango::core::Fieldset {
1154            title: #title,
1155            fields: &[ #( #field_lits ),* ],
1156        })
1157    });
1158
1159    let list_per_page = admin.list_per_page.unwrap_or(0);
1160
1161    let ordering_pairs = admin
1162        .ordering
1163        .as_ref()
1164        .map(|(v, _)| v.as_slice())
1165        .unwrap_or(&[]);
1166    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1167        let name = name.as_str();
1168        let desc = *desc;
1169        quote!((#name, #desc))
1170    });
1171
1172    quote! {
1173        ::core::option::Option::Some(&::rustango::core::AdminConfig {
1174            list_display: &[ #( #list_display_lits ),* ],
1175            search_fields: &[ #( #search_fields_lits ),* ],
1176            list_per_page: #list_per_page,
1177            ordering: &[ #( #ordering_tokens ),* ],
1178            readonly_fields: &[ #( #readonly_fields_lits ),* ],
1179            list_filter: &[ #( #list_filter_lits ),* ],
1180            actions: &[ #( #actions_lits ),* ],
1181            fieldsets: &[ #( #fieldset_tokens ),* ],
1182        })
1183    }
1184}
1185
1186fn inherent_impl_tokens(
1187    struct_name: &syn::Ident,
1188    fields: &CollectedFields,
1189    primary_key: Option<&(syn::Ident, String)>,
1190    column_consts: &TokenStream2,
1191    audited_fields: Option<&[&ColumnEntry]>,
1192) -> TokenStream2 {
1193    // Audit-emit fragments threaded into write paths. Non-empty only
1194    // when the model carries `#[rustango(audit(...))]`. They reborrow
1195    // `_executor` (a `&mut PgConnection` for audited models — the
1196    // macro switches the signature below) so the data write and the
1197    // audit INSERT both run on the same caller-supplied connection.
1198    let executor_passes_to_data_write = if audited_fields.is_some() {
1199        quote!(&mut *_executor)
1200    } else {
1201        quote!(_executor)
1202    };
1203    let executor_param = if audited_fields.is_some() {
1204        quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1205    } else {
1206        quote!(_executor: _E)
1207    };
1208    let executor_generics = if audited_fields.is_some() {
1209        quote!()
1210    } else {
1211        quote!(<'_c, _E>)
1212    };
1213    let executor_where = if audited_fields.is_some() {
1214        quote!()
1215    } else {
1216        quote! {
1217            where
1218                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1219        }
1220    };
1221    // For audited models the `_on` methods take `&mut PgConnection`, so
1222    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
1223    // must acquire a connection first. Non-audited models keep the
1224    // direct delegation since `&PgPool` IS an Executor.
1225    let pool_to_save_on = if audited_fields.is_some() {
1226        quote! {
1227            let mut _conn = pool.acquire().await?;
1228            self.save_on(&mut *_conn).await
1229        }
1230    } else {
1231        quote!(self.save_on(pool).await)
1232    };
1233    let pool_to_insert_on = if audited_fields.is_some() {
1234        quote! {
1235            let mut _conn = pool.acquire().await?;
1236            self.insert_on(&mut *_conn).await
1237        }
1238    } else {
1239        quote!(self.insert_on(pool).await)
1240    };
1241    let pool_to_delete_on = if audited_fields.is_some() {
1242        quote! {
1243            let mut _conn = pool.acquire().await?;
1244            self.delete_on(&mut *_conn).await
1245        }
1246    } else {
1247        quote!(self.delete_on(pool).await)
1248    };
1249    let pool_to_bulk_insert_on = if audited_fields.is_some() {
1250        quote! {
1251            let mut _conn = pool.acquire().await?;
1252            Self::bulk_insert_on(rows, &mut *_conn).await
1253        }
1254    } else {
1255        quote!(Self::bulk_insert_on(rows, pool).await)
1256    };
1257
1258    // Build the (column, JSON value) pair list used by every
1259    // snapshot-style audit emission. Reused across delete_on,
1260    // soft_delete_on, restore_on, and (later) bulk paths. Empty
1261    // when the model isn't audited.
1262    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1263        .map(|tracked| {
1264            tracked
1265                .iter()
1266                .map(|c| {
1267                    let column_lit = c.column.as_str();
1268                    let ident = &c.ident;
1269                    quote! {
1270                        (
1271                            #column_lit,
1272                            ::serde_json::to_value(&self.#ident)
1273                                .unwrap_or(::serde_json::Value::Null),
1274                        )
1275                    }
1276                })
1277                .collect()
1278        })
1279        .unwrap_or_default();
1280    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1281        if fields.pk_is_auto {
1282            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1283        } else {
1284            quote!(::std::format!("{}", &self.#pk_ident))
1285        }
1286    } else {
1287        quote!(::std::string::String::new())
1288    };
1289    let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1290        if audited_fields.is_some() {
1291            let pairs = audit_pair_tokens.iter();
1292            let pk_str = audit_pk_to_string.clone();
1293            quote! {
1294                let _audit_entry = ::rustango::audit::PendingEntry {
1295                    entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1296                    entity_pk: #pk_str,
1297                    operation: #op_path,
1298                    source: ::rustango::audit::current_source(),
1299                    changes: ::rustango::audit::snapshot_changes(&[
1300                        #( #pairs ),*
1301                    ]),
1302                };
1303                ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1304            }
1305        } else {
1306            quote!()
1307        }
1308    };
1309    let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1310    let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1311    let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1312    let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1313
1314    // Update emission captures both BEFORE and AFTER state — runs an
1315    // extra SELECT against `_executor` BEFORE the UPDATE, captures
1316    // each tracked field's prior value, then after the UPDATE diffs
1317    // against the in-memory `&self`. `diff_changes` drops unchanged
1318    // columns so the JSON only contains the actual delta.
1319    //
1320    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
1321    // and binds `_audit_before_pairs`; `audit_update_post` runs
1322    // after the UPDATE and emits the PendingEntry.
1323    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) =
1324        if let Some(tracked) = audited_fields {
1325            if tracked.is_empty() {
1326                (quote!(), quote!())
1327            } else {
1328                let select_cols: String = tracked
1329                    .iter()
1330                    .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
1331                    .collect::<Vec<_>>()
1332                    .join(", ");
1333                let pk_column_for_select = primary_key
1334                    .map(|(_, col)| col.clone())
1335                    .unwrap_or_default();
1336                let select_cols_lit = select_cols;
1337                let pk_column_lit_for_select = pk_column_for_select;
1338                let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
1339                    if fields.pk_is_auto {
1340                        quote!(self.#pk_ident.get().copied().unwrap_or_default())
1341                    } else {
1342                        quote!(::core::clone::Clone::clone(&self.#pk_ident))
1343                    }
1344                } else {
1345                    quote!(0_i64)
1346                };
1347                let before_pairs = tracked.iter().map(|c| {
1348                    let column_lit = c.column.as_str();
1349                    let value_ty = &c.value_ty;
1350                    quote! {
1351                        (
1352                            #column_lit,
1353                            match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
1354                                &_audit_before_row, #column_lit,
1355                            ) {
1356                                ::core::result::Result::Ok(v) => {
1357                                    ::serde_json::to_value(&v)
1358                                        .unwrap_or(::serde_json::Value::Null)
1359                                }
1360                                ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1361                            },
1362                        )
1363                    }
1364                });
1365                let after_pairs = tracked.iter().map(|c| {
1366                    let column_lit = c.column.as_str();
1367                    let ident = &c.ident;
1368                    quote! {
1369                        (
1370                            #column_lit,
1371                            ::serde_json::to_value(&self.#ident)
1372                                .unwrap_or(::serde_json::Value::Null),
1373                        )
1374                    }
1375                });
1376                let pk_str = audit_pk_to_string.clone();
1377                let pre = quote! {
1378                    let _audit_select_sql = ::std::format!(
1379                        r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
1380                        #select_cols_lit,
1381                        <Self as ::rustango::core::Model>::SCHEMA.table,
1382                        #pk_column_lit_for_select,
1383                    );
1384                    let _audit_before_pairs:
1385                        ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
1386                        match ::rustango::sql::sqlx::query(&_audit_select_sql)
1387                            .bind(#pk_value_for_bind)
1388                            .fetch_optional(&mut *_executor)
1389                            .await
1390                        {
1391                            ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
1392                                ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
1393                            }
1394                            _ => ::core::option::Option::None,
1395                        };
1396                };
1397                let post = quote! {
1398                    if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
1399                        let _audit_after:
1400                            ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
1401                            ::std::vec![ #( #after_pairs ),* ];
1402                        let _audit_entry = ::rustango::audit::PendingEntry {
1403                            entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1404                            entity_pk: #pk_str,
1405                            operation: ::rustango::audit::AuditOp::Update,
1406                            source: ::rustango::audit::current_source(),
1407                            changes: ::rustango::audit::diff_changes(
1408                                &_audit_before,
1409                                &_audit_after,
1410                            ),
1411                        };
1412                        ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1413                    }
1414                };
1415                (pre, post)
1416            }
1417        } else {
1418            (quote!(), quote!())
1419        };
1420
1421    // Bulk-insert audit: capture every row's tracked fields after the
1422    // RETURNING populates each PK, then push one batched INSERT INTO
1423    // audit_log via `emit_many`. One round-trip regardless of N rows.
1424    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
1425        let row_pk_str = if let Some((pk_ident, _)) = primary_key {
1426            if fields.pk_is_auto {
1427                quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1428            } else {
1429                quote!(::std::format!("{}", &_row.#pk_ident))
1430            }
1431        } else {
1432            quote!(::std::string::String::new())
1433        };
1434        let row_pairs = audited_fields
1435            .unwrap_or(&[])
1436            .iter()
1437            .map(|c| {
1438                let column_lit = c.column.as_str();
1439                let ident = &c.ident;
1440                quote! {
1441                    (
1442                        #column_lit,
1443                        ::serde_json::to_value(&_row.#ident)
1444                            .unwrap_or(::serde_json::Value::Null),
1445                    )
1446                }
1447            });
1448        quote! {
1449            let _audit_source = ::rustango::audit::current_source();
1450            let mut _audit_entries:
1451                ::std::vec::Vec<::rustango::audit::PendingEntry> =
1452                    ::std::vec::Vec::with_capacity(rows.len());
1453            for _row in rows.iter() {
1454                _audit_entries.push(::rustango::audit::PendingEntry {
1455                    entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1456                    entity_pk: #row_pk_str,
1457                    operation: ::rustango::audit::AuditOp::Create,
1458                    source: _audit_source.clone(),
1459                    changes: ::rustango::audit::snapshot_changes(&[
1460                        #( #row_pairs ),*
1461                    ]),
1462                });
1463            }
1464            ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
1465        }
1466    } else {
1467        quote!()
1468    };
1469
1470    let save_method = if fields.pk_is_auto {
1471        let (pk_ident, pk_column) = primary_key
1472            .expect("pk_is_auto implies primary_key is Some");
1473        let pk_column_lit = pk_column.as_str();
1474        let assignments = &fields.update_assignments;
1475        let upsert_cols = &fields.upsert_update_columns;
1476        let upsert_pushes = &fields.insert_pushes;
1477        let upsert_returning = &fields.returning_cols;
1478        let upsert_auto_assigns = &fields.auto_assigns;
1479        let conflict_clause = if fields.upsert_update_columns.is_empty() {
1480            quote!(::rustango::core::ConflictClause::DoNothing)
1481        } else {
1482            quote!(::rustango::core::ConflictClause::DoUpdate {
1483                target: ::std::vec![#pk_column_lit],
1484                update_columns: ::std::vec![ #( #upsert_cols ),* ],
1485            })
1486        };
1487        Some(quote! {
1488            /// Insert this row if its `Auto<T>` primary key is
1489            /// `Unset`, otherwise update the existing row matching the
1490            /// PK. Mirrors Django's `save()` — caller doesn't need to
1491            /// pick `insert` vs the bulk-update path manually.
1492            ///
1493            /// On the insert branch, populates the PK from `RETURNING`
1494            /// (same behavior as `insert`). On the update branch,
1495            /// writes every non-PK column back; if no row matches the
1496            /// PK, returns `Ok(())` silently.
1497            ///
1498            /// Only generated when the primary key is declared as
1499            /// `Auto<T>`. Models with a manually-managed PK must use
1500            /// `insert` or the QuerySet update builder.
1501            ///
1502            /// # Errors
1503            /// Returns [`::rustango::sql::ExecError`] for SQL-writing
1504            /// or driver failures.
1505            pub async fn save(
1506                &mut self,
1507                pool: &::rustango::sql::sqlx::PgPool,
1508            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1509                #pool_to_save_on
1510            }
1511
1512            /// Like [`Self::save`] but accepts any sqlx executor —
1513            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
1514            /// escape hatch for tenant-scoped writes: schema-mode
1515            /// tenants share the registry pool but rely on a per-
1516            /// checkout `SET search_path`, so passing `&PgPool` would
1517            /// silently hit the wrong schema. Acquire a connection
1518            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
1519            ///
1520            /// # Errors
1521            /// As [`Self::save`].
1522            pub async fn save_on #executor_generics (
1523                &mut self,
1524                #executor_param,
1525            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1526            #executor_where
1527            {
1528                if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1529                    return self.insert_on(#executor_passes_to_data_write).await;
1530                }
1531                #audit_update_pre
1532                let _query = ::rustango::core::UpdateQuery {
1533                    model: <Self as ::rustango::core::Model>::SCHEMA,
1534                    set: ::std::vec![ #( #assignments ),* ],
1535                    where_clause: ::rustango::core::WhereExpr::Predicate(
1536                        ::rustango::core::Filter {
1537                            column: #pk_column_lit,
1538                            op: ::rustango::core::Op::Eq,
1539                            value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1540                                ::core::clone::Clone::clone(&self.#pk_ident)
1541                            ),
1542                        }
1543                    ),
1544                };
1545                let _ = ::rustango::sql::update_on(
1546                    #executor_passes_to_data_write,
1547                    &_query,
1548                ).await?;
1549                #audit_update_post
1550                ::core::result::Result::Ok(())
1551            }
1552
1553            /// Per-call override for the audit source. Runs
1554            /// [`Self::save_on`] inside an [`::rustango::audit::with_source`]
1555            /// scope so the resulting audit entry records `source`
1556            /// instead of the task-local default. Useful for seed
1557            /// scripts and one-off CLI tools that don't sit inside an
1558            /// admin handler. The override applies only to this call;
1559            /// no global state changes.
1560            ///
1561            /// # Errors
1562            /// As [`Self::save_on`].
1563            pub async fn save_on_with #executor_generics (
1564                &mut self,
1565                #executor_param,
1566                source: ::rustango::audit::AuditSource,
1567            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1568            #executor_where
1569            {
1570                ::rustango::audit::with_source(source, self.save_on(_executor)).await
1571            }
1572
1573            /// Insert this row or update it in-place if the primary key already
1574            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
1575            ///
1576            /// With `Auto::Unset` PK the server assigns a new key and no conflict
1577            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
1578            /// inserted if absent or all non-PK columns are overwritten if present.
1579            ///
1580            /// # Errors
1581            /// As [`Self::insert_on`].
1582            pub async fn upsert(
1583                &mut self,
1584                pool: &::rustango::sql::sqlx::PgPool,
1585            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1586                self.upsert_on(pool).await
1587            }
1588
1589            /// Like [`Self::upsert`] but accepts any sqlx executor.
1590            /// See [`Self::save_on`] for tenancy-scoped rationale.
1591            ///
1592            /// # Errors
1593            /// As [`Self::upsert`].
1594            pub async fn upsert_on #executor_generics (
1595                &mut self,
1596                #executor_param,
1597            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1598            #executor_where
1599            {
1600                let mut _columns: ::std::vec::Vec<&'static str> =
1601                    ::std::vec::Vec::new();
1602                let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1603                    ::std::vec::Vec::new();
1604                #( #upsert_pushes )*
1605                let query = ::rustango::core::InsertQuery {
1606                    model: <Self as ::rustango::core::Model>::SCHEMA,
1607                    columns: _columns,
1608                    values: _values,
1609                    returning: ::std::vec![ #( #upsert_returning ),* ],
1610                    on_conflict: ::core::option::Option::Some(#conflict_clause),
1611                };
1612                let _returning_row = ::rustango::sql::insert_returning_on(
1613                    #executor_passes_to_data_write,
1614                    &query,
1615                ).await?;
1616                #( #upsert_auto_assigns )*
1617                ::core::result::Result::Ok(())
1618            }
1619        })
1620    } else {
1621        None
1622    };
1623
1624    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
1625        let pk_column_lit = pk_column.as_str();
1626        // Optional `soft_delete_on` / `restore_on` companions when the
1627        // model has a `#[rustango(soft_delete)]` column. They land
1628        // alongside the regular `delete_on` so callers have both
1629        // options — a hard delete (audit-tracked as a real DELETE) and
1630        // a logical delete (audit-tracked as an UPDATE setting the
1631        // deleted_at column to NOW()).
1632        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
1633            let col_lit = col;
1634            quote! {
1635                /// Soft-delete this row by setting its
1636                /// `#[rustango(soft_delete)]` column to `NOW()`.
1637                /// Mirrors Django's `SoftDeleteModel.delete()` shape:
1638                /// the row stays in the table; query helpers can
1639                /// filter it out by checking the column for `IS NOT
1640                /// NULL`.
1641                ///
1642                /// # Errors
1643                /// As [`Self::delete`].
1644                pub async fn soft_delete_on #executor_generics (
1645                    &self,
1646                    #executor_param,
1647                ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1648                #executor_where
1649                {
1650                    let _query = ::rustango::core::UpdateQuery {
1651                        model: <Self as ::rustango::core::Model>::SCHEMA,
1652                        set: ::std::vec![
1653                            ::rustango::core::Assignment {
1654                                column: #col_lit,
1655                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1656                                    ::chrono::Utc::now()
1657                                ),
1658                            },
1659                        ],
1660                        where_clause: ::rustango::core::WhereExpr::Predicate(
1661                            ::rustango::core::Filter {
1662                                column: #pk_column_lit,
1663                                op: ::rustango::core::Op::Eq,
1664                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1665                                    ::core::clone::Clone::clone(&self.#pk_ident)
1666                                ),
1667                            }
1668                        ),
1669                    };
1670                    let _affected = ::rustango::sql::update_on(
1671                        #executor_passes_to_data_write,
1672                        &_query,
1673                    ).await?;
1674                    #audit_softdelete_emit
1675                    ::core::result::Result::Ok(_affected)
1676                }
1677
1678                /// Inverse of [`Self::soft_delete_on`] — clears the
1679                /// soft-delete column back to NULL so the row is
1680                /// considered live again.
1681                ///
1682                /// # Errors
1683                /// As [`Self::delete`].
1684                pub async fn restore_on #executor_generics (
1685                    &self,
1686                    #executor_param,
1687                ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1688                #executor_where
1689                {
1690                    let _query = ::rustango::core::UpdateQuery {
1691                        model: <Self as ::rustango::core::Model>::SCHEMA,
1692                        set: ::std::vec![
1693                            ::rustango::core::Assignment {
1694                                column: #col_lit,
1695                                value: ::rustango::core::SqlValue::Null,
1696                            },
1697                        ],
1698                        where_clause: ::rustango::core::WhereExpr::Predicate(
1699                            ::rustango::core::Filter {
1700                                column: #pk_column_lit,
1701                                op: ::rustango::core::Op::Eq,
1702                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1703                                    ::core::clone::Clone::clone(&self.#pk_ident)
1704                                ),
1705                            }
1706                        ),
1707                    };
1708                    let _affected = ::rustango::sql::update_on(
1709                        #executor_passes_to_data_write,
1710                        &_query,
1711                    ).await?;
1712                    #audit_restore_emit
1713                    ::core::result::Result::Ok(_affected)
1714                }
1715            }
1716        } else {
1717            quote!()
1718        };
1719        quote! {
1720            /// Delete the row identified by this instance's primary key.
1721            ///
1722            /// Returns the number of rows affected (0 or 1).
1723            ///
1724            /// # Errors
1725            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
1726            /// driver failures.
1727            pub async fn delete(
1728                &self,
1729                pool: &::rustango::sql::sqlx::PgPool,
1730            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
1731                #pool_to_delete_on
1732            }
1733
1734            /// Like [`Self::delete`] but accepts any sqlx executor —
1735            /// for tenant-scoped deletes against an explicitly-acquired
1736            /// connection. See [`Self::save_on`] for the rationale.
1737            ///
1738            /// # Errors
1739            /// As [`Self::delete`].
1740            pub async fn delete_on #executor_generics (
1741                &self,
1742                #executor_param,
1743            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1744            #executor_where
1745            {
1746                let query = ::rustango::core::DeleteQuery {
1747                    model: <Self as ::rustango::core::Model>::SCHEMA,
1748                    where_clause: ::rustango::core::WhereExpr::Predicate(
1749                        ::rustango::core::Filter {
1750                            column: #pk_column_lit,
1751                            op: ::rustango::core::Op::Eq,
1752                            value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1753                                ::core::clone::Clone::clone(&self.#pk_ident)
1754                            ),
1755                        }
1756                    ),
1757                };
1758                let _affected = ::rustango::sql::delete_on(
1759                    #executor_passes_to_data_write,
1760                    &query,
1761                ).await?;
1762                #audit_delete_emit
1763                ::core::result::Result::Ok(_affected)
1764            }
1765
1766            /// Per-call audit-source override for [`Self::delete_on`].
1767            /// See [`Self::save_on_with`] for shape rationale.
1768            ///
1769            /// # Errors
1770            /// As [`Self::delete_on`].
1771            pub async fn delete_on_with #executor_generics (
1772                &self,
1773                #executor_param,
1774                source: ::rustango::audit::AuditSource,
1775            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1776            #executor_where
1777            {
1778                ::rustango::audit::with_source(source, self.delete_on(_executor)).await
1779            }
1780            #soft_delete_methods
1781        }
1782    });
1783
1784    let insert_method = if fields.has_auto {
1785        let pushes = &fields.insert_pushes;
1786        let returning_cols = &fields.returning_cols;
1787        let auto_assigns = &fields.auto_assigns;
1788        quote! {
1789            /// Insert this row into its table. Skips columns whose
1790            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
1791            /// sequence fills them in, then reads each `Auto` column
1792            /// back via `RETURNING` and stores it on `self`.
1793            ///
1794            /// # Errors
1795            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
1796            /// driver failures.
1797            pub async fn insert(
1798                &mut self,
1799                pool: &::rustango::sql::sqlx::PgPool,
1800            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1801                #pool_to_insert_on
1802            }
1803
1804            /// Like [`Self::insert`] but accepts any sqlx executor.
1805            /// See [`Self::save_on`] for tenancy-scoped rationale.
1806            ///
1807            /// # Errors
1808            /// As [`Self::insert`].
1809            pub async fn insert_on #executor_generics (
1810                &mut self,
1811                #executor_param,
1812            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1813            #executor_where
1814            {
1815                let mut _columns: ::std::vec::Vec<&'static str> =
1816                    ::std::vec::Vec::new();
1817                let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1818                    ::std::vec::Vec::new();
1819                #( #pushes )*
1820                let query = ::rustango::core::InsertQuery {
1821                    model: <Self as ::rustango::core::Model>::SCHEMA,
1822                    columns: _columns,
1823                    values: _values,
1824                    returning: ::std::vec![ #( #returning_cols ),* ],
1825                    on_conflict: ::core::option::Option::None,
1826                };
1827                let _returning_row = ::rustango::sql::insert_returning_on(
1828                    #executor_passes_to_data_write,
1829                    &query,
1830                ).await?;
1831                #( #auto_assigns )*
1832                #audit_insert_emit
1833                ::core::result::Result::Ok(())
1834            }
1835
1836            /// Per-call audit-source override for [`Self::insert_on`].
1837            /// See [`Self::save_on_with`] for shape rationale.
1838            ///
1839            /// # Errors
1840            /// As [`Self::insert_on`].
1841            pub async fn insert_on_with #executor_generics (
1842                &mut self,
1843                #executor_param,
1844                source: ::rustango::audit::AuditSource,
1845            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1846            #executor_where
1847            {
1848                ::rustango::audit::with_source(source, self.insert_on(_executor)).await
1849            }
1850        }
1851    } else {
1852        let insert_columns = &fields.insert_columns;
1853        let insert_values = &fields.insert_values;
1854        quote! {
1855            /// Insert this row into its table.
1856            ///
1857            /// # Errors
1858            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
1859            /// driver failures.
1860            pub async fn insert(
1861                &self,
1862                pool: &::rustango::sql::sqlx::PgPool,
1863            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1864                self.insert_on(pool).await
1865            }
1866
1867            /// Like [`Self::insert`] but accepts any sqlx executor.
1868            /// See [`Self::save_on`] for tenancy-scoped rationale.
1869            ///
1870            /// # Errors
1871            /// As [`Self::insert`].
1872            pub async fn insert_on<'_c, _E>(
1873                &self,
1874                _executor: _E,
1875            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1876            where
1877                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1878            {
1879                let query = ::rustango::core::InsertQuery {
1880                    model: <Self as ::rustango::core::Model>::SCHEMA,
1881                    columns: ::std::vec![ #( #insert_columns ),* ],
1882                    values: ::std::vec![ #( #insert_values ),* ],
1883                    returning: ::std::vec::Vec::new(),
1884                    on_conflict: ::core::option::Option::None,
1885                };
1886                ::rustango::sql::insert_on(_executor, &query).await
1887            }
1888        }
1889    };
1890
1891    let bulk_insert_method = if fields.has_auto {
1892        let cols_no_auto = &fields.bulk_columns_no_auto;
1893        let cols_all = &fields.bulk_columns_all;
1894        let pushes_no_auto = &fields.bulk_pushes_no_auto;
1895        let pushes_all = &fields.bulk_pushes_all;
1896        let returning_cols = &fields.returning_cols;
1897        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
1898        let uniformity = &fields.bulk_auto_uniformity;
1899        let first_auto_ident = fields
1900            .first_auto_ident
1901            .as_ref()
1902            .expect("has_auto implies first_auto_ident is Some");
1903        quote! {
1904            /// Bulk-insert `rows` in a single round-trip. Every row's
1905            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
1906            /// (sequence fills them in) or uniformly `Auto::Set(_)`
1907            /// (caller-supplied values). Mixed Set/Unset is rejected
1908            /// — call `insert` per row for that case.
1909            ///
1910            /// Empty slice is a no-op. Each row's `Auto` fields are
1911            /// populated from the `RETURNING` clause in input order
1912            /// before this returns.
1913            ///
1914            /// # Errors
1915            /// Returns [`::rustango::sql::ExecError`] for validation,
1916            /// SQL-writing, mixed-Auto rejection, or driver failures.
1917            pub async fn bulk_insert(
1918                rows: &mut [Self],
1919                pool: &::rustango::sql::sqlx::PgPool,
1920            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1921                #pool_to_bulk_insert_on
1922            }
1923
1924            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
1925            /// See [`Self::save_on`] for tenancy-scoped rationale.
1926            ///
1927            /// # Errors
1928            /// As [`Self::bulk_insert`].
1929            pub async fn bulk_insert_on #executor_generics (
1930                rows: &mut [Self],
1931                #executor_param,
1932            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1933            #executor_where
1934            {
1935                if rows.is_empty() {
1936                    return ::core::result::Result::Ok(());
1937                }
1938                let _first_unset = matches!(
1939                    rows[0].#first_auto_ident,
1940                    ::rustango::sql::Auto::Unset
1941                );
1942                #( #uniformity )*
1943
1944                let mut _all_rows: ::std::vec::Vec<
1945                    ::std::vec::Vec<::rustango::core::SqlValue>,
1946                > = ::std::vec::Vec::with_capacity(rows.len());
1947                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
1948                    for _row in rows.iter() {
1949                        let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
1950                            ::std::vec::Vec::new();
1951                        #( #pushes_no_auto )*
1952                        _all_rows.push(_row_vals);
1953                    }
1954                    ::std::vec![ #( #cols_no_auto ),* ]
1955                } else {
1956                    for _row in rows.iter() {
1957                        let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
1958                            ::std::vec::Vec::new();
1959                        #( #pushes_all )*
1960                        _all_rows.push(_row_vals);
1961                    }
1962                    ::std::vec![ #( #cols_all ),* ]
1963                };
1964
1965                let _query = ::rustango::core::BulkInsertQuery {
1966                    model: <Self as ::rustango::core::Model>::SCHEMA,
1967                    columns: _columns,
1968                    rows: _all_rows,
1969                    returning: ::std::vec![ #( #returning_cols ),* ],
1970                    on_conflict: ::core::option::Option::None,
1971                };
1972                let _returned = ::rustango::sql::bulk_insert_on(
1973                    #executor_passes_to_data_write,
1974                    &_query,
1975                ).await?;
1976                if _returned.len() != rows.len() {
1977                    return ::core::result::Result::Err(
1978                        ::rustango::sql::ExecError::Sql(
1979                            ::rustango::sql::SqlError::BulkInsertReturningMismatch {
1980                                expected: rows.len(),
1981                                actual: _returned.len(),
1982                            }
1983                        )
1984                    );
1985                }
1986                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
1987                    #auto_assigns_for_row
1988                }
1989                #audit_bulk_insert_emit
1990                ::core::result::Result::Ok(())
1991            }
1992        }
1993    } else {
1994        let cols_all = &fields.bulk_columns_all;
1995        let pushes_all = &fields.bulk_pushes_all;
1996        quote! {
1997            /// Bulk-insert `rows` in a single round-trip. Every row's
1998            /// fields are written verbatim — there are no `Auto<T>`
1999            /// fields on this model.
2000            ///
2001            /// Empty slice is a no-op.
2002            ///
2003            /// # Errors
2004            /// Returns [`::rustango::sql::ExecError`] for validation,
2005            /// SQL-writing, or driver failures.
2006            pub async fn bulk_insert(
2007                rows: &[Self],
2008                pool: &::rustango::sql::sqlx::PgPool,
2009            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2010                Self::bulk_insert_on(rows, pool).await
2011            }
2012
2013            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
2014            /// See [`Self::save_on`] for tenancy-scoped rationale.
2015            ///
2016            /// # Errors
2017            /// As [`Self::bulk_insert`].
2018            pub async fn bulk_insert_on<'_c, _E>(
2019                rows: &[Self],
2020                _executor: _E,
2021            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2022            where
2023                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2024            {
2025                if rows.is_empty() {
2026                    return ::core::result::Result::Ok(());
2027                }
2028                let mut _all_rows: ::std::vec::Vec<
2029                    ::std::vec::Vec<::rustango::core::SqlValue>,
2030                > = ::std::vec::Vec::with_capacity(rows.len());
2031                for _row in rows.iter() {
2032                    let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2033                        ::std::vec::Vec::new();
2034                    #( #pushes_all )*
2035                    _all_rows.push(_row_vals);
2036                }
2037                let _query = ::rustango::core::BulkInsertQuery {
2038                    model: <Self as ::rustango::core::Model>::SCHEMA,
2039                    columns: ::std::vec![ #( #cols_all ),* ],
2040                    rows: _all_rows,
2041                    returning: ::std::vec::Vec::new(),
2042                    on_conflict: ::core::option::Option::None,
2043                };
2044                let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
2045                ::core::result::Result::Ok(())
2046            }
2047        }
2048    };
2049
2050    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
2051        quote! {
2052            /// Hidden runtime accessor for the primary-key value as a
2053            /// [`SqlValue`]. Used by reverse-relation helpers
2054            /// (`<parent>::<child>_set`) emitted from sibling models'
2055            /// FK fields. Not part of the public API.
2056            #[doc(hidden)]
2057            pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
2058                ::core::convert::Into::<::rustango::core::SqlValue>::into(
2059                    ::core::clone::Clone::clone(&self.#pk_ident)
2060                )
2061            }
2062        }
2063    });
2064
2065    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
2066        quote! {
2067            impl ::rustango::sql::HasPkValue for #struct_name {
2068                fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
2069                    ::core::convert::Into::<::rustango::core::SqlValue>::into(
2070                        ::core::clone::Clone::clone(&self.#pk_ident)
2071                    )
2072                }
2073            }
2074        }
2075    });
2076
2077    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
2078
2079    let from_aliased_row_inits = &fields.from_aliased_row_inits;
2080    let aliased_row_helper = quote! {
2081        /// Decode a row's aliased target columns (produced by
2082        /// `select_related`'s LEFT JOIN) into a fresh instance of
2083        /// this model. Reads each column via
2084        /// `format!("{prefix}__{col}")`, matching the alias the
2085        /// SELECT writer emitted. Slice 9.0d.
2086        #[doc(hidden)]
2087        pub fn __rustango_from_aliased_row(
2088            row: &::rustango::sql::sqlx::postgres::PgRow,
2089            prefix: &str,
2090        ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
2091            ::core::result::Result::Ok(Self {
2092                #( #from_aliased_row_inits ),*
2093            })
2094        }
2095    };
2096
2097    let load_related_impl =
2098        load_related_impl_tokens(struct_name, &fields.fk_relations);
2099
2100    quote! {
2101        impl #struct_name {
2102            /// Start a new `QuerySet` over this model.
2103            #[must_use]
2104            pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
2105                ::rustango::query::QuerySet::new()
2106            }
2107
2108            #insert_method
2109
2110            #bulk_insert_method
2111
2112            #save_method
2113
2114            #pk_methods
2115
2116            #pk_value_helper
2117
2118            #aliased_row_helper
2119
2120            #column_consts
2121        }
2122
2123        #load_related_impl
2124
2125        #has_pk_value_impl
2126
2127        #fk_pk_access_impl
2128    }
2129}
2130
2131/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
2132/// `auto_assigns` but reading from `_returning_row` and writing to
2133/// `_row_mut` instead of `self`.
2134fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
2135    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
2136        let col_lit = column.as_str();
2137        quote! {
2138            _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
2139                _returning_row,
2140                #col_lit,
2141            )?;
2142        }
2143    });
2144    quote! { #( #lines )* }
2145}
2146
2147/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
2148fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
2149    let lines = entries.iter().map(|e| {
2150        let ident = &e.ident;
2151        let col_ty = column_type_ident(ident);
2152        quote! {
2153            #[allow(non_upper_case_globals)]
2154            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
2155        }
2156    });
2157    quote! { #(#lines)* }
2158}
2159
2160/// Emit a hidden per-model module carrying one zero-sized type per field,
2161/// each with a `Column` impl pointing back at the model.
2162fn column_module_tokens(
2163    module_ident: &syn::Ident,
2164    struct_name: &syn::Ident,
2165    entries: &[ColumnEntry],
2166) -> TokenStream2 {
2167    let items = entries.iter().map(|e| {
2168        let col_ty = column_type_ident(&e.ident);
2169        let value_ty = &e.value_ty;
2170        let name = &e.name;
2171        let column = &e.column;
2172        let field_type_tokens = &e.field_type_tokens;
2173        quote! {
2174            #[derive(::core::clone::Clone, ::core::marker::Copy)]
2175            pub struct #col_ty;
2176
2177            impl ::rustango::core::Column for #col_ty {
2178                type Model = super::#struct_name;
2179                type Value = #value_ty;
2180                const NAME: &'static str = #name;
2181                const COLUMN: &'static str = #column;
2182                const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
2183            }
2184        }
2185    });
2186    quote! {
2187        #[doc(hidden)]
2188        #[allow(non_camel_case_types, non_snake_case)]
2189        pub mod #module_ident {
2190            // Re-import the parent scope so field types referencing
2191            // sibling models (e.g. `ForeignKey<Author>`) resolve
2192            // inside this submodule. Without this we'd hit
2193            // `proc_macro_derive_resolution_fallback` warnings.
2194            #[allow(unused_imports)]
2195            use super::*;
2196            #(#items)*
2197        }
2198    }
2199}
2200
2201fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
2202    syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
2203}
2204
2205fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
2206    syn::Ident::new(
2207        &format!("__rustango_cols_{struct_name}"),
2208        struct_name.span(),
2209    )
2210}
2211
2212fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
2213    quote! {
2214        impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
2215            for #struct_name
2216        {
2217            fn from_row(
2218                row: &'r ::rustango::sql::sqlx::postgres::PgRow,
2219            ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
2220                ::core::result::Result::Ok(Self {
2221                    #( #from_row_inits ),*
2222                })
2223            }
2224        }
2225    }
2226}
2227
2228struct ContainerAttrs {
2229    table: Option<String>,
2230    display: Option<(String, proc_macro2::Span)>,
2231    /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
2232    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
2233    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
2234    /// at runtime — this attribute is the override for cases where
2235    /// the inference is wrong (e.g. a model that conceptually belongs
2236    /// to one app but is physically in another module).
2237    app: Option<String>,
2238    /// Django ModelAdmin-shape per-model knobs from
2239    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
2240    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
2241    /// admin code falls back to `AdminConfig::DEFAULT`.
2242    admin: Option<AdminAttrs>,
2243    /// Per-model audit configuration from `#[rustango(audit(...))]`.
2244    /// `None` when the model isn't audited — write paths emit no
2245    /// audit entries. When present, single-row writes capture
2246    /// before/after for the listed fields and bulk writes batch
2247    /// snapshots into one INSERT into `rustango_audit_log`.
2248    audit: Option<AuditAttrs>,
2249    /// `true` when `#[rustango(permissions)]` is present. Signals that
2250    /// `auto_create_permissions` should seed the four CRUD codenames for
2251    /// this model.
2252    permissions: bool,
2253    /// Many-to-many relations declared via
2254    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
2255    ///                 src = "post_id", dst = "tag_id"))]`.
2256    m2m: Vec<M2MAttr>,
2257    /// Composite indexes declared via
2258    /// `#[rustango(index("col1, col2"))]` or
2259    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
2260    /// Single-column indexes from `#[rustango(index)]` on fields are
2261    /// accumulated here during field collection.
2262    indexes: Vec<IndexAttr>,
2263    /// Table-level CHECK constraints declared via
2264    /// `#[rustango(check(name = "…", expr = "…"))]`.
2265    checks: Vec<CheckAttr>,
2266}
2267
2268/// Parsed form of one index declaration (field-level or container-level).
2269struct IndexAttr {
2270    /// Index name; auto-derived when `None` at parse time.
2271    name: Option<String>,
2272    /// Column names in the index.
2273    columns: Vec<String>,
2274    /// `true` for `CREATE UNIQUE INDEX`.
2275    unique: bool,
2276}
2277
2278/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
2279struct CheckAttr {
2280    name: String,
2281    expr: String,
2282}
2283
2284/// Parsed form of one `#[rustango(m2m(...))]` declaration.
2285struct M2MAttr {
2286    /// Accessor suffix: `tags` → generates `tags_m2m()`.
2287    name: String,
2288    /// Target table (e.g. `"app_tags"`).
2289    to: String,
2290    /// Junction table (e.g. `"post_tags"`).
2291    through: String,
2292    /// Source FK column in the junction table (e.g. `"post_id"`).
2293    src: String,
2294    /// Destination FK column in the junction table (e.g. `"tag_id"`).
2295    dst: String,
2296}
2297
2298/// Parsed shape of `#[rustango(audit(track = "name, body", source =
2299/// "user"))]`. `track` is a comma-separated list of field names whose
2300/// before/after values land in the JSONB `changes` column. `source`
2301/// is informational only — it pins a default source when the model
2302/// is written outside any `audit::with_source(...)` scope (rare).
2303#[derive(Default)]
2304struct AuditAttrs {
2305    /// Field names to capture in the `changes` JSONB. Validated
2306    /// against declared scalar fields at compile time. Empty means
2307    /// "track every scalar field" — Django's audit-everything default.
2308    track: Option<(Vec<String>, proc_macro2::Span)>,
2309}
2310
2311/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
2312/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
2313/// comma-separated strings; we validate each ident against the model's
2314/// declared fields at compile time.
2315#[derive(Default)]
2316struct AdminAttrs {
2317    list_display: Option<(Vec<String>, proc_macro2::Span)>,
2318    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
2319    list_per_page: Option<usize>,
2320    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
2321    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
2322    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
2323    /// Bulk action names. No field-validation against model fields —
2324    /// these are action handlers, not column references.
2325    actions: Option<(Vec<String>, proc_macro2::Span)>,
2326    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
2327    /// sections, comma-separated fields per section, optional
2328    /// `Title:` prefix. Empty title omits the `<legend>`.
2329    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
2330}
2331
2332fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
2333    let mut out = ContainerAttrs {
2334        table: None,
2335        display: None,
2336        app: None,
2337        admin: None,
2338        audit: None,
2339        permissions: false,
2340        m2m: Vec::new(),
2341        indexes: Vec::new(),
2342        checks: Vec::new(),
2343    };
2344    for attr in &input.attrs {
2345        if !attr.path().is_ident("rustango") {
2346            continue;
2347        }
2348        attr.parse_nested_meta(|meta| {
2349            if meta.path.is_ident("table") {
2350                let s: LitStr = meta.value()?.parse()?;
2351                out.table = Some(s.value());
2352                return Ok(());
2353            }
2354            if meta.path.is_ident("display") {
2355                let s: LitStr = meta.value()?.parse()?;
2356                out.display = Some((s.value(), s.span()));
2357                return Ok(());
2358            }
2359            if meta.path.is_ident("app") {
2360                let s: LitStr = meta.value()?.parse()?;
2361                out.app = Some(s.value());
2362                return Ok(());
2363            }
2364            if meta.path.is_ident("admin") {
2365                let mut admin = AdminAttrs::default();
2366                meta.parse_nested_meta(|inner| {
2367                    if inner.path.is_ident("list_display") {
2368                        let s: LitStr = inner.value()?.parse()?;
2369                        admin.list_display =
2370                            Some((split_field_list(&s.value()), s.span()));
2371                        return Ok(());
2372                    }
2373                    if inner.path.is_ident("search_fields") {
2374                        let s: LitStr = inner.value()?.parse()?;
2375                        admin.search_fields =
2376                            Some((split_field_list(&s.value()), s.span()));
2377                        return Ok(());
2378                    }
2379                    if inner.path.is_ident("readonly_fields") {
2380                        let s: LitStr = inner.value()?.parse()?;
2381                        admin.readonly_fields =
2382                            Some((split_field_list(&s.value()), s.span()));
2383                        return Ok(());
2384                    }
2385                    if inner.path.is_ident("list_per_page") {
2386                        let lit: syn::LitInt = inner.value()?.parse()?;
2387                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
2388                        return Ok(());
2389                    }
2390                    if inner.path.is_ident("ordering") {
2391                        let s: LitStr = inner.value()?.parse()?;
2392                        admin.ordering = Some((
2393                            parse_ordering_list(&s.value()),
2394                            s.span(),
2395                        ));
2396                        return Ok(());
2397                    }
2398                    if inner.path.is_ident("list_filter") {
2399                        let s: LitStr = inner.value()?.parse()?;
2400                        admin.list_filter =
2401                            Some((split_field_list(&s.value()), s.span()));
2402                        return Ok(());
2403                    }
2404                    if inner.path.is_ident("actions") {
2405                        let s: LitStr = inner.value()?.parse()?;
2406                        admin.actions =
2407                            Some((split_field_list(&s.value()), s.span()));
2408                        return Ok(());
2409                    }
2410                    if inner.path.is_ident("fieldsets") {
2411                        let s: LitStr = inner.value()?.parse()?;
2412                        admin.fieldsets =
2413                            Some((parse_fieldset_list(&s.value()), s.span()));
2414                        return Ok(());
2415                    }
2416                    Err(inner.error(
2417                        "unknown admin attribute (supported: \
2418                         `list_display`, `search_fields`, `readonly_fields`, \
2419                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
2420                         `fieldsets`)",
2421                    ))
2422                })?;
2423                out.admin = Some(admin);
2424                return Ok(());
2425            }
2426            if meta.path.is_ident("audit") {
2427                let mut audit = AuditAttrs::default();
2428                meta.parse_nested_meta(|inner| {
2429                    if inner.path.is_ident("track") {
2430                        let s: LitStr = inner.value()?.parse()?;
2431                        audit.track =
2432                            Some((split_field_list(&s.value()), s.span()));
2433                        return Ok(());
2434                    }
2435                    Err(inner.error(
2436                        "unknown audit attribute (supported: `track`)",
2437                    ))
2438                })?;
2439                out.audit = Some(audit);
2440                return Ok(());
2441            }
2442            if meta.path.is_ident("permissions") {
2443                out.permissions = true;
2444                return Ok(());
2445            }
2446            if meta.path.is_ident("index") {
2447                // Container-level composite index:
2448                // #[rustango(index("col1, col2"))]
2449                // #[rustango(index("col1, col2", unique, name = "my_idx"))]
2450                let cols_lit: LitStr = meta.value()?.parse()?;
2451                let columns = split_field_list(&cols_lit.value());
2452                let mut unique = false;
2453                let mut name: Option<String> = None;
2454                if meta.input.peek(syn::Token![,]) {
2455                    let _: syn::Token![,] = meta.input.parse()?;
2456                    // Parse remaining k=v or bare flags after the columns string
2457                    meta.parse_nested_meta(|inner| {
2458                        if inner.path.is_ident("unique") {
2459                            unique = true;
2460                            return Ok(());
2461                        }
2462                        if inner.path.is_ident("name") {
2463                            let s: LitStr = inner.value()?.parse()?;
2464                            name = Some(s.value());
2465                            return Ok(());
2466                        }
2467                        Err(inner.error("unknown index attribute (supported: `unique`, `name`)"))
2468                    })?;
2469                }
2470                out.indexes.push(IndexAttr { name, columns, unique });
2471                return Ok(());
2472            }
2473            if meta.path.is_ident("check") {
2474                // #[rustango(check(name = "…", expr = "…"))]
2475                let mut name: Option<String> = None;
2476                let mut expr: Option<String> = None;
2477                meta.parse_nested_meta(|inner| {
2478                    if inner.path.is_ident("name") {
2479                        let s: LitStr = inner.value()?.parse()?;
2480                        name = Some(s.value());
2481                        return Ok(());
2482                    }
2483                    if inner.path.is_ident("expr") {
2484                        let s: LitStr = inner.value()?.parse()?;
2485                        expr = Some(s.value());
2486                        return Ok(());
2487                    }
2488                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
2489                })?;
2490                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
2491                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
2492                out.checks.push(CheckAttr { name, expr });
2493                return Ok(());
2494            }
2495            if meta.path.is_ident("m2m") {
2496                let mut m2m = M2MAttr {
2497                    name: String::new(),
2498                    to: String::new(),
2499                    through: String::new(),
2500                    src: String::new(),
2501                    dst: String::new(),
2502                };
2503                meta.parse_nested_meta(|inner| {
2504                    if inner.path.is_ident("name") {
2505                        let s: LitStr = inner.value()?.parse()?;
2506                        m2m.name = s.value();
2507                        return Ok(());
2508                    }
2509                    if inner.path.is_ident("to") {
2510                        let s: LitStr = inner.value()?.parse()?;
2511                        m2m.to = s.value();
2512                        return Ok(());
2513                    }
2514                    if inner.path.is_ident("through") {
2515                        let s: LitStr = inner.value()?.parse()?;
2516                        m2m.through = s.value();
2517                        return Ok(());
2518                    }
2519                    if inner.path.is_ident("src") {
2520                        let s: LitStr = inner.value()?.parse()?;
2521                        m2m.src = s.value();
2522                        return Ok(());
2523                    }
2524                    if inner.path.is_ident("dst") {
2525                        let s: LitStr = inner.value()?.parse()?;
2526                        m2m.dst = s.value();
2527                        return Ok(());
2528                    }
2529                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
2530                })?;
2531                if m2m.name.is_empty() {
2532                    return Err(meta.error("m2m requires `name = \"...\"`"));
2533                }
2534                if m2m.to.is_empty() {
2535                    return Err(meta.error("m2m requires `to = \"...\"`"));
2536                }
2537                if m2m.through.is_empty() {
2538                    return Err(meta.error("m2m requires `through = \"...\"`"));
2539                }
2540                if m2m.src.is_empty() {
2541                    return Err(meta.error("m2m requires `src = \"...\"`"));
2542                }
2543                if m2m.dst.is_empty() {
2544                    return Err(meta.error("m2m requires `dst = \"...\"`"));
2545                }
2546                out.m2m.push(m2m);
2547                return Ok(());
2548            }
2549            Err(meta.error("unknown rustango container attribute"))
2550        })?;
2551    }
2552    Ok(out)
2553}
2554
2555/// Split a comma-separated field-name list (e.g. `"name, office"`) into
2556/// owned field names, trimming whitespace and skipping empty entries.
2557/// Field-name validation against the model is done by the caller.
2558fn split_field_list(raw: &str) -> Vec<String> {
2559    raw.split(',')
2560        .map(str::trim)
2561        .filter(|s| !s.is_empty())
2562        .map(str::to_owned)
2563        .collect()
2564}
2565
2566/// Parse the fieldsets DSL: pipe-separated sections, optional
2567/// `"Title:"` prefix on each, comma-separated field names after.
2568/// Examples:
2569/// * `"name, office"` → one untitled section with two fields
2570/// * `"Identity: name, office | Metadata: created_at"` → two titled
2571///   sections
2572///
2573/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
2574fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
2575    raw.split('|')
2576        .map(str::trim)
2577        .filter(|s| !s.is_empty())
2578        .map(|section| {
2579            // Split off an optional `Title:` prefix (first colon).
2580            let (title, rest) = match section.split_once(':') {
2581                Some((title, rest)) if !title.contains(',') => {
2582                    (title.trim().to_owned(), rest)
2583                }
2584                _ => (String::new(), section),
2585            };
2586            let fields = split_field_list(rest);
2587            (title, fields)
2588        })
2589        .collect()
2590}
2591
2592/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
2593/// Returns `(field_name, desc)` pairs in the same order as the input.
2594fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
2595    raw.split(',')
2596        .map(str::trim)
2597        .filter(|s| !s.is_empty())
2598        .map(|spec| {
2599            spec.strip_prefix('-')
2600                .map_or((spec.to_owned(), false), |rest| (rest.trim().to_owned(), true))
2601        })
2602        .collect()
2603}
2604
2605struct FieldAttrs {
2606    column: Option<String>,
2607    primary_key: bool,
2608    fk: Option<String>,
2609    o2o: Option<String>,
2610    on: Option<String>,
2611    max_length: Option<u32>,
2612    min: Option<i64>,
2613    max: Option<i64>,
2614    default: Option<String>,
2615    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
2616    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
2617    /// "gen_random_uuid()"`. The Rust field type must be
2618    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
2619    /// INSERTs so the DB DEFAULT fires.
2620    auto_uuid: bool,
2621    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
2622    /// Server-set on insert, immutable from app code afterwards.
2623    /// Implies `auto + default = "now()"`. Field type must be
2624    /// `DateTime<Utc>`.
2625    auto_now_add: bool,
2626    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
2627    /// every insert AND every update. Implies `auto + default =
2628    /// "now()"`; the macro additionally rewrites `update_on` /
2629    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
2630    /// field value.
2631    auto_now: bool,
2632    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
2633    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
2634    /// `soft_delete_on(executor)` and `restore_on(executor)`
2635    /// methods on the model.
2636    soft_delete: bool,
2637    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
2638    /// the column in the generated DDL.
2639    unique: bool,
2640    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
2641    /// generates a `CREATE INDEX` for this column. `unique` here means
2642    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
2643    index: bool,
2644    index_unique: bool,
2645    index_name: Option<String>,
2646}
2647
2648fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
2649    let mut out = FieldAttrs {
2650        column: None,
2651        primary_key: false,
2652        fk: None,
2653        o2o: None,
2654        on: None,
2655        max_length: None,
2656        min: None,
2657        max: None,
2658        default: None,
2659        auto_uuid: false,
2660        auto_now_add: false,
2661        auto_now: false,
2662        soft_delete: false,
2663        unique: false,
2664        index: false,
2665        index_unique: false,
2666        index_name: None,
2667    };
2668    for attr in &field.attrs {
2669        if !attr.path().is_ident("rustango") {
2670            continue;
2671        }
2672        attr.parse_nested_meta(|meta| {
2673            if meta.path.is_ident("column") {
2674                let s: LitStr = meta.value()?.parse()?;
2675                out.column = Some(s.value());
2676                return Ok(());
2677            }
2678            if meta.path.is_ident("primary_key") {
2679                out.primary_key = true;
2680                return Ok(());
2681            }
2682            if meta.path.is_ident("fk") {
2683                let s: LitStr = meta.value()?.parse()?;
2684                out.fk = Some(s.value());
2685                return Ok(());
2686            }
2687            if meta.path.is_ident("o2o") {
2688                let s: LitStr = meta.value()?.parse()?;
2689                out.o2o = Some(s.value());
2690                return Ok(());
2691            }
2692            if meta.path.is_ident("on") {
2693                let s: LitStr = meta.value()?.parse()?;
2694                out.on = Some(s.value());
2695                return Ok(());
2696            }
2697            if meta.path.is_ident("max_length") {
2698                let lit: syn::LitInt = meta.value()?.parse()?;
2699                out.max_length = Some(lit.base10_parse::<u32>()?);
2700                return Ok(());
2701            }
2702            if meta.path.is_ident("min") {
2703                out.min = Some(parse_signed_i64(&meta)?);
2704                return Ok(());
2705            }
2706            if meta.path.is_ident("max") {
2707                out.max = Some(parse_signed_i64(&meta)?);
2708                return Ok(());
2709            }
2710            if meta.path.is_ident("default") {
2711                let s: LitStr = meta.value()?.parse()?;
2712                out.default = Some(s.value());
2713                return Ok(());
2714            }
2715            if meta.path.is_ident("auto_uuid") {
2716                out.auto_uuid = true;
2717                // Implied: PK + auto + DEFAULT gen_random_uuid().
2718                // Each is also explicitly settable; the explicit
2719                // value wins if conflicting.
2720                out.primary_key = true;
2721                if out.default.is_none() {
2722                    out.default = Some("gen_random_uuid()".into());
2723                }
2724                return Ok(());
2725            }
2726            if meta.path.is_ident("auto_now_add") {
2727                out.auto_now_add = true;
2728                if out.default.is_none() {
2729                    out.default = Some("now()".into());
2730                }
2731                return Ok(());
2732            }
2733            if meta.path.is_ident("auto_now") {
2734                out.auto_now = true;
2735                if out.default.is_none() {
2736                    out.default = Some("now()".into());
2737                }
2738                return Ok(());
2739            }
2740            if meta.path.is_ident("soft_delete") {
2741                out.soft_delete = true;
2742                return Ok(());
2743            }
2744            if meta.path.is_ident("unique") {
2745                out.unique = true;
2746                return Ok(());
2747            }
2748            if meta.path.is_ident("index") {
2749                out.index = true;
2750                // Optional sub-attrs: #[rustango(index(unique, name = "…"))]
2751                if meta.input.peek(syn::token::Paren) {
2752                    meta.parse_nested_meta(|inner| {
2753                        if inner.path.is_ident("unique") {
2754                            out.index_unique = true;
2755                            return Ok(());
2756                        }
2757                        if inner.path.is_ident("name") {
2758                            let s: LitStr = inner.value()?.parse()?;
2759                            out.index_name = Some(s.value());
2760                            return Ok(());
2761                        }
2762                        Err(inner.error("unknown index sub-attribute (supported: `unique`, `name`)"))
2763                    })?;
2764                }
2765                return Ok(());
2766            }
2767            Err(meta.error("unknown rustango field attribute"))
2768        })?;
2769    }
2770    Ok(out)
2771}
2772
2773/// Parse a signed integer literal, accepting optional leading `-`.
2774fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
2775    let expr: syn::Expr = meta.value()?.parse()?;
2776    match expr {
2777        syn::Expr::Lit(syn::ExprLit {
2778            lit: syn::Lit::Int(lit),
2779            ..
2780        }) => lit.base10_parse::<i64>(),
2781        syn::Expr::Unary(syn::ExprUnary {
2782            op: syn::UnOp::Neg(_),
2783            expr,
2784            ..
2785        }) => {
2786            if let syn::Expr::Lit(syn::ExprLit {
2787                lit: syn::Lit::Int(lit),
2788                ..
2789            }) = *expr
2790            {
2791                let v: i64 = lit.base10_parse()?;
2792                Ok(-v)
2793            } else {
2794                Err(syn::Error::new_spanned(expr, "expected integer literal"))
2795            }
2796        }
2797        other => Err(syn::Error::new_spanned(
2798            other,
2799            "expected integer literal (signed)",
2800        )),
2801    }
2802}
2803
2804struct FieldInfo<'a> {
2805    ident: &'a syn::Ident,
2806    column: String,
2807    primary_key: bool,
2808    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
2809    /// skip this column when `Auto::Unset` and emit it under
2810    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
2811    auto: bool,
2812    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
2813    /// the `Column::Value` associated type for typed-column tokens.
2814    value_ty: &'a Type,
2815    /// `FieldType` variant tokens (`::rustango::core::FieldType::I64`).
2816    field_type_tokens: TokenStream2,
2817    schema: TokenStream2,
2818    from_row_init: TokenStream2,
2819    /// Variant of [`Self::from_row_init`] that reads the column via
2820    /// `format!("{prefix}__{col}")` so a model can be decoded out of
2821    /// the aliased columns of a JOINed row. Drives slice 9.0d's
2822    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
2823    /// helper that `select_related` calls when stitching loaded FKs.
2824    from_aliased_row_init: TokenStream2,
2825    /// Inner type from a `ForeignKey<T>` field, if any. The reverse-
2826    /// relation helper emit (`Author::<child>_set`) needs to know `T`
2827    /// to point the generated method at the right child model.
2828    fk_inner: Option<Type>,
2829    /// `true` when this column was marked `#[rustango(auto_now)]` —
2830    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
2831    /// column instead of the user-supplied value, so `updated_at`
2832    /// always reflects the latest write without the caller having
2833    /// to remember to set it.
2834    auto_now: bool,
2835    /// `true` when this column was marked `#[rustango(auto_now_add)]`
2836    /// — the column is server-set on INSERT (DB DEFAULT) and
2837    /// **immutable** afterwards. `update_on` / `save_on` skip the
2838    /// column entirely so a stale `created_at` value in memory never
2839    /// rewrites the persisted timestamp.
2840    auto_now_add: bool,
2841    /// `true` when this column was marked `#[rustango(soft_delete)]`.
2842    /// Triggers emission of `soft_delete_on(executor)` and
2843    /// `restore_on(executor)` on the model's inherent impl. There is
2844    /// at most one such column per model — emission asserts this.
2845    soft_delete: bool,
2846}
2847
2848fn process_field(field: &syn::Field) -> syn::Result<FieldInfo<'_>> {
2849    let attrs = parse_field_attrs(field)?;
2850    let ident = field
2851        .ident
2852        .as_ref()
2853        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
2854    let name = ident.to_string();
2855    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
2856    let primary_key = attrs.primary_key;
2857    let DetectedType {
2858        kind,
2859        nullable,
2860        auto: detected_auto,
2861        fk_inner,
2862    } = detect_type(&field.ty)?;
2863    check_bound_compatibility(field, &attrs, kind)?;
2864    let auto = detected_auto;
2865    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
2866    // INSERT path: the user must wrap the field in `Auto<T>`, which
2867    // marks the column as DB-default-supplied. The mixin attrs then
2868    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
2869    // for `auto_now`, force the value on UPDATE too.
2870    if attrs.auto_uuid {
2871        if kind != DetectedKind::Uuid {
2872            return Err(syn::Error::new_spanned(
2873                field,
2874                "`#[rustango(auto_uuid)]` requires the field type to be \
2875                 `Auto<uuid::Uuid>`",
2876            ));
2877        }
2878        if !detected_auto {
2879            return Err(syn::Error::new_spanned(
2880                field,
2881                "`#[rustango(auto_uuid)]` requires the field type to be \
2882                 wrapped in `Auto<...>` so the macro skips the column on \
2883                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
2884            ));
2885        }
2886    }
2887    if attrs.auto_now_add || attrs.auto_now {
2888        if kind != DetectedKind::DateTime {
2889            return Err(syn::Error::new_spanned(
2890                field,
2891                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
2892                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
2893            ));
2894        }
2895        if !detected_auto {
2896            return Err(syn::Error::new_spanned(
2897                field,
2898                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
2899                 the field type to be wrapped in `Auto<...>` so the macro skips \
2900                 the column on INSERT and the DB DEFAULT (`now()`) fires",
2901            ));
2902        }
2903    }
2904    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
2905        return Err(syn::Error::new_spanned(
2906            field,
2907            "`#[rustango(soft_delete)]` requires the field type to be \
2908             `Option<chrono::DateTime<chrono::Utc>>`",
2909        ));
2910    }
2911    let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
2912    if detected_auto && !primary_key && !is_mixin_auto {
2913        return Err(syn::Error::new_spanned(
2914            field,
2915            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
2916             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
2917             `auto_now`",
2918        ));
2919    }
2920    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
2921        return Err(syn::Error::new_spanned(
2922            field,
2923            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
2924             SERIAL / BIGSERIAL already supplies a default sequence.",
2925        ));
2926    }
2927    if fk_inner.is_some() && primary_key {
2928        return Err(syn::Error::new_spanned(
2929            field,
2930            "`ForeignKey<T>` is not allowed on a primary-key field — \
2931             a row's PK is its own identity, not a reference to a parent.",
2932        ));
2933    }
2934    let relation = relation_tokens(field, &attrs, fk_inner)?;
2935    let column_lit = column.as_str();
2936    let field_type_tokens = kind.variant_tokens();
2937    let max_length = optional_u32(attrs.max_length);
2938    let min = optional_i64(attrs.min);
2939    let max = optional_i64(attrs.max);
2940    let default = optional_str(attrs.default.as_deref());
2941
2942    let unique = attrs.unique;
2943    let schema = quote! {
2944        ::rustango::core::FieldSchema {
2945            name: #name,
2946            column: #column_lit,
2947            ty: #field_type_tokens,
2948            nullable: #nullable,
2949            primary_key: #primary_key,
2950            relation: #relation,
2951            max_length: #max_length,
2952            min: #min,
2953            max: #max,
2954            default: #default,
2955            auto: #auto,
2956            unique: #unique,
2957        }
2958    };
2959
2960    let from_row_init = quote! {
2961        #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
2962    };
2963    let from_aliased_row_init = quote! {
2964        #ident: ::rustango::sql::sqlx::Row::try_get(
2965            row,
2966            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
2967        )?
2968    };
2969
2970    Ok(FieldInfo {
2971        ident,
2972        column,
2973        primary_key,
2974        auto,
2975        value_ty: &field.ty,
2976        field_type_tokens,
2977        schema,
2978        from_row_init,
2979        from_aliased_row_init,
2980        fk_inner: fk_inner.cloned(),
2981        auto_now: attrs.auto_now,
2982        auto_now_add: attrs.auto_now_add,
2983        soft_delete: attrs.soft_delete,
2984    })
2985}
2986
2987fn check_bound_compatibility(
2988    field: &syn::Field,
2989    attrs: &FieldAttrs,
2990    kind: DetectedKind,
2991) -> syn::Result<()> {
2992    if attrs.max_length.is_some() && kind != DetectedKind::String {
2993        return Err(syn::Error::new_spanned(
2994            field,
2995            "`max_length` is only valid on `String` fields (or `Option<String>`)",
2996        ));
2997    }
2998    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
2999        return Err(syn::Error::new_spanned(
3000            field,
3001            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
3002        ));
3003    }
3004    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
3005        if min > max {
3006            return Err(syn::Error::new_spanned(
3007                field,
3008                format!("`min` ({min}) is greater than `max` ({max})"),
3009            ));
3010        }
3011    }
3012    Ok(())
3013}
3014
3015fn optional_u32(value: Option<u32>) -> TokenStream2 {
3016    if let Some(v) = value {
3017        quote!(::core::option::Option::Some(#v))
3018    } else {
3019        quote!(::core::option::Option::None)
3020    }
3021}
3022
3023fn optional_i64(value: Option<i64>) -> TokenStream2 {
3024    if let Some(v) = value {
3025        quote!(::core::option::Option::Some(#v))
3026    } else {
3027        quote!(::core::option::Option::None)
3028    }
3029}
3030
3031fn optional_str(value: Option<&str>) -> TokenStream2 {
3032    if let Some(v) = value {
3033        quote!(::core::option::Option::Some(#v))
3034    } else {
3035        quote!(::core::option::Option::None)
3036    }
3037}
3038
3039fn relation_tokens(
3040    field: &syn::Field,
3041    attrs: &FieldAttrs,
3042    fk_inner: Option<&syn::Type>,
3043) -> syn::Result<TokenStream2> {
3044    if let Some(inner) = fk_inner {
3045        if attrs.fk.is_some() || attrs.o2o.is_some() {
3046            return Err(syn::Error::new_spanned(
3047                field,
3048                "`ForeignKey<T>` already declares the FK target via the type parameter — \
3049                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
3050            ));
3051        }
3052        let on = attrs.on.as_deref().unwrap_or("id");
3053        return Ok(quote! {
3054            ::core::option::Option::Some(::rustango::core::Relation::Fk {
3055                to: <#inner as ::rustango::core::Model>::SCHEMA.table,
3056                on: #on,
3057            })
3058        });
3059    }
3060    match (&attrs.fk, &attrs.o2o) {
3061        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
3062            field,
3063            "`fk` and `o2o` are mutually exclusive",
3064        )),
3065        (Some(to), None) => {
3066            let on = attrs.on.as_deref().unwrap_or("id");
3067            Ok(quote! {
3068                ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #to, on: #on })
3069            })
3070        }
3071        (None, Some(to)) => {
3072            let on = attrs.on.as_deref().unwrap_or("id");
3073            Ok(quote! {
3074                ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #to, on: #on })
3075            })
3076        }
3077        (None, None) => {
3078            if attrs.on.is_some() {
3079                return Err(syn::Error::new_spanned(
3080                    field,
3081                    "`on` requires `fk` or `o2o`",
3082                ));
3083            }
3084            Ok(quote!(::core::option::Option::None))
3085        }
3086    }
3087}
3088
3089/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
3090/// about kinds without depending on `rustango-core` (which would require a
3091/// proc-macro/normal split it doesn't have today).
3092#[derive(Clone, Copy, PartialEq, Eq)]
3093enum DetectedKind {
3094    I32,
3095    I64,
3096    F32,
3097    F64,
3098    Bool,
3099    String,
3100    DateTime,
3101    Date,
3102    Uuid,
3103    Json,
3104}
3105
3106impl DetectedKind {
3107    fn variant_tokens(self) -> TokenStream2 {
3108        match self {
3109            Self::I32 => quote!(::rustango::core::FieldType::I32),
3110            Self::I64 => quote!(::rustango::core::FieldType::I64),
3111            Self::F32 => quote!(::rustango::core::FieldType::F32),
3112            Self::F64 => quote!(::rustango::core::FieldType::F64),
3113            Self::Bool => quote!(::rustango::core::FieldType::Bool),
3114            Self::String => quote!(::rustango::core::FieldType::String),
3115            Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
3116            Self::Date => quote!(::rustango::core::FieldType::Date),
3117            Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
3118            Self::Json => quote!(::rustango::core::FieldType::Json),
3119        }
3120    }
3121
3122    fn is_integer(self) -> bool {
3123        matches!(self, Self::I32 | Self::I64)
3124    }
3125}
3126
3127/// Result of walking a field's Rust type. `kind` is the underlying
3128/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
3129/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
3130/// `Some(<T>)` when the field was `ForeignKey<T>` (or
3131/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
3132#[derive(Clone, Copy)]
3133struct DetectedType<'a> {
3134    kind: DetectedKind,
3135    nullable: bool,
3136    auto: bool,
3137    fk_inner: Option<&'a syn::Type>,
3138}
3139
3140fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
3141    let Type::Path(TypePath { path, qself: None }) = ty else {
3142        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
3143    };
3144    let last = path
3145        .segments
3146        .last()
3147        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
3148
3149    if last.ident == "Option" {
3150        let inner = generic_inner(ty, &last.arguments, "Option")?;
3151        let inner_det = detect_type(inner)?;
3152        if inner_det.nullable {
3153            return Err(syn::Error::new_spanned(
3154                ty,
3155                "nested Option is not supported",
3156            ));
3157        }
3158        if inner_det.auto {
3159            return Err(syn::Error::new_spanned(
3160                ty,
3161                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
3162            ));
3163        }
3164        return Ok(DetectedType {
3165            nullable: true,
3166            ..inner_det
3167        });
3168    }
3169
3170    if last.ident == "Auto" {
3171        let inner = generic_inner(ty, &last.arguments, "Auto")?;
3172        let inner_det = detect_type(inner)?;
3173        if inner_det.auto {
3174            return Err(syn::Error::new_spanned(
3175                ty,
3176                "nested Auto is not supported",
3177            ));
3178        }
3179        if inner_det.nullable {
3180            return Err(syn::Error::new_spanned(
3181                ty,
3182                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
3183            ));
3184        }
3185        if inner_det.fk_inner.is_some() {
3186            return Err(syn::Error::new_spanned(
3187                ty,
3188                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
3189            ));
3190        }
3191        if !matches!(
3192            inner_det.kind,
3193            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
3194        ) {
3195            return Err(syn::Error::new_spanned(
3196                ty,
3197                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
3198                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
3199                 (DEFAULT now())",
3200            ));
3201        }
3202        return Ok(DetectedType {
3203            auto: true,
3204            ..inner_det
3205        });
3206    }
3207
3208    if last.ident == "ForeignKey" {
3209        let inner = generic_inner(ty, &last.arguments, "ForeignKey")?;
3210        // `ForeignKey<T>` is stored as BIGINT — same column shape as
3211        // the v0.1 `i64` + `#[rustango(fk = …)]` form. The macro does
3212        // not recurse into `T` because `T` is a Model struct, not a
3213        // primitive — its identity is opaque to schema detection.
3214        return Ok(DetectedType {
3215            kind: DetectedKind::I64,
3216            nullable: false,
3217            auto: false,
3218            fk_inner: Some(inner),
3219        });
3220    }
3221
3222    let kind = match last.ident.to_string().as_str() {
3223        "i32" => DetectedKind::I32,
3224        "i64" => DetectedKind::I64,
3225        "f32" => DetectedKind::F32,
3226        "f64" => DetectedKind::F64,
3227        "bool" => DetectedKind::Bool,
3228        "String" => DetectedKind::String,
3229        "DateTime" => DetectedKind::DateTime,
3230        "NaiveDate" => DetectedKind::Date,
3231        "Uuid" => DetectedKind::Uuid,
3232        "Value" => DetectedKind::Json,
3233        other => {
3234            return Err(syn::Error::new_spanned(
3235                ty,
3236                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)"),
3237            ));
3238        }
3239    };
3240    Ok(DetectedType {
3241        kind,
3242        nullable: false,
3243        auto: false,
3244        fk_inner: None,
3245    })
3246}
3247
3248fn generic_inner<'a>(
3249    ty: &'a Type,
3250    arguments: &'a PathArguments,
3251    wrapper: &str,
3252) -> syn::Result<&'a Type> {
3253    let PathArguments::AngleBracketed(args) = arguments else {
3254        return Err(syn::Error::new_spanned(
3255            ty,
3256            format!("{wrapper} requires a generic argument"),
3257        ));
3258    };
3259    args.args
3260        .iter()
3261        .find_map(|a| match a {
3262            GenericArgument::Type(t) => Some(t),
3263            _ => None,
3264        })
3265        .ok_or_else(|| {
3266            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
3267        })
3268}
3269
3270fn to_snake_case(s: &str) -> String {
3271    let mut out = String::with_capacity(s.len() + 4);
3272    for (i, ch) in s.chars().enumerate() {
3273        if ch.is_ascii_uppercase() {
3274            if i > 0 {
3275                out.push('_');
3276            }
3277            out.push(ch.to_ascii_lowercase());
3278        } else {
3279            out.push(ch);
3280        }
3281    }
3282    out
3283}
3284
3285// ============================================================
3286//  #[derive(Form)]  —  slice 8.4B
3287// ============================================================
3288
3289/// Per-field `#[form(...)]` attributes recognised by the derive.
3290#[derive(Default)]
3291struct FormFieldAttrs {
3292    min: Option<i64>,
3293    max: Option<i64>,
3294    min_length: Option<u32>,
3295    max_length: Option<u32>,
3296}
3297
3298/// Detected shape of a form field's Rust type.
3299#[derive(Clone, Copy)]
3300enum FormFieldKind {
3301    String,
3302    I32,
3303    I64,
3304    F32,
3305    F64,
3306    Bool,
3307}
3308
3309impl FormFieldKind {
3310    fn parse_method(self) -> &'static str {
3311        match self {
3312            Self::I32 => "i32",
3313            Self::I64 => "i64",
3314            Self::F32 => "f32",
3315            Self::F64 => "f64",
3316            // String + Bool don't go through `str::parse`; the codegen
3317            // handles them inline.
3318            Self::String | Self::Bool => "",
3319        }
3320    }
3321}
3322
3323fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
3324    let struct_name = &input.ident;
3325
3326    let Data::Struct(data) = &input.data else {
3327        return Err(syn::Error::new_spanned(
3328            struct_name,
3329            "Form can only be derived on structs",
3330        ));
3331    };
3332    let Fields::Named(named) = &data.fields else {
3333        return Err(syn::Error::new_spanned(
3334            struct_name,
3335            "Form requires a struct with named fields",
3336        ));
3337    };
3338
3339    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
3340    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
3341
3342    for field in &named.named {
3343        let ident = field
3344            .ident
3345            .as_ref()
3346            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
3347        let attrs = parse_form_field_attrs(field)?;
3348        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
3349
3350        let name_lit = ident.to_string();
3351        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
3352        field_blocks.push(parse_block);
3353        field_idents.push(ident);
3354    }
3355
3356    Ok(quote! {
3357        impl ::rustango::forms::Form for #struct_name {
3358            fn parse(
3359                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
3360            ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
3361                let mut __errors = ::rustango::forms::FormErrors::default();
3362                #( #field_blocks )*
3363                if !__errors.is_empty() {
3364                    return ::core::result::Result::Err(__errors);
3365                }
3366                ::core::result::Result::Ok(Self {
3367                    #( #field_idents ),*
3368                })
3369            }
3370        }
3371    })
3372}
3373
3374fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
3375    let mut out = FormFieldAttrs::default();
3376    for attr in &field.attrs {
3377        if !attr.path().is_ident("form") {
3378            continue;
3379        }
3380        attr.parse_nested_meta(|meta| {
3381            if meta.path.is_ident("min") {
3382                let lit: syn::LitInt = meta.value()?.parse()?;
3383                out.min = Some(lit.base10_parse::<i64>()?);
3384                return Ok(());
3385            }
3386            if meta.path.is_ident("max") {
3387                let lit: syn::LitInt = meta.value()?.parse()?;
3388                out.max = Some(lit.base10_parse::<i64>()?);
3389                return Ok(());
3390            }
3391            if meta.path.is_ident("min_length") {
3392                let lit: syn::LitInt = meta.value()?.parse()?;
3393                out.min_length = Some(lit.base10_parse::<u32>()?);
3394                return Ok(());
3395            }
3396            if meta.path.is_ident("max_length") {
3397                let lit: syn::LitInt = meta.value()?.parse()?;
3398                out.max_length = Some(lit.base10_parse::<u32>()?);
3399                return Ok(());
3400            }
3401            Err(meta.error(
3402                "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
3403            ))
3404        })?;
3405    }
3406    Ok(out)
3407}
3408
3409fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
3410    let Type::Path(TypePath { path, qself: None }) = ty else {
3411        return Err(syn::Error::new(
3412            span,
3413            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
3414        ));
3415    };
3416    let last = path
3417        .segments
3418        .last()
3419        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
3420
3421    if last.ident == "Option" {
3422        let inner = generic_inner(ty, &last.arguments, "Option")?;
3423        let (kind, nested) = detect_form_field(inner, span)?;
3424        if nested {
3425            return Err(syn::Error::new(
3426                span,
3427                "nested Option in Form fields is not supported",
3428            ));
3429        }
3430        return Ok((kind, true));
3431    }
3432
3433    let kind = match last.ident.to_string().as_str() {
3434        "String" => FormFieldKind::String,
3435        "i32" => FormFieldKind::I32,
3436        "i64" => FormFieldKind::I64,
3437        "f32" => FormFieldKind::F32,
3438        "f64" => FormFieldKind::F64,
3439        "bool" => FormFieldKind::Bool,
3440        other => {
3441            return Err(syn::Error::new(
3442                span,
3443                format!(
3444                    "Form field type `{other}` is not supported in v0.8 — use String / \
3445                     i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
3446                ),
3447            ));
3448        }
3449    };
3450    Ok((kind, false))
3451}
3452
3453#[allow(clippy::too_many_lines)]
3454fn render_form_field_parse(
3455    ident: &syn::Ident,
3456    name_lit: &str,
3457    kind: FormFieldKind,
3458    nullable: bool,
3459    attrs: &FormFieldAttrs,
3460) -> TokenStream2 {
3461    // Pull the raw &str from the payload. Uses variable name `data` to
3462    // match the new `Form::parse(data: &HashMap<…>)` signature.
3463    let lookup = quote! {
3464        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
3465    };
3466
3467    let parsed_value = match kind {
3468        FormFieldKind::Bool => quote! {
3469            let __v: bool = match __raw {
3470                ::core::option::Option::None => false,
3471                ::core::option::Option::Some(__s) => !matches!(
3472                    __s.to_ascii_lowercase().as_str(),
3473                    "" | "false" | "0" | "off" | "no"
3474                ),
3475            };
3476        },
3477        FormFieldKind::String => {
3478            if nullable {
3479                quote! {
3480                    let __v: ::core::option::Option<::std::string::String> = match __raw {
3481                        ::core::option::Option::None => ::core::option::Option::None,
3482                        ::core::option::Option::Some(__s) if __s.is_empty() => {
3483                            ::core::option::Option::None
3484                        }
3485                        ::core::option::Option::Some(__s) => {
3486                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
3487                        }
3488                    };
3489                }
3490            } else {
3491                quote! {
3492                    let __v: ::std::string::String = match __raw {
3493                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
3494                            ::core::clone::Clone::clone(__s)
3495                        }
3496                        _ => {
3497                            __errors.add(#name_lit, "This field is required.");
3498                            ::std::string::String::new()
3499                        }
3500                    };
3501                }
3502            }
3503        }
3504        FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64 => {
3505            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
3506            let ty_lit = kind.parse_method();
3507            let default_val = match kind {
3508                FormFieldKind::I32 => quote! { 0i32 },
3509                FormFieldKind::I64 => quote! { 0i64 },
3510                FormFieldKind::F32 => quote! { 0f32 },
3511                FormFieldKind::F64 => quote! { 0f64 },
3512                _ => quote! { Default::default() },
3513            };
3514            if nullable {
3515                quote! {
3516                    let __v: ::core::option::Option<#parse_ty> = match __raw {
3517                        ::core::option::Option::None => ::core::option::Option::None,
3518                        ::core::option::Option::Some(__s) if __s.is_empty() => {
3519                            ::core::option::Option::None
3520                        }
3521                        ::core::option::Option::Some(__s) => {
3522                            match __s.parse::<#parse_ty>() {
3523                                ::core::result::Result::Ok(__n) => {
3524                                    ::core::option::Option::Some(__n)
3525                                }
3526                                ::core::result::Result::Err(__e) => {
3527                                    __errors.add(
3528                                        #name_lit,
3529                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
3530                                    );
3531                                    ::core::option::Option::None
3532                                }
3533                            }
3534                        }
3535                    };
3536                }
3537            } else {
3538                quote! {
3539                    let __v: #parse_ty = match __raw {
3540                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
3541                            match __s.parse::<#parse_ty>() {
3542                                ::core::result::Result::Ok(__n) => __n,
3543                                ::core::result::Result::Err(__e) => {
3544                                    __errors.add(
3545                                        #name_lit,
3546                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
3547                                    );
3548                                    #default_val
3549                                }
3550                            }
3551                        }
3552                        _ => {
3553                            __errors.add(#name_lit, "This field is required.");
3554                            #default_val
3555                        }
3556                    };
3557                }
3558            }
3559        }
3560    };
3561
3562    let validators = render_form_validators(name_lit, kind, nullable, attrs);
3563
3564    quote! {
3565        let #ident = {
3566            #lookup
3567            #parsed_value
3568            #validators
3569            __v
3570        };
3571    }
3572}
3573
3574fn render_form_validators(
3575    name_lit: &str,
3576    kind: FormFieldKind,
3577    nullable: bool,
3578    attrs: &FormFieldAttrs,
3579) -> TokenStream2 {
3580    let mut checks: Vec<TokenStream2> = Vec::new();
3581
3582    let val_ref = if nullable {
3583        quote! { __v.as_ref() }
3584    } else {
3585        quote! { ::core::option::Option::Some(&__v) }
3586    };
3587
3588    let is_string = matches!(kind, FormFieldKind::String);
3589    let is_numeric = matches!(
3590        kind,
3591        FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64
3592    );
3593
3594    if is_string {
3595        if let Some(min_len) = attrs.min_length {
3596            let min_len_usize = min_len as usize;
3597            checks.push(quote! {
3598                if let ::core::option::Option::Some(__s) = #val_ref {
3599                    if __s.len() < #min_len_usize {
3600                        __errors.add(
3601                            #name_lit,
3602                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
3603                        );
3604                    }
3605                }
3606            });
3607        }
3608        if let Some(max_len) = attrs.max_length {
3609            let max_len_usize = max_len as usize;
3610            checks.push(quote! {
3611                if let ::core::option::Option::Some(__s) = #val_ref {
3612                    if __s.len() > #max_len_usize {
3613                        __errors.add(
3614                            #name_lit,
3615                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
3616                        );
3617                    }
3618                }
3619            });
3620        }
3621    }
3622
3623    if is_numeric {
3624        if let Some(min) = attrs.min {
3625            checks.push(quote! {
3626                if let ::core::option::Option::Some(__n) = #val_ref {
3627                    if (*__n as f64) < (#min as f64) {
3628                        __errors.add(
3629                            #name_lit,
3630                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
3631                        );
3632                    }
3633                }
3634            });
3635        }
3636        if let Some(max) = attrs.max {
3637            checks.push(quote! {
3638                if let ::core::option::Option::Some(__n) = #val_ref {
3639                    if (*__n as f64) > (#max as f64) {
3640                        __errors.add(
3641                            #name_lit,
3642                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
3643                        );
3644                    }
3645                }
3646            });
3647        }
3648    }
3649
3650    quote! { #( #checks )* }
3651}
3652
3653// ============================================================
3654//  #[derive(ViewSet)]
3655// ============================================================
3656
3657struct ViewSetAttrs {
3658    model: syn::Path,
3659    fields: Option<Vec<String>>,
3660    filter_fields: Vec<String>,
3661    search_fields: Vec<String>,
3662    /// (field_name, desc)
3663    ordering: Vec<(String, bool)>,
3664    page_size: Option<usize>,
3665    read_only: bool,
3666    perms: ViewSetPermsAttrs,
3667}
3668
3669#[derive(Default)]
3670struct ViewSetPermsAttrs {
3671    list: Vec<String>,
3672    retrieve: Vec<String>,
3673    create: Vec<String>,
3674    update: Vec<String>,
3675    destroy: Vec<String>,
3676}
3677
3678fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
3679    let struct_name = &input.ident;
3680
3681    // Must be a unit struct or an empty named struct.
3682    match &input.data {
3683        Data::Struct(s) => match &s.fields {
3684            Fields::Unit | Fields::Named(_) => {}
3685            Fields::Unnamed(_) => {
3686                return Err(syn::Error::new_spanned(
3687                    struct_name,
3688                    "ViewSet can only be derived on a unit struct or an empty named struct",
3689                ));
3690            }
3691        },
3692        _ => {
3693            return Err(syn::Error::new_spanned(
3694                struct_name,
3695                "ViewSet can only be derived on a struct",
3696            ));
3697        }
3698    }
3699
3700    let attrs = parse_viewset_attrs(input)?;
3701    let model_path = &attrs.model;
3702
3703    // `.fields(&[...])` call — None means skip (use all scalar fields).
3704    let fields_call = if let Some(ref fields) = attrs.fields {
3705        let lits = fields.iter().map(|f| f.as_str());
3706        quote!(.fields(&[ #(#lits),* ]))
3707    } else {
3708        quote!()
3709    };
3710
3711    let filter_fields_call = if attrs.filter_fields.is_empty() {
3712        quote!()
3713    } else {
3714        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
3715        quote!(.filter_fields(&[ #(#lits),* ]))
3716    };
3717
3718    let search_fields_call = if attrs.search_fields.is_empty() {
3719        quote!()
3720    } else {
3721        let lits = attrs.search_fields.iter().map(|f| f.as_str());
3722        quote!(.search_fields(&[ #(#lits),* ]))
3723    };
3724
3725    let ordering_call = if attrs.ordering.is_empty() {
3726        quote!()
3727    } else {
3728        let pairs = attrs.ordering.iter().map(|(f, desc)| {
3729            let f = f.as_str();
3730            quote!((#f, #desc))
3731        });
3732        quote!(.ordering(&[ #(#pairs),* ]))
3733    };
3734
3735    let page_size_call = if let Some(n) = attrs.page_size {
3736        quote!(.page_size(#n))
3737    } else {
3738        quote!()
3739    };
3740
3741    let read_only_call = if attrs.read_only {
3742        quote!(.read_only())
3743    } else {
3744        quote!()
3745    };
3746
3747    let perms = &attrs.perms;
3748    let perms_call = if perms.list.is_empty()
3749        && perms.retrieve.is_empty()
3750        && perms.create.is_empty()
3751        && perms.update.is_empty()
3752        && perms.destroy.is_empty()
3753    {
3754        quote!()
3755    } else {
3756        let list_lits = perms.list.iter().map(|s| s.as_str());
3757        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
3758        let create_lits = perms.create.iter().map(|s| s.as_str());
3759        let update_lits = perms.update.iter().map(|s| s.as_str());
3760        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
3761        quote! {
3762            .permissions(::rustango::viewset::ViewSetPerms {
3763                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
3764                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
3765                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
3766                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
3767                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
3768            })
3769        }
3770    };
3771
3772    Ok(quote! {
3773        impl #struct_name {
3774            /// Build an `axum::Router` with the six standard REST endpoints
3775            /// for this ViewSet, mounted at `prefix`.
3776            pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
3777                ::rustango::viewset::ViewSet::for_model(#model_path::SCHEMA)
3778                    #fields_call
3779                    #filter_fields_call
3780                    #search_fields_call
3781                    #ordering_call
3782                    #page_size_call
3783                    #perms_call
3784                    #read_only_call
3785                    .router(prefix, pool)
3786            }
3787        }
3788    })
3789}
3790
3791fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
3792    let mut model: Option<syn::Path> = None;
3793    let mut fields: Option<Vec<String>> = None;
3794    let mut filter_fields: Vec<String> = Vec::new();
3795    let mut search_fields: Vec<String> = Vec::new();
3796    let mut ordering: Vec<(String, bool)> = Vec::new();
3797    let mut page_size: Option<usize> = None;
3798    let mut read_only = false;
3799    let mut perms = ViewSetPermsAttrs::default();
3800
3801    for attr in &input.attrs {
3802        if !attr.path().is_ident("viewset") {
3803            continue;
3804        }
3805        attr.parse_nested_meta(|meta| {
3806            if meta.path.is_ident("model") {
3807                let path: syn::Path = meta.value()?.parse()?;
3808                model = Some(path);
3809                return Ok(());
3810            }
3811            if meta.path.is_ident("fields") {
3812                let s: LitStr = meta.value()?.parse()?;
3813                fields = Some(split_field_list(&s.value()));
3814                return Ok(());
3815            }
3816            if meta.path.is_ident("filter_fields") {
3817                let s: LitStr = meta.value()?.parse()?;
3818                filter_fields = split_field_list(&s.value());
3819                return Ok(());
3820            }
3821            if meta.path.is_ident("search_fields") {
3822                let s: LitStr = meta.value()?.parse()?;
3823                search_fields = split_field_list(&s.value());
3824                return Ok(());
3825            }
3826            if meta.path.is_ident("ordering") {
3827                let s: LitStr = meta.value()?.parse()?;
3828                ordering = parse_ordering_list(&s.value());
3829                return Ok(());
3830            }
3831            if meta.path.is_ident("page_size") {
3832                let lit: syn::LitInt = meta.value()?.parse()?;
3833                page_size = Some(lit.base10_parse::<usize>()?);
3834                return Ok(());
3835            }
3836            if meta.path.is_ident("read_only") {
3837                read_only = true;
3838                return Ok(());
3839            }
3840            if meta.path.is_ident("permissions") {
3841                meta.parse_nested_meta(|inner| {
3842                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
3843                        let s: LitStr = inner.value()?.parse()?;
3844                        Ok(split_field_list(&s.value()))
3845                    };
3846                    if inner.path.is_ident("list") {
3847                        perms.list = parse_codenames(&inner)?;
3848                    } else if inner.path.is_ident("retrieve") {
3849                        perms.retrieve = parse_codenames(&inner)?;
3850                    } else if inner.path.is_ident("create") {
3851                        perms.create = parse_codenames(&inner)?;
3852                    } else if inner.path.is_ident("update") {
3853                        perms.update = parse_codenames(&inner)?;
3854                    } else if inner.path.is_ident("destroy") {
3855                        perms.destroy = parse_codenames(&inner)?;
3856                    } else {
3857                        return Err(inner.error(
3858                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
3859                        ));
3860                    }
3861                    Ok(())
3862                })?;
3863                return Ok(());
3864            }
3865            Err(meta.error(
3866                "unknown viewset attribute (supported: model, fields, filter_fields, \
3867                 search_fields, ordering, page_size, read_only, permissions(...))",
3868            ))
3869        })?;
3870    }
3871
3872    let model = model.ok_or_else(|| {
3873        syn::Error::new_spanned(
3874            &input.ident,
3875            "`#[viewset(model = SomeModel)]` is required",
3876        )
3877    })?;
3878
3879    Ok(ViewSetAttrs {
3880        model,
3881        fields,
3882        filter_fields,
3883        search_fields,
3884        ordering,
3885        page_size,
3886        read_only,
3887        perms,
3888    })
3889}
3890
3891// ============================================================ #[derive(Serializer)]
3892
3893struct SerializerContainerAttrs {
3894    model: syn::Path,
3895}
3896
3897#[derive(Default)]
3898struct SerializerFieldAttrs {
3899    read_only: bool,
3900    write_only: bool,
3901    source: Option<String>,
3902    skip: bool,
3903}
3904
3905fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
3906    let mut model: Option<syn::Path> = None;
3907    for attr in &input.attrs {
3908        if !attr.path().is_ident("serializer") {
3909            continue;
3910        }
3911        attr.parse_nested_meta(|meta| {
3912            if meta.path.is_ident("model") {
3913                let _eq: syn::Token![=] = meta.input.parse()?;
3914                model = Some(meta.input.parse()?);
3915                return Ok(());
3916            }
3917            Err(meta.error("unknown serializer container attribute (supported: `model`)"))
3918        })?;
3919    }
3920    let model = model.ok_or_else(|| {
3921        syn::Error::new_spanned(
3922            &input.ident,
3923            "`#[serializer(model = SomeModel)]` is required",
3924        )
3925    })?;
3926    Ok(SerializerContainerAttrs { model })
3927}
3928
3929fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
3930    let mut out = SerializerFieldAttrs::default();
3931    for attr in &field.attrs {
3932        if !attr.path().is_ident("serializer") {
3933            continue;
3934        }
3935        attr.parse_nested_meta(|meta| {
3936            if meta.path.is_ident("read_only") {
3937                out.read_only = true;
3938                return Ok(());
3939            }
3940            if meta.path.is_ident("write_only") {
3941                out.write_only = true;
3942                return Ok(());
3943            }
3944            if meta.path.is_ident("skip") {
3945                out.skip = true;
3946                return Ok(());
3947            }
3948            if meta.path.is_ident("source") {
3949                let s: LitStr = meta.value()?.parse()?;
3950                out.source = Some(s.value());
3951                return Ok(());
3952            }
3953            Err(meta.error(
3954                "unknown serializer field attribute \
3955                 (supported: `read_only`, `write_only`, `source`, `skip`)",
3956            ))
3957        })?;
3958    }
3959    // Validate: read_only + write_only is nonsensical
3960    if out.read_only && out.write_only {
3961        return Err(syn::Error::new_spanned(
3962            field,
3963            "a field cannot be both `read_only` and `write_only`",
3964        ));
3965    }
3966    Ok(out)
3967}
3968
3969fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
3970    let struct_name = &input.ident;
3971    let struct_name_lit = struct_name.to_string();
3972
3973    let Data::Struct(data) = &input.data else {
3974        return Err(syn::Error::new_spanned(
3975            struct_name,
3976            "Serializer can only be derived on structs",
3977        ));
3978    };
3979    let Fields::Named(named) = &data.fields else {
3980        return Err(syn::Error::new_spanned(
3981            struct_name,
3982            "Serializer requires a struct with named fields",
3983        ));
3984    };
3985
3986    let container = parse_serializer_container_attrs(input)?;
3987    let model_path = &container.model;
3988
3989    // Classify each field. `ty` is only consumed by the
3990    // `#[cfg(feature = "openapi")]` block below, but we always
3991    // capture it to keep the field-info build a single pass.
3992    #[allow(dead_code)]
3993    struct FieldInfo {
3994        ident: syn::Ident,
3995        ty: syn::Type,
3996        attrs: SerializerFieldAttrs,
3997    }
3998    let mut fields_info: Vec<FieldInfo> = Vec::new();
3999    for field in &named.named {
4000        let ident = field.ident.clone().expect("named field has ident");
4001        let attrs = parse_serializer_field_attrs(field)?;
4002        fields_info.push(FieldInfo {
4003            ident,
4004            ty: field.ty.clone(),
4005            attrs,
4006        });
4007    }
4008
4009    // Generate from_model body: struct literal with each field assigned.
4010    let from_model_fields = fields_info.iter().map(|fi| {
4011        let ident = &fi.ident;
4012        if fi.attrs.write_only || fi.attrs.skip {
4013            // Not read from model — use default
4014            quote! { #ident: ::core::default::Default::default() }
4015        } else if let Some(src) = &fi.attrs.source {
4016            let src_ident = syn::Ident::new(src, ident.span());
4017            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
4018        } else {
4019            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
4020        }
4021    });
4022
4023    // Generate custom Serialize: skip write_only fields
4024    let output_fields: Vec<_> = fields_info
4025        .iter()
4026        .filter(|fi| !fi.attrs.write_only)
4027        .collect();
4028    let output_field_count = output_fields.len();
4029    let serialize_fields = output_fields.iter().map(|fi| {
4030        let ident = &fi.ident;
4031        let name_lit = ident.to_string();
4032        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
4033    });
4034
4035    // writable_fields: normal + write_only (not read_only, not skip)
4036    let writable_lits: Vec<_> = fields_info
4037        .iter()
4038        .filter(|fi| !fi.attrs.read_only && !fi.attrs.skip)
4039        .map(|fi| fi.ident.to_string())
4040        .collect();
4041
4042    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
4043    // Only includes fields shown in JSON output (skips write_only). For each
4044    // `Option<T>` field, omit from `required` and add `.nullable()`.
4045    let openapi_impl = {
4046        #[cfg(feature = "openapi")]
4047        {
4048            let property_calls = output_fields.iter().map(|fi| {
4049                let ident = &fi.ident;
4050                let name_lit = ident.to_string();
4051                let ty = &fi.ty;
4052                let nullable_call = if is_option(ty) {
4053                    quote! { .nullable() }
4054                } else {
4055                    quote! {}
4056                };
4057                quote! {
4058                    .property(
4059                        #name_lit,
4060                        <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
4061                            #nullable_call,
4062                    )
4063                }
4064            });
4065            let required_lits: Vec<_> = output_fields
4066                .iter()
4067                .filter(|fi| !is_option(&fi.ty))
4068                .map(|fi| fi.ident.to_string())
4069                .collect();
4070            quote! {
4071                impl ::rustango::openapi::OpenApiSchema for #struct_name {
4072                    fn openapi_schema() -> ::rustango::openapi::Schema {
4073                        ::rustango::openapi::Schema::object()
4074                            #( #property_calls )*
4075                            .required([ #( #required_lits ),* ])
4076                    }
4077                }
4078            }
4079        }
4080        #[cfg(not(feature = "openapi"))]
4081        {
4082            quote! {}
4083        }
4084    };
4085
4086    Ok(quote! {
4087        impl ::rustango::serializer::ModelSerializer for #struct_name {
4088            type Model = #model_path;
4089
4090            fn from_model(model: &Self::Model) -> Self {
4091                Self {
4092                    #( #from_model_fields ),*
4093                }
4094            }
4095
4096            fn writable_fields() -> &'static [&'static str] {
4097                &[ #( #writable_lits ),* ]
4098            }
4099        }
4100
4101        impl ::serde::Serialize for #struct_name {
4102            fn serialize<S>(&self, serializer: S)
4103                -> ::core::result::Result<S::Ok, S::Error>
4104            where
4105                S: ::serde::Serializer,
4106            {
4107                use ::serde::ser::SerializeStruct;
4108                let mut __state = serializer.serialize_struct(
4109                    #struct_name_lit,
4110                    #output_field_count,
4111                )?;
4112                #( #serialize_fields )*
4113                __state.end()
4114            }
4115        }
4116
4117        #openapi_impl
4118    })
4119}
4120
4121/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
4122/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
4123/// when the feature is off.
4124#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
4125fn is_option(ty: &syn::Type) -> bool {
4126    if let syn::Type::Path(p) = ty {
4127        if let Some(last) = p.path.segments.last() {
4128            return last.ident == "Option";
4129        }
4130    }
4131    false
4132}