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