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 `rustango::forms::FormStruct` (slice 8.4B). Generates a
27/// `parse(&HashMap<String, String>) -> Result<Self, FormError>` impl
28/// that walks every named field and:
29///
30/// * Parses the string value into the field's Rust type (`String`,
31///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
32///   nullable case).
33/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
34///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
35///   validators in declaration order, returning `FormError::Parse`
36///   on the first failure.
37///
38/// Example:
39///
40/// ```ignore
41/// #[derive(Form)]
42/// pub struct CreateItemForm {
43///     #[form(min_length = 1, max_length = 64)]
44///     pub name: String,
45///     #[form(min = 0, max = 150)]
46///     pub age: i32,
47///     pub active: bool,
48///     pub email: Option<String>,
49/// }
50///
51/// let parsed = CreateItemForm::parse(&form_map)?;
52/// ```
53#[proc_macro_derive(Form, attributes(form))]
54pub fn derive_form(input: TokenStream) -> TokenStream {
55    let input = parse_macro_input!(input as DeriveInput);
56    expand_form(&input)
57        .unwrap_or_else(syn::Error::into_compile_error)
58        .into()
59}
60
61/// Bake every `*.json` migration file in a directory into the binary
62/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
63/// of `(name, json_content)` pairs, lex-sorted by file stem.
64///
65/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
66/// behaviour as `migrate(pool, dir)` but with no filesystem access.
67/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
68/// (i.e. the crate that invokes the macro). Default is
69/// `"./migrations"` if no argument is supplied.
70///
71/// ```ignore
72/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
73/// // or:
74/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
75///
76/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
77/// ```
78///
79/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
80/// file's `name` field must equal its file stem, every `prev`
81/// reference must point to another migration in the same directory,
82/// and the JSON must parse. A broken chain — orphan `prev`, missing
83/// predecessor, malformed file — fails at macro-expansion time with
84/// a clear `compile_error!`. *No other Django-shape Rust framework
85/// validates migration chains at compile time*: Cot's migrations are
86/// imperative Rust code (no static chain), Loco's are SeaORM
87/// up/down (same), Rwf's are raw SQL (no chain at all).
88///
89/// Each migration is included via `include_str!` so cargo's rebuild
90/// detection picks up file *content* changes. **Caveat:** cargo
91/// doesn't watch directory listings, so adding or removing a
92/// migration file inside the dir won't auto-trigger a rebuild — run
93/// `cargo clean` (or just bump any other source file) when you add
94/// new migrations during embedded development.
95#[proc_macro]
96pub fn embed_migrations(input: TokenStream) -> TokenStream {
97    expand_embed_migrations(input.into())
98        .unwrap_or_else(syn::Error::into_compile_error)
99        .into()
100}
101
102/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
103/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
104/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
105/// functions are zero-boilerplate:
106///
107/// ```ignore
108/// #[rustango::main]
109/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
110///     rustango::server::Builder::from_env().await?
111///         .migrate("migrations").await?
112///         .api(my_app::urls::api())
113///         .seed_with(my_app::seed::run).await?
114///         .serve("0.0.0.0:8080").await
115/// }
116/// ```
117///
118/// Optional `flavor = "current_thread"` passes through to
119/// `#[tokio::main]`; default is the multi-threaded runtime.
120///
121/// Pulls `tracing-subscriber` into the rustango crate behind the
122/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
123/// out get plain `#[tokio::main]` ergonomics without the dependency.
124#[proc_macro_attribute]
125pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
126    expand_main(args.into(), item.into())
127        .unwrap_or_else(syn::Error::into_compile_error)
128        .into()
129}
130
131fn expand_main(
132    args: TokenStream2,
133    item: TokenStream2,
134) -> syn::Result<TokenStream2> {
135    let mut input: syn::ItemFn = syn::parse2(item)?;
136    if input.sig.asyncness.is_none() {
137        return Err(syn::Error::new(
138            input.sig.ident.span(),
139            "`#[rustango::main]` must wrap an `async fn`",
140        ));
141    }
142
143    // Parse optional `flavor = "..."` etc. from the attribute args
144    // and pass them straight through to `#[tokio::main(...)]`.
145    let tokio_attr = if args.is_empty() {
146        quote! { #[::tokio::main] }
147    } else {
148        quote! { #[::tokio::main(#args)] }
149    };
150
151    // Re-block the body so the tracing init runs before user code.
152    let body = input.block.clone();
153    input.block = syn::parse2(quote! {{
154        {
155            use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
156            // `try_init` so duplicate installers (e.g. tests already
157            // holding a subscriber) don't panic.
158            let _ = tracing_subscriber::fmt()
159                .with_env_filter(
160                    EnvFilter::try_from_default_env()
161                        .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
162                )
163                .try_init();
164        }
165        #body
166    }})?;
167
168    Ok(quote! {
169        #tokio_attr
170        #input
171    })
172}
173
174fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
175    // Default to "./migrations" if invoked without args.
176    let path_str = if input.is_empty() {
177        "./migrations".to_string()
178    } else {
179        let lit: LitStr = syn::parse2(input)?;
180        lit.value()
181    };
182
183    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
184        syn::Error::new(
185            proc_macro2::Span::call_site(),
186            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
187        )
188    })?;
189    let abs = std::path::Path::new(&manifest).join(&path_str);
190
191    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
192    if abs.is_dir() {
193        let read = std::fs::read_dir(&abs).map_err(|e| {
194            syn::Error::new(
195                proc_macro2::Span::call_site(),
196                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
197            )
198        })?;
199        for entry in read.flatten() {
200            let path = entry.path();
201            if !path.is_file() {
202                continue;
203            }
204            if path.extension().and_then(|s| s.to_str()) != Some("json") {
205                continue;
206            }
207            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
208                continue;
209            };
210            entries.push((stem.to_owned(), path));
211        }
212    }
213    entries.sort_by(|a, b| a.0.cmp(&b.0));
214
215    // Compile-time chain validation: read each migration's JSON,
216    // pull `name` and `prev` (file-stem-keyed for the chain check),
217    // and verify every `prev` points to another migration in the
218    // slice. Mismatches between the file stem and the embedded
219    // `name` field — and broken `prev` chains — fail at MACRO
220    // EXPANSION time so a misshapen migration set never compiles.
221    //
222    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
223    // migrations + a Rust proc-macro that reads them is the unique
224    // combo nothing else in the Django-shape Rust camp can match
225    // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
226    // Rwf's are raw SQL — none have a static chain to validate).
227    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
228    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
229    for (stem, path) in &entries {
230        let raw = std::fs::read_to_string(path).map_err(|e| {
231            syn::Error::new(
232                proc_macro2::Span::call_site(),
233                format!(
234                    "embed_migrations!: cannot read {} for chain validation: {e}",
235                    path.display()
236                ),
237            )
238        })?;
239        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
240            syn::Error::new(
241                proc_macro2::Span::call_site(),
242                format!(
243                    "embed_migrations!: {} is not valid JSON: {e}",
244                    path.display()
245                ),
246            )
247        })?;
248        let name = json
249            .get("name")
250            .and_then(|v| v.as_str())
251            .ok_or_else(|| {
252                syn::Error::new(
253                    proc_macro2::Span::call_site(),
254                    format!(
255                        "embed_migrations!: {} is missing the `name` field",
256                        path.display()
257                    ),
258                )
259            })?
260            .to_owned();
261        if name != *stem {
262            return Err(syn::Error::new(
263                proc_macro2::Span::call_site(),
264                format!(
265                    "embed_migrations!: file stem `{stem}` does not match the migration's \
266                     `name` field `{name}` — rename the file or fix the JSON",
267                ),
268            ));
269        }
270        let prev = json
271            .get("prev")
272            .and_then(|v| v.as_str())
273            .map(str::to_owned);
274        chain_names.push(name.clone());
275        prev_refs.push((name, prev));
276    }
277
278    let name_set: std::collections::HashSet<&str> =
279        chain_names.iter().map(String::as_str).collect();
280    for (name, prev) in &prev_refs {
281        if let Some(p) = prev {
282            if !name_set.contains(p.as_str()) {
283                return Err(syn::Error::new(
284                    proc_macro2::Span::call_site(),
285                    format!(
286                        "embed_migrations!: broken migration chain — `{name}` declares \
287                         prev=`{p}` but no migration with that name exists in {}",
288                        abs.display()
289                    ),
290                ));
291            }
292        }
293    }
294
295    let pairs: Vec<TokenStream2> = entries
296        .iter()
297        .map(|(name, path)| {
298            let path_lit = path.display().to_string();
299            quote! { (#name, ::core::include_str!(#path_lit)) }
300        })
301        .collect();
302
303    Ok(quote! {
304        {
305            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
306            __RUSTANGO_EMBEDDED
307        }
308    })
309}
310
311fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
312    let struct_name = &input.ident;
313
314    let Data::Struct(data) = &input.data else {
315        return Err(syn::Error::new_spanned(
316            struct_name,
317            "Model can only be derived on structs",
318        ));
319    };
320    let Fields::Named(named) = &data.fields else {
321        return Err(syn::Error::new_spanned(
322            struct_name,
323            "Model requires a struct with named fields",
324        ));
325    };
326
327    let container = parse_container_attrs(input)?;
328    let table = container
329        .table
330        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
331    let model_name = struct_name.to_string();
332
333    let collected = collect_fields(named)?;
334
335    // Validate that #[rustango(display = "…")] names a real field.
336    if let Some((ref display, span)) = container.display {
337        if !collected.field_names.iter().any(|n| n == display) {
338            return Err(syn::Error::new(
339                span,
340                format!("`display = \"{display}\"` does not match any field on this struct"),
341            ));
342        }
343    }
344    let display = container.display.map(|(name, _)| name);
345    let app_label = container.app.clone();
346
347    // Validate admin field-name lists against declared field names.
348    if let Some(admin) = &container.admin {
349        for (label, list) in [
350            ("list_display", &admin.list_display),
351            ("search_fields", &admin.search_fields),
352            ("readonly_fields", &admin.readonly_fields),
353            ("list_filter", &admin.list_filter),
354        ] {
355            if let Some((names, span)) = list {
356                for name in names {
357                    if !collected.field_names.iter().any(|n| n == name) {
358                        return Err(syn::Error::new(
359                            *span,
360                            format!(
361                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
362                            ),
363                        ));
364                    }
365                }
366            }
367        }
368        if let Some((pairs, span)) = &admin.ordering {
369            for (name, _) in pairs {
370                if !collected.field_names.iter().any(|n| n == name) {
371                    return Err(syn::Error::new(
372                        *span,
373                        format!(
374                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
375                        ),
376                    ));
377                }
378            }
379        }
380        if let Some((groups, span)) = &admin.fieldsets {
381            for (_, fields) in groups {
382                for name in fields {
383                    if !collected.field_names.iter().any(|n| n == name) {
384                        return Err(syn::Error::new(
385                            *span,
386                            format!(
387                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
388                            ),
389                        ));
390                    }
391                }
392            }
393        }
394    }
395    if let Some(audit) = &container.audit {
396        if let Some((names, span)) = &audit.track {
397            for name in names {
398                if !collected.field_names.iter().any(|n| n == name) {
399                    return Err(syn::Error::new(
400                        *span,
401                        format!(
402                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
403                        ),
404                    ));
405                }
406            }
407        }
408    }
409
410    let model_impl = model_impl_tokens(
411        struct_name,
412        &model_name,
413        &table,
414        display.as_deref(),
415        app_label.as_deref(),
416        container.admin.as_ref(),
417        &collected.field_schemas,
418        collected.soft_delete_column.as_deref(),
419    );
420    let module_ident = column_module_ident(struct_name);
421    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
422    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
423        let track_set: Option<std::collections::HashSet<&str>> = audit
424            .track
425            .as_ref()
426            .map(|(names, _)| names.iter().map(String::as_str).collect());
427        collected
428            .column_entries
429            .iter()
430            .filter(|c| {
431                track_set
432                    .as_ref()
433                    .map_or(true, |s| s.contains(c.name.as_str()))
434            })
435            .collect()
436    });
437    let inherent_impl = inherent_impl_tokens(
438        struct_name,
439        &collected,
440        collected.primary_key.as_ref(),
441        &column_consts,
442        audited_fields.as_deref(),
443    );
444    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
445    let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
446    let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
447
448    Ok(quote! {
449        #model_impl
450        #inherent_impl
451        #from_row_impl
452        #column_module
453        #reverse_helpers
454
455        ::rustango::core::inventory::submit! {
456            ::rustango::core::ModelEntry {
457                schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
458                // `module_path!()` evaluates at the registration site,
459                // so a Model declared in `crate::blog::models` records
460                // `"<crate>::blog::models"` and `resolved_app_label()`
461                // can infer "blog" without an explicit attribute.
462                module_path: ::core::module_path!(),
463            }
464        }
465    })
466}
467
468/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
469/// matches `field_name` against the model's FK fields and, for a
470/// match, decodes the FK target via the parent's macro-generated
471/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
472/// `ForeignKey::Loaded` on `self`.
473///
474/// Always emitted (with empty arms for FK-less models, which
475/// return `Ok(false)` for any field name) so the `T: LoadRelated`
476/// trait bound on `fetch_on` is universally satisfied — users
477/// never have to think about implementing it.
478fn load_related_impl_tokens(
479    struct_name: &syn::Ident,
480    fk_relations: &[FkRelation],
481) -> TokenStream2 {
482    let arms = fk_relations.iter().map(|rel| {
483        let parent_ty = &rel.parent_type;
484        let fk_col = rel.fk_column.as_str();
485        // FK field's Rust ident matches its SQL column name in v0.8
486        // (no `column = "..."` rename ships on FK fields).
487        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
488        quote! {
489            #fk_col => {
490                let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
491                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
492                    ::rustango::core::SqlValue::I64(v) => v,
493                    _ => 0i64,
494                };
495                self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
496                ::core::result::Result::Ok(true)
497            }
498        }
499    });
500    quote! {
501        impl ::rustango::sql::LoadRelated for #struct_name {
502            #[allow(unused_variables)]
503            fn __rustango_load_related(
504                &mut self,
505                row: &::rustango::sql::sqlx::postgres::PgRow,
506                field_name: &str,
507                alias: &str,
508            ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
509                match field_name {
510                    #( #arms )*
511                    _ => ::core::result::Result::Ok(false),
512                }
513            }
514        }
515    }
516}
517
518/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
519/// matches `field_name` against the model's FK fields and returns
520/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
521/// group children by parent PK.
522///
523/// Always emitted (with `_ => None` for FK-less models) so the
524/// trait bound on `fetch_with_prefetch` is universally satisfied.
525fn fk_pk_access_impl_tokens(
526    struct_name: &syn::Ident,
527    fk_relations: &[FkRelation],
528) -> TokenStream2 {
529    let arms = fk_relations.iter().map(|rel| {
530        let fk_col = rel.fk_column.as_str();
531        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
532        quote! {
533            #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
534        }
535    });
536    quote! {
537        impl ::rustango::sql::FkPkAccess for #struct_name {
538            #[allow(unused_variables)]
539            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
540                match field_name {
541                    #( #arms )*
542                    _ => ::core::option::Option::None,
543                }
544            }
545        }
546    }
547}
548
549/// For every `ForeignKey<Parent>` field on `Child`, emit
550/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
551/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
552/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
553/// — the canonical reverse-FK fetch. One round trip, no N+1.
554fn reverse_helper_tokens(
555    child_ident: &syn::Ident,
556    fk_relations: &[FkRelation],
557) -> TokenStream2 {
558    if fk_relations.is_empty() {
559        return TokenStream2::new();
560    }
561    // Snake-case the child struct name to derive the method suffix —
562    // `Post` → `post_set`, `BlogComment` → `blog_comment_set`. Avoids
563    // English-plural edge cases (Django's `<child>_set` convention).
564    let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
565    let method_ident = syn::Ident::new(&suffix, child_ident.span());
566    let impls = fk_relations.iter().map(|rel| {
567        let parent_ty = &rel.parent_type;
568        let fk_col = rel.fk_column.as_str();
569        let doc = format!(
570            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
571             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
572             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
573             further `{child_ident}::objects()` filters via direct queryset use."
574        );
575        quote! {
576            impl #parent_ty {
577                #[doc = #doc]
578                ///
579                /// # Errors
580                /// Returns [`::rustango::sql::ExecError`] for SQL-writing
581                /// or driver failures.
582                pub async fn #method_ident<'_c, _E>(
583                    &self,
584                    _executor: _E,
585                ) -> ::core::result::Result<
586                    ::std::vec::Vec<#child_ident>,
587                    ::rustango::sql::ExecError,
588                >
589                where
590                    _E: ::rustango::sql::sqlx::Executor<
591                        '_c,
592                        Database = ::rustango::sql::sqlx::Postgres,
593                    >,
594                {
595                    let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
596                    ::rustango::query::QuerySet::<#child_ident>::new()
597                        .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
598                        .fetch_on(_executor)
599                        .await
600                }
601            }
602        }
603    });
604    quote! { #( #impls )* }
605}
606
607struct ColumnEntry {
608    /// The struct field ident, used both for the inherent const name on
609    /// the model and for the inner column type's name.
610    ident: syn::Ident,
611    /// The struct's field type, used as `Column::Value`.
612    value_ty: Type,
613    /// Rust-side field name (e.g. `"id"`).
614    name: String,
615    /// SQL-side column name (e.g. `"user_id"`).
616    column: String,
617    /// `::rustango::core::FieldType::I64` etc.
618    field_type_tokens: TokenStream2,
619}
620
621struct CollectedFields {
622    field_schemas: Vec<TokenStream2>,
623    from_row_inits: Vec<TokenStream2>,
624    /// Aliased counterparts of `from_row_inits` — read columns via
625    /// `format!("{prefix}__{col}")` aliases so a Model can be
626    /// decoded from a JOINed row's projected target columns.
627    from_aliased_row_inits: Vec<TokenStream2>,
628    /// Static column-name list — used by the simple insert path
629    /// (no `Auto<T>` fields). Aligned with `insert_values`.
630    insert_columns: Vec<TokenStream2>,
631    /// Static `Into<SqlValue>` expressions, one per field. Aligned
632    /// with `insert_columns`. Used by the simple insert path only.
633    insert_values: Vec<TokenStream2>,
634    /// Per-field push expressions for the dynamic (Auto-aware)
635    /// insert path. Each statement either unconditionally pushes
636    /// `(column, value)` or, for an `Auto<T>` field, conditionally
637    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
638    insert_pushes: Vec<TokenStream2>,
639    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
640    /// when `has_auto == false`.
641    returning_cols: Vec<TokenStream2>,
642    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
643    /// field. Run after `insert_returning` to populate the model.
644    auto_assigns: Vec<TokenStream2>,
645    /// `(ident, column_literal)` pairs for every Auto field. Used by
646    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
647    /// instead of `self`.
648    auto_field_idents: Vec<(syn::Ident, String)>,
649    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
650    /// by the all-Auto-Unset bulk path (Auto cols dropped from
651    /// `columns`).
652    bulk_pushes_no_auto: Vec<TokenStream2>,
653    /// Bulk-insert per-row pushes for **all fields including Auto**.
654    /// Used by the all-Auto-Set bulk path (Auto col included with the
655    /// caller-supplied value).
656    bulk_pushes_all: Vec<TokenStream2>,
657    /// Column-name literals for non-Auto fields only (paired with
658    /// `bulk_pushes_no_auto`).
659    bulk_columns_no_auto: Vec<TokenStream2>,
660    /// Column-name literals for every field including Auto (paired
661    /// with `bulk_pushes_all`).
662    bulk_columns_all: Vec<TokenStream2>,
663    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
664    /// + the loop that asserts every row matches. One pair per Auto
665    /// field. Empty when `has_auto == false`.
666    bulk_auto_uniformity: Vec<TokenStream2>,
667    /// Identifier of the first Auto field, used as the witness for
668    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
669    first_auto_ident: Option<syn::Ident>,
670    /// `true` if any field on the struct is `Auto<T>`.
671    has_auto: bool,
672    /// `true` when the primary-key field's Rust type is `Auto<T>`.
673    /// Gates `save()` codegen — only Auto PKs let us infer
674    /// insert-vs-update from the in-memory value.
675    pk_is_auto: bool,
676    /// `Assignment` constructors for every non-PK column. Drives the
677    /// UPDATE branch of `save()`.
678    update_assignments: Vec<TokenStream2>,
679    primary_key: Option<(syn::Ident, String)>,
680    column_entries: Vec<ColumnEntry>,
681    /// Rust-side field names, in declaration order. Used to validate
682    /// container attributes like `display = "…"`.
683    field_names: Vec<String>,
684    /// FK fields on this child model. Drives the reverse-relation
685    /// helper emit — for each FK, the macro adds an inherent
686    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
687    /// method on the parent type.
688    fk_relations: Vec<FkRelation>,
689    /// SQL column name of the `#[rustango(soft_delete)]` field, if
690    /// the model has one. Drives emission of the `soft_delete_on` /
691    /// `restore_on` inherent methods. At most one such column per
692    /// model is allowed; collect_fields rejects duplicates.
693    soft_delete_column: Option<String>,
694}
695
696#[derive(Clone)]
697struct FkRelation {
698    /// Inner type of `ForeignKey<T>` — the parent model. The reverse
699    /// helper is emitted as `impl <ParentType> { … }`.
700    parent_type: Type,
701    /// SQL column name on the child table for this FK (e.g. `"author"`).
702    /// Used in the generated `WHERE <fk_column> = $1` clause.
703    fk_column: String,
704}
705
706fn collect_fields(named: &syn::FieldsNamed) -> syn::Result<CollectedFields> {
707    let cap = named.named.len();
708    let mut out = CollectedFields {
709        field_schemas: Vec::with_capacity(cap),
710        from_row_inits: Vec::with_capacity(cap),
711        from_aliased_row_inits: Vec::with_capacity(cap),
712        insert_columns: Vec::with_capacity(cap),
713        insert_values: Vec::with_capacity(cap),
714        insert_pushes: Vec::with_capacity(cap),
715        returning_cols: Vec::new(),
716        auto_assigns: Vec::new(),
717        auto_field_idents: Vec::new(),
718        bulk_pushes_no_auto: Vec::with_capacity(cap),
719        bulk_pushes_all: Vec::with_capacity(cap),
720        bulk_columns_no_auto: Vec::with_capacity(cap),
721        bulk_columns_all: Vec::with_capacity(cap),
722        bulk_auto_uniformity: Vec::new(),
723        first_auto_ident: None,
724        has_auto: false,
725        pk_is_auto: false,
726        update_assignments: Vec::with_capacity(cap),
727        primary_key: None,
728        column_entries: Vec::with_capacity(cap),
729        field_names: Vec::with_capacity(cap),
730        fk_relations: Vec::new(),
731        soft_delete_column: None,
732    };
733
734    for field in &named.named {
735        let info = process_field(field)?;
736        out.field_names.push(info.ident.to_string());
737        out.field_schemas.push(info.schema);
738        out.from_row_inits.push(info.from_row_init);
739        out.from_aliased_row_inits.push(info.from_aliased_row_init);
740        if let Some(parent_ty) = info.fk_inner.clone() {
741            out.fk_relations.push(FkRelation {
742                parent_type: parent_ty,
743                fk_column: info.column.clone(),
744            });
745        }
746        if info.soft_delete {
747            if out.soft_delete_column.is_some() {
748                return Err(syn::Error::new_spanned(
749                    field,
750                    "only one field may be marked `#[rustango(soft_delete)]`",
751                ));
752            }
753            out.soft_delete_column = Some(info.column.clone());
754        }
755        let column = info.column.as_str();
756        let ident = info.ident;
757        out.insert_columns.push(quote!(#column));
758        out.insert_values.push(quote! {
759            ::core::convert::Into::<::rustango::core::SqlValue>::into(
760                ::core::clone::Clone::clone(&self.#ident)
761            )
762        });
763        if info.auto {
764            out.has_auto = true;
765            if out.first_auto_ident.is_none() {
766                out.first_auto_ident = Some(ident.clone());
767            }
768            out.returning_cols.push(quote!(#column));
769            out.auto_field_idents
770                .push((ident.clone(), info.column.clone()));
771            out.auto_assigns.push(quote! {
772                self.#ident = ::rustango::sql::sqlx::Row::try_get(&_returning_row, #column)?;
773            });
774            out.insert_pushes.push(quote! {
775                if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
776                    _columns.push(#column);
777                    _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
778                        ::core::clone::Clone::clone(_v)
779                    ));
780                }
781            });
782            // Bulk: Auto fields appear only in the all-Set path,
783            // never in the Unset path (we drop them from `columns`).
784            out.bulk_columns_all.push(quote!(#column));
785            out.bulk_pushes_all.push(quote! {
786                _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
787                    ::core::clone::Clone::clone(&_row.#ident)
788                ));
789            });
790            // Uniformity check: every row's Auto state must match the
791            // first row's. Mixed Set/Unset within one bulk_insert is
792            // rejected here so the column list stays consistent.
793            let ident_clone = ident.clone();
794            out.bulk_auto_uniformity.push(quote! {
795                for _r in rows.iter().skip(1) {
796                    if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
797                        return ::core::result::Result::Err(
798                            ::rustango::sql::ExecError::Sql(
799                                ::rustango::sql::SqlError::BulkAutoMixed
800                            )
801                        );
802                    }
803                }
804            });
805        } else {
806            out.insert_pushes.push(quote! {
807                _columns.push(#column);
808                _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
809                    ::core::clone::Clone::clone(&self.#ident)
810                ));
811            });
812            // Bulk: non-Auto fields appear in BOTH paths.
813            out.bulk_columns_no_auto.push(quote!(#column));
814            out.bulk_columns_all.push(quote!(#column));
815            let push_expr = quote! {
816                _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
817                    ::core::clone::Clone::clone(&_row.#ident)
818                ));
819            };
820            out.bulk_pushes_no_auto.push(push_expr.clone());
821            out.bulk_pushes_all.push(push_expr);
822        }
823        if info.primary_key {
824            if out.primary_key.is_some() {
825                return Err(syn::Error::new_spanned(
826                    field,
827                    "only one field may be marked `#[rustango(primary_key)]`",
828                ));
829            }
830            out.primary_key = Some((ident.clone(), info.column.clone()));
831            if info.auto {
832                out.pk_is_auto = true;
833            }
834        } else if info.auto_now_add {
835            // Immutable post-insert: skip from UPDATE entirely.
836        } else if info.auto_now {
837            // `auto_now` columns: bind `chrono::Utc::now()` on every
838            // UPDATE so the column is always overridden with the
839            // wall-clock at write time, regardless of what value the
840            // user left in the struct field.
841            out.update_assignments.push(quote! {
842                ::rustango::core::Assignment {
843                    column: #column,
844                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
845                        ::chrono::Utc::now()
846                    ),
847                }
848            });
849        } else {
850            out.update_assignments.push(quote! {
851                ::rustango::core::Assignment {
852                    column: #column,
853                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
854                        ::core::clone::Clone::clone(&self.#ident)
855                    ),
856                }
857            });
858        }
859        out.column_entries.push(ColumnEntry {
860            ident: ident.clone(),
861            value_ty: info.value_ty.clone(),
862            name: ident.to_string(),
863            column: info.column.clone(),
864            field_type_tokens: info.field_type_tokens,
865        });
866    }
867    Ok(out)
868}
869
870fn model_impl_tokens(
871    struct_name: &syn::Ident,
872    model_name: &str,
873    table: &str,
874    display: Option<&str>,
875    app_label: Option<&str>,
876    admin: Option<&AdminAttrs>,
877    field_schemas: &[TokenStream2],
878    soft_delete_column: Option<&str>,
879) -> TokenStream2 {
880    let display_tokens = if let Some(name) = display {
881        quote!(::core::option::Option::Some(#name))
882    } else {
883        quote!(::core::option::Option::None)
884    };
885    let app_label_tokens = if let Some(name) = app_label {
886        quote!(::core::option::Option::Some(#name))
887    } else {
888        quote!(::core::option::Option::None)
889    };
890    let soft_delete_tokens = if let Some(col) = soft_delete_column {
891        quote!(::core::option::Option::Some(#col))
892    } else {
893        quote!(::core::option::Option::None)
894    };
895    let admin_tokens = admin_config_tokens(admin);
896    quote! {
897        impl ::rustango::core::Model for #struct_name {
898            const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
899                name: #model_name,
900                table: #table,
901                fields: &[ #(#field_schemas),* ],
902                display: #display_tokens,
903                app_label: #app_label_tokens,
904                admin: #admin_tokens,
905                soft_delete_column: #soft_delete_tokens,
906            };
907        }
908    }
909}
910
911/// Emit the `admin: Option<&'static AdminConfig>` field for the model
912/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
913/// otherwise a static reference to a populated `AdminConfig`.
914fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
915    let Some(admin) = admin else {
916        return quote!(::core::option::Option::None);
917    };
918
919    let list_display = admin
920        .list_display
921        .as_ref()
922        .map(|(v, _)| v.as_slice())
923        .unwrap_or(&[]);
924    let list_display_lits = list_display.iter().map(|s| s.as_str());
925
926    let search_fields = admin
927        .search_fields
928        .as_ref()
929        .map(|(v, _)| v.as_slice())
930        .unwrap_or(&[]);
931    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
932
933    let readonly_fields = admin
934        .readonly_fields
935        .as_ref()
936        .map(|(v, _)| v.as_slice())
937        .unwrap_or(&[]);
938    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
939
940    let list_filter = admin
941        .list_filter
942        .as_ref()
943        .map(|(v, _)| v.as_slice())
944        .unwrap_or(&[]);
945    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
946
947    let actions = admin
948        .actions
949        .as_ref()
950        .map(|(v, _)| v.as_slice())
951        .unwrap_or(&[]);
952    let actions_lits = actions.iter().map(|s| s.as_str());
953
954    let fieldsets = admin
955        .fieldsets
956        .as_ref()
957        .map(|(v, _)| v.as_slice())
958        .unwrap_or(&[]);
959    let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
960        let title = title.as_str();
961        let field_lits = fields.iter().map(|s| s.as_str());
962        quote!(::rustango::core::Fieldset {
963            title: #title,
964            fields: &[ #( #field_lits ),* ],
965        })
966    });
967
968    let list_per_page = admin.list_per_page.unwrap_or(0);
969
970    let ordering_pairs = admin
971        .ordering
972        .as_ref()
973        .map(|(v, _)| v.as_slice())
974        .unwrap_or(&[]);
975    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
976        let name = name.as_str();
977        let desc = *desc;
978        quote!((#name, #desc))
979    });
980
981    quote! {
982        ::core::option::Option::Some(&::rustango::core::AdminConfig {
983            list_display: &[ #( #list_display_lits ),* ],
984            search_fields: &[ #( #search_fields_lits ),* ],
985            list_per_page: #list_per_page,
986            ordering: &[ #( #ordering_tokens ),* ],
987            readonly_fields: &[ #( #readonly_fields_lits ),* ],
988            list_filter: &[ #( #list_filter_lits ),* ],
989            actions: &[ #( #actions_lits ),* ],
990            fieldsets: &[ #( #fieldset_tokens ),* ],
991        })
992    }
993}
994
995fn inherent_impl_tokens(
996    struct_name: &syn::Ident,
997    fields: &CollectedFields,
998    primary_key: Option<&(syn::Ident, String)>,
999    column_consts: &TokenStream2,
1000    audited_fields: Option<&[&ColumnEntry]>,
1001) -> TokenStream2 {
1002    // Audit-emit fragments threaded into write paths. Non-empty only
1003    // when the model carries `#[rustango(audit(...))]`. They reborrow
1004    // `_executor` (a `&mut PgConnection` for audited models — the
1005    // macro switches the signature below) so the data write and the
1006    // audit INSERT both run on the same caller-supplied connection.
1007    let executor_passes_to_data_write = if audited_fields.is_some() {
1008        quote!(&mut *_executor)
1009    } else {
1010        quote!(_executor)
1011    };
1012    let executor_param = if audited_fields.is_some() {
1013        quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1014    } else {
1015        quote!(_executor: _E)
1016    };
1017    let executor_generics = if audited_fields.is_some() {
1018        quote!()
1019    } else {
1020        quote!(<'_c, _E>)
1021    };
1022    let executor_where = if audited_fields.is_some() {
1023        quote!()
1024    } else {
1025        quote! {
1026            where
1027                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1028        }
1029    };
1030    // For audited models the `_on` methods take `&mut PgConnection`, so
1031    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
1032    // must acquire a connection first. Non-audited models keep the
1033    // direct delegation since `&PgPool` IS an Executor.
1034    let pool_to_save_on = if audited_fields.is_some() {
1035        quote! {
1036            let mut _conn = pool.acquire().await?;
1037            self.save_on(&mut *_conn).await
1038        }
1039    } else {
1040        quote!(self.save_on(pool).await)
1041    };
1042    let pool_to_insert_on = if audited_fields.is_some() {
1043        quote! {
1044            let mut _conn = pool.acquire().await?;
1045            self.insert_on(&mut *_conn).await
1046        }
1047    } else {
1048        quote!(self.insert_on(pool).await)
1049    };
1050    let pool_to_delete_on = if audited_fields.is_some() {
1051        quote! {
1052            let mut _conn = pool.acquire().await?;
1053            self.delete_on(&mut *_conn).await
1054        }
1055    } else {
1056        quote!(self.delete_on(pool).await)
1057    };
1058    let pool_to_bulk_insert_on = if audited_fields.is_some() {
1059        quote! {
1060            let mut _conn = pool.acquire().await?;
1061            Self::bulk_insert_on(rows, &mut *_conn).await
1062        }
1063    } else {
1064        quote!(Self::bulk_insert_on(rows, pool).await)
1065    };
1066
1067    // Build the (column, JSON value) pair list used by every
1068    // snapshot-style audit emission. Reused across delete_on,
1069    // soft_delete_on, restore_on, and (later) bulk paths. Empty
1070    // when the model isn't audited.
1071    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1072        .map(|tracked| {
1073            tracked
1074                .iter()
1075                .map(|c| {
1076                    let column_lit = c.column.as_str();
1077                    let ident = &c.ident;
1078                    quote! {
1079                        (
1080                            #column_lit,
1081                            ::serde_json::to_value(&self.#ident)
1082                                .unwrap_or(::serde_json::Value::Null),
1083                        )
1084                    }
1085                })
1086                .collect()
1087        })
1088        .unwrap_or_default();
1089    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1090        if fields.pk_is_auto {
1091            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1092        } else {
1093            quote!(::std::format!("{}", &self.#pk_ident))
1094        }
1095    } else {
1096        quote!(::std::string::String::new())
1097    };
1098    let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1099        if audited_fields.is_some() {
1100            let pairs = audit_pair_tokens.iter();
1101            let pk_str = audit_pk_to_string.clone();
1102            quote! {
1103                let _audit_entry = ::rustango::audit::PendingEntry {
1104                    entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1105                    entity_pk: #pk_str,
1106                    operation: #op_path,
1107                    source: ::rustango::audit::current_source(),
1108                    changes: ::rustango::audit::snapshot_changes(&[
1109                        #( #pairs ),*
1110                    ]),
1111                };
1112                ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1113            }
1114        } else {
1115            quote!()
1116        }
1117    };
1118    let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1119    let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1120    let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1121    let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1122
1123    // Update emission captures both BEFORE and AFTER state — runs an
1124    // extra SELECT against `_executor` BEFORE the UPDATE, captures
1125    // each tracked field's prior value, then after the UPDATE diffs
1126    // against the in-memory `&self`. `diff_changes` drops unchanged
1127    // columns so the JSON only contains the actual delta.
1128    //
1129    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
1130    // and binds `_audit_before_pairs`; `audit_update_post` runs
1131    // after the UPDATE and emits the PendingEntry.
1132    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) =
1133        if let Some(tracked) = audited_fields {
1134            if tracked.is_empty() {
1135                (quote!(), quote!())
1136            } else {
1137                let select_cols: String = tracked
1138                    .iter()
1139                    .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
1140                    .collect::<Vec<_>>()
1141                    .join(", ");
1142                let pk_column_for_select = primary_key
1143                    .map(|(_, col)| col.clone())
1144                    .unwrap_or_default();
1145                let select_cols_lit = select_cols;
1146                let pk_column_lit_for_select = pk_column_for_select;
1147                let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
1148                    if fields.pk_is_auto {
1149                        quote!(self.#pk_ident.get().copied().unwrap_or_default())
1150                    } else {
1151                        quote!(::core::clone::Clone::clone(&self.#pk_ident))
1152                    }
1153                } else {
1154                    quote!(0_i64)
1155                };
1156                let before_pairs = tracked.iter().map(|c| {
1157                    let column_lit = c.column.as_str();
1158                    let value_ty = &c.value_ty;
1159                    quote! {
1160                        (
1161                            #column_lit,
1162                            match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
1163                                &_audit_before_row, #column_lit,
1164                            ) {
1165                                ::core::result::Result::Ok(v) => {
1166                                    ::serde_json::to_value(&v)
1167                                        .unwrap_or(::serde_json::Value::Null)
1168                                }
1169                                ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1170                            },
1171                        )
1172                    }
1173                });
1174                let after_pairs = tracked.iter().map(|c| {
1175                    let column_lit = c.column.as_str();
1176                    let ident = &c.ident;
1177                    quote! {
1178                        (
1179                            #column_lit,
1180                            ::serde_json::to_value(&self.#ident)
1181                                .unwrap_or(::serde_json::Value::Null),
1182                        )
1183                    }
1184                });
1185                let pk_str = audit_pk_to_string.clone();
1186                let pre = quote! {
1187                    let _audit_select_sql = ::std::format!(
1188                        r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
1189                        #select_cols_lit,
1190                        <Self as ::rustango::core::Model>::SCHEMA.table,
1191                        #pk_column_lit_for_select,
1192                    );
1193                    let _audit_before_pairs:
1194                        ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
1195                        match ::rustango::sql::sqlx::query(&_audit_select_sql)
1196                            .bind(#pk_value_for_bind)
1197                            .fetch_optional(&mut *_executor)
1198                            .await
1199                        {
1200                            ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
1201                                ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
1202                            }
1203                            _ => ::core::option::Option::None,
1204                        };
1205                };
1206                let post = quote! {
1207                    if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
1208                        let _audit_after:
1209                            ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
1210                            ::std::vec![ #( #after_pairs ),* ];
1211                        let _audit_entry = ::rustango::audit::PendingEntry {
1212                            entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1213                            entity_pk: #pk_str,
1214                            operation: ::rustango::audit::AuditOp::Update,
1215                            source: ::rustango::audit::current_source(),
1216                            changes: ::rustango::audit::diff_changes(
1217                                &_audit_before,
1218                                &_audit_after,
1219                            ),
1220                        };
1221                        ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1222                    }
1223                };
1224                (pre, post)
1225            }
1226        } else {
1227            (quote!(), quote!())
1228        };
1229
1230    // Bulk-insert audit: capture every row's tracked fields after the
1231    // RETURNING populates each PK, then push one batched INSERT INTO
1232    // audit_log via `emit_many`. One round-trip regardless of N rows.
1233    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
1234        let row_pk_str = if let Some((pk_ident, _)) = primary_key {
1235            if fields.pk_is_auto {
1236                quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1237            } else {
1238                quote!(::std::format!("{}", &_row.#pk_ident))
1239            }
1240        } else {
1241            quote!(::std::string::String::new())
1242        };
1243        let row_pairs = audited_fields
1244            .unwrap_or(&[])
1245            .iter()
1246            .map(|c| {
1247                let column_lit = c.column.as_str();
1248                let ident = &c.ident;
1249                quote! {
1250                    (
1251                        #column_lit,
1252                        ::serde_json::to_value(&_row.#ident)
1253                            .unwrap_or(::serde_json::Value::Null),
1254                    )
1255                }
1256            });
1257        quote! {
1258            let _audit_source = ::rustango::audit::current_source();
1259            let mut _audit_entries:
1260                ::std::vec::Vec<::rustango::audit::PendingEntry> =
1261                    ::std::vec::Vec::with_capacity(rows.len());
1262            for _row in rows.iter() {
1263                _audit_entries.push(::rustango::audit::PendingEntry {
1264                    entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1265                    entity_pk: #row_pk_str,
1266                    operation: ::rustango::audit::AuditOp::Create,
1267                    source: _audit_source.clone(),
1268                    changes: ::rustango::audit::snapshot_changes(&[
1269                        #( #row_pairs ),*
1270                    ]),
1271                });
1272            }
1273            ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
1274        }
1275    } else {
1276        quote!()
1277    };
1278
1279    let save_method = if fields.pk_is_auto {
1280        let (pk_ident, pk_column) = primary_key
1281            .expect("pk_is_auto implies primary_key is Some");
1282        let pk_column_lit = pk_column.as_str();
1283        let assignments = &fields.update_assignments;
1284        Some(quote! {
1285            /// Insert this row if its `Auto<T>` primary key is
1286            /// `Unset`, otherwise update the existing row matching the
1287            /// PK. Mirrors Django's `save()` — caller doesn't need to
1288            /// pick `insert` vs the bulk-update path manually.
1289            ///
1290            /// On the insert branch, populates the PK from `RETURNING`
1291            /// (same behavior as `insert`). On the update branch,
1292            /// writes every non-PK column back; if no row matches the
1293            /// PK, returns `Ok(())` silently.
1294            ///
1295            /// Only generated when the primary key is declared as
1296            /// `Auto<T>`. Models with a manually-managed PK must use
1297            /// `insert` or the QuerySet update builder.
1298            ///
1299            /// # Errors
1300            /// Returns [`::rustango::sql::ExecError`] for SQL-writing
1301            /// or driver failures.
1302            pub async fn save(
1303                &mut self,
1304                pool: &::rustango::sql::sqlx::PgPool,
1305            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1306                #pool_to_save_on
1307            }
1308
1309            /// Like [`Self::save`] but accepts any sqlx executor —
1310            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
1311            /// escape hatch for tenant-scoped writes: schema-mode
1312            /// tenants share the registry pool but rely on a per-
1313            /// checkout `SET search_path`, so passing `&PgPool` would
1314            /// silently hit the wrong schema. Acquire a connection
1315            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
1316            ///
1317            /// # Errors
1318            /// As [`Self::save`].
1319            pub async fn save_on #executor_generics (
1320                &mut self,
1321                #executor_param,
1322            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1323            #executor_where
1324            {
1325                if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1326                    return self.insert_on(#executor_passes_to_data_write).await;
1327                }
1328                #audit_update_pre
1329                let _query = ::rustango::core::UpdateQuery {
1330                    model: <Self as ::rustango::core::Model>::SCHEMA,
1331                    set: ::std::vec![ #( #assignments ),* ],
1332                    where_clause: ::rustango::core::WhereExpr::Predicate(
1333                        ::rustango::core::Filter {
1334                            column: #pk_column_lit,
1335                            op: ::rustango::core::Op::Eq,
1336                            value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1337                                ::core::clone::Clone::clone(&self.#pk_ident)
1338                            ),
1339                        }
1340                    ),
1341                };
1342                let _ = ::rustango::sql::update_on(
1343                    #executor_passes_to_data_write,
1344                    &_query,
1345                ).await?;
1346                #audit_update_post
1347                ::core::result::Result::Ok(())
1348            }
1349
1350            /// Per-call override for the audit source. Runs
1351            /// [`Self::save_on`] inside an [`::rustango::audit::with_source`]
1352            /// scope so the resulting audit entry records `source`
1353            /// instead of the task-local default. Useful for seed
1354            /// scripts and one-off CLI tools that don't sit inside an
1355            /// admin handler. The override applies only to this call;
1356            /// no global state changes.
1357            ///
1358            /// # Errors
1359            /// As [`Self::save_on`].
1360            pub async fn save_on_with #executor_generics (
1361                &mut self,
1362                #executor_param,
1363                source: ::rustango::audit::AuditSource,
1364            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1365            #executor_where
1366            {
1367                ::rustango::audit::with_source(source, self.save_on(_executor)).await
1368            }
1369        })
1370    } else {
1371        None
1372    };
1373
1374    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
1375        let pk_column_lit = pk_column.as_str();
1376        // Optional `soft_delete_on` / `restore_on` companions when the
1377        // model has a `#[rustango(soft_delete)]` column. They land
1378        // alongside the regular `delete_on` so callers have both
1379        // options — a hard delete (audit-tracked as a real DELETE) and
1380        // a logical delete (audit-tracked as an UPDATE setting the
1381        // deleted_at column to NOW()).
1382        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
1383            let col_lit = col;
1384            quote! {
1385                /// Soft-delete this row by setting its
1386                /// `#[rustango(soft_delete)]` column to `NOW()`.
1387                /// Mirrors Django's `SoftDeleteModel.delete()` shape:
1388                /// the row stays in the table; query helpers can
1389                /// filter it out by checking the column for `IS NOT
1390                /// NULL`.
1391                ///
1392                /// # Errors
1393                /// As [`Self::delete`].
1394                pub async fn soft_delete_on #executor_generics (
1395                    &self,
1396                    #executor_param,
1397                ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1398                #executor_where
1399                {
1400                    let _query = ::rustango::core::UpdateQuery {
1401                        model: <Self as ::rustango::core::Model>::SCHEMA,
1402                        set: ::std::vec![
1403                            ::rustango::core::Assignment {
1404                                column: #col_lit,
1405                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1406                                    ::chrono::Utc::now()
1407                                ),
1408                            },
1409                        ],
1410                        where_clause: ::rustango::core::WhereExpr::Predicate(
1411                            ::rustango::core::Filter {
1412                                column: #pk_column_lit,
1413                                op: ::rustango::core::Op::Eq,
1414                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1415                                    ::core::clone::Clone::clone(&self.#pk_ident)
1416                                ),
1417                            }
1418                        ),
1419                    };
1420                    let _affected = ::rustango::sql::update_on(
1421                        #executor_passes_to_data_write,
1422                        &_query,
1423                    ).await?;
1424                    #audit_softdelete_emit
1425                    ::core::result::Result::Ok(_affected)
1426                }
1427
1428                /// Inverse of [`Self::soft_delete_on`] — clears the
1429                /// soft-delete column back to NULL so the row is
1430                /// considered live again.
1431                ///
1432                /// # Errors
1433                /// As [`Self::delete`].
1434                pub async fn restore_on #executor_generics (
1435                    &self,
1436                    #executor_param,
1437                ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1438                #executor_where
1439                {
1440                    let _query = ::rustango::core::UpdateQuery {
1441                        model: <Self as ::rustango::core::Model>::SCHEMA,
1442                        set: ::std::vec![
1443                            ::rustango::core::Assignment {
1444                                column: #col_lit,
1445                                value: ::rustango::core::SqlValue::Null,
1446                            },
1447                        ],
1448                        where_clause: ::rustango::core::WhereExpr::Predicate(
1449                            ::rustango::core::Filter {
1450                                column: #pk_column_lit,
1451                                op: ::rustango::core::Op::Eq,
1452                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1453                                    ::core::clone::Clone::clone(&self.#pk_ident)
1454                                ),
1455                            }
1456                        ),
1457                    };
1458                    let _affected = ::rustango::sql::update_on(
1459                        #executor_passes_to_data_write,
1460                        &_query,
1461                    ).await?;
1462                    #audit_restore_emit
1463                    ::core::result::Result::Ok(_affected)
1464                }
1465            }
1466        } else {
1467            quote!()
1468        };
1469        quote! {
1470            /// Delete the row identified by this instance's primary key.
1471            ///
1472            /// Returns the number of rows affected (0 or 1).
1473            ///
1474            /// # Errors
1475            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
1476            /// driver failures.
1477            pub async fn delete(
1478                &self,
1479                pool: &::rustango::sql::sqlx::PgPool,
1480            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
1481                #pool_to_delete_on
1482            }
1483
1484            /// Like [`Self::delete`] but accepts any sqlx executor —
1485            /// for tenant-scoped deletes against an explicitly-acquired
1486            /// connection. See [`Self::save_on`] for the rationale.
1487            ///
1488            /// # Errors
1489            /// As [`Self::delete`].
1490            pub async fn delete_on #executor_generics (
1491                &self,
1492                #executor_param,
1493            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1494            #executor_where
1495            {
1496                let query = ::rustango::core::DeleteQuery {
1497                    model: <Self as ::rustango::core::Model>::SCHEMA,
1498                    where_clause: ::rustango::core::WhereExpr::Predicate(
1499                        ::rustango::core::Filter {
1500                            column: #pk_column_lit,
1501                            op: ::rustango::core::Op::Eq,
1502                            value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1503                                ::core::clone::Clone::clone(&self.#pk_ident)
1504                            ),
1505                        }
1506                    ),
1507                };
1508                let _affected = ::rustango::sql::delete_on(
1509                    #executor_passes_to_data_write,
1510                    &query,
1511                ).await?;
1512                #audit_delete_emit
1513                ::core::result::Result::Ok(_affected)
1514            }
1515
1516            /// Per-call audit-source override for [`Self::delete_on`].
1517            /// See [`Self::save_on_with`] for shape rationale.
1518            ///
1519            /// # Errors
1520            /// As [`Self::delete_on`].
1521            pub async fn delete_on_with #executor_generics (
1522                &self,
1523                #executor_param,
1524                source: ::rustango::audit::AuditSource,
1525            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
1526            #executor_where
1527            {
1528                ::rustango::audit::with_source(source, self.delete_on(_executor)).await
1529            }
1530            #soft_delete_methods
1531        }
1532    });
1533
1534    let insert_method = if fields.has_auto {
1535        let pushes = &fields.insert_pushes;
1536        let returning_cols = &fields.returning_cols;
1537        let auto_assigns = &fields.auto_assigns;
1538        quote! {
1539            /// Insert this row into its table. Skips columns whose
1540            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
1541            /// sequence fills them in, then reads each `Auto` column
1542            /// back via `RETURNING` and stores it on `self`.
1543            ///
1544            /// # Errors
1545            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
1546            /// driver failures.
1547            pub async fn insert(
1548                &mut self,
1549                pool: &::rustango::sql::sqlx::PgPool,
1550            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1551                #pool_to_insert_on
1552            }
1553
1554            /// Like [`Self::insert`] but accepts any sqlx executor.
1555            /// See [`Self::save_on`] for tenancy-scoped rationale.
1556            ///
1557            /// # Errors
1558            /// As [`Self::insert`].
1559            pub async fn insert_on #executor_generics (
1560                &mut self,
1561                #executor_param,
1562            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1563            #executor_where
1564            {
1565                let mut _columns: ::std::vec::Vec<&'static str> =
1566                    ::std::vec::Vec::new();
1567                let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1568                    ::std::vec::Vec::new();
1569                #( #pushes )*
1570                let query = ::rustango::core::InsertQuery {
1571                    model: <Self as ::rustango::core::Model>::SCHEMA,
1572                    columns: _columns,
1573                    values: _values,
1574                    returning: ::std::vec![ #( #returning_cols ),* ],
1575                };
1576                let _returning_row = ::rustango::sql::insert_returning_on(
1577                    #executor_passes_to_data_write,
1578                    &query,
1579                ).await?;
1580                #( #auto_assigns )*
1581                #audit_insert_emit
1582                ::core::result::Result::Ok(())
1583            }
1584
1585            /// Per-call audit-source override for [`Self::insert_on`].
1586            /// See [`Self::save_on_with`] for shape rationale.
1587            ///
1588            /// # Errors
1589            /// As [`Self::insert_on`].
1590            pub async fn insert_on_with #executor_generics (
1591                &mut self,
1592                #executor_param,
1593                source: ::rustango::audit::AuditSource,
1594            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1595            #executor_where
1596            {
1597                ::rustango::audit::with_source(source, self.insert_on(_executor)).await
1598            }
1599        }
1600    } else {
1601        let insert_columns = &fields.insert_columns;
1602        let insert_values = &fields.insert_values;
1603        quote! {
1604            /// Insert this row into its table.
1605            ///
1606            /// # Errors
1607            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
1608            /// driver failures.
1609            pub async fn insert(
1610                &self,
1611                pool: &::rustango::sql::sqlx::PgPool,
1612            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1613                self.insert_on(pool).await
1614            }
1615
1616            /// Like [`Self::insert`] but accepts any sqlx executor.
1617            /// See [`Self::save_on`] for tenancy-scoped rationale.
1618            ///
1619            /// # Errors
1620            /// As [`Self::insert`].
1621            pub async fn insert_on<'_c, _E>(
1622                &self,
1623                _executor: _E,
1624            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1625            where
1626                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1627            {
1628                let query = ::rustango::core::InsertQuery {
1629                    model: <Self as ::rustango::core::Model>::SCHEMA,
1630                    columns: ::std::vec![ #( #insert_columns ),* ],
1631                    values: ::std::vec![ #( #insert_values ),* ],
1632                    returning: ::std::vec::Vec::new(),
1633                };
1634                ::rustango::sql::insert_on(_executor, &query).await
1635            }
1636        }
1637    };
1638
1639    let bulk_insert_method = if fields.has_auto {
1640        let cols_no_auto = &fields.bulk_columns_no_auto;
1641        let cols_all = &fields.bulk_columns_all;
1642        let pushes_no_auto = &fields.bulk_pushes_no_auto;
1643        let pushes_all = &fields.bulk_pushes_all;
1644        let returning_cols = &fields.returning_cols;
1645        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
1646        let uniformity = &fields.bulk_auto_uniformity;
1647        let first_auto_ident = fields
1648            .first_auto_ident
1649            .as_ref()
1650            .expect("has_auto implies first_auto_ident is Some");
1651        quote! {
1652            /// Bulk-insert `rows` in a single round-trip. Every row's
1653            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
1654            /// (sequence fills them in) or uniformly `Auto::Set(_)`
1655            /// (caller-supplied values). Mixed Set/Unset is rejected
1656            /// — call `insert` per row for that case.
1657            ///
1658            /// Empty slice is a no-op. Each row's `Auto` fields are
1659            /// populated from the `RETURNING` clause in input order
1660            /// before this returns.
1661            ///
1662            /// # Errors
1663            /// Returns [`::rustango::sql::ExecError`] for validation,
1664            /// SQL-writing, mixed-Auto rejection, or driver failures.
1665            pub async fn bulk_insert(
1666                rows: &mut [Self],
1667                pool: &::rustango::sql::sqlx::PgPool,
1668            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1669                #pool_to_bulk_insert_on
1670            }
1671
1672            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
1673            /// See [`Self::save_on`] for tenancy-scoped rationale.
1674            ///
1675            /// # Errors
1676            /// As [`Self::bulk_insert`].
1677            pub async fn bulk_insert_on #executor_generics (
1678                rows: &mut [Self],
1679                #executor_param,
1680            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1681            #executor_where
1682            {
1683                if rows.is_empty() {
1684                    return ::core::result::Result::Ok(());
1685                }
1686                let _first_unset = matches!(
1687                    rows[0].#first_auto_ident,
1688                    ::rustango::sql::Auto::Unset
1689                );
1690                #( #uniformity )*
1691
1692                let mut _all_rows: ::std::vec::Vec<
1693                    ::std::vec::Vec<::rustango::core::SqlValue>,
1694                > = ::std::vec::Vec::with_capacity(rows.len());
1695                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
1696                    for _row in rows.iter() {
1697                        let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
1698                            ::std::vec::Vec::new();
1699                        #( #pushes_no_auto )*
1700                        _all_rows.push(_row_vals);
1701                    }
1702                    ::std::vec![ #( #cols_no_auto ),* ]
1703                } else {
1704                    for _row in rows.iter() {
1705                        let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
1706                            ::std::vec::Vec::new();
1707                        #( #pushes_all )*
1708                        _all_rows.push(_row_vals);
1709                    }
1710                    ::std::vec![ #( #cols_all ),* ]
1711                };
1712
1713                let _query = ::rustango::core::BulkInsertQuery {
1714                    model: <Self as ::rustango::core::Model>::SCHEMA,
1715                    columns: _columns,
1716                    rows: _all_rows,
1717                    returning: ::std::vec![ #( #returning_cols ),* ],
1718                };
1719                let _returned = ::rustango::sql::bulk_insert_on(
1720                    #executor_passes_to_data_write,
1721                    &_query,
1722                ).await?;
1723                if _returned.len() != rows.len() {
1724                    return ::core::result::Result::Err(
1725                        ::rustango::sql::ExecError::Sql(
1726                            ::rustango::sql::SqlError::BulkInsertReturningMismatch {
1727                                expected: rows.len(),
1728                                actual: _returned.len(),
1729                            }
1730                        )
1731                    );
1732                }
1733                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
1734                    #auto_assigns_for_row
1735                }
1736                #audit_bulk_insert_emit
1737                ::core::result::Result::Ok(())
1738            }
1739        }
1740    } else {
1741        let cols_all = &fields.bulk_columns_all;
1742        let pushes_all = &fields.bulk_pushes_all;
1743        quote! {
1744            /// Bulk-insert `rows` in a single round-trip. Every row's
1745            /// fields are written verbatim — there are no `Auto<T>`
1746            /// fields on this model.
1747            ///
1748            /// Empty slice is a no-op.
1749            ///
1750            /// # Errors
1751            /// Returns [`::rustango::sql::ExecError`] for validation,
1752            /// SQL-writing, or driver failures.
1753            pub async fn bulk_insert(
1754                rows: &[Self],
1755                pool: &::rustango::sql::sqlx::PgPool,
1756            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1757                Self::bulk_insert_on(rows, pool).await
1758            }
1759
1760            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
1761            /// See [`Self::save_on`] for tenancy-scoped rationale.
1762            ///
1763            /// # Errors
1764            /// As [`Self::bulk_insert`].
1765            pub async fn bulk_insert_on<'_c, _E>(
1766                rows: &[Self],
1767                _executor: _E,
1768            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
1769            where
1770                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1771            {
1772                if rows.is_empty() {
1773                    return ::core::result::Result::Ok(());
1774                }
1775                let mut _all_rows: ::std::vec::Vec<
1776                    ::std::vec::Vec<::rustango::core::SqlValue>,
1777                > = ::std::vec::Vec::with_capacity(rows.len());
1778                for _row in rows.iter() {
1779                    let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
1780                        ::std::vec::Vec::new();
1781                    #( #pushes_all )*
1782                    _all_rows.push(_row_vals);
1783                }
1784                let _query = ::rustango::core::BulkInsertQuery {
1785                    model: <Self as ::rustango::core::Model>::SCHEMA,
1786                    columns: ::std::vec![ #( #cols_all ),* ],
1787                    rows: _all_rows,
1788                    returning: ::std::vec::Vec::new(),
1789                };
1790                let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
1791                ::core::result::Result::Ok(())
1792            }
1793        }
1794    };
1795
1796    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
1797        quote! {
1798            /// Hidden runtime accessor for the primary-key value as a
1799            /// [`SqlValue`]. Used by reverse-relation helpers
1800            /// (`<parent>::<child>_set`) emitted from sibling models'
1801            /// FK fields. Not part of the public API.
1802            #[doc(hidden)]
1803            pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
1804                ::core::convert::Into::<::rustango::core::SqlValue>::into(
1805                    ::core::clone::Clone::clone(&self.#pk_ident)
1806                )
1807            }
1808        }
1809    });
1810
1811    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
1812        quote! {
1813            impl ::rustango::sql::HasPkValue for #struct_name {
1814                fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
1815                    ::core::convert::Into::<::rustango::core::SqlValue>::into(
1816                        ::core::clone::Clone::clone(&self.#pk_ident)
1817                    )
1818                }
1819            }
1820        }
1821    });
1822
1823    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
1824
1825    let from_aliased_row_inits = &fields.from_aliased_row_inits;
1826    let aliased_row_helper = quote! {
1827        /// Decode a row's aliased target columns (produced by
1828        /// `select_related`'s LEFT JOIN) into a fresh instance of
1829        /// this model. Reads each column via
1830        /// `format!("{prefix}__{col}")`, matching the alias the
1831        /// SELECT writer emitted. Slice 9.0d.
1832        #[doc(hidden)]
1833        pub fn __rustango_from_aliased_row(
1834            row: &::rustango::sql::sqlx::postgres::PgRow,
1835            prefix: &str,
1836        ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
1837            ::core::result::Result::Ok(Self {
1838                #( #from_aliased_row_inits ),*
1839            })
1840        }
1841    };
1842
1843    let load_related_impl =
1844        load_related_impl_tokens(struct_name, &fields.fk_relations);
1845
1846    quote! {
1847        impl #struct_name {
1848            /// Start a new `QuerySet` over this model.
1849            #[must_use]
1850            pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
1851                ::rustango::query::QuerySet::new()
1852            }
1853
1854            #insert_method
1855
1856            #bulk_insert_method
1857
1858            #save_method
1859
1860            #pk_methods
1861
1862            #pk_value_helper
1863
1864            #aliased_row_helper
1865
1866            #column_consts
1867        }
1868
1869        #load_related_impl
1870
1871        #has_pk_value_impl
1872
1873        #fk_pk_access_impl
1874    }
1875}
1876
1877/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
1878/// `auto_assigns` but reading from `_returning_row` and writing to
1879/// `_row_mut` instead of `self`.
1880fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
1881    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
1882        let col_lit = column.as_str();
1883        quote! {
1884            _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
1885                _returning_row,
1886                #col_lit,
1887            )?;
1888        }
1889    });
1890    quote! { #( #lines )* }
1891}
1892
1893/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
1894fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
1895    let lines = entries.iter().map(|e| {
1896        let ident = &e.ident;
1897        let col_ty = column_type_ident(ident);
1898        quote! {
1899            #[allow(non_upper_case_globals)]
1900            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
1901        }
1902    });
1903    quote! { #(#lines)* }
1904}
1905
1906/// Emit a hidden per-model module carrying one zero-sized type per field,
1907/// each with a `Column` impl pointing back at the model.
1908fn column_module_tokens(
1909    module_ident: &syn::Ident,
1910    struct_name: &syn::Ident,
1911    entries: &[ColumnEntry],
1912) -> TokenStream2 {
1913    let items = entries.iter().map(|e| {
1914        let col_ty = column_type_ident(&e.ident);
1915        let value_ty = &e.value_ty;
1916        let name = &e.name;
1917        let column = &e.column;
1918        let field_type_tokens = &e.field_type_tokens;
1919        quote! {
1920            #[derive(::core::clone::Clone, ::core::marker::Copy)]
1921            pub struct #col_ty;
1922
1923            impl ::rustango::core::Column for #col_ty {
1924                type Model = super::#struct_name;
1925                type Value = #value_ty;
1926                const NAME: &'static str = #name;
1927                const COLUMN: &'static str = #column;
1928                const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
1929            }
1930        }
1931    });
1932    quote! {
1933        #[doc(hidden)]
1934        #[allow(non_camel_case_types, non_snake_case)]
1935        pub mod #module_ident {
1936            // Re-import the parent scope so field types referencing
1937            // sibling models (e.g. `ForeignKey<Author>`) resolve
1938            // inside this submodule. Without this we'd hit
1939            // `proc_macro_derive_resolution_fallback` warnings.
1940            #[allow(unused_imports)]
1941            use super::*;
1942            #(#items)*
1943        }
1944    }
1945}
1946
1947fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
1948    syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
1949}
1950
1951fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
1952    syn::Ident::new(
1953        &format!("__rustango_cols_{struct_name}"),
1954        struct_name.span(),
1955    )
1956}
1957
1958fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
1959    quote! {
1960        impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
1961            for #struct_name
1962        {
1963            fn from_row(
1964                row: &'r ::rustango::sql::sqlx::postgres::PgRow,
1965            ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
1966                ::core::result::Result::Ok(Self {
1967                    #( #from_row_inits ),*
1968                })
1969            }
1970        }
1971    }
1972}
1973
1974struct ContainerAttrs {
1975    table: Option<String>,
1976    display: Option<(String, proc_macro2::Span)>,
1977    /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
1978    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
1979    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
1980    /// at runtime — this attribute is the override for cases where
1981    /// the inference is wrong (e.g. a model that conceptually belongs
1982    /// to one app but is physically in another module).
1983    app: Option<String>,
1984    /// Django ModelAdmin-shape per-model knobs from
1985    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
1986    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
1987    /// admin code falls back to `AdminConfig::DEFAULT`.
1988    admin: Option<AdminAttrs>,
1989    /// Per-model audit configuration from `#[rustango(audit(...))]`.
1990    /// `None` when the model isn't audited — write paths emit no
1991    /// audit entries. When present, single-row writes capture
1992    /// before/after for the listed fields and bulk writes batch
1993    /// snapshots into one INSERT into `rustango_audit_log`.
1994    audit: Option<AuditAttrs>,
1995}
1996
1997/// Parsed shape of `#[rustango(audit(track = "name, body", source =
1998/// "user"))]`. `track` is a comma-separated list of field names whose
1999/// before/after values land in the JSONB `changes` column. `source`
2000/// is informational only — it pins a default source when the model
2001/// is written outside any `audit::with_source(...)` scope (rare).
2002#[derive(Default)]
2003struct AuditAttrs {
2004    /// Field names to capture in the `changes` JSONB. Validated
2005    /// against declared scalar fields at compile time. Empty means
2006    /// "track every scalar field" — Django's audit-everything default.
2007    track: Option<(Vec<String>, proc_macro2::Span)>,
2008}
2009
2010/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
2011/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
2012/// comma-separated strings; we validate each ident against the model's
2013/// declared fields at compile time.
2014#[derive(Default)]
2015struct AdminAttrs {
2016    list_display: Option<(Vec<String>, proc_macro2::Span)>,
2017    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
2018    list_per_page: Option<usize>,
2019    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
2020    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
2021    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
2022    /// Bulk action names. No field-validation against model fields —
2023    /// these are action handlers, not column references.
2024    actions: Option<(Vec<String>, proc_macro2::Span)>,
2025    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
2026    /// sections, comma-separated fields per section, optional
2027    /// `Title:` prefix. Empty title omits the `<legend>`.
2028    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
2029}
2030
2031fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
2032    let mut out = ContainerAttrs {
2033        table: None,
2034        display: None,
2035        app: None,
2036        admin: None,
2037        audit: None,
2038    };
2039    for attr in &input.attrs {
2040        if !attr.path().is_ident("rustango") {
2041            continue;
2042        }
2043        attr.parse_nested_meta(|meta| {
2044            if meta.path.is_ident("table") {
2045                let s: LitStr = meta.value()?.parse()?;
2046                out.table = Some(s.value());
2047                return Ok(());
2048            }
2049            if meta.path.is_ident("display") {
2050                let s: LitStr = meta.value()?.parse()?;
2051                out.display = Some((s.value(), s.span()));
2052                return Ok(());
2053            }
2054            if meta.path.is_ident("app") {
2055                let s: LitStr = meta.value()?.parse()?;
2056                out.app = Some(s.value());
2057                return Ok(());
2058            }
2059            if meta.path.is_ident("admin") {
2060                let mut admin = AdminAttrs::default();
2061                meta.parse_nested_meta(|inner| {
2062                    if inner.path.is_ident("list_display") {
2063                        let s: LitStr = inner.value()?.parse()?;
2064                        admin.list_display =
2065                            Some((split_field_list(&s.value()), s.span()));
2066                        return Ok(());
2067                    }
2068                    if inner.path.is_ident("search_fields") {
2069                        let s: LitStr = inner.value()?.parse()?;
2070                        admin.search_fields =
2071                            Some((split_field_list(&s.value()), s.span()));
2072                        return Ok(());
2073                    }
2074                    if inner.path.is_ident("readonly_fields") {
2075                        let s: LitStr = inner.value()?.parse()?;
2076                        admin.readonly_fields =
2077                            Some((split_field_list(&s.value()), s.span()));
2078                        return Ok(());
2079                    }
2080                    if inner.path.is_ident("list_per_page") {
2081                        let lit: syn::LitInt = inner.value()?.parse()?;
2082                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
2083                        return Ok(());
2084                    }
2085                    if inner.path.is_ident("ordering") {
2086                        let s: LitStr = inner.value()?.parse()?;
2087                        admin.ordering = Some((
2088                            parse_ordering_list(&s.value()),
2089                            s.span(),
2090                        ));
2091                        return Ok(());
2092                    }
2093                    if inner.path.is_ident("list_filter") {
2094                        let s: LitStr = inner.value()?.parse()?;
2095                        admin.list_filter =
2096                            Some((split_field_list(&s.value()), s.span()));
2097                        return Ok(());
2098                    }
2099                    if inner.path.is_ident("actions") {
2100                        let s: LitStr = inner.value()?.parse()?;
2101                        admin.actions =
2102                            Some((split_field_list(&s.value()), s.span()));
2103                        return Ok(());
2104                    }
2105                    if inner.path.is_ident("fieldsets") {
2106                        let s: LitStr = inner.value()?.parse()?;
2107                        admin.fieldsets =
2108                            Some((parse_fieldset_list(&s.value()), s.span()));
2109                        return Ok(());
2110                    }
2111                    Err(inner.error(
2112                        "unknown admin attribute (supported: \
2113                         `list_display`, `search_fields`, `readonly_fields`, \
2114                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
2115                         `fieldsets`)",
2116                    ))
2117                })?;
2118                out.admin = Some(admin);
2119                return Ok(());
2120            }
2121            if meta.path.is_ident("audit") {
2122                let mut audit = AuditAttrs::default();
2123                meta.parse_nested_meta(|inner| {
2124                    if inner.path.is_ident("track") {
2125                        let s: LitStr = inner.value()?.parse()?;
2126                        audit.track =
2127                            Some((split_field_list(&s.value()), s.span()));
2128                        return Ok(());
2129                    }
2130                    Err(inner.error(
2131                        "unknown audit attribute (supported: `track`)",
2132                    ))
2133                })?;
2134                out.audit = Some(audit);
2135                return Ok(());
2136            }
2137            Err(meta.error("unknown rustango container attribute"))
2138        })?;
2139    }
2140    Ok(out)
2141}
2142
2143/// Split a comma-separated field-name list (e.g. `"name, office"`) into
2144/// owned field names, trimming whitespace and skipping empty entries.
2145/// Field-name validation against the model is done by the caller.
2146fn split_field_list(raw: &str) -> Vec<String> {
2147    raw.split(',')
2148        .map(str::trim)
2149        .filter(|s| !s.is_empty())
2150        .map(str::to_owned)
2151        .collect()
2152}
2153
2154/// Parse the fieldsets DSL: pipe-separated sections, optional
2155/// `"Title:"` prefix on each, comma-separated field names after.
2156/// Examples:
2157/// * `"name, office"` → one untitled section with two fields
2158/// * `"Identity: name, office | Metadata: created_at"` → two titled
2159///   sections
2160///
2161/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
2162fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
2163    raw.split('|')
2164        .map(str::trim)
2165        .filter(|s| !s.is_empty())
2166        .map(|section| {
2167            // Split off an optional `Title:` prefix (first colon).
2168            let (title, rest) = match section.split_once(':') {
2169                Some((title, rest)) if !title.contains(',') => {
2170                    (title.trim().to_owned(), rest)
2171                }
2172                _ => (String::new(), section),
2173            };
2174            let fields = split_field_list(rest);
2175            (title, fields)
2176        })
2177        .collect()
2178}
2179
2180/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
2181/// Returns `(field_name, desc)` pairs in the same order as the input.
2182fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
2183    raw.split(',')
2184        .map(str::trim)
2185        .filter(|s| !s.is_empty())
2186        .map(|spec| {
2187            spec.strip_prefix('-')
2188                .map_or((spec.to_owned(), false), |rest| (rest.trim().to_owned(), true))
2189        })
2190        .collect()
2191}
2192
2193struct FieldAttrs {
2194    column: Option<String>,
2195    primary_key: bool,
2196    fk: Option<String>,
2197    o2o: Option<String>,
2198    on: Option<String>,
2199    max_length: Option<u32>,
2200    min: Option<i64>,
2201    max: Option<i64>,
2202    default: Option<String>,
2203    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
2204    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
2205    /// "gen_random_uuid()"`. The Rust field type must be
2206    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
2207    /// INSERTs so the DB DEFAULT fires.
2208    auto_uuid: bool,
2209    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
2210    /// Server-set on insert, immutable from app code afterwards.
2211    /// Implies `auto + default = "now()"`. Field type must be
2212    /// `DateTime<Utc>`.
2213    auto_now_add: bool,
2214    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
2215    /// every insert AND every update. Implies `auto + default =
2216    /// "now()"`; the macro additionally rewrites `update_on` /
2217    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
2218    /// field value.
2219    auto_now: bool,
2220    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
2221    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
2222    /// `soft_delete_on(executor)` and `restore_on(executor)`
2223    /// methods on the model.
2224    soft_delete: bool,
2225}
2226
2227fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
2228    let mut out = FieldAttrs {
2229        column: None,
2230        primary_key: false,
2231        fk: None,
2232        o2o: None,
2233        on: None,
2234        max_length: None,
2235        min: None,
2236        max: None,
2237        default: None,
2238        auto_uuid: false,
2239        auto_now_add: false,
2240        auto_now: false,
2241        soft_delete: false,
2242    };
2243    for attr in &field.attrs {
2244        if !attr.path().is_ident("rustango") {
2245            continue;
2246        }
2247        attr.parse_nested_meta(|meta| {
2248            if meta.path.is_ident("column") {
2249                let s: LitStr = meta.value()?.parse()?;
2250                out.column = Some(s.value());
2251                return Ok(());
2252            }
2253            if meta.path.is_ident("primary_key") {
2254                out.primary_key = true;
2255                return Ok(());
2256            }
2257            if meta.path.is_ident("fk") {
2258                let s: LitStr = meta.value()?.parse()?;
2259                out.fk = Some(s.value());
2260                return Ok(());
2261            }
2262            if meta.path.is_ident("o2o") {
2263                let s: LitStr = meta.value()?.parse()?;
2264                out.o2o = Some(s.value());
2265                return Ok(());
2266            }
2267            if meta.path.is_ident("on") {
2268                let s: LitStr = meta.value()?.parse()?;
2269                out.on = Some(s.value());
2270                return Ok(());
2271            }
2272            if meta.path.is_ident("max_length") {
2273                let lit: syn::LitInt = meta.value()?.parse()?;
2274                out.max_length = Some(lit.base10_parse::<u32>()?);
2275                return Ok(());
2276            }
2277            if meta.path.is_ident("min") {
2278                out.min = Some(parse_signed_i64(&meta)?);
2279                return Ok(());
2280            }
2281            if meta.path.is_ident("max") {
2282                out.max = Some(parse_signed_i64(&meta)?);
2283                return Ok(());
2284            }
2285            if meta.path.is_ident("default") {
2286                let s: LitStr = meta.value()?.parse()?;
2287                out.default = Some(s.value());
2288                return Ok(());
2289            }
2290            if meta.path.is_ident("auto_uuid") {
2291                out.auto_uuid = true;
2292                // Implied: PK + auto + DEFAULT gen_random_uuid().
2293                // Each is also explicitly settable; the explicit
2294                // value wins if conflicting.
2295                out.primary_key = true;
2296                if out.default.is_none() {
2297                    out.default = Some("gen_random_uuid()".into());
2298                }
2299                return Ok(());
2300            }
2301            if meta.path.is_ident("auto_now_add") {
2302                out.auto_now_add = true;
2303                if out.default.is_none() {
2304                    out.default = Some("now()".into());
2305                }
2306                return Ok(());
2307            }
2308            if meta.path.is_ident("auto_now") {
2309                out.auto_now = true;
2310                if out.default.is_none() {
2311                    out.default = Some("now()".into());
2312                }
2313                return Ok(());
2314            }
2315            if meta.path.is_ident("soft_delete") {
2316                out.soft_delete = true;
2317                return Ok(());
2318            }
2319            Err(meta.error("unknown rustango field attribute"))
2320        })?;
2321    }
2322    Ok(out)
2323}
2324
2325/// Parse a signed integer literal, accepting optional leading `-`.
2326fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
2327    let expr: syn::Expr = meta.value()?.parse()?;
2328    match expr {
2329        syn::Expr::Lit(syn::ExprLit {
2330            lit: syn::Lit::Int(lit),
2331            ..
2332        }) => lit.base10_parse::<i64>(),
2333        syn::Expr::Unary(syn::ExprUnary {
2334            op: syn::UnOp::Neg(_),
2335            expr,
2336            ..
2337        }) => {
2338            if let syn::Expr::Lit(syn::ExprLit {
2339                lit: syn::Lit::Int(lit),
2340                ..
2341            }) = *expr
2342            {
2343                let v: i64 = lit.base10_parse()?;
2344                Ok(-v)
2345            } else {
2346                Err(syn::Error::new_spanned(expr, "expected integer literal"))
2347            }
2348        }
2349        other => Err(syn::Error::new_spanned(
2350            other,
2351            "expected integer literal (signed)",
2352        )),
2353    }
2354}
2355
2356struct FieldInfo<'a> {
2357    ident: &'a syn::Ident,
2358    column: String,
2359    primary_key: bool,
2360    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
2361    /// skip this column when `Auto::Unset` and emit it under
2362    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
2363    auto: bool,
2364    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
2365    /// the `Column::Value` associated type for typed-column tokens.
2366    value_ty: &'a Type,
2367    /// `FieldType` variant tokens (`::rustango::core::FieldType::I64`).
2368    field_type_tokens: TokenStream2,
2369    schema: TokenStream2,
2370    from_row_init: TokenStream2,
2371    /// Variant of [`Self::from_row_init`] that reads the column via
2372    /// `format!("{prefix}__{col}")` so a model can be decoded out of
2373    /// the aliased columns of a JOINed row. Drives slice 9.0d's
2374    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
2375    /// helper that `select_related` calls when stitching loaded FKs.
2376    from_aliased_row_init: TokenStream2,
2377    /// Inner type from a `ForeignKey<T>` field, if any. The reverse-
2378    /// relation helper emit (`Author::<child>_set`) needs to know `T`
2379    /// to point the generated method at the right child model.
2380    fk_inner: Option<Type>,
2381    /// `true` when this column was marked `#[rustango(auto_now)]` —
2382    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
2383    /// column instead of the user-supplied value, so `updated_at`
2384    /// always reflects the latest write without the caller having
2385    /// to remember to set it.
2386    auto_now: bool,
2387    /// `true` when this column was marked `#[rustango(auto_now_add)]`
2388    /// — the column is server-set on INSERT (DB DEFAULT) and
2389    /// **immutable** afterwards. `update_on` / `save_on` skip the
2390    /// column entirely so a stale `created_at` value in memory never
2391    /// rewrites the persisted timestamp.
2392    auto_now_add: bool,
2393    /// `true` when this column was marked `#[rustango(soft_delete)]`.
2394    /// Triggers emission of `soft_delete_on(executor)` and
2395    /// `restore_on(executor)` on the model's inherent impl. There is
2396    /// at most one such column per model — emission asserts this.
2397    soft_delete: bool,
2398}
2399
2400fn process_field(field: &syn::Field) -> syn::Result<FieldInfo<'_>> {
2401    let attrs = parse_field_attrs(field)?;
2402    let ident = field
2403        .ident
2404        .as_ref()
2405        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
2406    let name = ident.to_string();
2407    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
2408    let primary_key = attrs.primary_key;
2409    let DetectedType {
2410        kind,
2411        nullable,
2412        auto: detected_auto,
2413        fk_inner,
2414    } = detect_type(&field.ty)?;
2415    check_bound_compatibility(field, &attrs, kind)?;
2416    let auto = detected_auto;
2417    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
2418    // INSERT path: the user must wrap the field in `Auto<T>`, which
2419    // marks the column as DB-default-supplied. The mixin attrs then
2420    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
2421    // for `auto_now`, force the value on UPDATE too.
2422    if attrs.auto_uuid {
2423        if kind != DetectedKind::Uuid {
2424            return Err(syn::Error::new_spanned(
2425                field,
2426                "`#[rustango(auto_uuid)]` requires the field type to be \
2427                 `Auto<uuid::Uuid>`",
2428            ));
2429        }
2430        if !detected_auto {
2431            return Err(syn::Error::new_spanned(
2432                field,
2433                "`#[rustango(auto_uuid)]` requires the field type to be \
2434                 wrapped in `Auto<...>` so the macro skips the column on \
2435                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
2436            ));
2437        }
2438    }
2439    if attrs.auto_now_add || attrs.auto_now {
2440        if kind != DetectedKind::DateTime {
2441            return Err(syn::Error::new_spanned(
2442                field,
2443                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
2444                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
2445            ));
2446        }
2447        if !detected_auto {
2448            return Err(syn::Error::new_spanned(
2449                field,
2450                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
2451                 the field type to be wrapped in `Auto<...>` so the macro skips \
2452                 the column on INSERT and the DB DEFAULT (`now()`) fires",
2453            ));
2454        }
2455    }
2456    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
2457        return Err(syn::Error::new_spanned(
2458            field,
2459            "`#[rustango(soft_delete)]` requires the field type to be \
2460             `Option<chrono::DateTime<chrono::Utc>>`",
2461        ));
2462    }
2463    let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
2464    if detected_auto && !primary_key && !is_mixin_auto {
2465        return Err(syn::Error::new_spanned(
2466            field,
2467            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
2468             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
2469             `auto_now`",
2470        ));
2471    }
2472    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
2473        return Err(syn::Error::new_spanned(
2474            field,
2475            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
2476             SERIAL / BIGSERIAL already supplies a default sequence.",
2477        ));
2478    }
2479    if fk_inner.is_some() && primary_key {
2480        return Err(syn::Error::new_spanned(
2481            field,
2482            "`ForeignKey<T>` is not allowed on a primary-key field — \
2483             a row's PK is its own identity, not a reference to a parent.",
2484        ));
2485    }
2486    let relation = relation_tokens(field, &attrs, fk_inner)?;
2487    let column_lit = column.as_str();
2488    let field_type_tokens = kind.variant_tokens();
2489    let max_length = optional_u32(attrs.max_length);
2490    let min = optional_i64(attrs.min);
2491    let max = optional_i64(attrs.max);
2492    let default = optional_str(attrs.default.as_deref());
2493
2494    let schema = quote! {
2495        ::rustango::core::FieldSchema {
2496            name: #name,
2497            column: #column_lit,
2498            ty: #field_type_tokens,
2499            nullable: #nullable,
2500            primary_key: #primary_key,
2501            relation: #relation,
2502            max_length: #max_length,
2503            min: #min,
2504            max: #max,
2505            default: #default,
2506            auto: #auto,
2507        }
2508    };
2509
2510    let from_row_init = quote! {
2511        #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
2512    };
2513    let from_aliased_row_init = quote! {
2514        #ident: ::rustango::sql::sqlx::Row::try_get(
2515            row,
2516            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
2517        )?
2518    };
2519
2520    Ok(FieldInfo {
2521        ident,
2522        column,
2523        primary_key,
2524        auto,
2525        value_ty: &field.ty,
2526        field_type_tokens,
2527        schema,
2528        from_row_init,
2529        from_aliased_row_init,
2530        fk_inner: fk_inner.cloned(),
2531        auto_now: attrs.auto_now,
2532        auto_now_add: attrs.auto_now_add,
2533        soft_delete: attrs.soft_delete,
2534    })
2535}
2536
2537fn check_bound_compatibility(
2538    field: &syn::Field,
2539    attrs: &FieldAttrs,
2540    kind: DetectedKind,
2541) -> syn::Result<()> {
2542    if attrs.max_length.is_some() && kind != DetectedKind::String {
2543        return Err(syn::Error::new_spanned(
2544            field,
2545            "`max_length` is only valid on `String` fields (or `Option<String>`)",
2546        ));
2547    }
2548    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
2549        return Err(syn::Error::new_spanned(
2550            field,
2551            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
2552        ));
2553    }
2554    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
2555        if min > max {
2556            return Err(syn::Error::new_spanned(
2557                field,
2558                format!("`min` ({min}) is greater than `max` ({max})"),
2559            ));
2560        }
2561    }
2562    Ok(())
2563}
2564
2565fn optional_u32(value: Option<u32>) -> TokenStream2 {
2566    if let Some(v) = value {
2567        quote!(::core::option::Option::Some(#v))
2568    } else {
2569        quote!(::core::option::Option::None)
2570    }
2571}
2572
2573fn optional_i64(value: Option<i64>) -> TokenStream2 {
2574    if let Some(v) = value {
2575        quote!(::core::option::Option::Some(#v))
2576    } else {
2577        quote!(::core::option::Option::None)
2578    }
2579}
2580
2581fn optional_str(value: Option<&str>) -> TokenStream2 {
2582    if let Some(v) = value {
2583        quote!(::core::option::Option::Some(#v))
2584    } else {
2585        quote!(::core::option::Option::None)
2586    }
2587}
2588
2589fn relation_tokens(
2590    field: &syn::Field,
2591    attrs: &FieldAttrs,
2592    fk_inner: Option<&syn::Type>,
2593) -> syn::Result<TokenStream2> {
2594    if let Some(inner) = fk_inner {
2595        if attrs.fk.is_some() || attrs.o2o.is_some() {
2596            return Err(syn::Error::new_spanned(
2597                field,
2598                "`ForeignKey<T>` already declares the FK target via the type parameter — \
2599                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
2600            ));
2601        }
2602        let on = attrs.on.as_deref().unwrap_or("id");
2603        return Ok(quote! {
2604            ::core::option::Option::Some(::rustango::core::Relation::Fk {
2605                to: <#inner as ::rustango::core::Model>::SCHEMA.table,
2606                on: #on,
2607            })
2608        });
2609    }
2610    match (&attrs.fk, &attrs.o2o) {
2611        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
2612            field,
2613            "`fk` and `o2o` are mutually exclusive",
2614        )),
2615        (Some(to), None) => {
2616            let on = attrs.on.as_deref().unwrap_or("id");
2617            Ok(quote! {
2618                ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #to, on: #on })
2619            })
2620        }
2621        (None, Some(to)) => {
2622            let on = attrs.on.as_deref().unwrap_or("id");
2623            Ok(quote! {
2624                ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #to, on: #on })
2625            })
2626        }
2627        (None, None) => {
2628            if attrs.on.is_some() {
2629                return Err(syn::Error::new_spanned(
2630                    field,
2631                    "`on` requires `fk` or `o2o`",
2632                ));
2633            }
2634            Ok(quote!(::core::option::Option::None))
2635        }
2636    }
2637}
2638
2639/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
2640/// about kinds without depending on `rustango-core` (which would require a
2641/// proc-macro/normal split it doesn't have today).
2642#[derive(Clone, Copy, PartialEq, Eq)]
2643enum DetectedKind {
2644    I32,
2645    I64,
2646    F32,
2647    F64,
2648    Bool,
2649    String,
2650    DateTime,
2651    Date,
2652    Uuid,
2653    Json,
2654}
2655
2656impl DetectedKind {
2657    fn variant_tokens(self) -> TokenStream2 {
2658        match self {
2659            Self::I32 => quote!(::rustango::core::FieldType::I32),
2660            Self::I64 => quote!(::rustango::core::FieldType::I64),
2661            Self::F32 => quote!(::rustango::core::FieldType::F32),
2662            Self::F64 => quote!(::rustango::core::FieldType::F64),
2663            Self::Bool => quote!(::rustango::core::FieldType::Bool),
2664            Self::String => quote!(::rustango::core::FieldType::String),
2665            Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
2666            Self::Date => quote!(::rustango::core::FieldType::Date),
2667            Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
2668            Self::Json => quote!(::rustango::core::FieldType::Json),
2669        }
2670    }
2671
2672    fn is_integer(self) -> bool {
2673        matches!(self, Self::I32 | Self::I64)
2674    }
2675}
2676
2677/// Result of walking a field's Rust type. `kind` is the underlying
2678/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
2679/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
2680/// `Some(<T>)` when the field was `ForeignKey<T>` (or
2681/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
2682#[derive(Clone, Copy)]
2683struct DetectedType<'a> {
2684    kind: DetectedKind,
2685    nullable: bool,
2686    auto: bool,
2687    fk_inner: Option<&'a syn::Type>,
2688}
2689
2690fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
2691    let Type::Path(TypePath { path, qself: None }) = ty else {
2692        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
2693    };
2694    let last = path
2695        .segments
2696        .last()
2697        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
2698
2699    if last.ident == "Option" {
2700        let inner = generic_inner(ty, &last.arguments, "Option")?;
2701        let inner_det = detect_type(inner)?;
2702        if inner_det.nullable {
2703            return Err(syn::Error::new_spanned(
2704                ty,
2705                "nested Option is not supported",
2706            ));
2707        }
2708        if inner_det.auto {
2709            return Err(syn::Error::new_spanned(
2710                ty,
2711                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
2712            ));
2713        }
2714        return Ok(DetectedType {
2715            nullable: true,
2716            ..inner_det
2717        });
2718    }
2719
2720    if last.ident == "Auto" {
2721        let inner = generic_inner(ty, &last.arguments, "Auto")?;
2722        let inner_det = detect_type(inner)?;
2723        if inner_det.auto {
2724            return Err(syn::Error::new_spanned(
2725                ty,
2726                "nested Auto is not supported",
2727            ));
2728        }
2729        if inner_det.nullable {
2730            return Err(syn::Error::new_spanned(
2731                ty,
2732                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
2733            ));
2734        }
2735        if inner_det.fk_inner.is_some() {
2736            return Err(syn::Error::new_spanned(
2737                ty,
2738                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
2739            ));
2740        }
2741        if !matches!(
2742            inner_det.kind,
2743            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
2744        ) {
2745            return Err(syn::Error::new_spanned(
2746                ty,
2747                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
2748                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
2749                 (DEFAULT now())",
2750            ));
2751        }
2752        return Ok(DetectedType {
2753            auto: true,
2754            ..inner_det
2755        });
2756    }
2757
2758    if last.ident == "ForeignKey" {
2759        let inner = generic_inner(ty, &last.arguments, "ForeignKey")?;
2760        // `ForeignKey<T>` is stored as BIGINT — same column shape as
2761        // the v0.1 `i64` + `#[rustango(fk = …)]` form. The macro does
2762        // not recurse into `T` because `T` is a Model struct, not a
2763        // primitive — its identity is opaque to schema detection.
2764        return Ok(DetectedType {
2765            kind: DetectedKind::I64,
2766            nullable: false,
2767            auto: false,
2768            fk_inner: Some(inner),
2769        });
2770    }
2771
2772    let kind = match last.ident.to_string().as_str() {
2773        "i32" => DetectedKind::I32,
2774        "i64" => DetectedKind::I64,
2775        "f32" => DetectedKind::F32,
2776        "f64" => DetectedKind::F64,
2777        "bool" => DetectedKind::Bool,
2778        "String" => DetectedKind::String,
2779        "DateTime" => DetectedKind::DateTime,
2780        "NaiveDate" => DetectedKind::Date,
2781        "Uuid" => DetectedKind::Uuid,
2782        "Value" => DetectedKind::Json,
2783        other => {
2784            return Err(syn::Error::new_spanned(
2785                ty,
2786                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)"),
2787            ));
2788        }
2789    };
2790    Ok(DetectedType {
2791        kind,
2792        nullable: false,
2793        auto: false,
2794        fk_inner: None,
2795    })
2796}
2797
2798fn generic_inner<'a>(
2799    ty: &'a Type,
2800    arguments: &'a PathArguments,
2801    wrapper: &str,
2802) -> syn::Result<&'a Type> {
2803    let PathArguments::AngleBracketed(args) = arguments else {
2804        return Err(syn::Error::new_spanned(
2805            ty,
2806            format!("{wrapper} requires a generic argument"),
2807        ));
2808    };
2809    args.args
2810        .iter()
2811        .find_map(|a| match a {
2812            GenericArgument::Type(t) => Some(t),
2813            _ => None,
2814        })
2815        .ok_or_else(|| {
2816            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
2817        })
2818}
2819
2820fn to_snake_case(s: &str) -> String {
2821    let mut out = String::with_capacity(s.len() + 4);
2822    for (i, ch) in s.chars().enumerate() {
2823        if ch.is_ascii_uppercase() {
2824            if i > 0 {
2825                out.push('_');
2826            }
2827            out.push(ch.to_ascii_lowercase());
2828        } else {
2829            out.push(ch);
2830        }
2831    }
2832    out
2833}
2834
2835// ============================================================
2836//  #[derive(Form)]  —  slice 8.4B
2837// ============================================================
2838
2839/// Per-field `#[form(...)]` attributes recognised by the derive.
2840#[derive(Default)]
2841struct FormFieldAttrs {
2842    min: Option<i64>,
2843    max: Option<i64>,
2844    min_length: Option<u32>,
2845    max_length: Option<u32>,
2846}
2847
2848/// Detected shape of a form field's Rust type.
2849#[derive(Clone, Copy)]
2850enum FormFieldKind {
2851    String,
2852    I32,
2853    I64,
2854    F32,
2855    F64,
2856    Bool,
2857}
2858
2859impl FormFieldKind {
2860    fn parse_method(self) -> &'static str {
2861        match self {
2862            Self::I32 => "i32",
2863            Self::I64 => "i64",
2864            Self::F32 => "f32",
2865            Self::F64 => "f64",
2866            // String + Bool don't go through `str::parse`; the codegen
2867            // handles them inline.
2868            Self::String | Self::Bool => "",
2869        }
2870    }
2871}
2872
2873fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
2874    let struct_name = &input.ident;
2875
2876    let Data::Struct(data) = &input.data else {
2877        return Err(syn::Error::new_spanned(
2878            struct_name,
2879            "Form can only be derived on structs",
2880        ));
2881    };
2882    let Fields::Named(named) = &data.fields else {
2883        return Err(syn::Error::new_spanned(
2884            struct_name,
2885            "Form requires a struct with named fields",
2886        ));
2887    };
2888
2889    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
2890    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
2891
2892    for field in &named.named {
2893        let ident = field
2894            .ident
2895            .as_ref()
2896            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
2897        let attrs = parse_form_field_attrs(field)?;
2898        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
2899
2900        let name_lit = ident.to_string();
2901        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
2902        field_blocks.push(parse_block);
2903        field_idents.push(ident);
2904    }
2905
2906    Ok(quote! {
2907        impl ::rustango::forms::FormStruct for #struct_name {
2908            fn parse(
2909                form: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
2910            ) -> ::core::result::Result<Self, ::rustango::forms::FormError> {
2911                #( #field_blocks )*
2912                ::core::result::Result::Ok(Self {
2913                    #( #field_idents ),*
2914                })
2915            }
2916        }
2917    })
2918}
2919
2920fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
2921    let mut out = FormFieldAttrs::default();
2922    for attr in &field.attrs {
2923        if !attr.path().is_ident("form") {
2924            continue;
2925        }
2926        attr.parse_nested_meta(|meta| {
2927            if meta.path.is_ident("min") {
2928                let lit: syn::LitInt = meta.value()?.parse()?;
2929                out.min = Some(lit.base10_parse::<i64>()?);
2930                return Ok(());
2931            }
2932            if meta.path.is_ident("max") {
2933                let lit: syn::LitInt = meta.value()?.parse()?;
2934                out.max = Some(lit.base10_parse::<i64>()?);
2935                return Ok(());
2936            }
2937            if meta.path.is_ident("min_length") {
2938                let lit: syn::LitInt = meta.value()?.parse()?;
2939                out.min_length = Some(lit.base10_parse::<u32>()?);
2940                return Ok(());
2941            }
2942            if meta.path.is_ident("max_length") {
2943                let lit: syn::LitInt = meta.value()?.parse()?;
2944                out.max_length = Some(lit.base10_parse::<u32>()?);
2945                return Ok(());
2946            }
2947            Err(meta.error(
2948                "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
2949            ))
2950        })?;
2951    }
2952    Ok(out)
2953}
2954
2955fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
2956    let Type::Path(TypePath { path, qself: None }) = ty else {
2957        return Err(syn::Error::new(
2958            span,
2959            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
2960        ));
2961    };
2962    let last = path
2963        .segments
2964        .last()
2965        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
2966
2967    if last.ident == "Option" {
2968        let inner = generic_inner(ty, &last.arguments, "Option")?;
2969        let (kind, nested) = detect_form_field(inner, span)?;
2970        if nested {
2971            return Err(syn::Error::new(
2972                span,
2973                "nested Option in Form fields is not supported",
2974            ));
2975        }
2976        return Ok((kind, true));
2977    }
2978
2979    let kind = match last.ident.to_string().as_str() {
2980        "String" => FormFieldKind::String,
2981        "i32" => FormFieldKind::I32,
2982        "i64" => FormFieldKind::I64,
2983        "f32" => FormFieldKind::F32,
2984        "f64" => FormFieldKind::F64,
2985        "bool" => FormFieldKind::Bool,
2986        other => {
2987            return Err(syn::Error::new(
2988                span,
2989                format!(
2990                    "Form field type `{other}` is not supported in v0.8 — use String / \
2991                     i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
2992                ),
2993            ));
2994        }
2995    };
2996    Ok((kind, false))
2997}
2998
2999#[allow(clippy::too_many_lines)]
3000fn render_form_field_parse(
3001    ident: &syn::Ident,
3002    name_lit: &str,
3003    kind: FormFieldKind,
3004    nullable: bool,
3005    attrs: &FormFieldAttrs,
3006) -> TokenStream2 {
3007    // Common helper: pull the raw &str out of the form. Bool's
3008    // checkbox semantics (absent = false) are handled inline; every
3009    // other type errors with `Missing` for absent required fields,
3010    // or yields `None` for absent nullable Option<T> fields.
3011    let lookup = quote! {
3012        let __raw: ::core::option::Option<&::std::string::String> = form.get(#name_lit);
3013    };
3014
3015    let parsed_value = match kind {
3016        FormFieldKind::Bool => quote! {
3017            // HTML checkbox: absent = false, anything-non-empty = true,
3018            // except literal "false"/"0"/"off"/"no".
3019            let __v: bool = match __raw {
3020                ::core::option::Option::None => false,
3021                ::core::option::Option::Some(__s) => !matches!(
3022                    __s.to_ascii_lowercase().as_str(),
3023                    "" | "false" | "0" | "off" | "no"
3024                ),
3025            };
3026        },
3027        FormFieldKind::String => {
3028            if nullable {
3029                quote! {
3030                    let __v: ::core::option::Option<::std::string::String> = match __raw {
3031                        ::core::option::Option::None => ::core::option::Option::None,
3032                        ::core::option::Option::Some(__s) if __s.is_empty() => {
3033                            ::core::option::Option::None
3034                        }
3035                        ::core::option::Option::Some(__s) => {
3036                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
3037                        }
3038                    };
3039                }
3040            } else {
3041                quote! {
3042                    let __v: ::std::string::String = match __raw {
3043                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
3044                            ::core::clone::Clone::clone(__s)
3045                        }
3046                        _ => {
3047                            return ::core::result::Result::Err(
3048                                ::rustango::forms::FormError::Missing {
3049                                    field: ::std::string::String::from(#name_lit),
3050                                }
3051                            );
3052                        }
3053                    };
3054                }
3055            }
3056        }
3057        FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64 => {
3058            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
3059            let ty_lit = kind.parse_method();
3060            let parse_expr = quote! {
3061                __s.parse::<#parse_ty>().map_err(|__e| {
3062                    ::rustango::forms::FormError::Parse {
3063                        field: ::std::string::String::from(#name_lit),
3064                        ty: #ty_lit,
3065                        value: ::core::clone::Clone::clone(__s),
3066                        detail: ::std::string::ToString::to_string(&__e),
3067                    }
3068                })
3069            };
3070            if nullable {
3071                quote! {
3072                    let __v: ::core::option::Option<#parse_ty> = match __raw {
3073                        ::core::option::Option::None => ::core::option::Option::None,
3074                        ::core::option::Option::Some(__s) if __s.is_empty() => {
3075                            ::core::option::Option::None
3076                        }
3077                        ::core::option::Option::Some(__s) => {
3078                            ::core::option::Option::Some(#parse_expr?)
3079                        }
3080                    };
3081                }
3082            } else {
3083                quote! {
3084                    let __v: #parse_ty = match __raw {
3085                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
3086                            #parse_expr?
3087                        }
3088                        _ => {
3089                            return ::core::result::Result::Err(
3090                                ::rustango::forms::FormError::Missing {
3091                                    field: ::std::string::String::from(#name_lit),
3092                                }
3093                            );
3094                        }
3095                    };
3096                }
3097            }
3098        }
3099    };
3100
3101    // Validator emission. min / max only make sense on numeric kinds;
3102    // min_length / max_length only on String. We silently ignore
3103    // wrong-shape combinations for now (a future commit can validate
3104    // attribute-vs-type at macro time).
3105    let validators = render_form_validators(name_lit, kind, nullable, attrs);
3106
3107    quote! {
3108        let #ident = {
3109            #lookup
3110            #parsed_value
3111            #validators
3112            __v
3113        };
3114    }
3115}
3116
3117fn render_form_validators(
3118    name_lit: &str,
3119    kind: FormFieldKind,
3120    nullable: bool,
3121    attrs: &FormFieldAttrs,
3122) -> TokenStream2 {
3123    let mut checks: Vec<TokenStream2> = Vec::new();
3124
3125    let val_ref = if nullable {
3126        // Validate the inner value when Some; skip when None.
3127        quote! { __v.as_ref() }
3128    } else {
3129        quote! { ::core::option::Option::Some(&__v) }
3130    };
3131
3132    let is_string = matches!(kind, FormFieldKind::String);
3133    let is_numeric = matches!(
3134        kind,
3135        FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64
3136    );
3137
3138    if is_string {
3139        if let Some(min_len) = attrs.min_length {
3140            let min_len_usize = min_len as usize;
3141            checks.push(quote! {
3142                if let ::core::option::Option::Some(__s) = #val_ref {
3143                    if __s.len() < #min_len_usize {
3144                        return ::core::result::Result::Err(
3145                            ::rustango::forms::FormError::Parse {
3146                                field: ::std::string::String::from(#name_lit),
3147                                ty: "String",
3148                                value: ::core::clone::Clone::clone(__s),
3149                                detail: ::std::format!(
3150                                    "shorter than min_length {}", #min_len_usize
3151                                ),
3152                            }
3153                        );
3154                    }
3155                }
3156            });
3157        }
3158        if let Some(max_len) = attrs.max_length {
3159            let max_len_usize = max_len as usize;
3160            checks.push(quote! {
3161                if let ::core::option::Option::Some(__s) = #val_ref {
3162                    if __s.len() > #max_len_usize {
3163                        return ::core::result::Result::Err(
3164                            ::rustango::forms::FormError::Parse {
3165                                field: ::std::string::String::from(#name_lit),
3166                                ty: "String",
3167                                value: ::core::clone::Clone::clone(__s),
3168                                detail: ::std::format!(
3169                                    "longer than max_length {}", #max_len_usize
3170                                ),
3171                            }
3172                        );
3173                    }
3174                }
3175            });
3176        }
3177    }
3178
3179    if is_numeric {
3180        if let Some(min) = attrs.min {
3181            checks.push(quote! {
3182                if let ::core::option::Option::Some(__n) = #val_ref {
3183                    let __nf = (*__n) as f64;
3184                    if __nf < (#min as f64) {
3185                        return ::core::result::Result::Err(
3186                            ::rustango::forms::FormError::Parse {
3187                                field: ::std::string::String::from(#name_lit),
3188                                ty: "numeric",
3189                                value: ::std::string::ToString::to_string(__n),
3190                                detail: ::std::format!("less than min {}", #min),
3191                            }
3192                        );
3193                    }
3194                }
3195            });
3196        }
3197        if let Some(max) = attrs.max {
3198            checks.push(quote! {
3199                if let ::core::option::Option::Some(__n) = #val_ref {
3200                    let __nf = (*__n) as f64;
3201                    if __nf > (#max as f64) {
3202                        return ::core::result::Result::Err(
3203                            ::rustango::forms::FormError::Parse {
3204                                field: ::std::string::String::from(#name_lit),
3205                                ty: "numeric",
3206                                value: ::std::string::ToString::to_string(__n),
3207                                detail: ::std::format!("greater than max {}", #max),
3208                            }
3209                        );
3210                    }
3211                }
3212            });
3213        }
3214    }
3215
3216    quote! { #( #checks )* }
3217}